/*
 * Copyright 2000-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 org.jetbrains.plugins.groovy.mvc.projectView;

import com.intellij.icons.AllIcons;
import com.intellij.ide.*;
import com.intellij.ide.projectView.BaseProjectTreeBuilder;
import com.intellij.ide.projectView.ViewSettings;
import com.intellij.ide.projectView.impl.*;
import com.intellij.ide.util.DeleteHandler;
import com.intellij.ide.util.DirectoryChooserUtil;
import com.intellij.ide.util.EditorHelper;
import com.intellij.ide.util.treeView.AbstractTreeBuilder;
import com.intellij.ide.util.treeView.AbstractTreeNode;
import com.intellij.ide.util.treeView.AbstractTreeUpdater;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.TextEditor;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ui.configuration.actions.ModuleDeleteProvider;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.openapi.wm.ex.ToolWindowEx;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiModificationTracker;
import com.intellij.ui.AutoScrollFromSourceHandler;
import com.intellij.ui.AutoScrollToSourceHandler;
import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentManager;
import com.intellij.util.IJSwingUtilities;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.tree.TreeUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.mvc.MvcFramework;

import javax.swing.*;
import javax.swing.tree.DefaultTreeModel;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;

/**
 * @author Dmitry Krasislchikov
 */
public class MvcProjectViewPane extends AbstractProjectViewPSIPane implements IdeView {
  private final CopyPasteDelegator myCopyPasteDelegator;
  private final JComponent myComponent;
  private final DeleteProvider myDeletePSIElementProvider;
  private final ModuleDeleteProvider myDeleteModuleProvider = new ModuleDeleteProvider();

  private final AutoScrollToSourceHandler myAutoScrollToSourceHandler;
  private final MyAutoScrollFromSourceHandler myAutoScrollFromSourceHandler;

  @NonNls private final String myId;
  private final MvcToolWindowDescriptor myDescriptor;

  private final MvcProjectViewState myViewState;

  public MvcProjectViewPane(final Project project, MvcToolWindowDescriptor descriptor) {
    super(project);
    myDescriptor = descriptor;
    myId = descriptor.getToolWindowId();

    myViewState = descriptor.getProjectViewState(project);

    class TreeUpdater implements Runnable, PsiModificationTracker.Listener {
      private volatile boolean myInQueue;

      @Override
      public void run() {
        if (getTree() != null && getTreeBuilder() != null) {
          updateFromRoot(true);
        }
        myInQueue = false;
      }

      @Override
      public void modificationCountChanged() {
        if (!myInQueue) {
          myInQueue = true;
          ApplicationManager.getApplication().invokeLater(this);
        }
      }
    }

    project.getMessageBus().connect(this).subscribe(PsiModificationTracker.TOPIC, new TreeUpdater());

    myComponent = createComponent();
    DataManager.registerDataProvider(myComponent, this);

    myAutoScrollFromSourceHandler = new MyAutoScrollFromSourceHandler();
    myAutoScrollToSourceHandler = new AutoScrollToSourceHandler() {
      @Override
      protected boolean isAutoScrollMode() {
        return myViewState.autoScrollToSource;
      }

      @Override
      protected void setAutoScrollMode(boolean state) {
        myViewState.autoScrollToSource = state;
      }
    };

    myAutoScrollFromSourceHandler.install();
    myAutoScrollToSourceHandler.install(getTree());
    myAutoScrollToSourceHandler.onMouseClicked(getTree());

    myCopyPasteDelegator = new CopyPasteDelegator(project, myComponent);
    myDeletePSIElementProvider = new DeleteHandler.DefaultDeleteProvider();
  }

