/*
 * 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.treeview;

import com.intellij.icons.AllIcons.General;
import com.intellij.ide.CommonActionsManager;
import com.intellij.ide.DefaultTreeExpander;
import com.intellij.ide.OccurenceNavigator;
import com.intellij.ide.OccurenceNavigatorSupport;
import com.intellij.ide.TreeExpander;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.actionSystem.ActionToolbar;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.actionSystem.ToggleAction;
import com.intellij.pom.Navigatable;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.usageView.UsageViewBundle;
import com.intellij.util.ui.tree.TreeUtil;
import jetbrains.mps.icons.MPSIcons.Actions;
import jetbrains.mps.ide.findusages.CantLoadSomethingException;
import jetbrains.mps.ide.findusages.CantSaveSomethingException;
import jetbrains.mps.ide.findusages.model.CategoryKind;
import jetbrains.mps.ide.findusages.model.SearchResults;
import jetbrains.mps.ide.findusages.view.icons.IconManager;
import jetbrains.mps.ide.findusages.view.icons.Icons;
import jetbrains.mps.ide.findusages.view.treeholder.tree.DataTree;
import jetbrains.mps.ide.findusages.view.treeholder.tree.DataTreeChangesNotifier;
import jetbrains.mps.ide.findusages.view.treeholder.tree.IChangeListener;
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.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.treeview.UsagesTree.UsagesTreeNode;
import jetbrains.mps.ide.findusages.view.treeholder.treeview.path.PathItemRole;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.ide.ui.tree.TreeHighlighterExtension;
import jetbrains.mps.project.Project;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jdom.Element;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNodeReference;

import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.border.EmptyBorder;
import javax.swing.tree.DefaultMutableTreeNode;
import java.awt.BorderLayout;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class UsagesTreeComponent extends JPanel implements IChangeListener {
  private static final Logger LOG = LogManager.getLogger(UsagesTreeComponent.class);

  private static final String CONTENTS = "contents";
  private static final String VIEW_OPTIONS = "view_options";
  private static final String NODE_REPRESENTATOR = "node_representator";
  private static final String CLASS_NAME = "class_name";

  private final Project myProject;
  private INodeRepresentator myNodeRepresentator = null;

  private UsagesTree myTree;
  private final DataTree myContents;
  private Set<PathItemRole> myPathProvider = new HashSet<>();

  private ViewToolbar myViewToolbar;
  private ActionsToolbar myActionsToolbar;

  private ViewOptions myViewOptions = new ViewOptions();
  private ViewOptions myDefaultOptions;

  private boolean mySearchedNodesButtonsVisible = true;
  private boolean myAdditionalInfoButtonVisible = true;
  private OccurenceNavigatorSupport myOccurrenceNavigator;

  public UsagesTreeComponent(ViewOptions defaultOptions, Project mpsProject, DataTreeChangesNotifier changeDispatch) {
    super(new BorderLayout());
    myProject = mpsProject;
    myContents = new DataTree(changeDispatch);

    myTree = new UsagesTree(mpsProject);
    myOccurrenceNavigator = new OccurenceNavigatorSupport(myTree) {
      @Override
      protected Navigatable createDescriptorForNode(DefaultMutableTreeNode node) {
        if (node.getChildCount() > 0) {
          return null;
        }
        if (!(node instanceof UsagesTreeNode)) {
          return null;
        }
        UsagesTreeNode treeNode = (UsagesTreeNode) node;

        if (treeNode.getUserObject() == null) {
          return null;
        }

        final BaseNodeData data = treeNode.getUserObject().getData();
        return toNavigatable(data);
      }


      @Override
      public String getNextOccurenceActionName() {
        return UsageViewBundle.message("action.next.occurrence");
      }

      @Override
      public String getPreviousOccurenceActionName() {
        return UsageViewBundle.message("action.previous.occurrence");
      }
    };

    TreeHighlighterExtension.attachHighlighters(myTree, ProjectHelper.toIdeaProject(mpsProject));
    myTree.setBorder(new EmptyBorder(3, 5, 3, 5));

    JScrollPane treePane = ScrollPaneFactory.createScrollPane(myTree);

    myPathProvider.add(PathItemRole.ROLE_MAIN_RESULTS);
    myPathProvider.add(PathItemRole.ROLE_TARGET_NODE);

    myViewToolbar = new ViewToolbar(myTree);
    myActionsToolbar = new ActionsToolbar();

    myDefaultOptions = defaultOptions;
    myViewOptions.setValues(myDefaultOptions);

    setComponentsViewOptions(myViewOptions);

    add(treePane, BorderLayout.CENTER);

    myContents.addChangeListener(this);
  }

  public void dispose() {
    myContents.removeChangeListeners(this);
    myContents.dispose();
    myTree.dispose();
  }

  public void setContents(final SearchResults contents) {
    // XXX no idea if there's real need to have read action here, just refactored ModelAccess static out of DataTree here.
    myProject.getModelAccess().runReadAction(() -> myContents.setContents(contents, myNodeRepresentator));
  }

  public OccurenceNavigator getOccurenceNavigator() {
    return myOccurrenceNavigator;
  }

  @Override
  public void changed() {
    myTree.setContents(myContents, myPathProvider);
  }

  public void addPathComponent(PathItemRole role) {
    if (!myPathProvider.contains(role)) {
      myPathProvider.add(role);
    }
    myTree.setResultPathProvider(myPathProvider);
  }

  public void removePathComponent(PathItemRole role) {
    myPathProvider.remove(role);
    myTree.setResultPathProvider(myPathProvider);
  }

  //MUST be called in construction time, introduced for "to do" functionality
  public void setCustomRepresentator(INodeRepresentator nodeRepresentator) {
    myNodeRepresentator = nodeRepresentator;
    myViewToolbar.recreateToolbar();
    myViewToolbar.setViewOptions(myViewOptions);
  }

  public void setComponentsViewOptions(ViewOptions options) {
    myViewToolbar.setViewOptions(options);
    myActionsToolbar.setViewOptions(options);
    myTree.setShowPopupMenu(options.myShowPopupMenu);
  }

  public void getComponentsViewOptions(ViewOptions options) {
    myViewToolbar.getViewOptions(options);
    myActionsToolbar.getViewOptions(options);
    options.myShowPopupMenu = myTree.isShowPopupMenu();
  }

  public void read(Element element, Project project) throws CantLoadSomethingException {
    myNodeRepresentator = null;
    Element nodeRepresentatorXML = element.getChild(NODE_REPRESENTATOR);
    if (nodeRepresentatorXML != null) {
      String className = nodeRepresentatorXML.getAttributeValue(CLASS_NAME);
      if (className != null) {
        try {
          Class nodeRepresentatorClass = Class.forName(className);
          myNodeRepresentator = (INodeRepresentator) nodeRepresentatorClass.newInstance();
          //noinspection ConstantConditions
          myNodeRepresentator.read(nodeRepresentatorXML, project);
        } catch (Throwable t) {
          LOG.error("Can't instantiate node representator " + className, t);
          throw new CantLoadSomethingException("Can't instantiate node representator " + className, t);
        }
      }
    }

    Element viewOptionsXML = element.getChild(VIEW_OPTIONS);
    myViewOptions.read(viewOptionsXML, project);
    setComponentsViewOptions(myViewOptions);

    Element contentsXML = element.getChild(CONTENTS);
    myContents.read(contentsXML, project);

    myTree.setContents(myContents, myPathProvider);
  }

  public void write(Element element, Project project) throws CantSaveSomethingException {
    Element nodeRepresentatorXML = new Element(NODE_REPRESENTATOR);
    if (myNodeRepresentator != null) {
      nodeRepresentatorXML.setAttribute(CLASS_NAME, myNodeRepresentator.getClass().getName());
      //noinspection ConstantConditions
      myNodeRepresentator.write(nodeRepresentatorXML, project);
    }
    element.addContent(nodeRepresentatorXML);

    Element viewOptionsXML = new Element(VIEW_OPTIONS);
    getComponentsViewOptions(myViewOptions);
    myViewOptions.write(viewOptionsXML, project);
    element.addContent(viewOptionsXML);

    Element contentsXML = new Element(CONTENTS);
    myContents.write(contentsXML, project);
    element.addContent(contentsXML);
  }

  public Set<SModel> getIncludedModels() {
    return myContents.getIncludedModels(myProject.getRepository());
  }

  public Set<SModel> getAllModels() {
    return myContents.getAllModels(myProject.getRepository());
  }

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

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

  public ActionGroup getActionsToolbar() {
    return myActionsToolbar.getActions();
  }

  public JComponent getViewToolbar() {
    return myViewToolbar;
  }

  public UsagesTree getTree() {
    return myTree;
  }

  private Navigatable toNavigatable(final BaseNodeData data) {
    if (!(data instanceof AbstractResultNodeData)) {
      return null;
    }
    return new Navigatable() {
      @Override
      public void navigate(boolean requestFocus) {
        boolean useProjectTree = !(data instanceof NodeNodeData);
        if(data instanceof NodeNodeData) {
          // Show nodes directly in editor instead of project pane
          useProjectTree = false;
        }
        if(data instanceof ModelNodeData || data instanceof ModuleNodeData) {
          // Leave focus in UsagesView or it became unusable
          requestFocus = false;
        }
        ((AbstractResultNodeData) data).navigate(myProject, useProjectTree, requestFocus);
      }

      @Override
      public boolean canNavigate() {
        return true;
      }

      @Override
      public boolean canNavigateToSource() {
        return true;
      }
    };
  }

  class ViewToolbar extends JPanel {
    private final JComponent myTargetComponent;
    private PathOptionsToolbar myPathOptionsToolbar;
    private ViewOptionsToolbar myViewOptionsToolbar;
    private JComponent myToolbar = null;

    public ViewToolbar(JComponent targetComponent) {
      myTargetComponent = targetComponent;
      myPathOptionsToolbar = new PathOptionsToolbar();
      myViewOptionsToolbar = new ViewOptionsToolbar();

      recreateToolbar();
    }

    private void recreateToolbar() {
      if (myToolbar != null) {
        remove(myToolbar);
      }

      DefaultActionGroup actionGroup = new DefaultActionGroup();
      myPathOptionsToolbar.recreateActions();
      actionGroup.addAll(myPathOptionsToolbar.getActions());
      actionGroup.addSeparator();
      actionGroup.addAll(myViewOptionsToolbar.getActions());
      ActionToolbar actionToolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, actionGroup, false);
      actionToolbar.setTargetComponent(myTargetComponent);
      myToolbar = actionToolbar.getComponent();
      add(myToolbar);
    }

    public void setViewOptions(ViewOptions options) {
      myPathOptionsToolbar.setViewOptions(options);
      myViewOptionsToolbar.setViewOptions(options);

      recreateToolbar();
    }

    public void getViewOptions(ViewOptions options) {
      myPathOptionsToolbar.getViewOptions(options);
      myViewOptionsToolbar.getViewOptions(options);
    }

    class ViewOptionsToolbar {
      private MyBaseToggleAction myAdditionalInfoNeededButton;
      private MyBaseToggleAction myShowSearchedNodesButton;
      private MyBaseToggleAction myGroupSearchedNodesButton;
      private DefaultActionGroup myActions;

      public ViewOptionsToolbar() {
        myAdditionalInfoNeededButton = new MyBaseToggleAction("Additional node info", "", General.Information) {
          @Override
          public boolean isSelected(AnActionEvent e) {
            return myTree.isAdditionalInfoNeeded();
          }

          @Override
          public void doSetSelected(AnActionEvent e, boolean state) {
            myTree.setAdditionalInfoNeeded(state);
          }
        };

        myShowSearchedNodesButton = new MyBaseToggleAction("Show searched nodes", "", Actions.SearchedNodes) {
          @Override
          public boolean isSelected(AnActionEvent e) {
            return myTree.isShowSearchedNodes();
          }

          @Override
          public void doSetSelected(AnActionEvent e, boolean state) {
            myTree.setShowSearchedNodes(state);
            if (!myTree.isShowSearchedNodes() && myGroupSearchedNodesButton.isSelected(null)) {
              myGroupSearchedNodesButton.doSetSelected(null, false);
            }
          }
        };

        myGroupSearchedNodesButton = new MyBaseToggleAction("Group searched nodes", "", Actions.GroupSearched) {
          @Override
          public boolean isSelected(AnActionEvent e) {
            return myTree.isGroupSearchedNodes();
          }

          @Override
          public void doSetSelected(AnActionEvent e, boolean state) {
            myTree.startAdjusting();
            myTree.setGroupSearchedNodes(state);
            if (state) {
              myTree.setShowSearchedNodes(true);
            }
            myTree.finishAdjusting();
          }
        };

        myActions = new DefaultActionGroup();
        myActions.addAction(myAdditionalInfoNeededButton);
        myActions.addAction(myShowSearchedNodesButton);
        myActions.addAction(myGroupSearchedNodesButton);
      }

      public ActionGroup getActions() {
        return myActions;
      }

      public void setViewOptions(ViewOptions options) {
        myTree.startAdjusting();

        myAdditionalInfoNeededButton.doSetSelected(null, options.myInfo);
        myShowSearchedNodesButton.doSetSelected(null, options.myShowSearchedNodes);
        myGroupSearchedNodesButton.doSetSelected(null, options.myGroupSearchedNodes);

        mySearchedNodesButtonsVisible = options.mySearchedNodesButtonsVisible;
        if (!mySearchedNodesButtonsVisible) {
          myActions.remove(myShowSearchedNodesButton);
          myActions.remove(myGroupSearchedNodesButton);
        }
        myAdditionalInfoButtonVisible = options.myAdditionalInfoButtonVisible;
        if (!myAdditionalInfoButtonVisible) {
          myActions.remove(myAdditionalInfoNeededButton);
        }
        myTree.finishAdjusting();
      }

      public void getViewOptions(ViewOptions options) {
        options.myCount = true;
        options.myInfo = myAdditionalInfoNeededButton.isSelected(null);
        options.myShowSearchedNodes = myShowSearchedNodesButton.isSelected(null);
        options.myGroupSearchedNodes = myGroupSearchedNodesButton.isSelected(null);

        options.mySearchedNodesButtonsVisible = mySearchedNodesButtonsVisible;
        options.myAdditionalInfoButtonVisible = myAdditionalInfoButtonVisible;
      }
    }

    class PathOptionsToolbar {
      private List<MyBaseToggleAction> myCategoryPathButtons = new ArrayList<>();
      private MyBaseToggleAction myModulePathButton;
      private MyBaseToggleAction myModelPathButton;
      private MyBaseToggleAction myRootPathButton;
      private MyBaseToggleAction myNamedConceptPathButton;
      private DefaultActionGroup myActions;

      public PathOptionsToolbar() {
        recreateActions();
      }

      private void recreateActions() {
        List<CategoryKind> categoryKinds = Collections.singletonList(
            new CategoryKind(CategoryKind.DEFAULT_CATEGORY_KIND.getName(), General.Filter, CategoryKind.DEFAULT_CATEGORY_KIND.getTooltip())
        );
        if (myNodeRepresentator != null) {
          categoryKinds = ((INodeRepresentator<?>) myNodeRepresentator).getCategoryKinds();
        }

        myCategoryPathButtons.clear();
        for (CategoryKind kind : categoryKinds) {
          myCategoryPathButtons.add(new MyBasePathToggleAction(
              PathItemRole.getCategoryRole(kind), kind.getTooltip(), IconManager.getIconForCategoryKind(kind)));
        }

        myModulePathButton = new MyBasePathToggleAction(PathItemRole.ROLE_MODULE, "Group by module", Icons.MODULE_ICON);
        myModelPathButton = new MyBasePathToggleAction(PathItemRole.ROLE_MODEL, "Group by model", Icons.MODEL_ICON);

        myRootPathButton = new MyBaseToggleAction("Group by root node", "", Icons.ROOT_ICON) {
          @Override
          public boolean isSelected(AnActionEvent e) {
            return myPathProvider.contains(PathItemRole.ROLE_ROOT);
          }

          @Override
          public void doSetSelected(AnActionEvent e, boolean state) {
            if (state) {
              addPathComponent(PathItemRole.ROLE_ROOT);
            } else {
              myTree.startAdjusting();
              if (myNamedConceptPathButton.isSelected(null)) {
                myNamedConceptPathButton.doSetSelected(null, false);
              }
              removePathComponent(PathItemRole.ROLE_ROOT);
              myTree.finishAdjusting();
            }
          }
        };

        myNamedConceptPathButton = new MyBaseToggleAction("Group by path", "", Icons.PATH_ICON) {
          @Override
          public boolean isSelected(AnActionEvent e) {
            return myPathProvider.contains(PathItemRole.ROLE_ROOT_TO_TARGET_NODE);
          }

          @Override
          public void doSetSelected(AnActionEvent e, boolean state) {
            if (state) {
              myTree.startAdjusting();
              if (!myRootPathButton.isSelected(null)) {
                myRootPathButton.doSetSelected(null, true);
              }
              addPathComponent(PathItemRole.ROLE_ROOT_TO_TARGET_NODE);
              myTree.finishAdjusting();
            } else {
              removePathComponent(PathItemRole.ROLE_ROOT_TO_TARGET_NODE);
            }
          }
        };

        myActions = new DefaultActionGroup();
        for (MyBaseToggleAction categoryPathButton : myCategoryPathButtons) {
          myActions.addAction(categoryPathButton);
        }
        myActions.addAction(myModulePathButton);
        myActions.addAction(myModelPathButton);
        myActions.addAction(myRootPathButton);
        myActions.addAction(myNamedConceptPathButton);
      }

      public void setViewOptions(ViewOptions options) {
        myTree.startAdjusting();

        int size = Math.min(myCategoryPathButtons.size(), options.myCategories.length);
        for (int i = 0; i < size; i++) {
          myCategoryPathButtons.get(i).doSetSelected(null, options.myCategories[i]);
        }

        myModulePathButton.doSetSelected(null, options.myModule);
        myModelPathButton.doSetSelected(null, options.myModel);
        myRootPathButton.doSetSelected(null, options.myRoot);
        myNamedConceptPathButton.doSetSelected(null, options.myNamedPath);

        myTree.finishAdjusting();
      }

      public void getViewOptions(ViewOptions options) {
        options.myCategories = new boolean[myCategoryPathButtons.size()];
        for (int i = 0; i < myCategoryPathButtons.size(); i++) {
          options.myCategories[i] = myCategoryPathButtons.get(i).isSelected(null);
        }

        options.myModule = myModulePathButton.isSelected(null);
        options.myModel = myModelPathButton.isSelected(null);
        options.myRoot = myRootPathButton.isSelected(null);
        options.myNamedPath = myNamedConceptPathButton.isSelected(null);
      }

      public ActionGroup getActions() {
        return myActions;
      }
    }

    class MyBasePathToggleAction extends MyBaseToggleAction {
      private PathItemRole myPathItemRole = null;

      public MyBasePathToggleAction(PathItemRole itemRole, String name, Icon icon) {
        super(name, "", icon);
        myPathItemRole = itemRole;
      }

      @Override
      public boolean isSelected(AnActionEvent e) {
        return myPathProvider.contains(myPathItemRole);
      }

      @Override
      public void doSetSelected(AnActionEvent e, boolean state) {
        if (myPathItemRole == null) {
          return;
        }
        if (state) {
          addPathComponent(myPathItemRole);
        } else {
          removePathComponent(myPathItemRole);
        }
      }
    }
  }

  class ActionsToolbar {
    private DefaultActionGroup myActions;
    private MyBaseToggleAction myAutoscrollButton;

    public ActionsToolbar() {
      myActions = new DefaultActionGroup();

      final CommonActionsManager actionsManager = CommonActionsManager.getInstance();
      final TreeExpander treeExpander = new DefaultTreeExpander(myTree) {
        @Override
        public void collapseAll() {
          super.collapseAll();
          TreeUtil.expand(myTree, 2);
        }
      };
      myActions.add(actionsManager.createExpandAllAction(treeExpander, myTree));
      myActions.add(actionsManager.createCollapseAllAction(treeExpander, myTree));
      myActions.add(actionsManager.createPrevOccurenceAction(getOccurenceNavigator()));
      myActions.add(actionsManager.createNextOccurenceAction(getOccurenceNavigator()));
      myAutoscrollButton = new MyBaseToggleAction("Autoscroll to source", "", Icons.AUTOSCROLL_ICON) {
        @Override
        public boolean isSelected(AnActionEvent e) {
          return myTree.isAutoscroll();
        }

        @Override
        public void doSetSelected(AnActionEvent e, boolean state) {
          myTree.setAutoscroll(state);
        }
      };
      myActions.addAction(myAutoscrollButton);
    }

    public void setViewOptions(ViewOptions options) {
      myAutoscrollButton.doSetSelected(null, options.myAutoscrolls);
    }

    public void getViewOptions(ViewOptions options) {
      options.myAutoscrolls = myAutoscrollButton.isSelected(null);
    }

    public ActionGroup getActions() {
      return myActions;
    }
  }

  private abstract class MyBaseToggleAction extends ToggleAction {
    protected MyBaseToggleAction(String text, String description, Icon icon) {
      super(text, description, icon);
    }

    @Override
    public final void setSelected(AnActionEvent e, boolean state) {
      doSetSelected(e, state);
      getComponentsViewOptions(myViewOptions);
      myDefaultOptions.setValues(myViewOptions);
    }

    public abstract void doSetSelected(AnActionEvent e, boolean state);
  }
}
