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

import com.intellij.ide.CopyProvider;
import com.intellij.ide.CutProvider;
import com.intellij.ide.PasteProvider;
import com.intellij.ide.projectView.ProjectView;
import com.intellij.ide.projectView.impl.AbstractProjectViewPane;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.actionSystem.ToggleAction;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.vfs.VirtualFileManagerListener;
import jetbrains.mps.classloading.DeployListener;
import jetbrains.mps.ide.MPSCoreComponents;
import jetbrains.mps.ide.actions.CopyNode_Action;
import jetbrains.mps.ide.actions.CutNode_Action;
import jetbrains.mps.ide.actions.PasteNode_Action;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.ide.projectPane.fileSystem.nodes.ProjectTreeNode;
import jetbrains.mps.ide.ui.tree.MPSTreeNode;
import jetbrains.mps.ide.ui.tree.MPSTreeNodeEx;
import jetbrains.mps.ide.ui.tree.module.GeneratorTreeNode;
import jetbrains.mps.ide.ui.tree.module.NamespaceTextNode;
import jetbrains.mps.ide.ui.tree.module.ProjectModuleTreeNode;
import jetbrains.mps.ide.ui.tree.module.TransientModelsTreeNode;
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.ide.vfs.VirtualFileUtils;
import jetbrains.mps.make.IMakeNotificationListener;
import jetbrains.mps.make.IMakeNotificationListener.Stub;
import jetbrains.mps.make.IMakeService;
import jetbrains.mps.make.MakeNotification;
import jetbrains.mps.module.ReloadableModule;
import jetbrains.mps.project.AbstractModule;
import jetbrains.mps.project.DevKit;
import jetbrains.mps.project.ProjectOperationContext;
import jetbrains.mps.project.Solution;
import jetbrains.mps.smodel.IOperationContext;
import jetbrains.mps.smodel.Language;
import jetbrains.mps.smodel.RepoListenerRegistrar;
import jetbrains.mps.util.Pair;
import jetbrains.mps.vfs.IFile;
import jetbrains.mps.workbench.ActionPlace;
import jetbrains.mps.workbench.FileSystemModelHelper;
import jetbrains.mps.workbench.MPSDataKeys;
import jetbrains.mps.workbench.action.ActionUtils;
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.SRepository;
import org.jetbrains.mps.openapi.module.SRepositoryContentAdapter;
import org.jetbrains.mps.openapi.repository.CommandListener;
import org.jetbrains.mps.openapi.util.ProgressMonitor;

import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

public abstract class BaseLogicalViewProjectPane extends AbstractProjectViewPane {
  private final ProjectView myProjectView;
  private VirtualFileManagerListener myRefreshListener = new RefreshListener();
  private final MyRepositoryListener myRepositoryListener = new MyRepositoryListener();
  protected boolean myDisposed;

  private DeployListener myClassesListener = new DeployListener() {
    @Override
    public void onUnloaded(Set<ReloadableModule> unloadedModules, @NotNull ProgressMonitor monitor) {
    }

    @Override
    public void onLoaded(Set<ReloadableModule> loadedModules, @NotNull ProgressMonitor monitor) {
      rebuild();
    }
  };

  private IMakeNotificationListener myMakeNotificationListener = new Stub() {
    @Override
    public void sessionClosed(MakeNotification notification) {
      // rebuild tree in case of 'cancel' too (need to get 'transient models' node rebuilt)
      rebuild();
    }
  };

  protected BaseLogicalViewProjectPane(Project project, ProjectView projectView) {
    super(project);
    myProjectView = projectView;
  }

  public Project getProject() {
    return myProject;
  }

  public ProjectView getProjectView() {
    return myProjectView;
  }

  public abstract void rebuild();

  public abstract void selectNextModel(SModel md);

