/*
 * Copyright 2003-2015 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.util.ui.UIUtil;
import gnu.trove.TIntObjectHashMap;
import gnu.trove.TIntProcedure;
import jetbrains.mps.editor.runtime.TextBuilderImpl;
import jetbrains.mps.editor.runtime.commands.EditorCommand;
import jetbrains.mps.editor.runtime.impl.CellUtil;
import jetbrains.mps.editor.runtime.impl.LayoutConstraints;
import jetbrains.mps.editor.runtime.style.StyleAttributes;
import jetbrains.mps.editor.runtime.style.StyleImpl;
import jetbrains.mps.lang.smodel.generator.smodelAdapter.AttributeOperations;
import jetbrains.mps.logging.Logger;
import jetbrains.mps.nodeEditor.EditorComponent;
import jetbrains.mps.nodeEditor.EditorManager;
import jetbrains.mps.nodeEditor.EditorMessage;
import jetbrains.mps.nodeEditor.EditorSettings;
import jetbrains.mps.nodeEditor.cellMenu.NodeSubstitutePatternEditor;
import jetbrains.mps.nodeEditor.cells.collections.Entry;
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.CellInfo;
import jetbrains.mps.openapi.editor.cells.CellMessagesUtil;
import jetbrains.mps.openapi.editor.cells.EditorCellContext;
import jetbrains.mps.openapi.editor.cells.KeyMap;
import jetbrains.mps.openapi.editor.cells.SubstituteAction;
import jetbrains.mps.openapi.editor.cells.SubstituteInfo;
import jetbrains.mps.openapi.editor.menus.transformation.TransformationMenuLookup;
import jetbrains.mps.openapi.editor.message.SimpleEditorMessage;
import jetbrains.mps.openapi.editor.style.Style;
import jetbrains.mps.smodel.IOperationContext;
import jetbrains.mps.smodel.ModelAccess;
import jetbrains.mps.smodel.SNodeLegacy;
import jetbrains.mps.smodel.adapter.MetaAdapterByDeclaration;
import jetbrains.mps.smodel.constraints.ModelConstraints;
import jetbrains.mps.util.Computable;
import jetbrains.mps.util.IterableUtil;
import jetbrains.mps.util.ListMap;
import org.apache.log4j.LogManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SConcept;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeId;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.model.SNodeUtil;
import org.jetbrains.mps.util.Condition;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.InputMethodEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

/**
 * Author: Sergey Dmitriev
 * Created Sep 14, 2003
 */
public abstract class EditorCell_Basic implements EditorCell, Entry<jetbrains.mps.openapi.editor.cells.EditorCell> {
  public static final Logger LOG = Logger.wrap(LogManager.getLogger(EditorCell_Basic.class));

  public static final int BRACKET_WIDTH = 7;

  private Map myUserObjects;

  protected int myX = 0;
  protected int myY = 0;
  protected int myWidth = 0;
  protected int myHeight = 0;
  private int myCaretX = 0;

  private boolean myErrorState;
  private boolean mySelected;

  @NotNull
  private final EditorContext myEditorContext;

  private EditorCell_Collection myParent = null;
  private SNode myNode;
  private SNodeId myNodeId;
  private SubstituteInfo mySubstituteInfo;
  private TransformationMenuLookup myTransformationMenuLookup;
  private TIntObjectHashMap<CellAction> myActionMap = new TIntObjectHashMap<CellAction>();

  private Style myStyle = new StyleImpl();

  private KeyMap myKeyMap;
  private String myCellId;
  private String myRole;
  private SNodeReference myLinkDeclarationPointer;
  private boolean myInTree;
  private boolean myIsReferenceCell = false;
  protected int myGapLeft;
  protected int myGapRight;

  private boolean myIsNeedRelayout = true;
  private boolean myBig;
  private EditorCellContext myCellContext;

  /**
   * {@link Entry} fields
   */
  private Entry<jetbrains.mps.openapi.editor.cells.EditorCell> myNext;
  private Entry<jetbrains.mps.openapi.editor.cells.EditorCell> myPrev;

  protected EditorCell_Basic(@NotNull EditorContext editorContext, SNode node) {
    myEditorContext = editorContext;
    myNode = node;
    myNodeId = node == null ? null : node.getNodeId();
  }

  @Override
  public EditorComponent getEditor() {
    return (EditorComponent) getContext().getEditorComponent();
  }

  @Override
  public jetbrains.mps.openapi.editor.EditorComponent getEditorComponent() {
    return getContext().getEditorComponent();
  }

  @Override
  public boolean isErrorState() {
    return myErrorState;
  }

