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

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import jetbrains.mps.datatransfer.PasteNodeData;
import jetbrains.mps.datatransfer.PastePlaceHint;
import jetbrains.mps.editor.runtime.cells.AbstractCellAction;
import jetbrains.mps.editor.runtime.commands.EditorCommand;
import jetbrains.mps.ide.datatransfer.CopyPasteUtil;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.logging.Logger;
import jetbrains.mps.nodeEditor.ChildrenCollectionFinder;
import jetbrains.mps.nodeEditor.EditorComponent;
import jetbrains.mps.nodeEditor.cells.EditorCell_Label;
import jetbrains.mps.nodeEditor.cells.GeometryUtil;
import jetbrains.mps.nodeEditor.datatransfer.NodePaster;
import jetbrains.mps.nodeEditor.datatransfer.NodePaster.NodeAndRole;
import jetbrains.mps.nodeEditor.selection.EditorCellLabelSelection;
import jetbrains.mps.nodeEditor.selection.EditorCellSelection;
import jetbrains.mps.openapi.editor.EditorContext;
import jetbrains.mps.openapi.editor.cells.CellConditions;
import jetbrains.mps.openapi.editor.cells.CellInfo;
import jetbrains.mps.openapi.editor.cells.CellTraversalUtil;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.editor.cells.EditorCell_Collection;
import jetbrains.mps.openapi.editor.selection.MultipleSelection;
import jetbrains.mps.openapi.editor.selection.Selection;
import jetbrains.mps.openapi.editor.selection.SelectionManager;
import jetbrains.mps.openapi.editor.selection.SingularSelection;
import jetbrains.mps.project.Project;
import jetbrains.mps.resolve.ResolverComponent;
import org.apache.log4j.LogManager;
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.SNodeReference;
import org.jetbrains.mps.openapi.model.SNodeUtil;
import org.jetbrains.mps.openapi.model.SReference;
import org.jetbrains.mps.openapi.module.SRepository;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;


/**
 * Author: Sergey Dmitriev.
 * Time: Nov 26, 2003 2:06:41 PM
 */
public class CellAction_PasteNode extends AbstractCellAction {
  private static final Logger LOG = Logger.wrap(LogManager.getLogger(CellAction_PasteNode.class));

  @Override
  public boolean canExecute(EditorContext context) {
    Selection selection = context.getSelectionManager().getSelection();
    List<SNode> selectedNodes = selection.getSelectedNodes();
    if (selectedNodes.isEmpty()) {
      return false;
    }
    List<SNode> pasteNodes = CopyPasteUtil.getNodesFromClipboard(selectedNodes.get(0).getModel());

    if (pasteNodes == null || pasteNodes.isEmpty()) {
      // it used to be ok because conversion would be invoked in this case
      return false;
    }

    boolean disposed = false;
    for (SNode node : selectedNodes) {
      if (!SNodeUtil.isAccessible(node, context.getRepository())) {
        disposed = true;
        break;
      }
    }

    boolean canPasteWithRemove = !disposed && canPasteViaNodePasterWithRemove(selectedNodes, pasteNodes);
    if (selection instanceof SingularSelection &&
        (selection instanceof EditorCellLabelSelection && !isCompletelySelected((EditorCellLabelSelection) selection) ||
         (selection instanceof EditorCellSelection && !canPasteWithRemove))) {
      EditorCell selectedCell = getCellToPasteTo(context.getSelectedCell());
      if (selectedCell == null) {
        return false;
      }
      SNode selectedNode = selectedCell.getSNode();
      if (selectedNode == null || !(SNodeUtil.isAccessible(selectedNode, context.getRepository()))) {
        return false;
      }

      return canPasteViaNodePaster(selectedCell, pasteNodes);

    } else if ((selection instanceof MultipleSelection || selection instanceof EditorCellSelection) && canPasteWithRemove) {
      return true;
    }
    return false;
  }

  private boolean isCompletelySelected(EditorCellLabelSelection labelSelection) {
    int textLength = labelSelection.getEditorCellLabel().getText().length();
    return labelSelection.getSelectionStart() == 0 && labelSelection.getSelectionEnd() == textLength && textLength > 0;
  }

