/*
 * Copyright 2003-2016 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package jetbrains.mps.nodeEditor.leftHighlighter;

import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.actionSystem.ActionPopupMenu;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.util.containers.SortedList;
import com.intellij.util.ui.UIUtil;
import gnu.trove.TIntObjectHashMap;
import gnu.trove.TIntObjectProcedure;
import jetbrains.mps.ide.actions.MPSActions;
import jetbrains.mps.ide.actions.MPSCommonDataKeys;
import jetbrains.mps.ide.editor.MPSEditorDataKeys;
import jetbrains.mps.ide.tooltips.MPSToolTipManager;
import jetbrains.mps.ide.tooltips.TooltipComponent;
import jetbrains.mps.nodeEditor.EditorComponent;
import jetbrains.mps.nodeEditor.EditorMessage;
import jetbrains.mps.nodeEditor.EditorMessageIconRenderer;
import jetbrains.mps.nodeEditor.EditorMessageIconRenderer.IconRendererType;
import jetbrains.mps.nodeEditor.EditorSettings;
import jetbrains.mps.nodeEditor.cells.APICellAdapter;
import jetbrains.mps.nodeEditor.cells.EditorCell_Label;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.editor.update.UpdaterListenerAdapter;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.workbench.action.ActionUtils;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeUtil;
import org.jetbrains.mps.openapi.module.SRepository;

import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NavigableSet;
import java.util.Set;
import java.util.TreeSet;

/**
 * This class should be called in UI (EventDispatch) thread only
 */
public class LeftEditorHighlighter extends JComponent implements TooltipComponent {
  private static final Logger LOG = LogManager.getLogger(LeftEditorHighlighter.class);
  public static final String ICON_AREA = "LeftEditorHighlighterIconArea";

  private static final int MIN_LEFT_TEXT_WIDTH = 0;
  private static final int MIN_ICON_RENDERERS_WIDTH = 14;
  private static final int MIN_LEFT_FOLDING_AREA_WIDTH = 7;
  private static final int MIN_RIGHT_FOLDING_AREA_WIDTH = 4;
  private static final int GAP_BETWEEN_ICONS = 3;
  private static final int LEFT_GAP = 1;
  private static final int FOLDING_LINE_WIDTH = 1;
  private static final Comparator<AbstractFoldingAreaPainter> FOLDING_AREA_PAINTERS_COMPARATOR = new Comparator<AbstractFoldingAreaPainter>() {
    @Override
    public int compare(AbstractFoldingAreaPainter afap1, AbstractFoldingAreaPainter afap2) {
      if (afap1.getWeight() == afap2.getWeight() && !afap1.equals(afap2)) {
        return System.identityHashCode(afap1) - System.identityHashCode(afap2);
      }
      return afap1.getWeight() - afap2.getWeight();
    }
  };

  private EditorComponent myEditorComponent;
  private NavigableSet<AbstractFoldingAreaPainter> myFoldingAreaPainters = new TreeSet<AbstractFoldingAreaPainter>(FOLDING_AREA_PAINTERS_COMPARATOR);
  private BracketsPainter myBracketsPainter;
  private FoldingButtonsPainter myFoldingButtonsPainter;

  private List<AbstractLeftColumn> myLeftColumns = new ArrayList<AbstractLeftColumn>();

  private Set<EditorMessageIconRenderer> myIconRenderers = new HashSet<EditorMessageIconRenderer>();
  private TIntObjectHashMap<List<IconRendererLayoutConstraint>> myLineToRenderersMap = new TIntObjectHashMap<List<IconRendererLayoutConstraint>>();
  private Comparator myIconRenderersComparator = new IconRendererLayoutConstraintComparator();
  private EditorMessageIconRenderer myRendererUnderMouse;
  private int myMaxIconHeight = 0;

  private boolean myMouseIsInFoldingArea;

  private int myFoldingLineX;
  private int myIconRenderersWidth;
  private int myTextColumnWidth;
  private int myLeftFoldingAreaWidth;
  private int myRightFoldingAreaWidth;
  private int myWidth;
  private int myHeight;
  private boolean myRightToLeft;

