/*
 * 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 com.intellij.ide.projectView.ProjectView;
import com.intellij.ide.projectView.impl.AbstractProjectViewPane;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.DumbService.DumbModeListener;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.util.ArrayUtil;
import com.intellij.util.messages.MessageBusConnection;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.ide.projectPane.BaseLogicalViewProjectPane;
import jetbrains.mps.ide.projectPane.ProjectPane;
import jetbrains.mps.ide.projectPane.ProjectPaneActionGroups;
import jetbrains.mps.ide.projectPane.ProjectPaneDnDListener;
import jetbrains.mps.ide.projectPane.logicalview.highlighting.ProjectPaneTreeHighlighter;
import jetbrains.mps.ide.ui.smodel.ConceptTreeNode;
import jetbrains.mps.ide.ui.smodel.PropertiesTreeNode;
import jetbrains.mps.ide.ui.smodel.ReferencesTreeNode;
import jetbrains.mps.ide.ui.tree.MPSTreeNode;
import jetbrains.mps.ide.ui.tree.module.ProjectModuleTreeNode;
import jetbrains.mps.ide.ui.tree.module.SModelsSubtree;
import jetbrains.mps.ide.ui.tree.smodel.NodeTargetProvider;
import jetbrains.mps.ide.ui.tree.smodel.PackageNode;
import jetbrains.mps.ide.ui.tree.smodel.SNodeTreeNode;
import jetbrains.mps.ide.ui.tree.smodel.SNodeTreeNode.NodeChildrenProvider;
import jetbrains.mps.openapi.navigation.EditorNavigator;
import jetbrains.mps.project.DevKit;
import jetbrains.mps.project.Solution;
import jetbrains.mps.smodel.Generator;
import jetbrains.mps.smodel.Language;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.smodel.SModelStereotype;
import jetbrains.mps.smodel.SNodeUtil;
import jetbrains.mps.util.Computable;
import jetbrains.mps.util.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeAccessUtil;
import org.jetbrains.mps.openapi.model.SNodeReference;

import javax.swing.tree.TreePath;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceAdapter;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DropTarget;
import java.awt.dnd.InvalidDnDOperationException;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * GUESS: while {@link ProjectTree} is deemed for embedded UI components, e.g. in a dialog,
 * this class is intended solely for ProjectPane, thus supports DnD, highlighting (although this might
 * need move to ProjectPane, as it's project stuff and needs Idea's project Message bus), integration with
 * editor (activation, auto-select/expand), etc.
 */
public class ProjectPaneTree extends ProjectTree implements NodeChildrenProvider, ProjectModuleTreeNode.ModuleNodeChildrenProvider {
  private ProjectPane myProjectPane;
  private KeyAdapter myKeyListener = new KeyAdapter() {
    @Override
    public void keyPressed(KeyEvent e) {
      if (e.getModifiers() != 0 || e.getKeyCode() != KeyEvent.VK_ENTER) {
        return;
      }

      TreePath selPath = getSelectionPath();
      if (selPath != null && selPath.getLastPathComponent() instanceof MPSTreeNode) {
        // reuse method for double click
        doubleClick((MPSTreeNode) selPath.getLastPathComponent());
        e.consume();
      }
    }
  };
  private final ProjectPaneTreeHighlighter myHighlighter;
  private final TreeStructureUpdate myStructureUpdate;

  public ProjectPaneTree(ProjectPane projectPane, Project project) {
    super(ProjectHelper.fromIdeaProject(project));
    myProjectPane = projectPane;

    myHighlighter = new ProjectPaneTreeHighlighter(this, ProjectHelper.fromIdeaProject(project));
    myHighlighter.init();
    myStructureUpdate = new TreeStructureUpdate(this);
    myStructureUpdate.init();
    //enter can't be listened using keyboard actions because in this case tree's UI receives it first and just expands a node
    addKeyListener(myKeyListener);

    //drag support is alive while the tree is alive
    DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer(this, DnDConstants.ACTION_MOVE, new MyDragGestureListener());
    new DropTarget(this, new ProjectPaneDnDListener(this, new MyTransferable(null).getTransferDataFlavors()[0]));

    MessageBusConnection connection = project.getMessageBus().connect();
    Disposer.register(this, connection);
    connection.subscribe(DumbService.DUMB_MODE, new DumbModeListener() {
      @Override
      public void enteredDumbMode() {
        // there used to be update both on enter and exit of the dumb mode, however, I don't see a reason to
        // do it twice. Moreover, there's guard condition in TreeUpdateVisitor that waits for dumb mode to complete.
      }

      @Override
      public void exitDumbMode() {
        myHighlighter.dumbUpdate();
      }
    });
  }

