// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.groovy.annotator.intentions.dynamic;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.psi.PsiManager;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiVariable;
import com.intellij.ui.treeStructure.treetable.ListTreeTableModelOnColumns;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.tree.TreeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.annotator.intentions.QuickfixUtil;
import org.jetbrains.plugins.groovy.annotator.intentions.dynamic.elements.*;
import org.jetbrains.plugins.groovy.annotator.intentions.dynamic.ui.DynamicElementSettings;

import javax.swing.tree.DefaultMutableTreeNode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

@State(name = "DynamicElementsStorage", storages = @Storage("dynamic.xml"))

public final class DynamicManagerImpl extends DynamicManager {
  private final Project myProject;
  private DRootElement myRootElement = new DRootElement();

  public DynamicManagerImpl(final Project project) {
    myProject = project;
  }

  public Project getProject() {
    return myProject;
  }

  @Override
  public void addProperty(DynamicElementSettings settings) {
    assert settings != null;
    assert !settings.isMethod();

    final DPropertyElement propertyElement = (DPropertyElement) createDynamicElement(settings);
    final DClassElement classElement = getOrCreateClassElement(myProject, settings.getContainingClassName());

    ToolWindow window = DynamicToolWindowWrapper.getInstance(myProject).getToolWindow(); //important to fetch myToolWindow before adding
    classElement.addProperty(propertyElement);
    addItemInTree(classElement, propertyElement, window);
  }

  private void removeItemFromTree(DItemElement itemElement, DClassElement classElement) {
    DynamicToolWindowWrapper wrapper = DynamicToolWindowWrapper.getInstance(myProject);
    ListTreeTableModelOnColumns model = wrapper.getTreeTableModel();
    Object classNode = TreeUtil.findNodeWithObject(classElement, model, model.getRoot());
    final DefaultMutableTreeNode node = (DefaultMutableTreeNode) TreeUtil.findNodeWithObject(itemElement, model, classNode);
    if (node == null) return;
    DefaultMutableTreeNode parent = (DefaultMutableTreeNode) node.getParent();

    doRemove(wrapper, node, parent);
  }

  private void removeClassFromTree(DClassElement classElement) {
    DynamicToolWindowWrapper wrapper = DynamicToolWindowWrapper.getInstance(myProject);
    ListTreeTableModelOnColumns model = wrapper.getTreeTableModel();
    final DefaultMutableTreeNode node = (DefaultMutableTreeNode) TreeUtil.findNodeWithObject(classElement, model, model.getRoot());
    if (node == null) return;
    DefaultMutableTreeNode parent = (DefaultMutableTreeNode) node.getParent();

    doRemove(wrapper, node, parent);
  }

  private static void doRemove(DynamicToolWindowWrapper wrapper, DefaultMutableTreeNode node, DefaultMutableTreeNode parent) {
    DefaultMutableTreeNode toSelect = (parent.getChildAfter(node) != null || parent.getChildCount() == 1 ?
        node.getNextNode() :
        node.getPreviousNode());


    wrapper.removeFromParent(parent, node);
    if (toSelect != null) {
      wrapper.setSelectedNode(toSelect);
    }
  }

  private void addItemInTree(final DClassElement classElement, final DItemElement itemElement, final ToolWindow window) {
    final ListTreeTableModelOnColumns myTreeTableModel = DynamicToolWindowWrapper.getInstance(myProject).getTreeTableModel();

    window.activate(() -> {
      final Object rootObject = myTreeTableModel.getRoot();
      if (!(rootObject instanceof DefaultMutableTreeNode rootNode)) return;

      DefaultMutableTreeNode node = new DefaultMutableTreeNode(itemElement);
      if (rootNode.getChildCount() > 0) {
        for (DefaultMutableTreeNode classNode = (DefaultMutableTreeNode) rootNode.getFirstChild();
             classNode != null;
             classNode = (DefaultMutableTreeNode) rootNode.getChildAfter(classNode)) {

          final Object classRow = classNode.getUserObject();
          if (!(classRow instanceof DClassElement otherClassName)) return;

          if (otherClassName.equals(classElement)) {
            int index = getIndexToInsert(classNode, itemElement);
            classNode.insert(node, index);
            myTreeTableModel.nodesWereInserted(classNode, new int[]{index});
            DynamicToolWindowWrapper.getInstance(myProject).setSelectedNode(node);
            return;
          }
        }
      }

      // if there is no such class in tree
      int index = getIndexToInsert(rootNode, classElement);
      DefaultMutableTreeNode classNode = new DefaultMutableTreeNode(classElement);
      rootNode.insert(classNode, index);
      myTreeTableModel.nodesWereInserted(rootNode, new int[]{index});

      classNode.add(node);
      myTreeTableModel.nodesWereInserted(classNode, new int[]{0});

      DynamicToolWindowWrapper.getInstance(myProject).setSelectedNode(node);
    }, true);
  }