  public LeftEditorHighlighter(@NotNull EditorComponent editorComponent, boolean rightToLeft) {
    setBackground(EditorSettings.getInstance().getLeftHighlighterBackgroundColor());
    myEditorComponent = editorComponent;
    myRightToLeft = rightToLeft;
    addMouseListener(new MouseAdapter() {
      @Override
      public void mouseExited(MouseEvent e) {
        mouseExitedFoldingArea(e);
        mouseExitedIconsArea(e);
      }

      @Override
      public void mouseEntered(MouseEvent e) {
        if (isInFoldingArea(e)) {
          mouseMovedInFoldingArea(e);
        } else if (isInTextArea(e)) {
          mouseMovedInTextArea(e);
        } else {
          mouseMovedInIconsArea(e);
        }
      }
    });
    addMouseMotionListener(new MouseMotionAdapter() {
      @Override
      public void mouseMoved(MouseEvent e) {
        if (isInFoldingArea(e)) {
          mouseExitedIconsArea(e);
          mouseMovedInFoldingArea(e);
        } else if (isInTextArea(e)) {
          mouseExitedFoldingArea(e);
          mouseExitedIconsArea(e);
          mouseMovedInTextArea(e);
        } else {
          mouseExitedFoldingArea(e);
          mouseMovedInIconsArea(e);
        }
      }
    });
    if (MPSToolTipManager.getInstance() != null) {
      MPSToolTipManager.getInstance().registerComponent(this);
    }
    editorComponent.getUpdater().addListener(new UpdaterListenerAdapter() {
      @Override
      public void editorUpdated(jetbrains.mps.openapi.editor.EditorComponent editorComponent) {
        assert SwingUtilities.isEventDispatchThread() : "LeftEditorHighlighter$RebuildListener should be called in eventDispatchThread";
        for (AbstractFoldingAreaPainter painter : myFoldingAreaPainters) {
          painter.editorRebuilt();
        }
        relayout(true);
      }
    });
    myBracketsPainter = new BracketsPainter(this, myRightToLeft);
    myFoldingButtonsPainter = new FoldingButtonsPainter(this);

    myFoldingAreaPainters.add(myBracketsPainter);
    myFoldingAreaPainters.add(myFoldingButtonsPainter);
  }

  @NotNull
  public EditorComponent getEditorComponent() {
    return myEditorComponent;
  }

  public int getRightFoldingAreaWidth() {
    return myRightFoldingAreaWidth;
  }

  public int getFoldingLineX() {
    return myFoldingLineX;
  }

  public int getIconRenderersOffset() {
    return myRightToLeft ? getFoldingAreaWidth() : myTextColumnWidth;
  }

  private int getFoldingAreaOffset() {
    return myRightToLeft ? 0 : myTextColumnWidth + myIconRenderersWidth;
  }

  private int getTextColumnOffset() {
    return myRightToLeft ? getFoldingAreaWidth() + myIconRenderersWidth : 0;
  }

  private int getFoldingAreaWidth() {
    return myLeftFoldingAreaWidth + FOLDING_LINE_WIDTH + myRightFoldingAreaWidth;
  }

  private boolean isInside(int offset, int width, int x) {
    return offset <= x && x < offset + width;
  }

  private boolean hasIntersection(int x1, int width1, Rectangle rectangle) {
    int x2 = rectangle.x;
    int width2 = rectangle.width;
    return x1 <= x2 ? x1 + width1 >= x2 : x2 + width2 >= x1;
  }

  public void dispose() {
    for (AbstractFoldingAreaPainter painter : myFoldingAreaPainters) {
      painter.dispose();
    }
    for (AbstractLeftColumn column : myLeftColumns) {
      column.dispose();
    }
    if (MPSToolTipManager.getInstance() != null) {
      MPSToolTipManager.getInstance().unregisterComponent(this);
    }
  }

  public void setDefaultFoldingAreaPaintersEnabled(boolean enabled) {
    if (enabled) {
      addFoldingAreaPainter(myBracketsPainter);
      addFoldingAreaPainter(myFoldingButtonsPainter);
    } else {
      removeFoldingAreaPainter(myBracketsPainter);
      removeFoldingAreaPainter(myFoldingButtonsPainter);
    }
  }

  public void addFoldingAreaPainter(AbstractFoldingAreaPainter painter) {
    if (myFoldingAreaPainters.contains(painter)) {
      return;
    }
    myFoldingAreaPainters.add(painter);
    relayout(true);
    repaint();
  }