  private boolean canPasteViaNodePaster(EditorCell selectedCell, List<SNode> pasteNodes) {
    if (!new NodePaster(pasteNodes).canPaste(selectedCell)) {
      LOG.debug("Couldn't paste node here");
      return false;
    }
    return true;
  }

  private boolean canPasteViaNodePasterWithRemove(List<SNode> pasteTargets, List<SNode> pasteNodes) {
    if (!new NodePaster(pasteNodes).canPasteWithRemove(pasteTargets)) {
      LOG.debug("Couldn't paste node here");
      return false;
    }
    return true;
  }

  @Override
  public void execute(final EditorContext context) {
    LOG.assertLog(context.getRepository().getModelAccess().isCommandAction(), "This action must be performed in command");
    final EditorComponent editorComponent = (EditorComponent) context.getEditorComponent();
    final Selection selection = editorComponent.getSelectionManager().getSelection();
    final List<SNode> selectedNodes = selection.getSelectedNodes();

    //this is used in case node is in repo to pass it into invokeLater
    final List<SNodeReference> selectedReferences = new ArrayList<>();
    for (SNode node : selectedNodes) {
      selectedReferences.add(node.getReference());
    }


    final CellInfo pasteTargetCellInfo;
    final SNode cellNodeSelected;
    final SNodeReference selectedCellReference;
    if (selection instanceof SingularSelection) {
      EditorCell pasteTargetCell = getCellToPasteTo(context.getSelectedCell());
      if (pasteTargetCell == null) {
        return;
      }
      pasteTargetCellInfo = pasteTargetCell.getCellInfo();
      cellNodeSelected = pasteTargetCell.getSNode();
      selectedCellReference = cellNodeSelected.getReference();
    } else {
      pasteTargetCellInfo = null;
      cellNodeSelected = null;
      selectedCellReference = null;
    }

    final SModel modelToPaste = selectedNodes.get(0).getModel();

    // sometimes model is not in repository (paste in merge dialog)
    final boolean inRepository = modelToPaste.getReference().resolve(context.getRepository()) != null;

    // FIXME relationship between Project and Editor needs attention, it's bad to extract
    final Project mpsProject = ProjectHelper.getProject(context.getRepository());
    if (mpsProject == null) {
      LOG.warning("Paste needs a project to show a dialog for additional imports");
      return;
    }

    final PasteNodeData pasteNodeData = CopyPasteUtil.getPasteNodeDataFromClipboard(modelToPaste);

    ApplicationManager.getApplication().invokeLater(() -> {
      final Runnable addImportsRunnable = CopyPasteUtil.addImportsWithDialog(pasteNodeData, modelToPaste, mpsProject);
      context.getRepository().getModelAccess().executeCommand(new EditorCommand(context) {
        @Override
        public void doExecute() {
          if (addImportsRunnable != null) {
            addImportsRunnable.run();
          }

          List<SNode> pasteNodes = pasteNodeData.getNodes();
          List<SNode> currentSelectedNodes;
          if (!inRepository) {
            currentSelectedNodes = selectedNodes;
          } else {
            currentSelectedNodes = new ArrayList<>();
            for (SNodeReference ref : selectedReferences) {
              currentSelectedNodes.add(ref.resolve(context.getRepository()));
            }
          }


          NodePaster nodePaster = new NodePaster(pasteNodes);
          boolean disposed = CellAction_PasteNode.this.checkDisposedSelectedNodes(context.getRepository(), currentSelectedNodes, selectedReferences);
          boolean canPasteWithRemove = !disposed && nodePaster.canPasteWithRemove(currentSelectedNodes);
          if (selection instanceof SingularSelection &&
              (selection instanceof EditorCellLabelSelection && !CellAction_PasteNode.this.isCompletelySelected((EditorCellLabelSelection) selection) ||
               (selection instanceof EditorCellSelection && !canPasteWithRemove))) {
            EditorCell selectedCell = pasteTargetCellInfo.findCell(editorComponent);
            assert selectedCell != null;


            if (CellAction_PasteNode.this.canPasteBefore(selectedCell, pasteNodes)) {
              SNode selectedNode = inRepository ? selectedCellReference.resolve(context.getRepository()) : cellNodeSelected;
              if (CellAction_PasteNode.this.checkDisposed(context.getRepository(), selectedCellReference, cellNodeSelected)) {
                return;
              }
              new NodePaster(pasteNodes).pasteRelative(selectedNode, PastePlaceHint.BEFORE_ANCHOR);
            } else {
              new NodePaster(pasteNodes).paste(selectedCell);
            }
          } else if ((selection instanceof MultipleSelection || selection instanceof EditorCellSelection) && canPasteWithRemove) {
            nodePaster.pasteWithRemove(currentSelectedNodes);
          } else {
            return;
          }

          Set<SReference> requireResolveReferences = new HashSet<>();
          for (SReference ref : pasteNodeData.getRequireResolveReferences()) {
            //ref can be detached from modeltoPaste while using copy/paste handlers
            if (ref.getSourceNode() == null || ref.getSourceNode().getModel() == null) {
              continue;
            }
            requireResolveReferences.add(ref);
          }

          ResolverComponent.getInstance().resolveScopesOnly(requireResolveReferences, context.getRepository());

          // set selection
          editorComponent.getUpdater().flushModelEvents();
          SNode lastNode = pasteNodes.get(pasteNodes.size() - 1);
          editorComponent.getSelectionManager().setSelection(lastNode, SelectionManager.LAST_CELL, -1);
        }
      });
    }, ModalityState.current());
  }