  @Override
  public boolean isPunctuationLayout() {
    return LayoutConstraints.PUNCTUATION_LAYOUT_CONSTRAINT.getName().equals(getStyle().get(StyleAttributes.LAYOUT_CONSTRAINT));
  }

  @Override
  public void setErrorState(boolean errorState) {
    boolean wasState = myErrorState;
    myErrorState = errorState;

    if (myInTree && wasState != myErrorState) {
      if (myErrorState) {
        getEditor().getCellTracker().addErrorCell(this);
      } else {
        getEditor().getCellTracker().removeErrorCell(this);
      }
    }
  }

  @Override
  public boolean validate(boolean strict, boolean canActivatePopup) {
    SubstituteInfo substituteInfo = getSubstituteInfo();
    if (substituteInfo == null) {
      return false;
    }

    String pattern = "";
    if (this instanceof EditorCell_Label) {
      pattern = ((EditorCell_Label) this).getText();
    }

    if (pattern.equals("")) {
      return false;
    }

    List<SubstituteAction> matchingActions = substituteInfo.getMatchingActions(pattern, strict);
    return APICellAdapter.substituteIfPossible(this, canActivatePopup, pattern, matchingActions);
  }

  public boolean isDrawBrackets() {
    return getStyle().get(StyleAttributes.DRAW_BRACKETS);
  }

  /**
   * @deprecated since MPS 3.4 use:
   * <code>cell.getStyle().set(StyleAttributes.BACKGROUND_COLOR, color)</code>
   */
  @Deprecated
  @Override
  public void setCellBackgroundColor(Color color) {
    getStyle().set(StyleAttributes.BACKGROUND_COLOR, color);
  }

  /**
   * @deprecated since MPS 3.4 use:
   * <code>cell.getStyle().get(StyleAttributes.BACKGROUND_COLOR)</code>
   */
  @Deprecated
  @Override
  public Color getCellBackgroundColor() {
    return getStyle().get(StyleAttributes.BACKGROUND_COLOR);
  }

  public Color getBracketsColor() {
    return getStyle().get(StyleAttributes.BRACKETS_COLOR);
  }

  @Override
  public CellAction getAction(CellActionType type) {
    return myActionMap.get(type.ordinal());
  }

  @Override
  public Collection<CellActionType> getAvailableActions() {
    final Collection<CellActionType> result = new ArrayList<CellActionType>(myActionMap.size());
    myActionMap.forEachKey(new TIntProcedure() {
      @Override
      public boolean execute(int value) {
        return result.add(CellActionType.values()[value]);
      }
    });
    return result;
  }

  @Override
  public void setAction(CellActionType type, CellAction action) {
    myActionMap.put(type.ordinal(), action);
  }

  @Override
  public void addKeyMap(KeyMap keyMap) {
    if (myKeyMap != null) {
      myKeyMap.addKeyMap(keyMap);
    } else {
      myKeyMap = keyMap;
    }
  }

  @Override
  public KeyMap getKeyMap() {
    return myKeyMap;
  }

  @Override
  public SNode getSNode() {
    return myNode;
  }

  public final void setSNode(@NotNull SNode node) {
    myNode = node;
    myNodeId = node.getNodeId();
  }

  @NotNull
  protected SNodeId getSNodeId() {
    return myNodeId;
  }

  @Override
  public int getHeight() {
    return myHeight;
  }

  @Override
  public void setHeight(int height) {
    myHeight = height;
  }

  @Override
  public int getBottom() {
    return getY() + getHeight();
  }

  @Override
  public int getRight() {
    return getX() + getWidth();
  }

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

  @Override
  public int getLeftInset() {
    return 0;
  }

  @Override
  public int getRightInset() {
    return 0;
  }

  @Override
  public int getTopInset() {
    return 0;
  }

  @Override
  public int getBottomInset() {
    return 0;
  }

  @Override
  public int getWidth() {
    return myWidth;
  }

  @Override
  public void setWidth(int width) {
    myWidth = width;
  }

  @Override
  public int getY() {
    return myY;
  }

  @Override
  public void setY(int y) {
    if (myY == y) {
      return;
    }
    myY = y;
  }

  @Override
  public int getX() {
    return myX;
  }

  @Override
  public void setX(int x) {
    if (myX == x) {
      return;
    }
    myX = x;
  }

  @Override
  public boolean isSelected() {
    return mySelected;
  }

  public boolean isWithinSelection() {
    return mySelected && getEditor().getDeepestSelectedCell() == this;
  }


  @Override
  public boolean isSelectable() {
    return getStyle().get(StyleAttributes.SELECTABLE);
  }

  @Override
  public void setSelectable(boolean selectable) {
    getStyle().set(StyleAttributes.SELECTABLE, selectable);
  }