  private static int getIndexToInsert(DefaultMutableTreeNode parent, DNamedElement namedElement) {
    if (parent.getChildCount() == 0) return 0;

    int res = 0;
    for (DefaultMutableTreeNode child = (DefaultMutableTreeNode) parent.getFirstChild();
         child != null;
         child = (DefaultMutableTreeNode) parent.getChildAfter(child)) {
      Object childObject = child.getUserObject();

      if (!(childObject instanceof DNamedElement)) return 0;

      String otherName = ((DNamedElement) childObject).getName();
      if (otherName.compareTo(namedElement.getName()) > 0) return res;
      res++;
    }
    return res;
  }

  @Override
  public void addMethod(DynamicElementSettings settings) {
    if (settings == null) return;
    assert settings.isMethod();

    final DMethodElement methodElement = (DMethodElement) createDynamicElement(settings);
    final DClassElement classElement = getOrCreateClassElement(myProject, settings.getContainingClassName());

    ToolWindow window = DynamicToolWindowWrapper.getInstance(myProject).getToolWindow(); //important to fetch myToolWindow before adding
    classElement.addMethod(methodElement);
    addItemInTree(classElement, methodElement, window);
  }

  @Override
  public void removeClassElement(DClassElement classElement) {

    final DRootElement rootElement = getRootElement();
    rootElement.removeClassElement(classElement.getName());
    removeClassFromTree(classElement);
  }

  private void removePropertyElement(DPropertyElement propertyElement) {
    final DClassElement classElement = getClassElementByItem(propertyElement);
    assert classElement != null;

    classElement.removeProperty(propertyElement);
  }

  @Override
  public @NotNull Collection<DPropertyElement> findDynamicPropertiesOfClass(String className) {
    final DClassElement classElement = findClassElement(getRootElement(), className);

    if (classElement != null) {
      return classElement.getProperties();
    }
    return new ArrayList<>();
  }

  @Override
  public @Nullable String getPropertyType(String className, String propertyName) {
    final DPropertyElement dynamicProperty = findConcreteDynamicProperty(getRootElement(), className, propertyName);

    if (dynamicProperty == null) return null;
    return dynamicProperty.getType();
  }

  @Override
  public @NotNull Collection<DClassElement> getAllContainingClasses() {
    //TODO: use iterator
    final DRootElement root = getRootElement();

    return root.getContainingClasses();
  }

  @Override
  public DRootElement getRootElement() {
    return myRootElement;
  }

  @Override
  public @Nullable String replaceDynamicPropertyName(String className, String oldPropertyName, String newPropertyName) {
    final DClassElement classElement = findClassElement(getRootElement(), className);
    if (classElement == null) return null;

    final DPropertyElement oldPropertyElement = classElement.getPropertyByName(oldPropertyName);
    if (oldPropertyElement == null) return null;
    classElement.removeProperty(oldPropertyElement);
    classElement.addProperty(new DPropertyElement(oldPropertyElement.isStatic(), newPropertyName, oldPropertyElement.getType()));
    fireChange();
    DynamicToolWindowWrapper.getInstance(getProject()).rebuildTreePanel();


    return newPropertyName;
  }

  @Override
  public @Nullable String replaceDynamicPropertyType(String className, String propertyName, String oldPropertyType, String newPropertyType) {
    final DPropertyElement property = findConcreteDynamicProperty(className, propertyName);

    if (property == null) return null;

    property.setType(newPropertyType);
    fireChange();
    return newPropertyType;
  }

  /*
  * Find dynamic property in class with name
  */

  private static @Nullable DMethodElement findConcreteDynamicMethod(DRootElement rootElement,
                                                                    String containingClassName,
                                                                    String methodName,
                                                                    String[] parametersTypes) {
    DClassElement classElement = findClassElement(rootElement, containingClassName);
    if (classElement == null) return null;

    return classElement.getMethod(methodName, parametersTypes);
  }

  //  @Nullable

  @Override
  public DMethodElement findConcreteDynamicMethod(String containingClassName, String name, String[] parameterTypes) {
    return findConcreteDynamicMethod(getRootElement(), containingClassName, name, parameterTypes);
  }