  @Override
  public Object getData(String dataId) {
    //MPSDK
    if (dataId.equals(MPSDataKeys.NODE.getName())) {
      return getSelectedSNode();
    }
    if (dataId.equals(MPSDataKeys.NODES.getName())) {
      return getSelectedSNodes();
    }

    if (dataId.equals(MPSDataKeys.MODEL.getName())) {
      return getSelectedModel();
    }
    if (dataId.equals(MPSDataKeys.CONTEXT_MODEL.getName())) {
      return getContextModel();
    }
    if (dataId.equals(MPSDataKeys.MODELS.getName())) {
      return getSelectedModels();
    }

    if (dataId.equals(MPSDataKeys.MODULE.getName())) {
      return getSelectedModule();
    }
    if (dataId.equals(MPSDataKeys.CONTEXT_MODULE.getName())) {
      return getContextModule();
    }
    if (dataId.equals(MPSDataKeys.MODULES.getName())) {
      return getSelectedModules();
    }

    if (dataId.equals(MPSDataKeys.VIRTUAL_PACKAGES.getName())) {
      return getSelectedPackages();
    }

    if (dataId.equals(MPSDataKeys.NAMESPACE.getName())) {
      NamespaceTextNode selectedNamespaceNode = getSelectedTreeNode(NamespaceTextNode.class);
      if (selectedNamespaceNode != null) {
        return selectedNamespaceNode.getNamespace();
      }
      return null;
    }

    if (dataId.equals(MPSDataKeys.OPERATION_CONTEXT.getName())) {
      return getContextForSelection();
    }
    if (dataId.equals(MPSDataKeys.TREE_NODE.getName())) {
      return getSelectedTreeNode(TreeNode.class);
    }
    if (dataId.equals(MPSDataKeys.TREE_NODES.getName())) {
      return getSelectedTreeNodes(TreeNode.class);
    }
    if (dataId.equals(MPSDataKeys.TREE_SELECTION_SIZE.getName())) {
      return getSelectionSize();
    }
    if (dataId.equals(MPSDataKeys.PLACE.getName())) {
      return getPlace();
    }

    //PDK
    if (dataId.equals(PlatformDataKeys.COPY_PROVIDER.getName())) {
      return new MyCopyProvider();
    }
    if (dataId.equals(PlatformDataKeys.PASTE_PROVIDER.getName())) {
      return new MyPasteProvider();
    }
    if (dataId.equals(PlatformDataKeys.CUT_PROVIDER.getName())) {
      return new MyCutProvider();
    }
    if (dataId.equals(PlatformDataKeys.VIRTUAL_FILE_ARRAY.getName())) {
      return getSelectedFiles();
    }

    //not found
    return null;
  }

  public boolean isDisposed() {
    return myDisposed;
  }

  @Override
  public void dispose() {
    if (isComponentCreated()) {
      removeListeners();
    }
    myDisposed = true;
    super.dispose();
  }

  public boolean showNodeStructure() {
    return !isDisposed() && getProjectView().isShowMembers(getId());
  }

  public boolean isSortByType() {
    return getProjectView().isSortByType(getId());
  }

  @Override
  public void installComparator() {
    // Overrode to avoid NPE
  }

  @Nullable
  public Comparator<Object> getTreeChildrenComparator() {
    return new Comparator<Object>() {
      @Override
      public int compare(final Object o1, final Object o2) {
        if (!(o1 instanceof SNode) || !(o2 instanceof SNode)) {
          return 0;
        }

        final SNode node1 = (SNode) o1;
        final SNode node2 = (SNode) o2;

        if (!isSortByType()) {
          return comparePresentations(node1, node2);
        }

        // (1) node.getConcept() doesn't require model read, nor concept.getQualifiedName
        // (2) If we got SNode, we're are already in model read
        int result = compareConceptFqNames(node1, node2);

        if (result != 0) {
          return result;
        }

        return comparePresentations(node1, node2);
      }

      private int compareConceptFqNames(SNode node1, SNode node2) {
        String concept1 = node1.getConcept().getQualifiedName();
        String concept2 = node2.getConcept().getQualifiedName();

        return concept1.compareTo(concept2);
      }

      private int comparePresentations(SNode node1, SNode node2) {
        String presentation1 = node1.getPresentation();
        String presentation2 = node2.getPresentation();

        if (presentation1 == null) {
          return presentation2 == null ? 0 : 1;
        }
        if (presentation2 == null) {
          return -1;
        }
        return presentation1.compareTo(presentation2);
      }
    };
  }

  @Override
  public void addToolbarActions(final DefaultActionGroup group) {
    group.addAction(new SortByTypeToggleAction()).setAsSecondary(true);
  }