  @Override
  public void setCellId(@NotNull String cellId) {
    assert myCellId == null;
    myCellId = cellId;
  }

  @Override
  public String getCellId() {
    return myCellId;
  }

  @Override
  public String getRole() {
    String role = getStyle().get(StyleAttributes.NAVIGATABLE_REFERENCE);
    if (role != null) {
      return role;
    }
    return myRole;
  }

  @Override
  public void setRole(String role) {
    myRole = role;
  }

  @Override
  public void setSelected(boolean selected) {
    mySelected = selected;
  }

  public boolean isDrawBorder() {
    return getStyle().get(StyleAttributes.DRAW_BORDER);
  }

  @Override
  public Object getUserObject(Object key) {
    if (myUserObjects == null) {
      return null;
    }
    return myUserObjects.get(key);
  }

  @Override
  public void moveTo(int x, int y) {
    myX = x;
    myY = y;
  }

  @Override
  public void putUserObject(Object key, Object value) {
    if (myUserObjects == null) {
      myUserObjects = new ListMap();
    }
    myUserObjects.put(key, value);
  }

  @Override
  public EditorContext getContext() {
    return myEditorContext;
  }

  public IOperationContext getOperationContext() {
    return getContext().getOperationContext();
  }

  @Override
  public final boolean processKeyPressed(KeyEvent e, boolean allowErrors) {
    if (e.isConsumed()) {
      return false;
    }
    return doProcessKeyPressed(e, allowErrors);
  }

  protected boolean doProcessKeyPressed(KeyEvent e, boolean allowErrors) {
    return false;
  }

  @Override
  public final boolean processKeyTyped(KeyEvent e, boolean allowErrors) {
    if (e.isConsumed()) {
      return false;
    }
    return doProcessKeyTyped(e, allowErrors);
  }

  protected boolean doProcessKeyTyped(final KeyEvent e, final boolean allowErrors) {
    if (getSNode() == null || !isBig() || !isTextTypedEvent(e)) {
      return false;
    }

    if (ModelAccess.instance().runReadAction(new Computable<Boolean>() {
      @Override
      public Boolean compute() {
        return getSNode().getModel() != null && getSNode().getParent() == null;
      }
    })) {
      return false;
    }

    getContext().getRepository().getModelAccess().executeCommand(new EditorCommand(getContext()) {
      @Override
      public void doExecute() {
        EditorComponent editor = getEditor();
        SNode oldNode = getSNode();
        SNode newNode = replaceWithDefault();
        if (newNode == null) {
          EditorCell_Label editable = CellFinderUtil.findFirstEditable(EditorCell_Basic.this);
          if (editable != null) {
            editor.changeSelection(editable);
            editor.processKeyTyped(e);
          }
          return;
        }

        newNode.putUserObject(EditorManager.OLD_NODE_FOR_SUBSTITUTION, oldNode);
        EditorCell nodeCell = editor.findNodeCell(newNode);
        if (nodeCell == null) {
          return;
        }
        EditorCell_Label editable = CellFinderUtil.findFirstEditable(nodeCell);
        if (editable != null) {
          editor.changeSelection(editable);
          editor.processKeyTyped(e);
        } else {
          editor.changeSelection(nodeCell);
          editor.processKeyTyped(e);
        }
      }
    });

    return true;
  }

  @Override
  public boolean processTextChanged(InputMethodEvent e) {
    return false;
  }

  protected boolean isTextTypedEvent(KeyEvent e) {
    return UIUtil.isReallyTypedEvent(e);
  }

  private SNode replaceWithDefault() {
    SNode node = getSNode();
    while (AttributeOperations.isAttribute(node)) {
      node = node.getParent();
    }
    SNode link = new SNodeLegacy(node).getRoleLink();
    SNode concept = CellUtil.getLinkDeclarationTarget(link);
    SConcept concreteConcept = ModelConstraints.getDefaultConcreteConcept(MetaAdapterByDeclaration.getConcept(concept));
    if (node.getConcept().equals(concreteConcept)) {
      return null;
    }
    jetbrains.mps.smodel.SNode newNode = new jetbrains.mps.smodel.SNode(concreteConcept);
    SNodeUtil.replaceWithAnother(node, newNode);
    getContext().flushEvents();
    return newNode;
  }

  @Override
  public void setCaretX(int x) {
    myCaretX = x;
  }

  @Override
  public int getCaretX() {
    return myCaretX;
  }

  @Override
  public void home() {

  }

  @Override
  public void end() {

  }

  @Override
  public jetbrains.mps.openapi.editor.cells.EditorCell findLeaf(int x, int y) {
    if (getX() <= x && x < getX() + getWidth() && getY() <= y && y < getY() + getHeight()) {
      return this;
    } else {
      return null;
    }
  }