  private void removeMethodElement(DMethodElement methodElement) {
    final DClassElement classElement = getClassElementByItem(methodElement);
    assert classElement != null;

    classElement.removeMethod(methodElement);
  }

  @Override
  public void removeItemElement(DItemElement element) {
    DClassElement classElement = getClassElementByItem(element);
    if (classElement == null) return;
    
    if (element instanceof DPropertyElement) {
      removePropertyElement(((DPropertyElement) element));
    } else if (element instanceof DMethodElement) {
      removeMethodElement(((DMethodElement) element));
    }

    removeItemFromTree(element, classElement);
  }

  @Override
  public void replaceDynamicMethodType(String className, String name, List<ParamInfo> myPairList, String oldType, String newType) {
    final DMethodElement method = findConcreteDynamicMethod(className, name, QuickfixUtil.getArgumentsTypes(myPairList));

    if (method == null) return;
    method.setType(newType);
    fireChange();
  }

  @Override
  public @NotNull DClassElement getOrCreateClassElement(Project project, String className) {
    DClassElement classElement = DynamicManager.getInstance(myProject).getRootElement().getClassElement(className);
    if (classElement == null) {
      return new DClassElement(project, className);
    }

    return classElement;
  }

  @Override
  public @Nullable DClassElement getClassElementByItem(DItemElement itemElement) {
    final Collection<DClassElement> classes = getAllContainingClasses();
    for (DClassElement aClass : classes) {
      if (aClass.containsElement(itemElement)) return aClass;
    }
    return null;
  }

  @Override
  public void replaceDynamicMethodName(String className, String oldName, String newName, String[] types) {
    final DMethodElement oldMethodElement = findConcreteDynamicMethod(className, oldName, types);
    if (oldMethodElement != null) {
      oldMethodElement.setName(newName);
    }
    DynamicToolWindowWrapper.getInstance(getProject()).rebuildTreePanel();
    fireChange();
  }

  @Override
  public Iterable<PsiMethod> getMethods(final String classQname) {
    DClassElement classElement = getRootElement().getClassElement(classQname);
    if (classElement == null) return Collections.emptyList();
    return ContainerUtil.map(classElement.getMethods(), methodElement -> methodElement.getPsi(PsiManager.getInstance(myProject), classQname));
  }

  @Override
  public Iterable<PsiVariable> getProperties(final String classQname) {
    DClassElement classElement = getRootElement().getClassElement(classQname);
    if (classElement == null) return Collections.emptyList();
    return ContainerUtil.map(classElement.getProperties(), propertyElement -> propertyElement.getPsi(PsiManager.getInstance(myProject), classQname));
  }

  @Override
  public void replaceClassName(final DClassElement oldClassElement, String newClassName) {
    if (oldClassElement == null) return;

    final DRootElement rootElement = getRootElement();
    rootElement.removeClassElement(oldClassElement.getName());

    oldClassElement.setName(newClassName);
    rootElement.mergeAddClass(oldClassElement);

    fireChange();
  }

  @Override
  public void fireChange() {
    ApplicationManager.getApplication().invokeLater(() -> PsiManager.getInstance(myProject).dropPsiCaches(), myProject.getDisposed());
  }

  @Override
  public @Nullable DPropertyElement findConcreteDynamicProperty(final String containingClassName, final String propertyName) {
    return findConcreteDynamicProperty(getRootElement(), containingClassName, propertyName);
  }

  private static @Nullable DPropertyElement findConcreteDynamicProperty(DRootElement rootElement, final String containingClassName, final String propertyName) {
    final DClassElement classElement = rootElement.getClassElement(containingClassName);

    if (classElement == null) return null;

    return classElement.getPropertyByName(propertyName);
  }

  private static @Nullable DClassElement findClassElement(DRootElement rootElement, final String containingClassName) {
    return rootElement.getClassElement(containingClassName);
  }

  @Override
  public DRootElement getState() {
    return myRootElement;
  }

  @Override
  public void loadState(@NotNull DRootElement element) {
    myRootElement = element;
  }

  @Override
  public DItemElement createDynamicElement(DynamicElementSettings settings) {
    DItemElement itemElement;
    if (settings.isMethod()) {
      itemElement = new DMethodElement(settings.isStatic(), settings.getName(), settings.getType(), settings.getParams());
    } else {
      itemElement = new DPropertyElement(settings.isStatic(), settings.getName(), settings.getType());
    }
    return itemElement;
  }
}
