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

import com.intellij.openapi.command.CommandProcessor;
import jetbrains.mps.editor.runtime.TextBuilderImpl;
import jetbrains.mps.editor.runtime.cells.AbstractCellAction;
import jetbrains.mps.editor.runtime.cells.CaretState;
import jetbrains.mps.editor.runtime.commands.EditorComputable;
import jetbrains.mps.editor.runtime.style.Padding;
import jetbrains.mps.editor.runtime.style.StyleAttributes;
import jetbrains.mps.editor.runtime.style.StyleAttributesUtil;
import jetbrains.mps.ide.datatransfer.CopyPasteUtil;
import jetbrains.mps.ide.datatransfer.TextPasteUtil;
import jetbrains.mps.nodeEditor.CellSide;
import jetbrains.mps.nodeEditor.IntelligentInputUtil;
import jetbrains.mps.nodeEditor.cellMenu.NodeSubstitutePatternEditor;
import jetbrains.mps.nodeEditor.selection.EditorCellLabelSelection;
import jetbrains.mps.nodeEditor.ui.InputMethodListenerImpl;
import jetbrains.mps.openapi.editor.EditorContext;
import jetbrains.mps.openapi.editor.TextBuilder;
import jetbrains.mps.openapi.editor.cells.CellAction;
import jetbrains.mps.openapi.editor.cells.CellActionType;
import jetbrains.mps.openapi.editor.cells.CellTraversalUtil;
import jetbrains.mps.openapi.editor.cells.SubstituteInfo;
import jetbrains.mps.openapi.editor.cells.optional.WithCaret;
import jetbrains.mps.openapi.editor.selection.MultipleSelection;
import jetbrains.mps.openapi.editor.selection.SelectionManager;
import jetbrains.mps.smodel.SNodeUndoableAction;
import jetbrains.mps.smodel.UndoHelper;
import jetbrains.mps.smodel.UndoRunnable;
import jetbrains.mps.util.EqualUtil;
import jetbrains.mps.util.NameUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.module.ModelAccess;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.InputMethodEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;

public abstract class EditorCell_Label extends EditorCell_Basic implements jetbrains.mps.openapi.editor.cells.EditorCell_Label, WithCaret {
  protected boolean myNoTextSet;
  protected TextLine myTextLine;
  protected TextLine myNullTextLine;
  protected CaretState myCaretState = new CaretState();

  protected EditorCell_Label(@NotNull jetbrains.mps.openapi.editor.EditorContext editorContext, SNode node, String text) {
    super(editorContext, node);
    myTextLine = new TextLine("", getStyle(), false);
    myNullTextLine = new TextLine("", getStyle(), true);

    myTextLine.setCaretEnabled(true);
    myNullTextLine.setCaretEnabled(true);
    setText(text);

    setAction(CellActionType.LEFT, new MoveLeft(false));
    setAction(CellActionType.RIGHT, new MoveRight(false));

    setAction(CellActionType.LOCAL_HOME, new LocalHome(false));
    setAction(CellActionType.LOCAL_END, new LocalEnd(false));

    setAction(CellActionType.SELECT_RIGHT, new MoveRight(true));
    setAction(CellActionType.SELECT_LEFT, new MoveLeft(true));

    setAction(CellActionType.SELECT_HOME, new SelectHome());
    setAction(CellActionType.SELECT_END, new SelectEnd());

    setAction(CellActionType.SELECT_LOCAL_HOME, new LocalHome(true));
    setAction(CellActionType.SELECT_LOCAL_END, new LocalEnd(true));

    setAction(CellActionType.COPY, new CopyLabelText());
    setAction(CellActionType.PASTE, new PasteIntoLabelText());
    setAction(CellActionType.CUT, new CutLabelText());
    setAction(CellActionType.CLEAR_SELECTION, new ClearSelection());
  }

  @Override
  public boolean isFirstPositionInBigCell() {
    return CellTraversalUtil.getFirstLeaf(CellTraversalUtil.getContainingBigCell(this)) == this && isFirstCaretPosition();
  }

  @Override
  public boolean isLastPositionInBigCell() {
    return CellTraversalUtil.getLastLeaf(CellTraversalUtil.getContainingBigCell(this)) == this && isLastCaretPosition() &&
           !getTextLine().hasNonTrivialSelection();
  }

  public boolean canPasteText() {
    return true;
  }

  @Override
  public void setSelected(boolean selected) {
    super.setSelected(selected);
    if (!selected && !getEditor().selectionStackContains(this)) {
      myTextLine.resetSelection();
    }
    myCaretState.touch(selected);
  }

  @Override
  public String getText() {
    return myTextLine.getText();
  }

  public String getNullText() {
    return myNullTextLine.getText();
  }