  @Override
  public jetbrains.mps.openapi.editor.cells.EditorCell findNearestLeafOnLine(int x, int y, Condition<jetbrains.mps.openapi.editor.cells.EditorCell> condition) {
    if (getY() <= y && y < getY() + getHeight() && condition.met(this)) {
      return this;
    } else {
      return null;
    }
  }

  /**
   * @deprecated since MPS 3.4 use {@link #findLeaf(int, int)} and check the condition upon returned cell
   */
  @Deprecated
  @Override
  public EditorCell findLeaf(int x, int y, Condition<EditorCell> condition) {
    if (myX <= x && x < myX + myWidth && myY <= y && y < myY + myHeight && condition.met(this)) {
      return this;
    }
    return null;
  }

  /**
   * @deprecated since MPS 3.4 use {@link #findNearestLeafOnLine(int, int, Condition)}
   * using {@link com.intellij.openapi.util.Conditions#TRUE} as a parameter
   */
  @Deprecated
  @Override
  public final EditorCell findCellWeak(int x, int y) {
    return findCellWeak(x, y, Condition.TRUE_CONDITION);
  }

  /**
   * @deprecated since MPS 3.4 use {@link #findNearestLeafOnLine(int, int, Condition)}
   */
  @Deprecated
  @Override
  public EditorCell findCellWeak(int x, int y, Condition<EditorCell> condition) {
    Set<EditorCell> candidates = new LinkedHashSet<EditorCell>();
    collectCellsWithY(this, y, candidates);

    EditorCell best = findClosestHorizontal(x, condition, candidates);

    if (best == null) {
      best = findClosestHorizontal(x, Condition.TRUE_CONDITION, candidates);
      if (best != null) {
        best = best.getPrevLeaf(condition);
      }
    }

    return best;
  }

  private EditorCell findClosestHorizontal(int x, Condition<? super EditorCell> condition, Set<EditorCell> candidates) {
    EditorCell best = null;
    int bestDistance = -1;
    for (EditorCell cell : candidates) {
      if (!condition.met(cell)) {
        continue;
      }

      int distance = horizontalDistance(x, cell);
      if (bestDistance == -1 || distance < bestDistance) {
        best = cell;
        bestDistance = distance;
      }
    }
    return best;
  }

  private int horizontalDistance(int x, EditorCell cell) {
    if (x >= cell.getX() && x <= cell.getX() + cell.getWidth()) {
      return 0;
    }
    return Math.min(Math.abs(x - cell.getX()), Math.abs(x - cell.getX() - cell.getWidth()));
  }

  private void collectCellsWithY(EditorCell current, int y, Set<EditorCell> cells) {
    collectCellsWithY(current, y, cells, true);
  }

  private void collectCellsWithY(EditorCell current, int y, Set<EditorCell> cells, boolean leafsOnly) {
    if (y >= current.getY() && y <= current.getY() + current.getHeight() && (!leafsOnly || current.isLeaf())) {
      cells.add(current);
    }

    if (current instanceof EditorCell_Collection) {
      for (EditorCell cell : ((EditorCell_Collection) current).getCells()) {
        collectCellsWithY(cell, y, cells);
      }
    }
  }

  @Override
  public EditorCell_Collection getParent() {
    return myParent;
  }

  @Override
  public boolean isSingleNodeCell() {
    if (myParent == null) {
      return true;
    }
    if (myParent.getSNode() != myNode) {
      return true;
    }
    if (myParent.getCellsCount() == 1) {
      return myParent.isSingleNodeCell();
    }
    return false;
  }

  void setParent(EditorCell_Collection parent) {
    myParent = parent;
  }

  @Override
  public boolean processMousePressed(MouseEvent e) {
    return false;
  }

  @Override
  public NodeSubstitutePatternEditor createSubstitutePatternEditor() {
    return new NodeSubstitutePatternEditor();
  }

  @Override
  public void setSubstituteInfo(SubstituteInfo info) {
    mySubstituteInfo = info;
  }

  @Override
  public SubstituteInfo getSubstituteInfo() {
    return mySubstituteInfo;
  }

  @Nullable
  public TransformationMenuLookup getTransformationMenuLookup() {
    return myTransformationMenuLookup;
  }

  public void setTransformationMenuLookup(@Nullable TransformationMenuLookup transformationMenuLookup) {
    myTransformationMenuLookup = transformationMenuLookup;
  }

  @Override
  public void setBig(boolean big) {
    myBig = big;
  }

  @Override
  public boolean isBig() {
    return myBig;
  }