  public void setup(ToolWindowEx toolWindow) {
    JPanel p = new JPanel(new BorderLayout());
    p.add(myComponent, BorderLayout.CENTER);

    ContentManager contentManager = toolWindow.getContentManager();
    Content content = contentManager.getFactory().createContent(p, null, false);
    content.setDisposer(this);
    content.setCloseable(false);

    content.setPreferredFocusableComponent(createComponent());
    contentManager.addContent(content);

    contentManager.setSelectedContent(content, true);

    DefaultActionGroup group = new DefaultActionGroup();
    group.add(new HideEmptyMiddlePackagesAction());
    group.add(myAutoScrollToSourceHandler.createToggleAction());
    group.add(myAutoScrollFromSourceHandler.createToggleAction());

    toolWindow.setAdditionalGearActions(group);

    TreeExpander expander = new DefaultTreeExpander(myTree);
    CommonActionsManager actionsManager = CommonActionsManager.getInstance();
    AnAction collapseAction = actionsManager.createCollapseAllAction(expander, myTree);
    collapseAction.getTemplatePresentation().setIcon(AllIcons.General.CollapseAll);

    toolWindow.setTitleActions(new AnAction[]{new ScrollFromSourceAction(), collapseAction});
  }

  @NotNull
  @Override
  public String getTitle() {
    throw new UnsupportedOperationException();
  }

  @NotNull
  @Override
  public Icon getIcon() {
    return myDescriptor.getFramework().getIcon();
  }

  @Override
  @NotNull
  public String getId() {
    return myId;
  }

  @Override
  public int getWeight() {
    throw new UnsupportedOperationException();
  }

  @Override
  public boolean isInitiallyVisible() {
    throw new UnsupportedOperationException();
  }

  @NotNull
  @Override
  public SelectInTarget createSelectInTarget() {
    throw new UnsupportedOperationException();
  }

  @NotNull
  @Override
  protected BaseProjectTreeBuilder createBuilder(@NotNull final DefaultTreeModel treeModel) {
    return new ProjectTreeBuilder(myProject, myTree, treeModel, null, (ProjectAbstractTreeStructureBase)myTreeStructure) {
      @Override
      protected AbstractTreeUpdater createUpdater() {
        return createTreeUpdater(this);
      }
    };
  }

  @NotNull
  @Override
  protected ProjectAbstractTreeStructureBase createStructure() {
    final String id = getId();
    return new ProjectTreeStructure(myProject, id) {

      @Override
      public boolean isHideEmptyMiddlePackages() {
        return myViewState.hideEmptyMiddlePackages;
      }

      @Override
      protected AbstractTreeNode createRoot(final Project project, ViewSettings settings) {
        return new MvcProjectNode(project, this, myDescriptor);
      }
    };
  }

  @NotNull
  @Override
  protected ProjectViewTree createTree(@NotNull final DefaultTreeModel treeModel) {
    return new ProjectViewTree(treeModel) {
      public String toString() {
        return myDescriptor.getFramework().getDisplayName() + " " + super.toString();
      }
    };
  }

  @NotNull
  @Override
  protected AbstractTreeUpdater createTreeUpdater(@NotNull final AbstractTreeBuilder treeBuilder) {
    return new AbstractTreeUpdater(treeBuilder);
  }

  @Override
  public Object getData(@NotNull String dataId) {
    if (CommonDataKeys.PSI_ELEMENT.getName().equals(dataId)) {
      final PsiElement[] elements = getSelectedPSIElements();
      return elements.length == 1 ? elements[0] : null;
    }
    if (LangDataKeys.PSI_ELEMENT_ARRAY.getName().equals(dataId)) {
      return getSelectedPSIElements();
    }
    if (LangDataKeys.MODULE_CONTEXT.getName().equals(dataId)) {
      final Object element = getSelectedElement();
      if (element instanceof Module) {
        return element;
      }
      return null;
    }
    if (LangDataKeys.MODULE_CONTEXT_ARRAY.getName().equals(dataId)) {
      final List<Module> moduleList = ContainerUtil.findAll(getSelectedElements(), Module.class);
      if (!moduleList.isEmpty()) {
        return moduleList.toArray(Module.EMPTY_ARRAY);
      }
      return null;
    }
    if (dataId.equals(LangDataKeys.IDE_VIEW.getName())) {
      return this;
    }
    if (dataId.equals(PlatformDataKeys.HELP_ID.getName())) {
      return "reference.toolwindows." + myId.toLowerCase();
    }
    if (PlatformDataKeys.CUT_PROVIDER.getName().equals(dataId)) {
      return myCopyPasteDelegator.getCutProvider();
    }
    if (PlatformDataKeys.COPY_PROVIDER.getName().equals(dataId)) {
      return myCopyPasteDelegator.getCopyProvider();
    }
    if (PlatformDataKeys.PASTE_PROVIDER.getName().equals(dataId)) {
      return myCopyPasteDelegator.getPasteProvider();
    }
    if (PlatformDataKeys.DELETE_ELEMENT_PROVIDER.getName().equals(dataId)) {
      for (final Object element : getSelectedElements()) {
        if (element instanceof Module) {
          return myDeleteModuleProvider;
        }
      }
      return myDeletePSIElementProvider;
    }
    return super.getData(dataId);
  }