  public String getRenderedText() {
    return getRenderedTextLine().getText();
  }

  public Font getFont() {
    return getRenderedTextLine().getFont();
  }

  public void setTextColor(Color color) {
    getStyle().set(StyleAttributes.TEXT_COLOR, color);
  }

  public void setNullTextColor(Color color) {
    getStyle().set(StyleAttributes.NULL_TEXT_COLOR, color);
  }

  public void setTextBackgroundColor(Color color) {
    getStyle().set(StyleAttributes.TEXT_BACKGROUND_COLOR, color);
  }

  public void setNullTextBackgroundColor(Color color) {
    getStyle().set(StyleAttributes.NULL_TEXT_BACKGROUND_COLOR, color);
  }

  public void setSelectedTextBackgroundColor(Color color) {
    getStyle().set(StyleAttributes.SELECTED_TEXT_BACKGROUND_COLOR, color);
  }

  public void setNullSelectedTextBackgroundColor(Color color) {
    getStyle().set(StyleAttributes.NULL_SELECTED_TEXT_BACKGROUND_COLOR, color);
  }

  @Override
  public int getCaretPosition() {
    return myTextLine.getCaretPosition();
  }

  @Override
  public void setCaretPosition(int position) {
    setCaretPosition(position, false);
  }

  public void setCaretPositionIfPossible(int position) {
    if (isCaretPositionAllowed(position)) {
      setCaretPosition(position, false);
    }
  }

  public void setCaretPosition(int position, boolean selection) {
    assert isCaretPositionAllowed(position) : "Position " + position + " is not allowed for EditorCell_Label: \"" + myTextLine.getText() + "\"";
    repaintCaret();
    myTextLine.setCaretPosition(position, selection);
    myCaretState.touch();
    repaintCaret();
  }

  public boolean isCaretPositionAllowed(int position) {
    if (!StyleAttributesUtil.isFirstPositionAllowed(getStyle()) && position == 0) {
      return false;
    }
    if (!StyleAttributesUtil.isLastPositionAllowed(getStyle()) && position == myTextLine.getText().length()) {
      return false;
    }
    return position >= 0 && position <= myTextLine.getText().length();
  }

  @Override
  public void home() {
    int textLength = getText().length();
    if (StyleAttributesUtil.isFirstPositionAllowed(getStyle())) {
      if (textLength > 0 || StyleAttributesUtil.isLastPositionAllowed(getStyle())) {
        setCaretPosition(0);
      }
    } else {
      if (textLength > 0 && (textLength > 1 || StyleAttributesUtil.isLastPositionAllowed(getStyle()))) {
        setCaretPosition(1);
      }
    }
  }

  @Override
  public void end() {
    int textLength = getText().length();
    if (StyleAttributesUtil.isLastPositionAllowed(getStyle())) {
      if (textLength > 0 || StyleAttributesUtil.isFirstPositionAllowed(getStyle())) {
        setCaretPosition(getText().length());
      }
    } else {
      if (textLength > 0 && (textLength > 1 || StyleAttributesUtil.isFirstPositionAllowed(getStyle()))) {
        setCaretPosition(getText().length() - 1);
      }
    }
  }

  @Override
  public boolean isSelectable() {
    if (!super.isSelectable()) {
      return false;
    }

    if (getText() == null || getText().length() == 0) {
      return StyleAttributesUtil.isFirstPositionAllowed(getStyle()) && StyleAttributesUtil.isLastPositionAllowed(getStyle());
    }

    if (getText().length() == 1) {
      return StyleAttributesUtil.isFirstPositionAllowed(getStyle()) || StyleAttributesUtil.isLastPositionAllowed(getStyle());
    }

    return true;
  }

  @Override
  public boolean isFirstCaretPosition() {
    if (!StyleAttributesUtil.isFirstPositionAllowed(getStyle())) {
      return getCaretPosition() == 1;
    } else {
      return getCaretPosition() == 0;
    }
  }

  @Override
  public boolean isLastCaretPosition() {
    if (!StyleAttributesUtil.isLastPositionAllowed(getStyle())) {
      return getCaretPosition() == getText().length() - 1;
    } else {
      return getCaretPosition() == getText().length();
    }
  }

  @Override
  public boolean isCaretEnabled() {
    return myTextLine.isCaretEnabled();
  }

  @Override
  public void setCaretEnabled(boolean enabled) {
    myTextLine.setCaretEnabled(enabled);
  }

  public void setText(String text) {
    myNoTextSet = (text == null || text.length() == 0);
    myTextLine.setText(myNoTextSet ? null : text);
    requestRelayout();
  }

  public void setDefaultText(String text) {
    myNullTextLine.setText(text);
  }