  protected void removeListeners() {
    ApplicationManager.getApplication().getComponent(MPSCoreComponents.class).getClassLoaderManager().removeListener(myClassesListener);
    jetbrains.mps.project.Project mpsProject = ProjectHelper.fromIdeaProject(getProject());
    mpsProject.getModelAccess().removeCommandListener(myRepositoryListener);
    new RepoListenerRegistrar(mpsProject.getRepository(), myRepositoryListener).detach();
    if (IMakeService.INSTANCE.hasMakeService()) {
      IMakeService.INSTANCE.get().removeListener(myMakeNotificationListener);
    }
    VirtualFileManager.getInstance().removeVirtualFileManagerListener(myRefreshListener);
  }

  protected void addListeners() {
    VirtualFileManager.getInstance().addVirtualFileManagerListener(myRefreshListener);
    jetbrains.mps.project.Project mpsProject = ProjectHelper.fromIdeaProject(getProject());
    new RepoListenerRegistrar(mpsProject.getRepository(), myRepositoryListener).attach();
    mpsProject.getModelAccess().addCommandListener(myRepositoryListener);
    if (IMakeService.INSTANCE.hasMakeService()) {
      IMakeService.INSTANCE.get().addListener(myMakeNotificationListener);
    }
    ApplicationManager.getApplication().getComponent(MPSCoreComponents.class).getClassLoaderManager().addListener(myClassesListener);
  }

  public SNode getSelectedSNode() {
    List<SNode> result = getSelectedSNodes();
    if (result.size() != 1) {
      return null;
    }
    return result.get(0);
  }

  public List<SNode> getSelectedSNodes() {
    List<SNode> result = new ArrayList<>();
    for (MPSTreeNodeEx node : getSelectedTreeNodes(MPSTreeNodeEx.class)) {
      SNode snode = node.getSNode();
      if (snode == null) {
        continue;
      }
      result.add(snode);
    }
    return result;
  }

  public List<SModel> getSelectedModels() {
    List<SModel> result = new ArrayList<>();
    for (SModelTreeNode node : getSelectedTreeNodes(SModelTreeNode.class)) {
      result.add(node.getModel());
    }
    return result;
  }

  public SModel getSelectedModel() {
    SModelTreeNode selectedTreeNode = getSelectedTreeNode(SModelTreeNode.class);
    if (selectedTreeNode == null) {
      return null;
    }
    return selectedTreeNode.getModel();
  }

  public SModel getContextModel() {
    MPSTreeNode treeNode = (MPSTreeNode) getSelectedTreeNode(TreeNode.class);
    while (treeNode != null && !(treeNode instanceof SModelTreeNode)) {
      treeNode = (MPSTreeNode) treeNode.getParent();
    }
    if (treeNode == null) {
      return null;
    }
    return ((SModelTreeNode) treeNode).getModel();
  }

  public SModule getSelectedModule() {
    ProjectModuleTreeNode selectedTreeNode = getSelectedTreeNode(ProjectModuleTreeNode.class);
    if (selectedTreeNode == null) {
      return null;
    }
    return selectedTreeNode.getModule();
  }

  public SModule getContextModule() {
    MPSTreeNode treeNode = (MPSTreeNode) getSelectedTreeNode(TreeNode.class);
    while (treeNode != null && !(treeNode instanceof ProjectModuleTreeNode)) {
      treeNode = (MPSTreeNode) treeNode.getParent();
    }
    if (treeNode == null) {
      return null;
    }
    return ((ProjectModuleTreeNode) treeNode).getModule();
  }

  public List<SModule> getSelectedModules() {
    List<SModule> result = new ArrayList<>();
    for (ProjectModuleTreeNode node : getSelectedTreeNodes(ProjectModuleTreeNode.class)) {
      result.add(node.getModule());
    }
    return result;
  }

  public List<Pair<SModel, String>> getSelectedPackages() {
    List<Pair<SModel, String>> result = new ArrayList<>();
    TreePath[] paths = getTree().getSelectionPaths();
    SRepository projectRepo = ProjectHelper.getProjectRepository(getProject());
    if (paths == null || projectRepo == null) {
      return result;
    }
    for (TreePath path : paths) {
      MPSTreeNode node = (MPSTreeNode) path.getLastPathComponent();
      if (node instanceof PackageNode) {
        PackageNode pn = (PackageNode) node;
        result.add(new Pair<>(pn.getModelReference().resolve(projectRepo), pn.getFullPackage()));
      }
    }
    return result;
  }

