/*
 * Copyright 2003-2016 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package jetbrains.mps.ide.projectPane.logicalview;

import jetbrains.mps.ide.ui.tree.MPSTreeNode;
import jetbrains.mps.ide.ui.tree.MPSTreeNodeEx;
import jetbrains.mps.ide.ui.tree.module.AccessoriesModelTreeNode;
import jetbrains.mps.ide.ui.tree.module.NamespaceTextNode;
import jetbrains.mps.ide.ui.tree.module.ProjectLanguageTreeNode;
import jetbrains.mps.ide.ui.tree.module.ProjectLanguageTreeNode.AllModelsTreeNode;
import jetbrains.mps.ide.ui.tree.module.ProjectModuleTreeNode;
import jetbrains.mps.ide.ui.tree.module.ProjectModulesPoolTreeNode;
import jetbrains.mps.ide.ui.tree.module.SModelsSubtree.StubsTreeNode;
import jetbrains.mps.ide.ui.tree.module.SModelsSubtree.TestsTreeNode;
import jetbrains.mps.ide.ui.tree.smodel.PackageNode;
import jetbrains.mps.ide.ui.tree.smodel.SModelTreeNode;
import jetbrains.mps.ide.ui.tree.smodel.SNodeTreeNode;
import jetbrains.mps.project.Project;
import jetbrains.mps.smodel.Generator;
import jetbrains.mps.smodel.Language;
import jetbrains.mps.smodel.SNodeUtil;
import jetbrains.mps.util.SNodeOperations;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.util.Condition;

import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeNode;
import java.util.ArrayDeque;
import java.util.LinkedList;

public final class ProjectTreeFindHelper {
  private ProjectTree myProjectTree;

  public ProjectTreeFindHelper(ProjectTree projectTree) {
    myProjectTree = projectTree;
  }

  public ProjectModuleTreeNode findMostSuitableModuleTreeNode(final @NotNull SModule module) {
    ProjectModuleTreeNode result = findModuleTreeNodeInProject(module);
    if (result != null) return result;

    ProjectModulesPoolTreeNode modulesPoolNode = getTree().getModulesPoolNode();
    if (!modulesPoolNode.isInitialized()) {
      modulesPoolNode.init();
    }

    return findModuleTreeNodeAnywhere(module);
  }

  protected ProjectModuleTreeNode findModuleTreeNodeInProject(final @NotNull SModule module) {
    return (ProjectModuleTreeNode) findTreeNode(getTree().getRootNode(),
        new ModuleInProjectCondition(),
        new NodeForModuleCondition(module));
  }

  protected ProjectModuleTreeNode findModuleTreeNodeAnywhere(@NotNull SModule module) {
    return (ProjectModuleTreeNode) findTreeNode(getTree().getRootNode(),
        new ModuleEverywhereCondition(),
        new NodeForModuleCondition(module));
  }

  public SModelTreeNode findMostSuitableModelTreeNode(@NotNull SModel model) {
    SModule module = getModuleForModel(getProject(), model);
    if (module == null) return findModelTreeNodeAnywhere(model, getTree().getRootNode());

    ProjectModuleTreeNode moduleTreeNode = findMostSuitableModuleTreeNode(module);
    if (moduleTreeNode == null) return findModelTreeNodeAnywhere(model, getTree().getRootNode());

    return findModelTreeNodeInModule(model, moduleTreeNode);
  }

  protected SModelTreeNode findModelTreeNodeInModule(final @NotNull SModel model, @NotNull ProjectModuleTreeNode moduleNode) {
    return (SModelTreeNode) findTreeNode(moduleNode, new ModelInModuleCondition(model), new NodeForModelCondition(model));
  }

  protected SModelTreeNode findModelTreeNodeAnywhere(@NotNull SModel model, @NotNull MPSTreeNode parentNode) {
    return (SModelTreeNode) findTreeNode(parentNode, new ModelEverywhereCondition(model), new NodeForModelCondition(model));
  }

  public MPSTreeNodeEx findMostSuitableSNodeTreeNode(@NotNull SNode node) {
    SModel model = node.getModel();
    if (model == null) return null;
    SModelTreeNode modelNode = findMostSuitableModelTreeNode(model);
    if (modelNode == null) return null;

    return findSNodeTreeNodeInParent(node, modelNode);
  }

  //todo rewrite using findTreeNode
  protected MPSTreeNodeEx findSNodeTreeNodeInParent(@NotNull final SNode node, @NotNull final SModelTreeNode parent) {
    LinkedList<SNode> ancestors = new LinkedList<SNode>();
    SNode current = node;
    while (current != null) {
      ancestors.addFirst(current);
      current = current.getParent();
    }

    MPSTreeNode currentTreeNode = parent;
    for (final SNode anc : ancestors) {
      final MPSTreeNode finalCurrentTreeNode = currentTreeNode;
      if (!currentTreeNode.isInitialized() && !currentTreeNode.hasInfiniteSubtree()) currentTreeNode.init();

      currentTreeNode = findTreeNode(finalCurrentTreeNode,
          new Condition<MPSTreeNode>() {
            @Override
            public boolean met(MPSTreeNode object) {
              if (object == finalCurrentTreeNode) return true;
              if (!(object instanceof PackageNode)) return false;
              String pack = ((PackageNode) object).getFullPackage();
              String vp = node.getContainingRoot().getProperty(SNodeUtil.property_BaseConcept_virtualPackage);
              return vp != null && vp.startsWith(pack);
            }
          }, new Condition<MPSTreeNode>() {
            @Override
            public boolean met(MPSTreeNode tNode) {
              return (tNode instanceof SNodeTreeNode) && (((SNodeTreeNode) tNode).getSNode() == anc);
            }
          }
      );
      if (currentTreeNode == null) return null;
    }

    return (MPSTreeNodeEx) currentTreeNode;
  }

  @Nullable
  protected MPSTreeNode findTreeNode(MPSTreeNode start, Condition<MPSTreeNode> descendCondition, Condition<MPSTreeNode> resultCondition) {
    // breadth-first to find top-most module (e.g. not the one under 'runtime' dependencies)
    ArrayDeque<MPSTreeNode> queue = new ArrayDeque<MPSTreeNode>(128);
    queue.add(start);
    while (!queue.isEmpty()) {
      MPSTreeNode tn = queue.removeFirst();
      if (resultCondition.met(tn)) {
        return tn;
      }

      if (descendCondition.met(tn)) {
        if (!tn.isInitialized()) {
          tn.init();
        }
        for (MPSTreeNode node : tn) {
          queue.addLast(node);
        }
      }
    }

    return null;
  }

  //----find next queries----

  //todo: will work bad e.g. if operating with project data from modules pool
  public MPSTreeNode findNextTreeNode(SNode node) {
    MPSTreeNode foundNode = findMostSuitableSNodeTreeNode(node);
    if (foundNode == null) return null;
    DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) foundNode.getParent();
    TreeNode result = parentNode.getChildAfter(foundNode);
    if (result == null) result = parentNode.getChildBefore(foundNode);
    if (result == null) result = parentNode;
    return (MPSTreeNode) result;
  }

  //todo: will work bad e.g. if operating with project data from modules pool
  public MPSTreeNode findNextTreeNode(SModel modelDescriptor) {
    SModelTreeNode sModelNode = findMostSuitableModelTreeNode(modelDescriptor);
    if (sModelNode == null) return null;
    DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) sModelNode.getParent();
    TreeNode result = parentNode.getChildAfter(sModelNode);
    if (result == null) result = parentNode.getChildBefore(sModelNode);
    if (result == null) result = parentNode;
    return (MPSTreeNode) result;
  }

  //----node find conditions----

  private static class ModuleInProjectCondition extends ModuleEverywhereCondition {
    @Override
    public boolean met(MPSTreeNode object) {
      if (!super.met(object)) return false;
      return !(object instanceof ProjectModulesPoolTreeNode);
    }
  }

  private static class ModuleEverywhereCondition implements Condition<MPSTreeNode> {
    @Override
    public boolean met(MPSTreeNode node) {
      if (node instanceof ProjectModuleTreeNode && !(node instanceof ProjectLanguageTreeNode)) return false;
      if (node instanceof SModelTreeNode) return false;
/*
      todo: extract optimal module finding process. Used method only works when there is a single ability of selection
      //need to go into devkits
      if (node instanceof ProjectDevKitTreeNode) return true;
*/
      return true;
    }
  }

  private static class ModelInModuleCondition extends ModelEverywhereCondition {
    private ModelInModuleCondition(SModel model) {
      super(model);
    }

    @Override
    public boolean met(MPSTreeNode node) {
      if (!super.met(node)) return false;

      if (node instanceof SModelTreeNode) {
        SModelTreeNode modelNode = (SModelTreeNode) node;
        if (!modelNode.hasModelsUnder()) return false;

        String outerName = SNodeOperations.getModelLongName(modelNode.getModel());
        String innerName = SNodeOperations.getModelLongName(myModel);
        return innerName.startsWith(outerName + ".");
      }

      boolean descent = false;

      if (node instanceof ProjectModuleTreeNode) descent = true;
      if (node instanceof NamespaceTextNode) descent = true;
      if (node instanceof AccessoriesModelTreeNode) descent = true;
      if (node instanceof StubsTreeNode) descent = true;
      if (node instanceof AllModelsTreeNode) descent = true;
      if (node instanceof TestsTreeNode) descent = true;

      if (!descent) return false;

      if (!node.isInitialized() && !node.hasInfiniteSubtree()) {
        node.init();
        return true;
      }

      return node.isInitialized();
    }
  }

  private static class ModelEverywhereCondition implements Condition<MPSTreeNode> {
    protected SModel myModel;

    public ModelEverywhereCondition(SModel model) {
      myModel = model;
    }

    @Override
    public boolean met(MPSTreeNode node) {
      if (node instanceof SNodeTreeNode) return false;
      if (node instanceof SModelTreeNode) {
        SModelTreeNode modelNode = (SModelTreeNode) node;
        if (!modelNode.hasModelsUnder()) return false;

        String outerName = SNodeOperations.getModelLongName(modelNode.getModel());
        String innerName = jetbrains.mps.util.SNodeOperations.getModelLongName(myModel);
        return innerName.startsWith(outerName + ".");
      }
      if (!node.isInitialized() && !node.hasInfiniteSubtree()) {
        node.init();
        return true;
      }
      return node.isInitialized();
    }
  }

  private static class NodeForModuleCondition implements Condition<MPSTreeNode> {
    private final SModule myModule;

    public NodeForModuleCondition(SModule module) {
      myModule = module;
    }

    @Override
    public boolean met(MPSTreeNode treeNode) {
      if (!(treeNode instanceof ProjectModuleTreeNode)) return false;
      return ((ProjectModuleTreeNode) treeNode).getModule() == myModule;
    }
  }

  private static class NodeForModelCondition implements Condition<MPSTreeNode> {
    private final SModel myModel;

    public NodeForModelCondition(SModel model) {
      myModel = model;
    }

    @Override
    public boolean met(MPSTreeNode node) {
      if (!(node instanceof SModelTreeNode)) return false;
      SModelTreeNode modelNode = (SModelTreeNode) node;
      SModel modelDescriptor = modelNode.getModel();
      SModelReference modelReference = modelDescriptor.getReference();
      return modelReference.equals(myModel.getReference());
    }
  }

  //-----------getters----------

  protected Project getProject() {
    return getTree().getProject();
  }

  protected ProjectTree getTree() {
    return myProjectTree;
  }

  //-----------find module by model------------

  private static SModule getModuleForModel(Project project, SModel model) {
    //language's and solution's own models (+generator models in language)
    SModule owner = model.getModule();
    if (owner == null) return null;
    SModule mainModule = owner instanceof Generator ? ((Generator) owner).getSourceLanguage() : owner;
    if (project.isProjectModule(mainModule)) return owner;

    //accessories models in languages
    /*
      //with this enabled, alt-f1 does not work in case node is in non-owned accessory model to a project language
      for (Language l : project.getProjectLanguages()) {
        if (l.isAccessoryModel(model.getSModelReference())) return l;
      }
    */

    //runtime models in languages
    for (Language l : project.getProjectModules(Language.class)) {
      for (SModuleReference depModule : l.getRuntimeModulesReferences()) {
        if (depModule.equals(mainModule.getModuleReference())) return owner;
      }
    }

    //accessories models in devkits

    //runtime models in devkits

    return owner;
  }
}