  @Override
  public int getEffectiveWidth() {
    return getTextLineWidth();
  }

  @Override
  public int getLeftInset() {
    return getRenderedTextLine().getPaddingLeft() + myGapLeft;
  }

  @Override
  public int getRightInset() {
    return getRenderedTextLine().getPaddingRight() + myGapRight;
  }

  @Override
  public int getTopInset() {
    return getRenderedTextLine().getPaddingTop();
  }

  @Override
  public int getBottomInset() {
    return getRenderedTextLine().getPaddingBottom();
  }

  public int getTextLineWidth() {
    int textLineWidth;
    if (myNoTextSet && myTextLine.getText().length() == 0) {
      textLineWidth = myNullTextLine.getEffectiveWidth();
    } else {
      textLineWidth = myTextLine.getEffectiveWidth();
    }
    if (isDrawBrackets()) {
      textLineWidth += 2 * BRACKET_WIDTH;
    }
    return textLineWidth;
  }

  public boolean isEditable() {
    return getStyle().get(StyleAttributes.EDITABLE);
  }

  public void setEditable(boolean editable) {
    getStyle().set(StyleAttributes.EDITABLE, editable);
  }

  @Override
  public void setErrorState(boolean errorState) {
    super.setErrorState(errorState);
    if (errorState) {
      myTextLine.showErrorColor();
      myNullTextLine.showErrorColor();
    } else {
      myTextLine.showTextColor();
      myNullTextLine.showTextColor();
    }
  }

  @Override
  public void relayoutImpl() {
    if (isPunctuationLayout()) {
      getStyle().set(StyleAttributes.PADDING_LEFT, new Padding(0.0));
      getStyle().set(StyleAttributes.PADDING_RIGHT, new Padding(0.0));
    }

    if (myNoTextSet && myTextLine.getText().length() == 0) {
      myNullTextLine.relayout();
      myHeight = myNullTextLine.getHeight();
      myWidth = myNullTextLine.getWidth();
    } else {
      myTextLine.relayout();
      myHeight = myTextLine.getHeight();
      myWidth = myTextLine.getWidth();
    }
  }

  @Override
  public void switchCaretVisible() {
    myCaretState.tick();
    repaintCaret();
  }

  @Override
  public void setCaretVisible(boolean visible) {
    myCaretState.touch(visible);
    repaintCaret();
  }

  private void repaintCaret() {
    getRenderedTextLine().repaintCaret(getEditor(), getShiftX(), getY());
  }

  @Override
  protected boolean isSelectionPainted() {
    return isSelected() && getEditor().getSelectionManager().getSelection() instanceof MultipleSelection;
  }

  @Override
  protected void paintContent(Graphics g, ParentSettings parentSettings) {
    TextLine textLine = getRenderedTextLine();
    boolean toShowCaret = toShowCaret();
    boolean selected = isSelectionPaintedOnAncestor(parentSettings).isSelectionPainted();
    textLine.setSelected(selected);
    textLine.setShowCaret(toShowCaret);
    Color cellFontColor = getEditor().getAdditionalCellFontColor(this);
    textLine.paint(g, getShiftX(), myY, cellFontColor);
  }

  @Override
  public void paintSelection(Graphics g, Color c, boolean drawBorder, ParentSettings parentSettings) {
    if (!isSelectionPaintedOnAncestor(parentSettings).isSelectionPainted() && getEditor().getAdditionalCellFontColor(this) != null) {
      /*
       * Suppressing selection painting in case this cell is not actually selected and additionalCellFontColor() for it is not null.
       * This will hide messages feedback if there is an AdditionalPainter instance (with specified cellFontColor) covering this cell.
       * Probably it's good idea to use separate property (not cellFontColor) to determine if this AdditionalPainter is "hiding" messages feedback
       * or simply let some additional painters paint background below and above editor messages.
       */
      return;
    }
    super.paintSelection(g, c, drawBorder, parentSettings);
  }

  protected boolean toShowCaret() {
    return myCaretState.isVisible() && isWithinSelection();
  }

  TextLine getTextLine() {
    return myTextLine;
  }

  TextLine getNullTextLine() {
    return myNullTextLine;
  }

  public TextLine getRenderedTextLine() {
    TextLine textLine;
    if (myNoTextSet && myTextLine.getText().length() == 0) {
      textLine = myNullTextLine;
    } else {
      textLine = myTextLine;
    }
    return textLine;
  }

  @Override
  public void setCaretX(int x) {
    myTextLine.setCaretByXCoord(x - getShiftX());
    makePositionValid();
  }

  @Override
  public int getCaretX() {
    return myTextLine.getCaretX(getShiftX());
  }