  public void removeFoldingAreaPainter(AbstractFoldingAreaPainter painter) {
    if (!myFoldingAreaPainters.contains(painter)) {
      return;
    }
    myFoldingAreaPainters.remove(painter);
    relayout(true);
    repaint();
  }

  public void addLeftColumn(AbstractLeftColumn column) {
    myLeftColumns.add(column);
    relayoutOnLeftColumnChange();
    repaint();
  }

  public void removeLeftColumn(AbstractLeftColumn column) {
    myLeftColumns.remove(column);
    relayoutOnLeftColumnChange();
    repaint();
  }

  public List<AbstractLeftColumn> getLeftColumns() {
    return myLeftColumns;
  }

  @Override
  public void paintComponent(Graphics g) {
    Rectangle clipBounds = g.getClipBounds();
    paintBackgroundAndFoldingLine(g, clipBounds);
    paintTextColumns(g, clipBounds);
    paintIconRenderers(g, clipBounds);
    paintFoldingArea(g, clipBounds);
  }

  private void paintFoldingArea(Graphics g, Rectangle clipBounds) {
    if (!hasIntersection(getFoldingAreaOffset(), getFoldingAreaWidth(), clipBounds)) {
      return;
    }
    for (AbstractFoldingAreaPainter painter : myFoldingAreaPainters) {
      painter.paint(g);
    }
  }

  private void paintBackgroundAndFoldingLine(Graphics g, Rectangle clipBounds) {
    Graphics2D g2d = (Graphics2D) g;
    g.setColor(getBackground());
    g.fillRect(clipBounds.x, clipBounds.y, Math.min(clipBounds.width, myFoldingLineX - clipBounds.x), clipBounds.height);
    g.setColor(getEditorComponent().getBackground());
    g.fillRect(Math.max(clipBounds.x, myFoldingLineX), clipBounds.y, clipBounds.width - Math.max(0, myFoldingLineX - clipBounds.x), clipBounds.height);

    // same as in EditorComponent.paint() method
    EditorCell deepestCell = myEditorComponent.getDeepestSelectedCell();
    if (deepestCell instanceof EditorCell_Label) {
      int selectedCellY = deepestCell.getY();
      int selectedCellHeight = deepestCell.getHeight() - deepestCell.getTopInset() - deepestCell.getBottomInset();
      if (g.hitClip(clipBounds.x, selectedCellY, clipBounds.width, selectedCellHeight)) {
        g.setColor(EditorSettings.getInstance().getCaretRowColor());
        g.fillRect(clipBounds.x, selectedCellY, clipBounds.width, selectedCellHeight);
        // Drawing folding line
        UIUtil.drawVDottedLine(g2d, myFoldingLineX, clipBounds.y, selectedCellY, getBackground(),
            EditorSettings.getInstance().getLeftHighlighterTearLineColor());
        UIUtil.drawVDottedLine(g2d, myFoldingLineX, selectedCellY, selectedCellY + selectedCellHeight, EditorSettings.getInstance().getCaretRowColor(),
            EditorSettings.getInstance().getLeftHighlighterTearLineColor());
        UIUtil.drawVDottedLine(g2d, myFoldingLineX, selectedCellY + selectedCellHeight, clipBounds.y + clipBounds.height, getBackground(),
            EditorSettings.getInstance().getLeftHighlighterTearLineColor());
        return;
      }
    }
    // Drawing folding line
    // COLORS: Remove hardcoded color
    UIUtil.drawVDottedLine(g2d, myFoldingLineX, clipBounds.y, clipBounds.y + clipBounds.height, getBackground(), Color.gray);
  }

  private void paintIconRenderers(final Graphics g, Rectangle clipBounds) {
    if (!hasIntersection(getIconRenderersOffset(), myIconRenderersWidth, clipBounds)) {
      return;
    }
    final int startY = clipBounds.y;
    final int endY = clipBounds.y + clipBounds.height;
    myLineToRenderersMap.forEachEntry(new TIntObjectProcedure<List<IconRendererLayoutConstraint>>() {
      @Override
      public boolean execute(int y, List<IconRendererLayoutConstraint> rendererConstraints) {
        if (startY <= y && y <= endY) {
          for (IconRendererLayoutConstraint constraint : rendererConstraints) {
            constraint.getIconRenderer().getIcon().paintIcon(LeftEditorHighlighter.this, g, constraint.getX(), y);
          }
        }
        return true;
      }
    });
  }