  public void setReferenceCell(boolean isReference) {
    myIsReferenceCell = isReference;
  }

  @Override
  public boolean isReferenceCell() {
    return myIsReferenceCell;
  }

  @Override
  public void paint(Graphics g) {
    paintCell(g, ParentSettings.createDefaultSetting());
    paintDecorations(g);
  }

  @Override
  public void paintCell(Graphics g, ParentSettings parentSettings) {
    fillBackground(g, parentSettings);
    paintContent(g, parentSettings);
  }

  @Override
  public boolean isInClipRegion(Graphics g) {
    // The +1 for width and height takes into account decorations such as selection or border, which may currently be drawn outside the cell.
    return g.hitClip(getX(), getY(), getWidth() + 1, getHeight() + 1);
  }

  protected ParentSettings fillBackground(Graphics g, ParentSettings parentSettings) {
    ParentSettings settings = isSelectionPaintedOnAncestor(parentSettings);
    if (!settings.isSelectionPainted()) {
      if (!parentSettings.isSkipBackground()) {
        Color backgroundColor = getStyle().get(StyleAttributes.BACKGROUND_COLOR);
        if (backgroundColor != null) {
          g.setColor(backgroundColor);
          paintBackground(g);
        }
      }
      boolean hasMessages = false;
      for (EditorMessage message : CellMessagesUtil.getMessages(this, EditorMessage.class)) {
        if (message != null && message.showInEditor() && message.isBackground()) {
          message.paint(g, getEditor(), this);
          hasMessages = true;
        }
      }
      settings = ParentSettings.createBackgroundlessSetting(hasMessages).combineWith(parentSettings);
    }
    paintSelectionIfRequired(g, parentSettings);
    return settings;
  }

  protected ParentSettings isSelectionPaintedOnAncestor(ParentSettings parentSettings) {
    return ParentSettings.createSelectedSetting(isSelectionPainted()).combineWith(parentSettings);
  }

  protected void paintBackground(Graphics g) {
    g.fillRect(getX(), getY(), getWidth(), getHeight());
  }

  protected boolean isSelectionPainted() {
    return isSelected();
  }

  protected void paintSelectionIfRequired(Graphics g, ParentSettings parentSettings) {
    if (isSelectionPainted()) {
      paintSelection(g, getSelectionColor(), true, parentSettings);
    }
  }

  protected abstract void paintContent(Graphics g, ParentSettings parentSettings);

  public void paintDecorations(Graphics g) {
    int effectiveWidth = getEffectiveWidth();

    if (isDrawBorder()) {
      // COLORS: Remove hardcoded color
      g.setColor(Color.lightGray);
      g.drawRect(myX, myY, getWidth(), getHeight());
    }

    int leftInternalInset = getLeftInset();

    if (isDrawBrackets()) {
      g.setColor(getBracketsColor());

      // opening bracket
      g.fillRect(myX + leftInternalInset + 2, myY + 3, 2, myHeight - 5);
      g.fillRect(myX + leftInternalInset + 3, myY + 2, BRACKET_WIDTH - 3, 2);
      g.fillRect(myX + leftInternalInset + 3, myY + myHeight - 3, BRACKET_WIDTH - 3, 2);

      // closing bracket
      g.fillRect(myX + effectiveWidth - 3, myY + 3, 2, myHeight - 5);
      g.fillRect(myX + effectiveWidth - BRACKET_WIDTH + 1, myY + 2, BRACKET_WIDTH - 3, 2);
      g.fillRect(myX + effectiveWidth - BRACKET_WIDTH + 1, myY + myHeight - 3, BRACKET_WIDTH - 3, 2);
    }

    List<EditorMessage> messages = CellMessagesUtil.getMessages(this, EditorMessage.class);
    for (EditorMessage message : messages) {
      if (message != null && message.showInEditor() && !message.isBackground()) {
        message.paint(g, getEditor(), this);
      }
    }
  }

  @Override
  public List<SimpleEditorMessage> getMessages() {
    return getEditor().getHighlightManager().getMessages(this);
  }

  @Override
  public void synchronizeViewWithModel() {
  }

  @Override
  public void setBaseline(int y) {
    int relBaseline = getAscent();
    moveTo(myX, y - relBaseline);
  }

  @Override
  public int getBaseline() {
    return myY + getAscent();
  }

  @Override
  public int getAscent() {
    return myHeight;
  }

  @Override
  public int getDescent() {
    return myHeight - getAscent();
  }

  @Override
  public void paintSelection(Graphics g, Color c, boolean drawBorder) {
    paintSelection(g, c, drawBorder, ParentSettings.createDefaultSetting());
  }