  private int getShiftX() {
    return isDrawBrackets() ? myX + myGapLeft + BRACKET_WIDTH : myX + myGapLeft;
  }

  private void makePositionValid() {
    if (myTextLine.getCaretPosition() == 0 && !StyleAttributesUtil.isFirstPositionAllowed(getStyle()) && isCaretPositionAllowed(1)) {
      setCaretPosition(1);
    }
    if (myTextLine.getCaretPosition() == getText().length() && !StyleAttributesUtil.isLastPositionAllowed(getStyle()) &&
        isCaretPositionAllowed(getText().length() - 1)) {
      setCaretPosition(getText().length() - 1);
    }
    getEditor().getBracesHighlighter().updateBracesSelection(this);
  }

  @Override
  public boolean processMousePressed(MouseEvent e) {
    setCaretX(e.getX());
    getEditor().repaint(this);
    return true;
  }

  public void ensureCaretVisible() {
    getEditor().scrollRectToVisible(new Rectangle(getCaretX() - 2 * myTextLine.charWidth(), myY, 4 * myTextLine.charWidth(), myHeight));
  }

  @Override
  protected boolean doProcessKeyTyped(final KeyEvent keyEvent, final boolean allowErrors) {
    if (!isTextTypedEvent(keyEvent)) {
      return false;
    }

    int caretPosition = getCaretPosition();
    CellSide side;
    if (caretPosition == 0) {
      side = CellSide.LEFT;
    } else if (caretPosition == getRenderedText().length()) {
      side = CellSide.RIGHT;
    } else {
      side = null;
    }

    ModelAccess modelAccess = getContext().getRepository().getModelAccess();
    String text = String.valueOf(keyEvent.getKeyChar());
    if (isEditable()) {
      ModifyTextCommand keyTypedCommand = new ModifyTextCommand(text, allowErrors, side, getContext());
      modelAccess.executeCommand(keyTypedCommand);
      getEditor().relayout();
      return keyTypedCommand.getResult();
    } else if (side != null) {
      String pattern = getUpdatedText(text);
      if (!pattern.equals(getRenderedText())) {
        return IntelligentInputUtil.processCell(this, getContext(), pattern, side);
      }
    }
    return false;
  }

  @Override
  public boolean processTextChanged(InputMethodEvent e) {
    if (!isEditable()) {
      return false;
    }
    ModelAccess modelAccess = getContext().getRepository().getModelAccess();

    if (!myTextLine.hasNonTrivialSelection()) {
      // selecting last symbol entered by user in order to replace if with the result fo input method processing
      int caretPosition = getCaretPosition();
      if (isCaretPositionAllowed(caretPosition - 1)) {
        setCaretPosition(caretPosition - 1, true);
      }
    }
    ModifyTextCommand keyTypedCommand = new ModifyTextCommand(InputMethodListenerImpl.getText(e), true, null, getContext());
    modelAccess.executeCommand(keyTypedCommand);
    if (keyTypedCommand.getResult()) {
      getEditor().relayout();
    }
    return keyTypedCommand.getResult();
  }

  private void addChangeTextUndoableAction() {
    UndoHelper.getInstance().addUndoableAction(new DummyUndoableAction(getSNode()));
  }

  @Override
  public boolean executeTextAction(CellActionType type, boolean allowErrors) {
    assert type == CellActionType.DELETE || type == CellActionType.BACKSPACE;
    if (!isEditable()) {
      return false;
    }
    // TODO: check if we need command here or we can execute command from UI action...
    ProcessTextActionCommand textAction = new ProcessTextActionCommand(getContext(), type, allowErrors);
    getContext().getRepository().getModelAccess().executeCommand(textAction);
    return textAction.getResult();
  }

  /**
   * @param replacingText the text, replacing selected fragment
   * @return the string contained in myTextLine, updated by replacing selected text fragment with the replacingText parameter
   */
  private String getUpdatedText(String replacingText) {
    String currentText = myTextLine.getText();
    int startSelection = myTextLine.getStartTextSelectionPosition();
    int endSelection = myTextLine.getEndTextSelectionPosition();
    return currentText.substring(0, startSelection) + replacingText + currentText.substring(endSelection);
  }

  private boolean canDeleteFrom(jetbrains.mps.openapi.editor.cells.EditorCell cell) {
    if (getText().length() == 0) {
      return false;
    }
    if (!(cell instanceof EditorCell_Label)) {
      return false;
    }
    EditorCell_Label label = (EditorCell_Label) cell;
    return label.isEditable() && label.isSelectable();
  }