  private void paintTextColumns(Graphics g, Rectangle clipBounds) {
    if (!hasIntersection(getTextColumnOffset(), myTextColumnWidth, clipBounds)) {
      return;
    }
    for (AbstractLeftColumn column : myLeftColumns) {
      if (clipBounds.x > column.getX() + column.getWidth()) {
        continue;
      }
      column.paint(g);
      //  COLORS: find out where it is and remove hardcoded color
      UIUtil.drawVDottedLine((Graphics2D) g, myRightToLeft ? column.getX() : column.getX() + column.getWidth() - 1,
          (int) clipBounds.getMinY(), (int) clipBounds.getMaxY(), getBackground(), Color.GRAY);
    }
  }

  public void unHighlight(EditorCell cell) {
    assert SwingUtilities.isEventDispatchThread() : "LeftEditorHighlighter.unHighlight() should be called in eventDispatchThread";
    myBracketsPainter.removeBracket(cell);
    relayout(true);
    repaint();
  }

  public void highlight(EditorCell cell, EditorCell cell2, Color c) {
    assert SwingUtilities.isEventDispatchThread() : "LeftEditorHighlighter.highlight() should be called in eventDispatchThread";
    assert cell.getEditorComponent() == myEditorComponent : "cell must be from my editor";
    myBracketsPainter.addBracket(cell, cell2, c);
    relayout(true);
    repaint();
  }

  public void relayout(boolean updateFolding) {
    assert SwingUtilities.isEventDispatchThread() : "LeftEditorHighlighter.relayout() should be executed in eventDispatchThread";
    final SNode editedNode = myEditorComponent.getEditedNode();
    // additional check - during editor dispose process some Folding area painters can be removed calling relayout()..
    if (myEditorComponent.isDisposed()) return;
    if (editedNode != null) {
      SRepository repository = myEditorComponent.getEditorContext().getRepository();
      if (new ModelAccessHelper(repository).runReadAction(() -> !SNodeUtil.isAccessible(editedNode, repository))) {
        return;
      }
    }
    if (myRightToLeft) {
      recalculateFoldingAreaWidth();
      updateSeparatorLinePosition();
      if (updateFolding) {
        for (AbstractFoldingAreaPainter painter : myFoldingAreaPainters) {
          painter.relayout();
        }
        // wee need to recalculateIconRenderersWidth only if one of collections was folded/unfolded
        recalculateIconRenderersWidth();
      }
      recalculateTextColumnWidth();
    } else {
      recalculateTextColumnWidth();
      if (updateFolding) {
        for (AbstractFoldingAreaPainter painter : myFoldingAreaPainters) {
          painter.relayout();
        }
        // wee need to recalculateIconRenderersWidth only if one of collections was folded/unfolded
        recalculateIconRenderersWidth();
      }
      recalculateFoldingAreaWidth();
      updateSeparatorLinePosition();
    }
    updatePreferredSize();
  }

  // Optimization: partly layouting
  private void relayoutOnIconRendererChanges() {
    if (myRightToLeft) {
      recalculateIconRenderersWidth();
      recalculateTextColumnWidth();
    } else {
      recalculateIconRenderersWidth();
      updateSeparatorLinePosition();
    }
    updatePreferredSize();
  }

  // Optimization: partly layouting
  private void relayoutOnLeftColumnChange() {
    if (myRightToLeft) {
      recalculateTextColumnWidth();
    } else {
      recalculateTextColumnWidth();
      recalculateIconRenderersWidth();
      updateSeparatorLinePosition();
    }
    updatePreferredSize();
  }

  private void recalculateFoldingAreaWidth() {
    myLeftFoldingAreaWidth = myRightToLeft ? MIN_RIGHT_FOLDING_AREA_WIDTH : MIN_LEFT_FOLDING_AREA_WIDTH;
    myRightFoldingAreaWidth = myRightToLeft ? MIN_LEFT_FOLDING_AREA_WIDTH : MIN_RIGHT_FOLDING_AREA_WIDTH;
    // Layouting painters
    for (AbstractFoldingAreaPainter painter : myFoldingAreaPainters) {
      myLeftFoldingAreaWidth = Math.max(myLeftFoldingAreaWidth, painter.getLeftAreaWidth());
      myRightFoldingAreaWidth = Math.max(myRightFoldingAreaWidth, painter.getRightAreaWidth());
    }
  }