  @Override
  public void paintSelection(Graphics g, Color c, boolean drawBorder, ParentSettings parentSettings) {
    g.setColor(c);
    g.fillRect(getX(), getY() /*+ getTopInset()*/, getWidth(), getHeight() - getTopInset() - getBottomInset());
    if (getEditor().hasFocus() && drawBorder) {
      g.setColor(c.darker());
      g.drawRect(getX(), getY(), getWidth(), getHeight());
    }
  }

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

  @Override
  public final void relayout() {
    if (!myIsNeedRelayout) {
      return;
    }
    boolean drawBrackets = isDrawBrackets();
    if (drawBrackets) {
      myX += BRACKET_WIDTH;
    }
    myX += myGapLeft;

    relayoutImpl();

    if (drawBrackets) {
      myX -= BRACKET_WIDTH;
      myWidth += 2 * BRACKET_WIDTH;
    }
    myX -= myGapLeft;
    myWidth += myGapLeft + myGapRight;
    myIsNeedRelayout = false;
  }

  protected void relayoutImpl() {

  }

  /**
   * @deprecated since MPS 3.4 some cells can implement {@link jetbrains.mps.openapi.editor.cells.optional.WithCaret}
   * interface in order to have this method.
   */
  @Deprecated
  @Override
  public void switchCaretVisible() {

  }

  @NotNull
  @Override
  public CellInfo getCellInfo() {
    return new DefaultCellInfo(this);
  }

  /**
   * @deprecated since MPS 3.4 not used
   */
  @Deprecated
  @Override
  public boolean isAncestorOf(EditorCell cell) {
    jetbrains.mps.openapi.editor.cells.EditorCell_Collection parent = cell.getParent();
    while (parent != null) {
      if (parent == this) {
        return true;
      }
      parent = parent.getParent();
    }
    return false;
  }

  public Color getSelectionColor() {
    return EditorSettings.getInstance().getSelectionBackgroundColor();
  }

  public static Color getRangeSelectionColor() {
    return EditorSettings.getInstance().getRangeSelectionForegroundColor();
  }

  /**
   * @deprecated since MPS 3.4 not used
   */
  @Deprecated
  @Override
  public Iterator<EditorCell_Collection> parents() {
    return new Iterator<EditorCell_Collection>() {
      private EditorCell myCurrentCell = EditorCell_Basic.this;

      @Override
      public boolean hasNext() {
        return myCurrentCell.getParent() != null;
      }

      @Override
      public EditorCell_Collection next() {
        EditorCell_Collection parent = (EditorCell_Collection) myCurrentCell.getParent();
        if (parent == null) {
          throw new NoSuchElementException();
        }
        myCurrentCell = parent;
        return parent;
      }

      @Override
      public void remove() {
        throw new UnsupportedOperationException();
      }
    };
  }

  /**
   * @deprecated since MPS 3.4 not used
   */
  @Deprecated
  @Override
  public EditorCell_Collection findParent(Condition<EditorCell_Collection> condition) {
    if (this instanceof EditorCell_Collection && condition.met((EditorCell_Collection) this)) {
      return (EditorCell_Collection) this;
    }
    for (EditorCell_Collection collection : IterableUtil.asIterable(parents())) {
      if (condition.met(collection)) {
        return collection;
      }
    }
    return null;
  }

  @Override
  public EditorCell getRootParent() {
    EditorCell cell = this;
    EditorCell prevCell = null;
    while (cell != null) {
      prevCell = cell;
      cell = (EditorCell) cell.getParent();
    }
    return prevCell;
  }

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

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

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

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

  /**
   * @deprecated since MPS 3.4 use {@link jetbrains.mps.openapi.editor.cells.CellTraversalUtil#getContainingBigCell(jetbrains.mps.openapi.editor.cells.EditorCell)}
   */
  @Deprecated
  @Override
  public EditorCell getContainingBigCell() {
    if (isBig() || getParent() == null) {
      return this;
    }
    return getParent().getContainingBigCell();
  }

  /**
   * @deprecated since MPS 3.4 use {@link GeometryUtil#isAbove(jetbrains.mps.openapi.editor.cells.EditorCell, jetbrains.mps.openapi.editor.cells.EditorCell)}
   * like: isAbove(this, cell)
   */
  @Deprecated
  @Override
  public boolean isAbove(EditorCell cell) {
    return GeometryUtil.isAbove(this, cell);
  }

  /**
   * @deprecated since MPS 3.4 use {@link GeometryUtil#isAbove(jetbrains.mps.openapi.editor.cells.EditorCell, jetbrains.mps.openapi.editor.cells.EditorCell)}
   * like: isAbove(cell, this)
   */
  @Deprecated
  @Override
  public boolean isBelow(EditorCell cell) {
    return GeometryUtil.isAbove(cell, this);
  }