  @Nullable
  public static MvcProjectViewPane getView(final Project project, MvcFramework framework) {
    final ToolWindow window = ToolWindowManager.getInstance(project).getToolWindow(MvcToolWindowDescriptor.getToolWindowId(framework));
    final Content content = window == null ? null : window.getContentManager().getContent(0);
    return content == null ? null : (MvcProjectViewPane)content.getDisposer();
  }

  @Override
  public void selectElement(PsiElement element) {
    PsiFileSystemItem psiFile;
    if (element instanceof PsiFileSystemItem) {
      psiFile = (PsiFileSystemItem)element;
    }
    else {
      psiFile = element.getContainingFile();
      if (psiFile == null) {
        return;
      }
    }

    VirtualFile virtualFile = psiFile.getVirtualFile();
    if (virtualFile == null) {
      return;
    }

    selectFile(virtualFile, false);

    boolean requestFocus = true;

    if (psiFile instanceof PsiFile) {
      Editor editor = EditorHelper.openInEditor(element);
      if (editor != null) {
        ToolWindowManager.getInstance(myProject).activateEditorComponent();
        requestFocus = false;
      }
    }

    if (requestFocus) {
      selectFile(virtualFile, true);
    }
  }

  @NotNull
  @Override
  public PsiDirectory[] getDirectories() {
    return getSelectedDirectories();
  }

  @Override
  public PsiDirectory getOrChooseDirectory() {
    return DirectoryChooserUtil.getOrChooseDirectory(this);
  }

  public static boolean canSelectFile(@NotNull Project project, @NotNull MvcFramework framework, VirtualFile file) {
    return getSelectPath(project, framework, file) != null;
  }

  @Nullable
  private List<Object> getSelectPath(VirtualFile file) {
    return getSelectPath(myProject, myDescriptor.getFramework(), file);
  }

  @Nullable
  private static List<Object> getSelectPath(@NotNull Project project, @NotNull MvcFramework framework, VirtualFile file) {
      if (file == null) {
      return null;
    }

    final Module module = ModuleUtilCore.findModuleForFile(file, project);
    if (module == null || !framework.hasSupport(module)) {
      return null;
    }
    List<Object> result = new ArrayList<>();

    final MvcProjectViewPane view = getView(project, framework);
    if (view == null) {
      return null;
    }

    final MvcProjectNode root = (MvcProjectNode)view.getTreeBuilder().getTreeStructure().getRootElement();
    result.add(root);

    for (AbstractTreeNode moduleNode : root.getChildren()) {
      if (moduleNode.getValue() == module) {
        result.add(moduleNode);

        AbstractTreeNode<?> cur = moduleNode;

        path:
        while (true) {
          for (AbstractTreeNode descriptor : cur.getChildren()) {
            if (descriptor instanceof AbstractFolderNode) {
              final AbstractFolderNode folderNode = (AbstractFolderNode)descriptor;
              final VirtualFile dir = folderNode.getVirtualFile();
              if (dir != null && VfsUtilCore.isAncestor(dir, file, false)) {
                cur = folderNode;
                result.add(folderNode);
                if (dir.equals(file)) {
                  return result;
                }
                continue path;
              }
            }
            if (descriptor instanceof AbstractMvcPsiNodeDescriptor) {
              if (file.equals(((AbstractMvcPsiNodeDescriptor)descriptor).getVirtualFile())) {
                result.add(descriptor);
                return result;
              }
            }
          }
          return null;
        }
      }
    }
    return null;
  }