  private void deleteIfPossible(CellActionType actionType) {
    assert CellActionType.DELETE == actionType || CellActionType.BACKSPACE == actionType;
    if ("".equals(getText()) && getStyle().get(StyleAttributes.AUTO_DELETABLE)) {
      // TODO: just use delete action (do not call getSNode().delete()) in the end if acton was not found or is not applicable
      CellAction deleteAction = getEditorComponent().getActionHandler().getApplicableCellAction(this, actionType);
      if (deleteAction != null) {
        deleteAction.execute(getContext());
      }
    }
  }

  @Override
  public String getSelectedText() {
    return myTextLine.getTextuallySelectedText();
  }

  @Override
  public int getSelectionStart() {
    return myTextLine.getStartTextSelectionPosition();
  }

  @Override
  public void setSelectionStart(int position) {
    myTextLine.setStartTextSelectionPosition(position);
  }

  @Override
  public int getSelectionEnd() {
    return myTextLine.getEndTextSelectionPosition();
  }

  @Override
  public void setSelectionEnd(int position) {
    myTextLine.setEndTextSelectionPosition(position);
  }

  public void deleteSelection() {
    String myText = myTextLine.getText();
    int stSel = myTextLine.getStartTextSelectionPosition();
    int endSel = myTextLine.getEndTextSelectionPosition();
    changeText(myText.substring(0, stSel) + myText.substring(endSel));
    myTextLine.setCaretPosition(stSel);
    addChangeTextUndoableAction();
    fireSelectionChanged();
    ensureCaretVisible();
  }

  public void changeText(final String text) {
    String oldText = getText();
    setText(text);
    updateVfsTimestamp(text, oldText);
  }

  private void updateVfsTimestamp(String text, String oldText) {
    if (EqualUtil.equals(oldText, text) || isValidText(text)) {
      return;
    }
    if (CommandProcessor.getInstance().getCurrentCommand() == null) {
      return;
    }
    getEditorComponent().touch();
  }

  public void insertText(String text) {
    int startSelectionPosition = myTextLine.getStartTextSelectionPosition();
    int endSelectionPosition = myTextLine.getEndTextSelectionPosition();
    if (startSelectionPosition >= endSelectionPosition) {
      startSelectionPosition = myTextLine.getCaretPosition();
      endSelectionPosition = myTextLine.getCaretPosition();
    }
    String oldText = getText();
    changeText(oldText.substring(0, startSelectionPosition) + text + oldText.substring(endSelectionPosition));
    myTextLine.setCaretPosition(startSelectionPosition);
    myTextLine.setCaretPosition(startSelectionPosition + text.length(), true);
    addChangeTextUndoableAction();
  }

  public boolean isValidText(String text) {
    return true;
  }

  public void setUnderlined(boolean b) {
    getStyle().set(StyleAttributes.UNDERLINED, b);
  }

  @Override
  public int getAscent() {
    return getRenderedTextLine().getAscent();
  }

  @Override
  public int getDescent() {
    return getRenderedTextLine().getDescent();
  }

  @Override
  public NodeSubstitutePatternEditor createSubstitutePatternEditor() {
    NodeSubstitutePatternEditor pattern = new NodeSubstitutePatternEditor();
    pattern.setText(getText());
    pattern.setCaretPosition(getCaretPosition());
    return pattern;
  }

  public void selectWordOrAll() {
    if (getTextLine().getStartTextSelectionPosition() != getTextLine().getEndTextSelectionPosition()) {
      selectAll();
      return;
    }

    int start = getPrevLocalHome(false);
    int end = getNextLocalEnd(false);

    if (start != end) {
      select(start, end);
    } else {
      selectAll();
    }

  }

  private void select(int start, int end) {
    assert start <= end;
    setSelectionStart(start);
    setSelectionEnd(end);
  }

  private int getNextLocalEnd(boolean skipLeadingSpaces) {
    int length = getText().length();
    assert getCaretPosition() <= length;
    for (int i = getCaretPosition(); i != length; ++i) {

      if (Character.isWhitespace(getText().charAt(i))) {
        if (skipLeadingSpaces) {
          if (i == length - 1 || !Character.isWhitespace(getText().charAt(i + 1))) {
            return i + 1;
          }
        } else {
          return i;
        }
      }
    }
    return length;
  }

  private int getPrevLocalHome(boolean skipLeadingSpaces) {
    assert getCaretPosition() >= 0;

    for (int i = getCaretPosition(); i >= 1; --i) {
      char c = getText().charAt(i - 1);
      if (Character.isWhitespace(c) && !skipLeadingSpaces) {
        return i;
      }

      if (!Character.isWhitespace(c)) {
        skipLeadingSpaces = false;
      }
    }
    return 0;
  }

  public void selectAll() {
    getTextLine().selectAll();
  }

  @Override
  public void deselectAll() {
    getTextLine().deselectAll();
  }