  public void addIconRenderer(EditorMessageIconRenderer renderer) {
    assert SwingUtilities.isEventDispatchThread() : "LeftEditorHighlighter.addIconRenderer() should be called in eventDispatchThread";
    myIconRenderers.add(renderer);
    relayoutOnIconRendererChanges();
  }

  public void addAllIconRenderers(Collection<EditorMessageIconRenderer> renderers) {
    assert SwingUtilities.isEventDispatchThread() : "LeftEditorHighlighter.addAllIconRenderers() should be called in eventDispatchThread";
    myIconRenderers.addAll(renderers);
    relayoutOnIconRendererChanges();
  }

  public void removeIconRenderer(EditorMessageIconRenderer renderer) {
    assert SwingUtilities.isEventDispatchThread() : "LeftEditorHighlighter.removeIconRenderer() should be called in eventDispatchThread";
    if (myIconRenderers.remove(renderer)) {
      relayoutOnIconRendererChanges();
    }
  }

  public void removeIconRenderer(SNode snode, IconRendererType type) {
    assert SwingUtilities.isEventDispatchThread() : "LeftEditorHighlighter.removeIconRenderer() should be called in eventDispatchThread";
    boolean wasModified = false;
    for (Iterator<EditorMessageIconRenderer> it = myIconRenderers.iterator(); it.hasNext(); ) {
      EditorMessageIconRenderer renderer = it.next();
      // XXX I would prefer removeIconRenderer that takes SNodeReference instead of an SNode (e.g. BreakpointsUiComponentEx won't need to record
      // renderer instances then).
      if (renderer.getNode() == snode && (type == null || renderer.getType() == type)) {
        it.remove();
        wasModified = true;
      }
    }
    if (wasModified) {
      relayoutOnIconRendererChanges();
    }
  }

  public void removeAllIconRenderers(Collection<? extends EditorMessageIconRenderer> renderers) {
    assert SwingUtilities.isEventDispatchThread() : "LeftEditorHighlighter.removeAllIconRenderers() should be called in eventDispatchThread";
    if (myIconRenderers.removeAll(renderers)) {
      relayoutOnIconRendererChanges();
    }
  }

  public void removeAllIconRenderers(IconRendererType type) {
    assert SwingUtilities.isEventDispatchThread() : "LeftEditorHighlighter.removeAllIconRenderers() should be called in eventDispatchThread";
    boolean wasModified = false;
    for (Iterator<EditorMessageIconRenderer> it = myIconRenderers.iterator(); it.hasNext(); ) {
      EditorMessageIconRenderer renderer = it.next();
      if (renderer.getType() == type) {
        it.remove();
        wasModified = true;
      }
    }
    if (wasModified) {
      relayoutOnIconRendererChanges();
    }
  }

  private void recalculateIconRenderersWidth() {
    myLineToRenderersMap.clear();
    for (EditorMessageIconRenderer renderer : myIconRenderers) {
      int yCoordinate = getIconCoordinate(renderer);
      if (yCoordinate < 0) {
        continue;
      }
      List<IconRendererLayoutConstraint> renderersForLine = myLineToRenderersMap.get(yCoordinate);
      if (renderersForLine == null) {
        renderersForLine = new SortedList(myIconRenderersComparator);
        myLineToRenderersMap.put(yCoordinate, renderersForLine);
      }
      renderersForLine.add(new IconRendererLayoutConstraint(renderer));
    }

    myIconRenderersWidth = MIN_ICON_RENDERERS_WIDTH;
    myMaxIconHeight = 0;
    int[] sortedYCoordinates = myLineToRenderersMap.keys();
    Arrays.sort(sortedYCoordinates);

    int initialOffset = getIconRenderersOffset();
    for (int y : sortedYCoordinates) {
      List<IconRendererLayoutConstraint> row = myLineToRenderersMap.get(y);
      assert row.size() != 0;
      int maxIconHeight = 0;
      for (IconRendererLayoutConstraint rendererConstraint : row) {
        maxIconHeight = Math.max(maxIconHeight, rendererConstraint.getIconRenderer().getIcon().getIconHeight());
      }
      myMaxIconHeight = Math.max(myMaxIconHeight, maxIconHeight);
      int offset = initialOffset + LEFT_GAP;
      for (Iterator<IconRendererLayoutConstraint> it = row.iterator(); it.hasNext(); ) {
        IconRendererLayoutConstraint rendererConstraint = it.next();
        rendererConstraint.setX(offset);
        offset += rendererConstraint.getIconRenderer().getIcon().getIconWidth();
        if (it.hasNext()) {
          offset += GAP_BETWEEN_ICONS;
        }
      }
      myIconRenderersWidth = Math.max(myIconRenderersWidth, offset - initialOffset);
    }
  }