  public boolean canSelectFile(VirtualFile file) {
    return getSelectPath(file) != null;
  }

  private void selectElementAtCaret(Editor editor, boolean requestFocus) {
    PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(editor.getDocument());
    selectFile(file, requestFocus);
  }

  public void selectFile(@Nullable PsiFile file, boolean requestFocus) {
    if (file == null) return;
    selectFile(file.getVirtualFile(), requestFocus);
  }

  public void selectFile(@Nullable VirtualFile file, boolean requestFocus) {
    if (file == null) return;

    final List<Object> path = getSelectPath(file);
    if (path == null) return;

    final Object value = ((AbstractTreeNode)path.get(path.size() - 1)).getValue();
    select(value, file, requestFocus);
  }

  public void scrollFromSource() {
    FileEditorManager fileEditorManager = FileEditorManager.getInstance(myProject);
    Editor selectedTextEditor = fileEditorManager.getSelectedTextEditor();
    if (selectedTextEditor != null) {
      selectElementAtCaret(selectedTextEditor, false);
      return;
    }
    final FileEditor[] editors = fileEditorManager.getSelectedEditors();
    for (FileEditor fileEditor : editors) {
      if (fileEditor instanceof TextEditor) {
        Editor editor = ((TextEditor)fileEditor).getEditor();
        selectElementAtCaret(editor, false);
        return;
      }
    }
    final VirtualFile[] selectedFiles = fileEditorManager.getSelectedFiles();
    if (selectedFiles.length > 0) {
      PsiFile file = PsiManager.getInstance(myProject).findFile(selectedFiles[0]);
      selectFile(file, false);
    }
  }

  private void selectElementAtCaretNotLosingFocus() {
    if (IJSwingUtilities.hasFocus(this.getComponentToFocus())) return;
    scrollFromSource();
  }

  private class ScrollFromSourceAction extends AnAction implements DumbAware {
    private ScrollFromSourceAction() {
      super("Scroll from Source", "Select the file open in the active editor", AllIcons.General.Locate);
    }

    @Override
    public void actionPerformed(@NotNull AnActionEvent e) {
      scrollFromSource();
    }
  }

  private class MyAutoScrollFromSourceHandler extends AutoScrollFromSourceHandler {
    protected MyAutoScrollFromSourceHandler() {
      super(MvcProjectViewPane.this.myProject, myComponent, MvcProjectViewPane.this);
    }

    @Override
    protected boolean isAutoScrollEnabled() {
      return myViewState.autoScrollFromSource;
    }

    @Override
    protected void setAutoScrollEnabled(boolean state) {
      myViewState.autoScrollFromSource = state;
      if (state) {
        selectElementAtCaretNotLosingFocus();
      }
    }

    @Override
    protected void selectElementFromEditor(@NotNull FileEditor editor) {
     selectElementAtCaretNotLosingFocus();
    }
  }

  private class HideEmptyMiddlePackagesAction extends ToggleAction implements DumbAware {
    private HideEmptyMiddlePackagesAction() {
      super("Compact Empty Middle Packages", "Show/Compact Empty Middle Packages",
            AllIcons.ObjectBrowser.CompactEmptyPackages);
    }

    @Override
    public boolean isSelected(@NotNull AnActionEvent e) {
      return myViewState.hideEmptyMiddlePackages;
    }

    @Override
    public void setSelected(@NotNull AnActionEvent event, boolean flag) {
      myViewState.hideEmptyMiddlePackages = flag;
      TreeUtil.collapseAll(myTree, 1);
    }
  }

}