  public int getSelectionSize() {
    TreePath[] selection = getSelectionPaths();
    return selection == null ? 0 : selection.length;
  }

  private IOperationContext getContextForSelection() {
    TreePath[] selection = getTree().getSelectionPaths();
    if (selection == null) {
      return null;
    }
    if (selection.length > 0) {
      return new ProjectOperationContext(ProjectHelper.fromIdeaProject(myProject));
    }
    return null;
  }

  private <T extends TreeNode> T getSelectedTreeNode(Class<T> nodeClass) {
    TreePath selectionPath = getTree().getSelectionPath();
    if (selectionPath == null) {
      return null;
    }
    Object selectedNode = selectionPath.getLastPathComponent();
    if (!(nodeClass.isInstance(selectedNode))) {
      return null;
    }
    return (T) selectedNode;
  }

  private ActionPlace getPlace() {
    TreeNode treeNode = getSelectedTreeNode(TreeNode.class);
    if (treeNode instanceof SNodeTreeNode) {
      return ActionPlace.PROJECT_PANE_SNODE;
    } else if (treeNode instanceof SModelTreeNode) {
      return ActionPlace.PROJECT_PANE_SMODEL;
    } else if ((treeNode instanceof ProjectTreeNode) || (treeNode instanceof jetbrains.mps.ide.ui.tree.module.ProjectTreeNode)) {
      return ActionPlace.PROJECT_PANE_PROJECT;
    } else if (treeNode instanceof GeneratorTreeNode) {
      return ActionPlace.PROJECT_PANE_GENERATOR;
    } else if (treeNode instanceof TransientModelsTreeNode) {
      return ActionPlace.PROJECT_PANE_TRANSIENT_MODULES;
    } else if (treeNode instanceof PackageNode) {
      return ActionPlace.PROJECT_PANE_PACKAGE;
    } else if (treeNode instanceof NamespaceTextNode) {
      return ActionPlace.PROJECT_PANE_NAMESPACE;
    } else if (treeNode instanceof ProjectModuleTreeNode) {
      SModule module = getSelectedModule();
      if (module instanceof Language) {
        return ActionPlace.PROJECT_PANE_LANGUAGE;
      } else if (module instanceof DevKit) {
        return ActionPlace.PROJECT_PANE_DEVKIT;
      } else if (module instanceof Solution) {
        return ActionPlace.PROJECT_PANE_SOLUTION;
      }
    }
    return ActionPlace.PROJECT_PANE;
  }

  public <T extends TreeNode> List<T> getSelectedTreeNodes(Class<T> nodeClass) {
    TreePath[] selectionPaths = getTree().getSelectionPaths();
    if (selectionPaths == null) {
      return new ArrayList<>();
    }

    List<T> selectedTreeNodes = new ArrayList<>(selectionPaths.length);

    for (TreePath selectionPath : selectionPaths) {
      if (selectionPath == null) {
        continue;
      }
      Object selectedNode = selectionPath.getLastPathComponent();
      if (!(nodeClass.isInstance(selectedNode))) {
        continue;
      }
      selectedTreeNodes.add((T) selectedNode);
    }
    return selectedTreeNodes;
  }

  private VirtualFile[] getSelectedFiles() {
    List<VirtualFile> selectedFilesList = new LinkedList<>();

    // add selected model files
    List<SModel> descriptors = getSelectedModels();
    if (descriptors != null) {
      for (SModel descriptor : descriptors) {
        selectedFilesList.addAll(new FileSystemModelHelper(descriptor).getVirtualFiles());
      }
    }

    // add selected modules files
    List<SModule> modules = getSelectedModules();
    if (modules != null) {
      for (SModule m : modules) {
        if (!(m instanceof AbstractModule)) {
          continue;
        }
        AbstractModule module = (AbstractModule) m;
        IFile home = module.getModuleSourceDir();
        if (home != null) {
          VirtualFile vfile = VirtualFileUtils.getProjectVirtualFile(home);
          if (vfile != null) {
            selectedFilesList.add(vfile);
          }
        }
        IFile ifile = module.getDescriptorFile();
        if (ifile != null) {
          VirtualFile vfile = VirtualFileUtils.getProjectVirtualFile(ifile);
          if (vfile != null) {
            selectedFilesList.add(vfile);
          }
        }
      }
    }

    if (selectedFilesList.isEmpty()) {
      return null;
    }

    return selectedFilesList.toArray(new VirtualFile[selectedFilesList.size()]);
  }