  private void recalculateTextColumnWidth() {
    int initialOffset = getTextColumnOffset();
    int offset = initialOffset;
    for (AbstractLeftColumn column : myLeftColumns) {
      column.setX(offset);
      column.relayout();
      offset += column.getWidth();
    }
    myTextColumnWidth = Math.max(MIN_LEFT_TEXT_WIDTH, offset - initialOffset);
  }

  private void updateSeparatorLinePosition() {
    myFoldingLineX = getFoldingAreaOffset() + myLeftFoldingAreaWidth;
  }

  private void updatePreferredSize() {
    int newWidth = myTextColumnWidth + myIconRenderersWidth + getFoldingAreaWidth();
    int newHeight = myEditorComponent.getPreferredSize().height;
    if (myWidth != newWidth || myHeight != newHeight) {
      myWidth = newWidth;
      myHeight = newHeight;
      firePreferredSizeChanged();
    }
  }

  private void firePreferredSizeChanged() {
    processComponentEvent(new ComponentEvent(this, ComponentEvent.COMPONENT_RESIZED));
  }

  @Override
  public Dimension getPreferredSize() {
    return new Dimension(myWidth + 1, myEditorComponent.getPreferredSize().height);
  }

  public int getIconCoordinate(EditorMessageIconRenderer renderer) {
    jetbrains.mps.openapi.editor.cells.EditorCell anchorCell = getAnchorCell(renderer);
    if (anchorCell == null || APICellAdapter.isUnderFolded(anchorCell)) {
      // no anchorCell 
      return -1;
    }
    if (renderer.getIcon() == null) {
      LOG.error("null icon was returned by renderer: " + renderer);
      return -1;
    }
    return anchorCell.getY() + anchorCell.getHeight() / 2 - renderer.getIcon().getIconHeight() / 2;
  }

  private jetbrains.mps.openapi.editor.cells.EditorCell getAnchorCell(final EditorMessageIconRenderer renderer) {
    final jetbrains.mps.openapi.editor.cells.EditorCell[] cell = new jetbrains.mps.openapi.editor.cells.EditorCell[1];
    myEditorComponent.getEditorContext().getRepository().getModelAccess().runReadAction(new Runnable() {
      @Override
      public void run() {
        SNode rendererNode = renderer.getNode();
        EditorCell nodeCell = myEditorComponent.findNodeCell(rendererNode);
        if (nodeCell != null) {
          cell[0] = renderer.getAnchorCell(nodeCell);
        }
        // no cell for node?..
      }
    });
    return cell[0];
  }

  @Override
  public String getMPSTooltipText(MouseEvent e) {
    if (isInFoldingArea(e)) {
      for (AbstractFoldingAreaPainter painter : myFoldingAreaPainters) {
        if (painter.getToolTipText() != null) {
          return painter.getToolTipText();
        }
      }
    } else if (isInTextArea(e)) {
      AbstractLeftColumn column = getTextColumnByX(e.getX());
      if (column != null) {
        return column.getTooltipText(e);
      }
    } else {
      EditorMessageIconRenderer iconRenderer = getIconRendererUnderMouse(e);
      if (iconRenderer != null) {
        return iconRenderer.getTooltipText();
      }
    }
    return null;
  }

  @Override
  protected void processMouseEvent(MouseEvent e) {
    switch (e.getID()) {
      case MouseEvent.MOUSE_PRESSED:
      case MouseEvent.MOUSE_RELEASED:
      case MouseEvent.MOUSE_CLICKED:
        if (isInFoldingArea(e)) {
          mousePressedInFoldingArea(e);
        } else if (isInTextArea(e)) {
          mousePressedInTextArea(e);
        } else {
          mousePressedInIconsArea(e);
        }
        if (!e.isConsumed() && e.getButton() == MouseEvent.BUTTON3 && e.getID() == MouseEvent.MOUSE_PRESSED) {
          DefaultActionGroup actionGroup = ActionUtils.getDefaultGroup(MPSActions.EDITOR_LEFTPANEL_GROUP);
          if (actionGroup != null) {
            ActionPopupMenu popupMenu = ActionManager.getInstance().createActionPopupMenu(ActionPlaces.EDITOR_POPUP, actionGroup);
            popupMenu.getComponent().show(e.getComponent(), e.getX(), e.getY());
            e.consume();
          }
        }
    }

    // suppressing future event processig in case event was consumed by one of LeftHighlighter elements
    if (!e.isConsumed()) {
      super.processMouseEvent(e);
    }
  }

