/*
 * Copyright 2003-2011 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;

import jetbrains.mps.core.aspects.behaviour.SMethodTrimmedId;
import jetbrains.mps.editor.runtime.SideTransformInfoUtil;
import jetbrains.mps.editor.runtime.commands.EditorComputable;
import jetbrains.mps.nodeEditor.cellActions.SideTransformSubstituteInfo;
import jetbrains.mps.nodeEditor.cellActions.SideTransformSubstituteInfo.Side;
import jetbrains.mps.nodeEditor.cellMenu.NullSubstituteInfo;
import jetbrains.mps.nodeEditor.cells.CellFinderUtil;
import jetbrains.mps.nodeEditor.cells.CellFinderUtil.Finder;
import jetbrains.mps.nodeEditor.cells.EditorCell_Constant;
import jetbrains.mps.nodeEditor.cells.EditorCell_Label;
import jetbrains.mps.nodeEditor.sidetransform.EditorCell_STHint;
import jetbrains.mps.openapi.editor.EditorContext;
import jetbrains.mps.openapi.editor.cells.CellAction;
import jetbrains.mps.openapi.editor.cells.CellActionType;
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.SubstituteAction;
import jetbrains.mps.openapi.editor.cells.SubstituteInfo;
import jetbrains.mps.smodel.adapter.MetaAdapterByDeclaration;
import jetbrains.mps.smodel.behaviour.BHReflection;
import jetbrains.mps.typesystem.inference.ITypechecking.Computation;
import jetbrains.mps.typesystem.inference.TypeCheckingContext;
import jetbrains.mps.typesystem.inference.TypeContextManager;
import org.apache.log4j.Logger;
import org.jetbrains.mps.openapi.model.SNode;

import java.util.List;

public class IntelligentInputUtil {
  private static final Logger LOG = Logger.getLogger(IntelligentInputUtil.class);

  public static boolean processCell(final EditorCell_Label cell, final EditorContext editorContext, final String pattern,
      final CellSide side) {
    if (pattern == null || pattern.equals("")) {
      return false;
    }

    EditorComputable<Boolean> sideTransformCommand = new EditorComputable<Boolean>(editorContext) {
      @Override
      protected Boolean doCompute() {
        return TypeContextManager.getInstance().runTypeCheckingComputation(
            ((EditorComponent) editorContext.getEditorComponent()).getTypecheckingContextOwner(),
            editorContext.getEditorComponent().getEditedNode(),
            new Computation<Boolean>() {
              @Override
              public Boolean compute(TypeCheckingContext context) {
                if (cell instanceof EditorCell_STHint) {
                  EditorCell_STHint rtHintCell = (EditorCell_STHint) cell;
                  return processSTHintCell(rtHintCell, editorContext, pattern);
                }

                if (side == CellSide.LEFT) {
                  String head = "" + pattern.charAt(0);
                  String smallPattern = pattern.substring(1);
                  return processCellAtStart(cell, editorContext, head, smallPattern);
                } else {
                  String smallPattern = pattern.substring(0, pattern.length() - 1);
                  String tail = pattern.substring(pattern.length() - 1, pattern.length());
                  return processCellAtEnd(cell, editorContext, smallPattern, tail);
                }
              }
            });
      }
    };
    editorContext.getRepository().getModelAccess().executeCommand(sideTransformCommand);
    return sideTransformCommand.getResult();
  }

  private static boolean processSTHintCell(EditorCell_STHint cell, EditorContext editorContext, String pattern) {
    SubstituteInfo info = cell.getSubstituteInfo();
    String smallPattern = pattern.substring(0, pattern.length() - 1);
    String tail = "" + pattern.charAt(pattern.length() - 1);
    EditorCell nextCell = CellTraversalUtil.getNextLeaf(cell);
    while (nextCell != null && !nextCell.isSelectable()) {
      nextCell = CellTraversalUtil.getNextLeaf(nextCell);
    }

    if (canCompleteSmallPatternImmediately(info, pattern, "") ||
        canCompleteSmallPatternImmediately(info, trimLeft(pattern), "")) {

      if (!canCompleteSmallPatternImmediately(info, pattern, "")) {
        pattern = trimLeft(pattern);
      }

      info.getMatchingActions(pattern, true).get(0).substitute(editorContext, pattern);
      return true;
    } else if (pattern.length() > 0 && (canCompleteSmallPatternImmediately(info, smallPattern, tail) ||
        canCompleteSmallPatternImmediately(info, trimLeft(smallPattern), tail))) {

      if (!canCompleteSmallPatternImmediately(info, smallPattern, tail)) {
        smallPattern = trimLeft(smallPattern);
      }

      List<SubstituteAction> matchingActions = info.getMatchingActions(smallPattern, true);
      SubstituteAction item = matchingActions.get(0);
      SNode newNode = item.substitute(editorContext, smallPattern);
      if (newNode == null) {
        newNode = editorContext.getSelectedNode();
      }
      editorContext.flushEvents();
      EditorCell cellForNewNode;
      cellForNewNode = editorContext.getEditorComponent().findNodeCell(newNode);
      if (cellForNewNode != null) {
        EditorCell_Label target = null;
        EditorCell errorCell =
            CellFinderUtil.findChildByManyFinders(cellForNewNode, true, Finder.FIRST_ERROR);
        if (errorCell instanceof EditorCell_Label) {
          target = (EditorCell_Label) errorCell;
        }

        if (target != null) {
          target.changeText(tail);
          target.end();

          if (target.isErrorState()) {
            target.validate(true, false);
          }

          editorContext.flushEvents();

          if (editorContext.getSelectedCell() instanceof EditorCell_Label) {
            EditorCell_Label label = (EditorCell_Label) editorContext.getSelectedCell();
            label.end();
          }
        }
      }
      return true;
    } else if (info.getMatchingActions(pattern, false).isEmpty() &&
        info.getMatchingActions(trimLeft(pattern), false).isEmpty() &&
        nextCell != null && nextCell.isErrorState() && nextCell instanceof EditorCell_Label && ((EditorCell_Label) nextCell).isEditable()) {

      SideTransformInfoUtil.removeTransformInfo(cell.getSNode());

      EditorCell_Label label = (EditorCell_Label) nextCell;
      label.changeText(pattern);
      label.end();
      editorContext.getEditorComponent().changeSelection(label);
      return true;
    } else if (isInOneStepAmbigousPosition(info, smallPattern + tail)) {
      activateNodeSubstituteChooser(editorContext, cell, info);
    }
    return false;
  }

  private static boolean processCellAtEnd(EditorCell_Label cell, final EditorContext editorContext, String smallPattern,
      final String tail) {
    SubstituteInfo substituteInfo = cell.getSubstituteInfo();
    if (substituteInfo == null) {
      substituteInfo = new NullSubstituteInfo();
    }

    EditorCell cellForNewNode;
    final SNode newNode;
    if (cell.isValidText(smallPattern) && !"".equals(smallPattern)
        && substituteInfo.hasExactlyNActions(smallPattern + tail, false, 0)) {
      newNode = cell.getSNode();
      cellForNewNode = cell;
      return applyRigthTransform(editorContext, smallPattern, tail, cellForNewNode, newNode);
    } else if (canCompleteSmallPatternImmediately(substituteInfo, smallPattern, tail) ||
        canCompleteSmallPatternImmediately(substituteInfo, trimLeft(smallPattern), tail)) {

      if (!canCompleteSmallPatternImmediately(substituteInfo, smallPattern, tail) &&
          canCompleteSmallPatternImmediately(substituteInfo, trimLeft(smallPattern), tail)) {
        smallPattern = trimLeft(smallPattern);
      }

      List<SubstituteAction> matchingActions = substituteInfo.getMatchingActions(smallPattern, true);
      SubstituteAction item = matchingActions.get(0);
      item.substitute(editorContext, smallPattern);

      newNode = editorContext.getSelectedCell().getSNode();

      if (newNode == null) {
        return true;
      }

      cellForNewNode = editorContext.getEditorComponent().findNodeCell(newNode);
      EditorCell_Label errorCell = CellFinderUtil.findFirstError(cellForNewNode, true);

      if (errorCell != null) {
        editorContext.flushEvents();
        EditorCell cellForNewNode1 = editorContext.getEditorComponent().findNodeCell(newNode);
        EditorCell_Label errorCell1 = CellFinderUtil.findFirstError(cellForNewNode1, true);
        errorCell1.changeText(tail);
        errorCell1.setCaretPosition(tail.length());
        return true;
      }

      return applyRigthTransform(editorContext, smallPattern, tail, cellForNewNode, newNode);
    } else if (canCompleteTheWholeStringImmediately(substituteInfo, smallPattern + tail) ||
        canCompleteTheWholeStringImmediately(substituteInfo, trimLeft(smallPattern) + tail)) {

      if (!canCompleteTheWholeStringImmediately(substituteInfo, smallPattern + tail) &&
          canCompleteTheWholeStringImmediately(substituteInfo, trimLeft(smallPattern) + tail)) {
        smallPattern = trimLeft(smallPattern);
      }

      List<SubstituteAction> matchingActions = substituteInfo.getMatchingActions(smallPattern + tail, true);
      SubstituteAction item = matchingActions.get(0);
      item.substitute(editorContext, smallPattern + tail);
      return true;
    } else {
      if (isInOneStepAmbigousPosition(substituteInfo, smallPattern + tail)) {
        if (tryToSubstituteFirstSutable(editorContext, smallPattern + tail, substituteInfo)) {
          return true;
        }
        activateNodeSubstituteChooser(editorContext, cell, substituteInfo);
      } else if (isInAmbigousPosition(substituteInfo, smallPattern, tail)) {
        if (tryToSubstituteFirstSutable(editorContext, smallPattern, substituteInfo)) {
          return true;
        }
        cell.setText(smallPattern);
        activateNodeSubstituteChooser(editorContext, cell, substituteInfo);
      }
    }
    return false;
  }

  private static boolean applyRigthTransform(EditorContext editorContext, String smallPattern, final String tail,
      final EditorCell cellForNewNode, SNode newNode) {
    EditorCell selectableChild = CellFinderUtil.findLastSelectableLeaf(cellForNewNode, true);
    CellAction rtAction = selectableChild != null ?
        editorContext.getEditorComponent().getActionHandler().getApplicableCellAction(selectableChild, CellActionType.RIGHT_TRANSFORM) : null;

    boolean hasSideActions = hasSideActions(cellForNewNode, Side.RIGHT, tail);

    if (rtAction == null || !hasSideActions) {
      final CellInfo cellInfo = cellForNewNode.getCellInfo();
      return putTextInErrorChild(cellInfo, smallPattern + tail, editorContext);
    }

    if (cellForNewNode instanceof EditorCell_Label) {
      ((EditorCell_Label) cellForNewNode).changeText(smallPattern);
    }

    rtAction.execute(editorContext);

    EditorCell rtHintCell = prepareSTCell(editorContext, newNode, tail);
    if (rtHintCell != null) {
      final SubstituteInfo rtSubstituteInfo = rtHintCell.getSubstituteInfo();
      assert rtSubstituteInfo != null;
      List<SubstituteAction> rtMatchingActions = rtSubstituteInfo.getMatchingActions(tail, true);

      if (!canCompleteSmallPatternImmediately(rtSubstituteInfo, tail, "")) { //don't execute non-unique action on RT hint cell
        editorContext.flushEvents();

        EditorCell_Label foundCell = prepareRTCell(editorContext, newNode, tail);
        if (foundCell != null) {
          editorContext.getEditorComponent().changeSelection(foundCell);
          processCell(foundCell, editorContext, tail, CellSide.RIGHT);
        }
        return true;
      }

      SubstituteAction rtItem = rtMatchingActions.get(0);
      SNode yetNewNode = rtItem.substitute(editorContext, tail);

      editorContext.flushEvents();

      if (yetNewNode != null) {
        EditorCell yetNewNodeCell = findNodeCell(editorContext, yetNewNode);
        if (yetNewNodeCell == null) {
          LOG.warn("Unable to find editor cell for the node returned as a result of right-transform: " + yetNewNode.toString() + "(" + yetNewNode.getConcept() +
              "). Seems like the node is invisible in editor. Node was created by RT: " + rtItem.toString());
          return true;
        }
        EditorCell errorCell = CellFinderUtil.findFirstError(yetNewNodeCell, true);
        if (errorCell != null) {
          editorContext.selectWRTFocusPolicy(errorCell);
        } else {
          editorContext.selectWRTFocusPolicy(yetNewNodeCell);
        }
      }
    } else {
      editorContext.flushEvents();
      EditorCell_Label rtCell = prepareRTCell(editorContext, newNode, tail);
      if (rtCell != null) {
        processCell(rtCell, editorContext, tail, CellSide.RIGHT);
      }
    }
    return true;
  }

  private static boolean tryToSubstituteFirstSutable(EditorContext editorContext, String text, SubstituteInfo substituteInfo) {
    SNode concept = substituteInfo.getMatchingActions(text, true).get(0).getOutputConcept();
    if (concept == null) {
      return false;
    }
    boolean property = (Boolean) BHReflection.invoke(MetaAdapterByDeclaration.getConcept(concept),
        SMethodTrimmedId.create("substituteInAmbigousPosition", null, "1653mnvAgq$"));

    if (property) {
      SNode outputConcept = substituteInfo.getMatchingActions(text, true).get(0).getOutputConcept();
      for (SubstituteAction action : substituteInfo.getMatchingActions(text, true)) {
        if (outputConcept != action.getOutputConcept()) {
          return false;
        }
      }
      SubstituteAction action = substituteInfo.getMatchingActions(text, true).get(0);
      action.substitute(editorContext, text);
      return true;
    }
    return false;
  }

  private static boolean processCellAtStart(EditorCell_Label cell, final EditorContext editorContext, String head,
      String smallPattern) {
    SubstituteInfo info = cell.getSubstituteInfo();
    if (info == null) {
      info = new NullSubstituteInfo();
    }
    EditorCell cellForNewNode;
    SNode newNode;

    if (cell.isValidText(smallPattern) && (!"".equals(smallPattern) || cell instanceof EditorCell_Constant)
        && info.hasExactlyNActions(head + smallPattern, false, 0)) {
      newNode = cell.getSNode();
      cellForNewNode = cell;
      return applyLeftTransform(editorContext, head, smallPattern, cellForNewNode, newNode, true);
    } else if (canCompleteSmallPatternImmediatelyLeft(info, head, smallPattern) &&
        !canCompleteTheWholeStringImmediately(info, head + smallPattern)) {
      final SubstituteAction substituteAction = info.getMatchingActions(smallPattern, true).get(0);
      newNode = substituteAction.substitute(editorContext, smallPattern);
      if (newNode == null) {
        newNode = editorContext.getSelectedNode();
      }
      if (newNode == null) {
        return true;
      }

      cellForNewNode = findNodeCell(editorContext, newNode);
      return applyLeftTransform(editorContext, head, smallPattern, cellForNewNode, newNode, false);
    } else if (canCompleteTheWholeStringImmediately(info, head + smallPattern)) {

      List<SubstituteAction> matchingActions = info.getMatchingActions(head + smallPattern, true);
      SubstituteAction item = matchingActions.get(0);
      item.substitute(editorContext, head + smallPattern);
      return true;
    }
    return false;
  }

  private static boolean applyLeftTransform(EditorContext editorContext, final String head, String smallPattern, final EditorCell cellForNewNode, SNode newNode,
      boolean sourceCellRemains) {
    CellAction ltAction =
        editorContext.getEditorComponent().getActionHandler().getApplicableCellAction(CellFinderUtil.findFirstSelectableLeaf(cellForNewNode, true),
            CellActionType.LEFT_TRANSFORM);
    boolean hasSideActions = hasSideActions(cellForNewNode, Side.LEFT, head);

    if (ltAction == null || !hasSideActions) {
      CellInfo cellInfo = cellForNewNode.getCellInfo();
      if (!sourceCellRemains) {
        return putTextInErrorChild(cellInfo, head + smallPattern, editorContext);
      } else {
        return false;
      }
    }

    if (sourceCellRemains) {
      ((EditorCell_Label) cellForNewNode).changeText(smallPattern);
    }

    ltAction.execute(editorContext);

    final EditorCell_Label ltCell = prepareSTCell(editorContext, newNode, head);
    if (ltCell instanceof EditorCell_STHint && canCompleteSmallPatternImmediately(ltCell.getSubstituteInfo(), head, "")) {
      ltCell.getSubstituteInfo().getMatchingActions(head, true).get(0).substitute(editorContext, head);
    }
    return true;
  }

  public static String trimLeft(String text) {
    for (int i = 0; i < text.length(); i++) {
      if (!Character.isWhitespace(text.charAt(i))) {
        return text.substring(i);
      }
    }
    return "";
  }

  private static boolean canCompleteSmallPatternImmediatelyLeft(SubstituteInfo info, String head, String smallPattern) {
    return info.hasExactlyNActions(smallPattern, true, 1) && info.hasExactlyNActions(head + smallPattern, false, 0);
  }

  private static boolean canCompleteSmallPatternImmediately(SubstituteInfo info, String smallPattern, String tail) {
    if ("".equals(tail)) {
      return info.hasExactlyNActions(smallPattern, true, 1) && info.hasExactlyNActions(smallPattern, false, 1);
    }

    // * has special meaning in completion patterns but we often want to complete multiplication
    // operations
    return info.hasExactlyNActions(smallPattern, true, 1) && (tail.equals("*") || info.hasExactlyNActions(smallPattern + tail, false, 0));
  }

  private static boolean canCompleteTheWholeStringImmediately(SubstituteInfo info, String pattern) {
    return info.hasExactlyNActions(pattern, true, 1) && info.hasExactlyNActions(pattern, false, 1);
  }

  private static boolean isInAmbigousPosition(SubstituteInfo info, String smallPattern, String tail) {
    return info.getMatchingActions(smallPattern, true).size() > 1 && info.getMatchingActions(smallPattern + tail, false).isEmpty();
  }

  private static boolean isInOneStepAmbigousPosition(SubstituteInfo info, String smallPattern) {
    return info.getMatchingActions(smallPattern, true).size() > 1 &&
        info.getMatchingActions(smallPattern, true).size() == info.getMatchingActions(smallPattern, false).size();
  }

  private static EditorCell_Label prepareSTCell(EditorContext context, SNode transformingNode, String textToSet) {
    EditorCell_Label rtCell = EditorCell_STHint.getSTHintCell(transformingNode, context.getEditorComponent());
    if (rtCell == null) {
      EditorCell selectedCell = context.getSelectedCell();
      if (selectedCell instanceof EditorCell_Label && selectedCell.isErrorState()) {
        rtCell = (EditorCell_Label) selectedCell;
      } else {
        return null;
      }
    }
    rtCell.changeText(textToSet);
    rtCell.end();

    return rtCell;
  }

  private static EditorCell_Label prepareRTCell(EditorContext context, SNode node, String textToSet) {
    EditorCell root = findNodeCell(context, node);
    if (root == null) {
      return null;
    }
    return prepareSTCell(context, node, textToSet);
  }

  private static boolean putTextInErrorChild(CellInfo cellInfo, String textToSet, EditorContext editorContext) {
    editorContext.flushEvents();
    EditorComponent component = (EditorComponent) editorContext.getEditorComponent();
    EditorCell cellToSelect = cellInfo.findCell(component);
    if (cellToSelect != null) {
      EditorCell_Label label = CellFinderUtil.findFirstError(cellToSelect, true);
      if (label != null && label != cellToSelect && label.isEditable() && !(label instanceof EditorCell_Constant)) {
        label.changeText(textToSet);
        label.end();
        return true;
      }
    }
    return false;
  }

  private static boolean hasSideActions(EditorCell cell, Side side, String prefix) {
    SubstituteInfo info = new SideTransformSubstituteInfo(cell, side);
    return !info.hasExactlyNActions(prefix, false, 0);
  }

  private static void activateNodeSubstituteChooser(EditorContext editorContext, EditorCell cell, SubstituteInfo info) {
    ((EditorComponent) editorContext.getEditorComponent()).activateNodeSubstituteChooser(cell, info, false);
  }

  private static EditorCell findNodeCell(EditorContext editorContext, SNode newNode) {
    return editorContext.getEditorComponent().findNodeCell(newNode);
  }
}