  public boolean isEverythingSelected() {
    return getTextLine().isEverythingSelected();
  }

  @Override
  public SubstituteInfo getSubstituteInfo() {
    SubstituteInfo substituteInfo = super.getSubstituteInfo();
    if (substituteInfo != null) {
      substituteInfo.setOriginalText(getText() == null || getText().equals("") ? getNullText() : getText());
    }
    return substituteInfo;
  }

  public String toString() {
    return NameUtil.shortNameFromLongName(getClass().getName()) + "[text=" + getText() + "]";
  }

  @Override
  public TextBuilder renderText() {
    return new TextBuilderImpl(getRenderedText());
  }

  public int getCharWidth() {
    return getRenderedTextLine().charWidth();
  }

  public String getTextBeforeCaret() {
    return myTextLine.getTextBeforeCaret();
  }

  public String getTextAfterCaret() {
    return myTextLine.getTextAfterCaret();
  }

  private void fireSelectionChanged() {
    getEditorComponent().getSelectionManager().setSelection(getEditorComponent().getSelectionManager().getSelection());
  }

  private boolean isTheOnlyCompletelySelectedLabelInBigCell() {
    jetbrains.mps.openapi.editor.cells.EditorCell containingBigCell = CellTraversalUtil.getContainingBigCell(this);
    return CellTraversalUtil.getFirstLeaf(containingBigCell) == this && CellTraversalUtil.getLastLeaf(containingBigCell) == this &&
           getText().equals(getSelectedText());
  }

  public String getCommandGroupId() {
    return getCellId() + "_" + String.valueOf(getSNodeId());
  }

  private class MoveLeft extends AbstractCellAction {
    private boolean myWithSelection;

    private MoveLeft(boolean withSelection) {
      myWithSelection = withSelection;
    }

    @Override
    public boolean canExecute(EditorContext context) {
      return isCaretPositionAllowed(getCaretPosition() - 1);
    }

    @Override
    public void execute(EditorContext context) {
      setCaretPosition(getCaretPosition() - 1, myWithSelection);
      fireSelectionChanged();
      ensureCaretVisible();
    }
  }

  private class MoveRight extends AbstractCellAction {
    private boolean myWithSelection;

    private MoveRight(boolean withSelection) {
      myWithSelection = withSelection;
    }

    @Override
    public boolean canExecute(EditorContext context) {
      return isCaretPositionAllowed(getCaretPosition() + 1);
    }

    @Override
    public void execute(EditorContext context) {
      setCaretPosition(getCaretPosition() + 1, myWithSelection);
      fireSelectionChanged();
      ensureCaretVisible();
    }
  }

  private class SelectHome extends AbstractCellAction {
    @Override
    public boolean canExecute(EditorContext context) {
      return isCaretPositionAllowed(0);
    }

    @Override
    public void execute(EditorContext context) {
      setCaretPosition(0, true);
      fireSelectionChanged();
      ensureCaretVisible();
    }
  }

  private class SelectEnd extends AbstractCellAction {
    @Override
    public boolean canExecute(EditorContext context) {
      return isCaretPositionAllowed(getText().length());
    }

    @Override
    public void execute(EditorContext context) {
      setCaretPosition(getText().length(), true);
      fireSelectionChanged();
      ensureCaretVisible();
    }
  }

  private class CopyLabelText extends AbstractCellAction {
    @Override
    public boolean canExecute(EditorContext context) {
      SelectionManager selectionManager = context.getEditorComponent().getSelectionManager();
      if (selectionManager.getSelection() instanceof EditorCellLabelSelection) {
        EditorCellLabelSelection labelSelection = (EditorCellLabelSelection) selectionManager.getSelection();
        return labelSelection.getEditorCellLabel().getSelectedText().length() > 0;
      }
      return false;
    }

    @Override
    public void execute(EditorContext context) {
      // TODO: use EditorCell_Label.this. instead..
      EditorCell_Label label = (EditorCell_Label) context.getSelectedCell();
      if (label.isTheOnlyCompletelySelectedLabelInBigCell()) {
        CopyPasteUtil.copyTextAndNodeToClipboard(label.getSelectedText(), getSNode());
      } else {
        CopyPasteUtil.copyTextToClipboard(label.getSelectedText());
      }
    }
  }

  private class LocalHome extends AbstractCellAction {
    private boolean mySelect;

    private LocalHome(boolean select) {
      mySelect = select;
    }

    @Override
    public boolean canExecute(EditorContext context) {
      return !isFirstCaretPosition() && (StyleAttributesUtil.isFirstPositionAllowed(getStyle()) || getPrevLocalHome(true) != 0);
    }

    @Override
    public void execute(EditorContext context) {
      setCaretPosition(getPrevLocalHome(true), mySelect);
    }

  }