  private void mousePressedInIconsArea(MouseEvent e) {
    EditorMessageIconRenderer iconRenderer = getIconRendererUnderMouse(e);
    if (iconRenderer != null) {
      if (e.getButton() == MouseEvent.BUTTON3) {
        JPopupMenu popupMenu = iconRenderer.getPopupMenu();
        if (popupMenu != null && e.getID() == MouseEvent.MOUSE_PRESSED) {
          e.consume();
          Component component = e.getComponent();
          popupMenu.show(component == null ? myEditorComponent : component, e.getX(), e.getY());
        }
        return;
      }
      AnAction action = iconRenderer.getClickAction();
      if (e.getButton() == MouseEvent.BUTTON1 && action != null) {
        if (e.getID() == MouseEvent.MOUSE_CLICKED) {
          AnActionEvent actionEvent =
              new AnActionEvent(e, new LeftEditorHighlighterDataContext(myEditorComponent, iconRenderer.getNode()), ICON_AREA, action.getTemplatePresentation(),
                  ActionManager.getInstance(), e.getModifiers());
          action.update(actionEvent);
          action.actionPerformed(actionEvent);
        }
        e.consume();
      }
    }
  }

  private void mousePressedInFoldingArea(MouseEvent e) {
    for (Iterator<AbstractFoldingAreaPainter> it = myFoldingAreaPainters.descendingIterator(); it.hasNext(); ) {
      AbstractFoldingAreaPainter painter = it.next();
      painter.mousePressed(e);
      if (e.isConsumed()) {
        break;
      }
    }
  }

  private void mousePressedInTextArea(MouseEvent e) {
    if (e.isConsumed()) return;
    AbstractLeftColumn column = getTextColumnByX(e.getX());
    if (column != null) {
      column.mousePressed(e);
      e.consume();
    }
  }

  private void mouseExitedFoldingArea(MouseEvent e) {
    if (myMouseIsInFoldingArea) {
      for (AbstractFoldingAreaPainter painter : myFoldingAreaPainters) {
        painter.mouseExited(e);
      }
    }
  }

  private void mouseMovedInFoldingArea(MouseEvent e) {
    myMouseIsInFoldingArea = true;
    for (Iterator<AbstractFoldingAreaPainter> it = myFoldingAreaPainters.descendingIterator(); it.hasNext(); ) {
      AbstractFoldingAreaPainter painter = it.next();
      painter.mouseMoved(e);
      if (e.isConsumed()) {
        break;
      }
    }
  }

  private void mouseExitedIconsArea(MouseEvent e) {
    if (!myMouseIsInFoldingArea && myRendererUnderMouse != null && !isInTextArea(e)) {
      setCursor(null);
    }
  }

  private void mouseMovedInIconsArea(MouseEvent e) {
    myMouseIsInFoldingArea = false;
    EditorMessageIconRenderer newRendererUnderMouse = getIconRendererUnderMouse(e);
    if (newRendererUnderMouse != null) {
      setCursor(newRendererUnderMouse.getMouseOverCursor());
    } else if (myRendererUnderMouse != null) {
      setCursor(null);
    }
    myRendererUnderMouse = newRendererUnderMouse;
  }

  private void mouseMovedInTextArea(MouseEvent e) {
    myMouseIsInFoldingArea = false;
    AbstractLeftColumn textColumn = getTextColumnByX(e.getX());
    if (textColumn != null) {
      setCursor(textColumn.getCursor(e));
    } else {
      setCursor(null);
    }
  }

  private boolean isInFoldingArea(MouseEvent e) {
    return isInside(getFoldingAreaOffset(), getFoldingAreaWidth(), e.getX());
  }

  private boolean isInTextArea(MouseEvent e) {
    return isInside(getTextColumnOffset(), myTextColumnWidth, e.getX());
  }

