/*
 * 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.findusages.view.treeholder.tree;

import jetbrains.mps.icons.MPSIcons;
import jetbrains.mps.ide.findusages.CantLoadSomethingException;
import jetbrains.mps.ide.findusages.CantSaveSomethingException;
import jetbrains.mps.ide.findusages.IExternalizeable;
import jetbrains.mps.ide.findusages.model.CategoryKind;
import jetbrains.mps.ide.findusages.model.SearchResult;
import jetbrains.mps.ide.findusages.model.SearchResults;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.AbstractResultNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.BaseNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.CategoryNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.MainNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.ModelNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.ModuleNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.NodeNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.ResultsNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.SearchedNodesNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.treeview.INodeRepresentator;
import jetbrains.mps.ide.findusages.view.treeholder.treeview.path.PathItem;
import jetbrains.mps.ide.findusages.view.treeholder.treeview.path.PathItemRole;
import jetbrains.mps.ide.findusages.view.treeholder.treeview.path.PathProvider;
import jetbrains.mps.ide.icons.IdeIcons;
import jetbrains.mps.openapi.navigation.ProjectPaneNavigator;
import jetbrains.mps.project.Project;
import jetbrains.mps.util.Pair;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SLanguage;
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.model.SNodeReference;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.module.SRepository;

import javax.swing.Icon;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class DataTree implements IExternalizeable, IChangeListener {
  private DataNode myTreeRoot = createTreeRoot();
  private final List<IChangeListener> myListeners = new ArrayList<IChangeListener>(2);
  private final DataTreeChangesNotifier myChangesNotifier;

  //this is only used in 3.2 to make rebuild faster in case of many nodes.
  //in 3.3 it will be fixed by introducing path providers
  //this cache is only alive during read action in build() method
  private Map<Pair<DataNode, Object>, DataNode> myRebuildCache;

  public DataTree(@NotNull DataTreeChangesNotifier changeDispatch) {
    myChangesNotifier = changeDispatch;
  }

  public DataNode getTreeRoot() {
    return myTreeRoot;
  }

  //----EXCLUSION/EXPANSION----

  public void setExcluded(Set<DataNode> nodes, boolean value) {
    for (DataNode node : nodes) {
      setExcludedRecursively(nodes, node, value);
    }
    checkExcluded();
    notifyChangeListeners();
  }

  //doNotProcess is needed as there might be many pairs of nodes in the list, one of which is a child of another
  private void setExcludedRecursively(Set<DataNode> doNotProcess, DataNode node, boolean value) {
    node.getData().setExcluded(value);
    for (DataNode child : node.getChildren()) {
      if (doNotProcess.contains(child)) continue;
      setExcludedRecursively(doNotProcess, child, value);
    }
  }

  private void checkExcluded() {
    checkNodeExcluded(myTreeRoot);
  }

  private void checkNodeExcluded(DataNode node) {
    if (node.getChildren().size() == 0) {
      return;
    }

    for (DataNode child : node.getChildren()) {
      checkNodeExcluded(child);
    }
    boolean allChildrenExcluded = true;
    for (DataNode child : node.getChildren()) {
      allChildrenExcluded = allChildrenExcluded && child.getData().isExcluded();
    }
    node.getData().setExcluded(allChildrenExcluded);
  }

  //----DATA QUERY----

  public Set<SModel> getIncludedModels(SRepository repository) {
    return getResultsNode().getIncludedModels(repository);
  }

  public Set<SModel> getAllModels(SRepository repository) {
    return getResultsNode().getAllModels(repository);
  }

  public List<SNodeReference> getIncludedResultNodes() {
    return getResultsNode().getIncludedResultNodes();
  }

  public List<SNodeReference> getAllResultNodes() {
    return getResultsNode().getAllResultNodes();
  }

  private DataNode getResultsNode() {
    return myTreeRoot.getChildren().get(1);
  }

  //----CONTENT MANAGEMENT----

  public void setContents(SearchResults results, INodeRepresentator nodeRepresentator) {
    setContents(build(results, nodeRepresentator));
  }

  protected void setContents(DataNode root) {
    myTreeRoot = root;
    stopListening();
    startListening();
    notifyChangeListeners();
  }

  private void startListening() {
    HashSet<SNodeReference> nodes = new HashSet<SNodeReference>();
    HashSet<SModelReference> models = new HashSet<SModelReference>();
    HashSet<SModuleReference> modules = new HashSet<SModuleReference>();

    nodes.addAll(Arrays.asList(myTreeRoot.getNodeDataStream().filter(nd -> nd instanceof NodeNodeData).map(
        nd -> ((NodeNodeData) nd).getNodePointer()).toArray(SNodeReference[]::new)));
    models.addAll(Arrays.asList(myTreeRoot.getNodeDataStream().filter(nd -> nd instanceof ModelNodeData).map(
        nd -> ((ModelNodeData) nd).getModelReference()).toArray(SModelReference[]::new)));

    modules.addAll(Arrays.asList(myTreeRoot.getNodeDataStream().filter(nd -> nd instanceof ModuleNodeData).map(
        nd -> ((ModuleNodeData) nd).getModuleReference()).toArray(SModuleReference[]::new)));

    myChangesNotifier.trackNodes(this, nodes);
    myChangesNotifier.trackModels(this, models);
    myChangesNotifier.trackModules(this, modules);
  }

  private void stopListening() {
    myChangesNotifier.unregister(this);
  }

  public void dispose() {
    stopListening();
  }


  //----TREE BUILD STUFF----

  private static DataNode createTreeRoot() {
    return new DataNode(new MainNodeData(PathItemRole.ROLE_MAIN_ROOT));
  }

  private DataNode build(final SearchResults<?> results, final INodeRepresentator nodeRepresentator) {
      myRebuildCache = new HashMap<Pair<DataNode, Object>, DataNode>();

      DataNode root = createTreeRoot();

      DataNode nodesRoot = new DataNode(new SearchedNodesNodeData(PathItemRole.ROLE_MAIN_SEARCHED_NODES));
      for (Object node : results.getAliveNodes()) {
        addSearchedNode(nodesRoot, node);
      }
      root.add(nodesRoot);

      DataNode resultsRoot = new DataNode(new ResultsNodeData(PathItemRole.ROLE_MAIN_RESULTS, nodeRepresentator));
      for (SearchResult<?> result : results.getAliveResults()) {
        addResultWithPresentation(resultsRoot, result, nodeRepresentator);
      }
      root.add(resultsRoot);

      myRebuildCache = null;
      return root;
  }

  private void addSearchedNode(DataNode root, Object node) {
    List<PathItem> path = PathProvider.getPathForSearchResult(new SearchResult<Object>(node, SearchedNodesNodeData.CATEGORY_NAME));
    createPath(path, root, null, false, null);
  }

  private void addResultWithPresentation(DataNode root, SearchResult result, INodeRepresentator nodeRepresentator) {
    List<PathItem> path = PathProvider.getPathForSearchResult(result);
    createPath(path, root, nodeRepresentator, true, result);
  }

  private void createPath(List<PathItem> path, DataNode parent, @Nullable INodeRepresentator<Object> nodeRepresentator,
      boolean results, @Nullable SearchResult result) {

    assert !path.isEmpty();
    final PathItem pathTail = path.get(path.size() - 1);

    final String tailCustomCaption;
    if (result != null && nodeRepresentator != null) {
      tailCustomCaption = nodeRepresentator.getPresentation(result.getObject());
    } else {
      tailCustomCaption = null;
    }


    for (PathItem currentPathItem : path) {
      Object currentIdObject = currentPathItem.getIdObject();
      final boolean isPathTail = currentPathItem == pathTail;

      DataNode next = myRebuildCache.get(new Pair<DataNode, Object>(parent, currentIdObject));

      if (next == null) {
        PathItemRole creator = currentPathItem.getRole();
        BaseNodeData data = null;

        final String caption = isPathTail ? tailCustomCaption : null;

        if (currentIdObject instanceof SModule) {
          data = new ModuleNodeData(creator, caption, ((SModule) currentIdObject).getModuleReference(), isPathTail, results);
        } else if (currentIdObject instanceof SModuleReference) {
          data = new ModuleNodeData(creator, caption, (SModuleReference) currentIdObject, isPathTail, results);
        } else if (currentIdObject instanceof SModelReference) {
          data = new ModelNodeData(creator, caption, (SModelReference) currentIdObject, isPathTail, results);
        } else if (currentIdObject instanceof SNode) {
          data = new NodeNodeData(creator, caption, (SNode) currentIdObject, isPathTail, results);
        } else if (currentIdObject instanceof SLanguage) {
          final SLanguage l = (SLanguage) currentIdObject;
          data = new AbstractResultNodeData(creator, caption == null ? l.getQualifiedName() : caption, "", false, isPathTail, results) {
            @Override
            protected String createIdObject() {
              return l.toString();
            }

            @Override
            public Icon getIcon() {
              return MPSIcons.LanguageRuntime;
            }

            @Override
            public void navigate(Project mpsProject, boolean useProjectTree, boolean focus) {
              new ProjectPaneNavigator(mpsProject).shallFocus(focus).select(l);
            }
          };
        } else if (currentIdObject instanceof Pair) {
          Pair<CategoryKind, String> category = (Pair<CategoryKind, String>) currentIdObject;
          data = new CategoryNodeData(creator, category.o1.getName(), category.o2, results, nodeRepresentator);
        }

        assert data != null : currentIdObject;

        next = new DataNode(data);
        parent.add(next);
        myRebuildCache.put(new Pair<DataNode, Object>(parent, data.getIdObject()), next);
      } else {
        if (isPathTail) {
          next.getData().setIsPathTail_internal(true);
        }
      }
      parent = next;
    }
  }

  //----READ/WRITE STUFF----

  @Override
  public void read(Element element, Project project) throws CantLoadSomethingException {
    myTreeRoot.read(element, project);
  }

  @Override
  public void write(Element element, Project project) throws CantSaveSomethingException {
    myTreeRoot.write(element, project);
  }

  //----LISTENERS STUFF----

  public void addChangeListener(IChangeListener listener) {
    myListeners.add(listener);
  }

  public void removeChangeListeners(IChangeListener listener) {
    myListeners.remove(listener);
  }

  public void notifyChangeListeners() {
    for (IChangeListener listener : myListeners) {
      listener.changed();
    }
  }

  @Override
  public void changed() {
    notifyChangeListeners();
  }
}