  private class LocalEnd extends AbstractCellAction {
    private boolean mySelect;

    private LocalEnd(boolean select) {
      mySelect = select;
    }

    @Override
    public boolean canExecute(EditorContext context) {
      return !isLastCaretPosition() && (StyleAttributesUtil.isLastPositionAllowed(getStyle()) || getNextLocalEnd(true) != getText().length());
    }

    @Override
    public void execute(EditorContext context) {
      setCaretPosition(getNextLocalEnd(true), mySelect);
    }
  }

  private class ClearSelection extends AbstractCellAction {

    @Override
    public boolean canExecute(EditorContext context) {
      return myTextLine.hasNonTrivialSelection();
    }

    @Override
    public void execute(EditorContext context) {
      myTextLine.resetSelection();
    }
  }

  private class PasteIntoLabelText extends AbstractCellAction {
    @Override
    public boolean canExecute(EditorContext context) {
      if (!(context.getSelectedCell() instanceof EditorCell_Label)) {
        return false;
      }
      // TODO: use EditorCell_Label.this. instead..
      EditorCell_Label label = (EditorCell_Label) context.getSelectedCell();
      SNode node = label.getSNode();

      // If selected cell is:
      // - the only completely selected label in big cell
      // - the cursor is in the beginning of this cell
      // - the cursor is in the end of this cell
      // then we paste text into the cell only if it is on top of clipboard (text was copied from another cell)
      // otherwise in this case this action will not be applicable, so node paste action should perform actual pasting
      //
      // if non of above is true then just pasting text from the clipboard into this cell (e.g. you can copy 1 + 2 and
      // paste it into the name label).
      return node != null && label.canPasteText() && label.isEditable() &&
             (label.isTheOnlyCompletelySelectedLabelInBigCell() || isFirstCaretPosition() && !getTextLine().hasNonTrivialSelection() ||
              isLastCaretPosition() && !getTextLine().hasNonTrivialSelection() ? CopyPasteUtil.isStringOnTopOfClipboard() :
              TextPasteUtil.hasStringInClipboard());
    }

    @Override
    public void execute(EditorContext context) {
      EditorCell_Label cell = (EditorCell_Label) context.getSelectedCell();
      final String s = TextPasteUtil.getStringFromClipboard();
      cell.insertText(NameUtil.escapeInvisibleCharacters(s));
      fireSelectionChanged();
      cell.ensureCaretVisible();
    }
  }

  private class CutLabelText extends AbstractCellAction {
    @Override
    public boolean canExecute(EditorContext context) {
      SelectionManager selectionManager = context.getEditorComponent().getSelectionManager();
      if (selectionManager.getSelection() instanceof EditorCellLabelSelection) {
        EditorCellLabelSelection labelSelection = (EditorCellLabelSelection) selectionManager.getSelection();
        return labelSelection.getEditorCellLabel().getSelectedText().length() > 0;
      }
      return false;
    }

    @Override
    public void execute(EditorContext context) {
      // TODO: use EditorCell_Label.this. instead..
      EditorCell_Label label = (EditorCell_Label) context.getSelectedCell();
      if (label.isTheOnlyCompletelySelectedLabelInBigCell()) {
        CopyPasteUtil.copyTextAndNodeToClipboard(label.getSelectedText(), getSNode());
      } else {
        CopyPasteUtil.copyTextToClipboard(label.getSelectedText());
      }
      if (label.canPasteText()) {
        label.deleteSelection();
      }
    }
  }

  private class ProcessTextActionCommand extends EditorComputable<Boolean> implements UndoRunnable {

    private CellActionType myActionType;
    private boolean myAllowErrors;

    ProcessTextActionCommand(EditorContext context, CellActionType type, boolean allowErrors) {
      super(context);
      myActionType = type;
      myAllowErrors = allowErrors;
    }