  /*package*/
  static AnActionEvent createEvent(DataContext context) {
    return ActionUtils.createEvent(ActionPlaces.PROJECT_VIEW_POPUP, context);
  }

  protected abstract boolean isComponentCreated();

  private static class MyCopyProvider implements CopyProvider {
    private CopyNode_Action myAction = new CopyNode_Action();

    @Override
    public void performCopy(@NotNull DataContext dataContext) {
      AnActionEvent event = createEvent(dataContext);
      ActionUtils.updateAndPerformAction(myAction, event);
    }

    @Override
    public boolean isCopyEnabled(@NotNull DataContext dataContext) {
      AnActionEvent event = createEvent(dataContext);
      myAction.update(event);
      return event.getPresentation().isEnabled();
    }

    @Override
    public boolean isCopyVisible(@NotNull DataContext dataContext) {
      return true;
    }
  }

  private static class MyPasteProvider implements PasteProvider {
    private PasteNode_Action myAction = new PasteNode_Action();

    @Override
    public void performPaste(@NotNull DataContext dataContext) {
      AnActionEvent event = createEvent(dataContext);
      ActionUtils.updateAndPerformAction(myAction, event);
    }

    @Override
    public boolean isPastePossible(@NotNull DataContext dataContext) {
      return true;
    }

    @Override
    public boolean isPasteEnabled(@NotNull DataContext dataContext) {
      AnActionEvent event = createEvent(dataContext);
      myAction.update(event);
      return event.getPresentation().isEnabled();
    }
  }

  private static class MyCutProvider implements CutProvider {
    private CutNode_Action myAction = new CutNode_Action();

    @Override
    public void performCut(@NotNull DataContext dataContext) {
      AnActionEvent event = createEvent(dataContext);
      ActionUtils.updateAndPerformAction(myAction, event);
    }

    @Override
    public boolean isCutEnabled(@NotNull DataContext dataContext) {
      AnActionEvent event = createEvent(dataContext);
      myAction.update(event);
      return event.getPresentation().isEnabled();
    }

    @Override
    public boolean isCutVisible(@NotNull DataContext dataContext) {
      return true;
    }
  }

  private class MyRepositoryListener extends SRepositoryContentAdapter implements CommandListener {
    private boolean myNeedRebuild = false;

    /*package*/ void rebuildTreeIfNeeded() {
      if (myNeedRebuild) {
        rebuild();
        myNeedRebuild = false;
      }
    }

    @Override
    public void repositoryChanged() {
      myNeedRebuild = true;
    }

    @Override
    public void modelRenamed(SModule module, SModel model, SModelReference oldRef) {
      myNeedRebuild = true;
    }

    @Override
    protected void startListening(SModel model) {
      if (!model.isReadOnly()) {
        model.addModelListener(this);
      }
    }

    @Override
    protected void stopListening(SModel model) {
      model.removeModelListener(this);
    }

    @Override
    public void modelReplaced(SModel model) {
      myNeedRebuild = true;
    }

    @Override
    public void commandStarted() {
      myNeedRebuild = false;
    }

    @Override
    public void commandFinished() {
      rebuildTreeIfNeeded();
    }
  }

  private class RefreshListener implements VirtualFileManagerListener {
    @Override
    public void beforeRefreshStart(boolean asynchronous) {
    }

    @Override
    public void afterRefreshFinish(boolean asynchronous) {
      myRepositoryListener.rebuildTreeIfNeeded();
    }
  }

  private class SortByTypeToggleAction extends ToggleAction {
    public SortByTypeToggleAction() {
      super("Sort Roots by Concept", "Sort root nodes by concept", null);
    }

    @Override
    public boolean isSelected(@Nullable AnActionEvent e) {
      return isSortByType();
    }

    @Override
    public void setSelected(@Nullable AnActionEvent e, boolean state) {
      if (state != isSortByType()) {
        getProjectView().setSortByType(getId(), state);
        rebuild();
      }
    }
  }
}