  @Override
  public boolean isToLeft(EditorCell cell) {
    return GeometryUtil.isLeftToRight(this, cell);
  }

  @Override
  public boolean isToRight(EditorCell cell) {
    return cell.isToLeft(this);
  }

  /**
   * @deprecated since MPS 3.4 not used
   */
  @Deprecated
  @Override
  public EditorCell getUpper(Condition<EditorCell> condition, int baseX) {
    EditorCell bestMatch = null;
    EditorCell current = getPrevLeaf(condition);

    while (current != null) {
      if (GeometryUtil.isAbove(current, this)) {
        if (bestMatch != null && GeometryUtil.isAbove(current, bestMatch)) {
          break;
        }

        if (bestMatch != null) {
          if (GeometryUtil.getHorizontalDistance(bestMatch, baseX) > GeometryUtil.getHorizontalDistance(current, baseX)) {
            bestMatch = current;
          }
        } else {
          bestMatch = current;
        }
      }

      current = current.getPrevLeaf(condition);
    }

    return bestMatch;
  }

  /**
   * @deprecated since MPS 3.4 not used
   */
  @Deprecated
  @Override
  public EditorCell getLower(Condition<EditorCell> condition, int baseX) {
    EditorCell bestMatch = null;
    EditorCell current = getNextLeaf(condition);

    while (current != null) {
      if (current.isBelow(this)) {
        if (bestMatch != null && current.isBelow(bestMatch)) {
          break;
        }

        if (bestMatch != null) {
          if (GeometryUtil.getHorizontalDistance(bestMatch, baseX) > GeometryUtil.getHorizontalDistance(current, baseX)) {
            bestMatch = current;
          }
        } else {
          bestMatch = current;
        }
      }

      current = current.getNextLeaf(condition);
    }

    return bestMatch;
  }

  /**
   * @deprecated since MPS 3.4 not used
   */
  @Deprecated
  @Override
  public EditorCell getEndCell(Condition<EditorCell> condition) {
    EditorCell current = this;
    while (current.getLeafToRight(condition) != null) {
      current = current.getLeafToRight(condition);
    }
    return current.getLastLeaf(condition);
  }

  /**
   * @deprecated since MPS 3.4 not used
   */
  @Deprecated
  @Override
  public EditorCell getHomeCell(Condition<EditorCell> condition) {
    EditorCell current = this;
    while (current.getLeafToLeft(condition) != null) {
      current = current.getLeafToLeft(condition);
    }
    return current.getFirstLeaf();
  }

  /**
   * @deprecated since MPS 3.4 not used
   */
  @Deprecated
  @Override
  public EditorCell getLeafToLeft(Condition<EditorCell> condition) {
    return getPrevLeaf(new Condition<EditorCell>() {
      @Override
      public boolean met(EditorCell current) {
        return current.isSelectable() && !GeometryUtil.isAbove(EditorCell_Basic.this, current) && !isBelow(current) && isToRight(current);
      }
    });
  }

  /**
   * @deprecated since MPS 3.4 not used
   */
  @Deprecated
  @Override
  public EditorCell getLeafToRight(Condition<EditorCell> condition) {
    return getNextLeaf(new Condition<EditorCell>() {
      @Override
      public boolean met(EditorCell current) {
        return current.isSelectable() && !GeometryUtil.isAbove(EditorCell_Basic.this, current) && !isBelow(current) && isToLeft(current);
      }
    });
  }

  @Override
  public EditorCell getNextSibling() {
    return myNext != null ? (EditorCell) myNext.getItem() : null;
  }

  @Override
  public EditorCell getPrevSibling() {
    return myPrev != null && myPrev.getNext() != null ? (EditorCell) myPrev.getItem() : null;
  }

  /**
   * @deprecated since MPS 3.4 not used
   */
  @Deprecated
  @Override
  public EditorCell getNextLeaf() {
    if (getNextSibling() != null) {
      return getNextSibling().getFirstLeaf();
    }
    if (myParent != null) {
      return myParent.getNextLeaf();
    }
    return null;
  }

  /**
   * @deprecated since MPS 3.4 not used
   */
  @Deprecated
  @Override
  public EditorCell getNextLeaf(Condition<EditorCell> condition) {
    EditorCell current = getNextLeaf();
    while (current != null) {
      if (condition.met(current)) {
        return current;
      }
      current = current.getNextLeaf();
    }
    return null;
  }

  /**
   * @deprecated since MPS 3.4 not used
   */
  @Deprecated
  @Override
  public EditorCell getPrevLeaf() {
    if (getPrevSibling() != null) {
      return getPrevSibling().getLastLeaf();
    }
    if (myParent != null) {
      return myParent.getPrevLeaf();
    }
    return null;
  }