    @Override
    public Boolean doCompute() {
      String oldText = myTextLine.getText();
      int caretPosition = myTextLine.getCaretPosition();

      if (myActionType == CellActionType.BACKSPACE) {
        if (myTextLine.hasNonTrivialSelection()) {
          deleteSelection();
          deleteIfPossible(myActionType);
          return true;
        }

        if (caretPosition > 0) {
          String newText = oldText.substring(0, caretPosition - 1) + oldText.substring(caretPosition);
          if (!myAllowErrors && !isValidText(newText)) {
            return false;
          }
          changeText(newText);
          addChangeTextUndoableAction();
          if (!isCaretPositionAllowed(caretPosition - 1)) {
            return false;
          }
          setCaretPosition(caretPosition - 1);
          ensureCaretVisible();
          deleteIfPossible(myActionType);
          return true;
        } else {
          jetbrains.mps.openapi.editor.cells.EditorCell prevLeaf = CellTraversalUtil.getPrevLeaf(EditorCell_Label.this);
          if (myAllowErrors && canDeleteFrom(prevLeaf)) {
            EditorCell_Label label = (EditorCell_Label) prevLeaf;
            getEditor().changeSelection(label);
            label.end();
            label.executeTextAction(myActionType, true);
            return true;
          }
          return false;
        }
      } else if (myActionType == CellActionType.DELETE) {
        if (myTextLine.hasNonTrivialSelection()) {
          deleteSelection();
          deleteIfPossible(myActionType);
          return true;
        } else if (caretPosition < oldText.length()) {
          String newText = oldText.substring(0, caretPosition) + oldText.substring(caretPosition + 1);
          if (!myAllowErrors && !isValidText(newText)) {
            return false;
          }
          changeText(newText);
          addChangeTextUndoableAction();
          ensureCaretVisible();
          deleteIfPossible(myActionType);
          return true;
        } else {
          jetbrains.mps.openapi.editor.cells.EditorCell nextLeaf = CellTraversalUtil.getNextLeaf(EditorCell_Label.this);
          if (myAllowErrors && canDeleteFrom(nextLeaf)) {
            EditorCell_Label label = (EditorCell_Label) nextLeaf;
            getEditor().changeSelection(label);
            label.home();
            label.executeTextAction(myActionType, true);
            return true;
          }
          return false;
        }
      }

      return false;
    }

    @Nullable
    @Override
    public String getName() {
      return null;
    }

    @Nullable
    @Override
    public String getGroupId() {
      return getCommandGroupId();
    }

    @Override
    public boolean shallConfirmUndo() {
      return false;
    }
  }

  public class ModifyTextCommand extends EditorComputable<Boolean> implements UndoRunnable {
    private final String myReplacingText;
    private final boolean myAllowErrors;
    private final CellSide mySide;

    public ModifyTextCommand(String replacingText, boolean allowErrors, CellSide side, EditorContext context) {
      super(context);
      myReplacingText = replacingText;
      myAllowErrors = allowErrors;
      mySide = side;
    }

    @Override
    protected Boolean doCompute() {
      if (processMutableKeyTyped(myReplacingText, myAllowErrors)) {
        getContext().flushEvents();
        addChangeTextUndoableAction();

        if (isErrorState() && mySide != null && IntelligentInputUtil.processCell(EditorCell_Label.this, getContext(), getRenderedText(), mySide)) {
          /**
           * Resetting current command group ID if cell was side-transformed. In such situations
           * side-transforming command as well as char typing command should be separate part of
           * undo-redo process, not connected with eytyping events which are grouped together.
           */
          CommandProcessor.getInstance().setCurrentCommandGroupId(null);
        }
        return true;
      }
      return isErrorState() && mySide == CellSide.LEFT && " ".equals(myReplacingText);
    }

    private boolean processMutableKeyTyped(String replacement, final boolean allowErrors) {
      String newText = getUpdatedText(replacement);
      if (!allowErrors && !isValidText(newText)) {
        return false;
      }

      int startSelection = myTextLine.getStartTextSelectionPosition();
      changeText(newText);
      setCaretPositionIfPossible(startSelection + myReplacingText.length());
      myTextLine.resetSelection();
      fireSelectionChanged();
      ensureCaretVisible();
      return true;
    }

    @Nullable
    @Override
    public String getName() {
      return null;
    }

    @Nullable
    @Override
    public String getGroupId() {
      return getCommandGroupId();
    }

    @Override
    public boolean shallConfirmUndo() {
      return false;
    }
  }

  /**
   * This action can be used to introduce empty action into the stack of actions within UndoHelper
   * forcing it to add undoable command into IDEA undo stack: see {@link jetbrains.mps.ide.undo.WorkbenchUndoHandler}
   * flushCommand() method implementation. This method will not add {@link jetbrains.mps.ide.undo.SNodeIdeaUndoableAction}
   * action into IDEA undo stack if it has no own undoable actions.
   * <p/>
   * This is helpful in case of UI-only modifications performed upon the cells. For example, if the textual cell is modified
   * so it represents invalid value. In this case this value cannot be committed into the model and will stay in the editor
   * cell/memento objects only. Empty command in this case will add a "mark" in IDEA undo stack, so corresponding editor
   * memento will be restored on udo/redo of this empty command.
   */
  protected static class DummyUndoableAction extends SNodeUndoableAction {
    protected DummyUndoableAction(SNode node) {
      super(node);
    }

    @Override
    protected void doUndo() {
    }

    @Override
    protected void doRedo() {
    }
  }
}