  private EditorMessageIconRenderer getIconRendererUnderMouse(MouseEvent e) {
    final int mouseX = e.getX();
    final int mouseY = e.getY();
    final EditorMessageIconRenderer[] theRenderer = new EditorMessageIconRenderer[]{null};
    myLineToRenderersMap.forEachEntry(new TIntObjectProcedure<List<IconRendererLayoutConstraint>>() {
      @Override
      public boolean execute(int y, List<IconRendererLayoutConstraint> layoutConstraints) {
        if (y <= mouseY && mouseY <= y + myMaxIconHeight) {
          for (IconRendererLayoutConstraint constraint : layoutConstraints) {
            int x = constraint.getX();
            if (y <= mouseY && mouseY <= y + constraint.getIconRenderer().getIcon().getIconHeight() &&
                x <= mouseX && mouseX <= x + constraint.getIconRenderer().getIcon().getIconWidth()) {
              theRenderer[0] = constraint.getIconRenderer();
              return false;
            }
          }
        }
        return true;
      }
    });
    return theRenderer[0];
  }

  private AbstractLeftColumn getTextColumnByX(int x) {
    for (AbstractLeftColumn column : myLeftColumns) {
      int columnX = column.getX();
      if (columnX <= x && columnX + column.getWidth() > x) {
        return column;
      }
    }
    return null;
  }

  private class IconRendererLayoutConstraintComparator implements Comparator<IconRendererLayoutConstraint> {
    @Override
    public int compare(IconRendererLayoutConstraint constraint1, IconRendererLayoutConstraint constraint2) {
      if (constraint1 == constraint2) {
        return 0;
      }
      EditorMessageIconRenderer renderer1 = constraint1.getIconRenderer();
      EditorMessageIconRenderer renderer2 = constraint2.getIconRenderer();
      if (renderer1.getType() != renderer2.getType()) {
        return renderer1.getType().getWeight() - renderer2.getType().getWeight();
      }
      jetbrains.mps.openapi.editor.cells.EditorCell anchorCell1 = getAnchorCell(renderer1);
      jetbrains.mps.openapi.editor.cells.EditorCell anchorCell2 = getAnchorCell(renderer2);
      // [++] Debugging assertion
      if (anchorCell1 == anchorCell2 && renderer1 instanceof EditorMessage && renderer2 instanceof EditorMessage) {
        EditorMessage editorMessage1 = (EditorMessage) renderer1;
        EditorMessage editorMessage2 = (EditorMessage) renderer2;
        assert false :
            "Two EditorMessages with same type are attached to the same EditorCell: m1 = " +
                editorMessage1 + ", m2 = " + editorMessage2 +
                "; owner1 = " + editorMessage1.getOwner() + ", owner2 = " + editorMessage2.getOwner();
      }
      // [--] Debugging assertion
      if (anchorCell1 != null) {
        if (anchorCell2 == null) {
          return 1;
        } else {
          return anchorCell1.getX() - anchorCell2.getX();
        }
      } else if (anchorCell2 != null) {
        return -1;
      }
      return 0;
    }
  }

  private static class IconRendererLayoutConstraint {
    private EditorMessageIconRenderer myIconRenderer;
    private int myX;

    public IconRendererLayoutConstraint(EditorMessageIconRenderer iconRenderer) {
      myIconRenderer = iconRenderer;
    }

    public void setX(int x) {
      myX = x;
    }

    public int getX() {
      return myX;
    }

    public EditorMessageIconRenderer getIconRenderer() {
      return myIconRenderer;
    }
  }

  private static class LeftEditorHighlighterDataContext implements DataContext {
    private DataContext myEditorDataContext;
    private SNode mySelectedNode;
    private EditorCell myNodeCell;

    public LeftEditorHighlighterDataContext(@NotNull EditorComponent editorComponent, SNode selectedNode) {
      myEditorDataContext = DataManager.getInstance().getDataContext(editorComponent);
      mySelectedNode = selectedNode;
      myNodeCell = editorComponent.findNodeCell(mySelectedNode);
    }

    @Override
    public Object getData(@NonNls String dataId) {
      if (MPSCommonDataKeys.NODE.getName().equals(dataId)) {
        return mySelectedNode;
      }
      if (MPSEditorDataKeys.EDITOR_CELL.getName().equals(dataId)) {
        return myNodeCell;
      }
      return myEditorDataContext.getData(dataId);
    }
  }
}