  @Override
  public void runRebuildAction(Runnable rebuildAction, boolean saveExpansion) {
    super.runRebuildAction(rebuildAction, saveExpansion);
    // gen status is tracked on model level. If there are no model nodes shown yet, generation status for module and
    // namespace nodes shall be updated. Other alternative is to do it in ModuleNodeListener#attach() or in
    // ProjectPaneTreeHighlighter#moduleNodeAdded. MNL at the moment doesn't track gen status notifications, so it's odd to put
    // update there. PPTH#moduleNodeAdded() is decent alternative and perhaps the right thing to do, however, I decided
    // to give it a 'single-shot' approach, to re-highlight a tree once rebuild is over, which seems reasonable.
    myHighlighter.dumbUpdate();
  }

  @Override
  public void dispose() {
    myStructureUpdate.dispose();
    myHighlighter.dispose();
    removeKeyListener(myKeyListener);
    super.dispose();
  }


  @Override
  public Comparator<Object> getChildrenComparator() {
    return myProjectPane.getTreeChildrenComparator();
  }

  @Override
  protected void doubleClick(@NotNull MPSTreeNode nodeToClick) {
    if (nodeToClick instanceof NodeTargetProvider) {
      final SNodeReference navigationTarget = ((NodeTargetProvider) nodeToClick).getNavigationTarget();
      if (navigationTarget != null) {
        new EditorNavigator(getProject()).shallFocus(true).selectIfChild().open(navigationTarget);
        return;
      }
      // fall-through
    }
    super.doubleClick(nodeToClick);
  }

  @Override
  protected void autoscroll(@NotNull MPSTreeNode nodeToClick) {
    if (nodeToClick instanceof NodeTargetProvider) {
      final SNodeReference navigationTarget = ((NodeTargetProvider) nodeToClick).getNavigationTarget();
      if (navigationTarget != null) {
        new EditorNavigator(getProject()).shallFocus(false).selectIfChild().open(navigationTarget);
        return;
      }
      // fall-through
    }
    super.autoscroll(nodeToClick);
  }

  @Override
  public boolean isAutoOpen() {
    return myProjectPane.getProjectView().isAutoscrollToSource(myProjectPane.getId());
  }

  @Override
  protected String getPopupMenuPlace() {
    return ActionPlaces.PROJECT_VIEW_POPUP;
  }

  @Override
  protected ActionGroup createPopupActionGroup(final MPSTreeNode node) {
    return new ModelAccessHelper(getProject().getModelAccess()).runReadAction(new Computable<ActionGroup>() {
      @Override
      public ActionGroup compute() {
        return ProjectPaneActionGroups.getActionGroup(node);
      }
    });
  }

  @Override
  public void populate(SNodeTreeNode treeNode) {
    if (myProjectPane.showNodeStructure()) {
      SNode n = treeNode.getSNode();
      if (n == null || n.getModel() == null) {
        return;
      }

      treeNode.add(new ConceptTreeNode(n));
      treeNode.add(new PropertiesTreeNode(n));
      treeNode.add(new ReferencesTreeNode(n));
    }
  }

  @Override
  public boolean populate(MPSTreeNode treeNode, Language language) {
    return false;
  }

  @Override
  public boolean populate(MPSTreeNode treeNode, Solution solution) {
    return false;
  }