  /**
   * @deprecated since MPS 3.4 not used
   */
  @Deprecated
  @Override
  public EditorCell getPrevLeaf(Condition<EditorCell> condition) {
    EditorCell current = getPrevLeaf();
    while (current != null) {
      if (condition.met(current)) {
        return current;
      }
      current = current.getPrevLeaf();
    }
    return null;
  }

  /**
   * @deprecated since MPS 3.4 not used
   */
  @Deprecated
  @Override
  public EditorCell getFirstLeaf() {
    return this;
  }

  /**
   * @deprecated since MPS 3.4 not used
   */
  @Deprecated
  @Override
  public EditorCell getLastLeaf() {
    return this;
  }

  /**
   * @deprecated since MPS 3.4 not used
   */
  @Deprecated
  @Override
  public EditorCell getFirstLeaf(final Condition<EditorCell> condition) {
    EditorCell firstLeaf = getFirstLeaf();
    if (condition.met(firstLeaf)) {
      return firstLeaf;
    }
    return firstLeaf.getNextLeaf(new Condition<EditorCell>() {
      @Override
      public boolean met(EditorCell object) {
        return isAncestorOf(object) && condition.met(object);
      }
    });
  }

  /**
   * @deprecated since MPS 3.4 not used
   */
  @Deprecated
  @Override
  public EditorCell getLastLeaf(final Condition<EditorCell> condition) {
    EditorCell lastLeaf = getLastLeaf();
    if (condition.met(lastLeaf)) {
      return lastLeaf;
    }
    return lastLeaf.getPrevLeaf(new Condition<EditorCell>() {
      @Override
      public boolean met(EditorCell object) {
        return isAncestorOf(object) && condition.met(object);
      }
    });
  }

  @Override
  public EditorCell getLastChild() {
    return this;
  }

  @Override
  public EditorCell getFirstChild() {
    return this;
  }

  @Override
  public Style getStyle() {
    return myStyle;
  }

  @Override
  public boolean isLeaf() {
    return true;
  }

  public boolean isInTree() {
    return myInTree;
  }

  public void onAdd() {
    myInTree = true;

    if (isErrorState()) {
      getEditor().getCellTracker().addErrorCell(this);
    }
  }

  public void onRemove() {
    myInTree = false;

    if (isErrorState()) {
      getEditor().getCellTracker().removeErrorCell(this);
    }
  }

  @Override
  public void setLeftGap(int gap) {
    // TODO: remove this line and modify getEffectiveWidth() method in order to return
    // getWidth() + myGapRight + myGapLeft
    // most of getWidth() usages must be replaced with getEffectiveWidth() then.
    myWidth = myWidth - myGapLeft + gap;
    myGapLeft = gap;
  }

  public int getLeftGap() {
    return myGapLeft;
  }

  @Override
  public void setRightGap(int gap) {
    // TODO: remove this line and modify getEffectiveWidth() method in order to return
    // getWidth() + myGapRight + myGapLeft
    // most of getWidth() usages must be replaced with getEffectiveWidth() then.
    myWidth = myWidth - myGapRight + gap;
    myGapRight = gap;
  }

  public int getRightGap() {
    return myGapRight;
  }

  public void requestRelayout() {
    if (myIsNeedRelayout) {
      return;
    }
    myIsNeedRelayout = true;
    if (getParent() != null) {
      getParent().requestRelayout();
    }
  }

  // Following methods are used from layout algorythms
  @Override
  public boolean wasRelayoutRequested() {
    return myIsNeedRelayout;
  }

  public void unrequestLayout() {
    myIsNeedRelayout = false;
  }

  @Override
  public void setCellContext(EditorCellContext cellContext) {
    myCellContext = cellContext;
  }

  @Override
  public EditorCellContext getCellContext() {
    return myCellContext;
  }

  /**
   * {@link Entry} methods
   */

  @NotNull
  @Override
  public jetbrains.mps.openapi.editor.cells.EditorCell getItem() {
    return this;
  }

  @Override
  public Entry<jetbrains.mps.openapi.editor.cells.EditorCell> getNext() {
    return myNext;
  }

  @Override
  public void setNext(Entry<jetbrains.mps.openapi.editor.cells.EditorCell> next) {
    myNext = next;
  }

  @Override
  public Entry<jetbrains.mps.openapi.editor.cells.EditorCell> getPrev() {
    return myPrev;
  }

  @Override
  public void setPrev(Entry<jetbrains.mps.openapi.editor.cells.EditorCell> prev) {
    myPrev = prev;
  }
}