  private boolean checkDisposedSelectedNodes(SRepository repository, List<SNode> currentSelectedNodes, List<SNodeReference> selectedReferences) {
    Iterator<SNodeReference> referenceIterator = selectedReferences.iterator();
    for (SNode node : currentSelectedNodes) {
      SNodeReference reference = referenceIterator.next();
      if (checkDisposed(repository, reference, node)) {
        return true;
      }
    }
    return false;
  }

  private boolean checkDisposed(SRepository repository, SNodeReference currentSelectedReference, SNode selectedNode) {
    if (!SNodeUtil.isAccessible(selectedNode, repository)) {
      LOG.error(String.format("Selected node is disposed: node = %s ; node pointer = (%s)", selectedNode, currentSelectedReference));
      return true;
    }
    return false;
  }

  private boolean canPasteBefore(EditorCell selectedCell, List<SNode> pasteNodes) {
    if (!GeometryUtil.isFirstPositionInBigCell(selectedCell)) {
      return false;
    }
    SNode anchor = selectedCell.getSNode();
    if (anchor.getParent() == null) {
      return false;
    }

    final String role = anchor.getContainmentLink() != null ? anchor.getContainmentLink().getName() : null;
    NodeAndRole nodeAndRole = new NodePaster(pasteNodes).getActualAnchorNode(anchor, role, false);
    if (nodeAndRole == null) {
      return false;
    }

    EditorCell targetCell = selectedCell.getEditorComponent().findNodeCell(nodeAndRole.myNode);
    return targetCell != null && isFirstSelectableInTarget(selectedCell, targetCell) && new NodePaster(pasteNodes).canPasteRelative(nodeAndRole.myNode);
  }

  private boolean isFirstSelectableInTarget(@NotNull EditorCell selectedCell, @NotNull EditorCell targetCell) {
    if (!CellTraversalUtil.getParents(selectedCell, true).contains(targetCell)) {
      return false;
    }
    EditorCell prevSelectable = CellTraversalUtil.getPrevLeaf(selectedCell, CellConditions.SELECTABLE);
    return prevSelectable == null || !CellTraversalUtil.getParents(prevSelectable, false).contains(targetCell);
  }

  private EditorCell getCellToPasteTo(EditorCell cell) {
    if (cell == null) {
      return null;
    }
    if (GeometryUtil.isLastPositionInBigCell(cell)) {
      return cell;
    }

    if (cell instanceof EditorCell_Label && cell.getRole() == null) {
      EditorCell result = new ChildrenCollectionFinder(cell, true, false).find();
      if (result != null) {
        return result;
      }
      result = new ChildrenCollectionFinder(cell, false, false).find();
      if (result != null) {
        if (result instanceof EditorCell_Collection) {
          return ((EditorCell_Collection) result).lastCell();
        }
        return result;
      }
    }
    return cell;
  }

}