  @Override
  public boolean populate(MPSTreeNode treeNode, Generator generator) {
    if (myProjectPane.isDescriptorModelInGeneratorVisible()) {
      return false;
    }
    Predicate<SModel> isDescriptorModel = SModelStereotype::isDescriptorModel;
    new SModelsSubtree(treeNode).create(generator.getModels().stream().filter(isDescriptorModel.negate()).collect(Collectors.toList()));
    return true;
  }

  @Override
  public boolean populate(MPSTreeNode treeNode, DevKit devkit) {
    return false;
  }

  private class MyTransferable implements Transferable {
    private final String mySupportedFlavor = "MPSNodeToMoveFlavor";
    private Object myObject;

    public MyTransferable(Object o) {
      myObject = o;
    }

    @Override
    public DataFlavor[] getTransferDataFlavors() {
      Class aClass = MyTransferable.class;
      DataFlavor dataFlavor = null;
      try {
        dataFlavor = new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType + ";class=" + aClass.getName(),
            mySupportedFlavor, aClass.getClassLoader());
      } catch (ClassNotFoundException ignored) {
      }
      return new DataFlavor[]{dataFlavor};
    }

    @Override
    public boolean isDataFlavorSupported(DataFlavor flavor) {
      DataFlavor[] flavors = getTransferDataFlavors();
      return ArrayUtil.find(flavors, flavor) != -1;
    }

    @Override
    public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
      return myObject;
    }
  }

  private class MyDragSourceListener extends DragSourceAdapter {
    @Override
    public void dragEnter(DragSourceDragEvent dsde) {
      dsde.getDragSourceContext().setCursor(null);
    }

    @Override
    public void dropActionChanged(DragSourceDragEvent dsde) {
      dsde.getDragSourceContext().setCursor(null);
    }
  }

  private class MyDragGestureListener implements DragGestureListener {
    @Override
    public void dragGestureRecognized(final DragGestureEvent dge) {
      if ((dge.getDragAction() & DnDConstants.ACTION_COPY_OR_MOVE) == 0) {
        return;
      }
      ProjectView projectView = ProjectView.getInstance(myProjectPane.getProject());
      if (projectView == null) {
        return;
      }
      final AbstractProjectViewPane currentPane = projectView.getCurrentProjectViewPane();
      if (!(currentPane instanceof BaseLogicalViewProjectPane)) {
        return;
      }

      final List<Pair<SNodeReference, String>> result = new ArrayList<>();

      getProject().getModelAccess().runReadAction(new Runnable() {
        @Override
        public void run() {
          for (SNode node : myProjectPane.getSelectedSNodes()) {
            result.add(new Pair<>(new jetbrains.mps.smodel.SNodePointer(node), ""));
          }
          SModel contextDescriptor = myProjectPane.getContextModel();
          if (contextDescriptor != null) {
            for (PackageNode treeNode : myProjectPane.getSelectedTreeNodes(PackageNode.class)) {
              String searchedPack = treeNode.getFullPackage();
              if (treeNode.getChildCount() == 0 || searchedPack == null) {
                continue;
              }
              for (final SNode node : contextDescriptor.getRootNodes()) {
                String nodePack = SNodeAccessUtil.getProperty(node, SNodeUtil.propertyName_BaseConcept_virtualPackage);
                if (nodePack == null) {
                  continue;
                }
                if (!nodePack.startsWith(searchedPack)) {
                  continue;
                }

                StringBuilder basePack = new StringBuilder();
                String firstPart = treeNode.getPackage();
                String secondPart = "";
                if (nodePack.startsWith(searchedPack + ".")) {
                  secondPart = nodePack.replaceFirst(searchedPack + ".", "");
                }
                basePack.append(firstPart);
                if (!firstPart.isEmpty() && !secondPart.isEmpty()) {
                  basePack.append(".");
                }
                basePack.append(secondPart);
                result.add(new Pair<>(new jetbrains.mps.smodel.SNodePointer(node), basePack.toString()));
              }
            }
          }
        }
      });
      if (result.isEmpty()) {
        return;
      }

      try {
        dge.startDrag(DragSource.DefaultMoveNoDrop, new MyTransferable(result), new MyDragSourceListener());
      } catch (InvalidDnDOperationException ignored) {
      }
    }
  }
}
