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

import com.intellij.icons.AllIcons.General;
import com.intellij.openapi.editor.colors.CodeInsightColors;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.ui.ColorUtil;
import com.intellij.util.IconUtil;
import com.intellij.util.containers.SortedList;
import com.intellij.util.ui.ButtonlessScrollBarUI;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.update.MergingUpdateQueue;
import com.intellij.util.ui.update.Update;
import jetbrains.mps.errors.MessageStatus;
import jetbrains.mps.ide.tooltips.MPSToolTipManager;
import jetbrains.mps.ide.tooltips.TooltipComponent;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.editor.cells.EditorCell_Collection;
import jetbrains.mps.openapi.editor.message.EditorMessageOwner;
import jetbrains.mps.openapi.editor.message.SimpleEditorMessage;

import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import java.awt.Adjustable;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class MessagesGutter extends ButtonlessScrollBarUI.Transparent implements TooltipComponent, MouseMotionListener, MouseListener {
  private static final Comparator<GutterMark> EDITOR_MESSAGES_COMPARATOR = (mark, otherMark) -> otherMark.getPriority() != mark.getPriority() ?
                                                                                                otherMark.getPriority() - mark.getPriority() :
                                                                                                otherMark.getStatus().ordinal() - mark.getStatus().ordinal();

  private EditorComponent myEditorComponent;
  private MyErrorsButton myErrorsButton = new MyErrorsButton();
  private List<SimpleEditorMessage> myMessages = new CopyOnWriteArrayList<>();
  private List<GutterMark> myGutterMarks = Collections.emptyList();
  private boolean myRightToLeft;
  private MergingUpdateQueue myUpdateQueue;
  private Object myUpdateIdentity = new Object();

  public MessagesGutter(EditorComponent editorComponent, boolean rightToLeft) {
    myEditorComponent = editorComponent;
    myRightToLeft = rightToLeft;
  }

  @Override
  protected JButton createDecreaseButton(int orientation) {
    return myErrorsButton;
  }

  @Override
  protected void installListeners() {
    super.installListeners();
    if (MPSToolTipManager.getInstance() != null) {
      MPSToolTipManager.getInstance().registerComponentRightAligned(scrollbar);
    }
    scrollbar.addMouseListener(this);
    scrollbar.addMouseMotionListener(this);
  }

  @Override
  public void uninstallListeners() {
    scrollbar.removeMouseMotionListener(this);
    scrollbar.removeMouseListener(this);
    if (MPSToolTipManager.getInstance() != null) {
      MPSToolTipManager.getInstance().unregisterComponentRightAligned(scrollbar);
    }
    super.uninstallListeners();
  }

  //copied from com.intellij.openapi.editor.impl.EditorMarkupModelImpl
  @Override
  protected Color adjustColor(Color c) {
    if (isMacOverlayScrollbar()) {
      return super.adjustColor(c);
    }

    if (UIUtil.isUnderDarcula()) {
      return c;
    }
    return ColorUtil.withAlpha(ColorUtil.shift(super.adjustColor(c), 0.9), 0.85);
  }

  //copied from com.intellij.openapi.editor.impl.EditorMarkupModelImpl
  @Override
  protected void paintThumb(Graphics g, JComponent c, Rectangle thumbBounds) {

    if (isMacOverlayScrollbar()) {
      if (!myRightToLeft) {
        super.paintThumb(g, c, thumbBounds);
      } else {
        Graphics2D g2d = (Graphics2D) g;
        AffineTransform old = g2d.getTransform();

        AffineTransform tx = AffineTransform.getScaleInstance(-1, 1);
        tx.translate(-c.getWidth(), 0);
        g2d.transform(tx);
        g.translate(1, 0);
        super.paintThumb(g, c, thumbBounds);
        g2d.setTransform(old);
      }
    } else {
      int shift = myRightToLeft ? -9 : 9;
      g.translate(shift, 0);
      super.paintThumb(g, c, thumbBounds);
      g.translate(-shift, 0);
    }
  }

  @Override
  protected void doPaintTrack(Graphics g, JComponent c, Rectangle bounds) {
    super.doPaintTrack(g, c, bounds);
    g.setColor(getEditorComponent().getBackground());
    g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);

    drawMarks(g);
  }

  @Override
  protected int getThickness() {
    // TODO: remove magic number!
    return super.getThickness() + 7;
  }

  @Override
  public void mouseDragged(MouseEvent e) {
  }

  @Override
  public void mouseMoved(MouseEvent e) {
    List<GutterMark> gutterMarks = getGutterMarksAt(e.getY());
    if (gutterMarks.size() > 0) {
      scrollbar.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
    } else {
      scrollbar.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
    }
  }


  @Override
  public void mouseClicked(MouseEvent e) {
  }

  @Override
  public void mousePressed(MouseEvent e) {
    int y = e.getY();
    List<GutterMark> gutterMarks = getGutterMarksAt(y);
    if (gutterMarks.size() > 0) {
      GutterMark mark = gutterMarks.get(0);
      SimpleEditorMessage message = mark.getEditorMessage();
      if (message instanceof EditorMessage) {
        ((EditorMessage) message).doNavigate(myEditorComponent);
      } else {
        // (markY - y) / markHeight = (realY - start) / height
        int realY = message.getStart(myEditorComponent) + (mark.getY() - y) * message.getHeight(myEditorComponent) / mark.getHeight();
        EditorCell editorCell = myEditorComponent.findCellWeak(1, realY + 1);
        if (editorCell != null) {
          myEditorComponent.changeSelection(editorCell);
        }
      }
    }
  }

  @Override
  public void mouseReleased(MouseEvent e) {
  }

  @Override
  public void mouseEntered(MouseEvent e) {
  }

  @Override
  public void mouseExited(MouseEvent e) {
  }

  public EditorComponent getEditorComponent() {
    return myEditorComponent;
  }

  private void updateGutterMarks() {
    if (scrollbar == null) {
      return;
    }
    getUpdateQueue().queue(new Update(myUpdateIdentity) {
      @Override
      public void run() {
        GutterStatus status = GutterStatus.OK;
        List<GutterMark> marks = new ArrayList<>();
        for (SimpleEditorMessage message : myMessages) {
          GutterMark mark = new GutterMark(message);
          if (!mark.isValid()) {
            continue;
          }

          GutterStatus messageStatus = GutterStatus.getStatus(message.getStatus());
          if (messageStatus.ordinal() > status.ordinal()) {
            status = messageStatus;
          }
          marks.add(mark);
        }

        marks.sort(new Comparator<GutterMark>() {
          @Override
          public int compare(GutterMark mark1, GutterMark mark2) {
            if (mark1 == mark2) {
              return 0;
            }
            SimpleEditorMessage message1 = mark1.getEditorMessage();
            SimpleEditorMessage message2 = mark2.getEditorMessage();
            if (message1 instanceof EditorMessage == message2 instanceof EditorMessage) {
              if (message1 instanceof EditorMessage) {
                return mark1.getStatus() != mark2.getStatus() ? mark1.getStatus().ordinal() - mark2.getStatus().ordinal() : mark1.getY() - mark2.getY();
              } else {
                return mark1.getY() - mark2.getY();
              }
            } else {
              return message1 instanceof EditorMessage ? 1 : -1;
            }
          }
        });

        myGutterMarks = marks;
        setStatus(status);
        //otherwise some messages (removal of which does not affect model) could be not repainted
        if (scrollbar != null) {
          scrollbar.repaint();
        }
      }
    });
  }

  private MergingUpdateQueue getUpdateQueue() {
    if (myUpdateQueue == null) {
      myUpdateQueue = new MergingUpdateQueue("MessagesGutter", 500, true, myEditorComponent, null, null, true);
      myUpdateQueue.setRestartTimerOnAdd(true);
      // TODO add update queue to the disposables tree
    }
    return myUpdateQueue;
  }

  @Override
  public boolean alwaysShowTrack() {
    return (scrollbar.getOrientation() == Adjustable.VERTICAL) || super.alwaysShowTrack();
  }

  private void setStatus(GutterStatus status) {
    switch (status) {
      case OK:
        myErrorsButton.setIcon(General.InspectionsOK);
        break;
      case WARNING:
        final Color warningStripeColor =
            EditorColorsManager.getInstance().getGlobalScheme().getAttributes(CodeInsightColors.WARNINGS_ATTRIBUTES).getErrorStripeColor();
        myErrorsButton.setIcon(IconUtil.colorize(General.InspectionsError, warningStripeColor));
        break;
      case ERROR:
        final Color errorStripeColor =
            EditorColorsManager.getInstance().getGlobalScheme().getAttributes(CodeInsightColors.ERRORS_ATTRIBUTES).getErrorStripeColor();
        myErrorsButton.setIcon(IconUtil.colorize(General.InspectionsError, errorStripeColor));
        break;
      case IN_PROGRESS:
        myErrorsButton.setIcon(General.InspectionsEye);
        break;
    }
  }

  public void add(SimpleEditorMessage message) {
    myMessages.add(message);
    updateGutterMarks();
  }

  public void remove(SimpleEditorMessage message) {
    myMessages.remove(message);
    updateGutterMarks();
  }

  public boolean removeMessages(EditorMessageOwner owner) {
    boolean removedAnything = false;
    for (SimpleEditorMessage m : new ArrayList<>(myMessages)) {
      if (m.getOwner() == owner) {
        myMessages.remove(m);
        removedAnything = true;
      }
    }
    updateGutterMarks();
    return removedAnything;
  }

  public void dispose() {
    if (myUpdateQueue != null) {
      // TODO unsure if this is the right way to dispose the queue
      myUpdateQueue.dispose();
    }
  }

  private void drawMarks(Graphics graphics) {
    for (GutterMark mark : myGutterMarks) {
      if (graphics.hitClip(mark.getX(), mark.getY(), mark.getWidth(), mark.getHeight())) {
        mark.paint(graphics);
      }
    }
  }

  private int getMessagesAreaShift() {
    return Math.max(0, getDecrementButtonHeight() - scrollbar.getBounds().y);
  }

  private int getMessagesAreaHeight() {
    return scrollbar.getHeight() - getIncrementButtonHeight() - Math.max(getDecrementButtonHeight(), scrollbar.getBounds().y);
  }

  private List<GutterMark> getGutterMarksAt(int y) {
    List<GutterMark> result = new SortedList<>(EDITOR_MESSAGES_COMPARATOR);
    for (GutterMark gutterMark : myGutterMarks) {
      int start = gutterMark.getY();
      int end = start + gutterMark.getHeight();
      if (start - 3 <= y && y <= end + 3) {
        result.add(gutterMark);
      }
    }
    return result;
  }

  @Override
  public String getMPSTooltipText(MouseEvent event) {
    int y = event.getY();

    List<GutterMark> gutterMarks = getGutterMarksAt(y);
    if (gutterMarks.size() > 0) {
      StringBuilder text = new StringBuilder();
      for (GutterMark mark : gutterMarks) {
        if (text.length() > 0) {
          text.append("\n");
        }
        text.append(mark.getEditorMessage().getMessage());
      }
      return text.toString();
    }

    return null;
  }

  private class GutterMark {
    private int myX, myY, myWidth, myHeight;
    private Color myColor;
    private SimpleEditorMessage myMessage;
    private boolean myValid = false;

    GutterMark(SimpleEditorMessage message) {
      if ((myMessage = message) == null || (myColor = myMessage.getColor()) == null ||
          myMessage instanceof EditorMessage && !((EditorMessage) myMessage).isValid(myEditorComponent)) {
        return;
      }
      myValid = true;

      myX = myRightToLeft ? 3 : 5;
      myWidth = General.InspectionsOK.getIconWidth() - 1;
      if (!(myMessage instanceof EditorMessage)) {
        // thin

        myWidth /= 2;
        myWidth += 1;
        myX = myRightToLeft ? myWidth + 2 : 0;
      }
      myY = calculateY(myMessage);
      myHeight = calculateHeight(myMessage);
    }

    private int calculateY(SimpleEditorMessage message) {
      return getMessagesAreaShift() +
             (int) (message.getStart(myEditorComponent) * (((double) getMessagesAreaHeight()) / ((double) myEditorComponent.getHeight())));
    }

    private int calculateHeight(SimpleEditorMessage message) {
      int height = message.getHeight(myEditorComponent);
      if (message instanceof EditorMessage) {
        EditorCell cell = ((EditorMessage) message).getCell(myEditorComponent);
        if (cell != null) {
          while (cell instanceof EditorCell_Collection) {
            cell = ((EditorCell_Collection) cell).lastCell();
          }
          if (cell != null) {
            height -= cell.getHeight();
          }
        }
      }
      return (int) (height * (((double) getMessagesAreaHeight()) / ((double) myEditorComponent.getHeight()))) + 2;
    }

    public boolean isValid() {
      return myValid;
    }

    public void paint(Graphics g) {
      assert myValid;
      g.setColor(myColor);
      int x = getX();
      int y = getY();
      int height = Math.max(getHeight(), 3);
      int width = getWidth();
      g.fillRect(x + 1, y, width - 2, height);

      Color brighter = myColor.brighter();
      g.setColor(brighter);
      // left decoration
      UIUtil.drawLine(g, x, y, x, y + height);
      // top decoration
      UIUtil.drawLine(g, x + 1, y, x + width - 2, y);

      Color darker = ColorUtil.shift(myColor, 0.75);
      g.setColor(darker);
      // bottom decoration
      UIUtil.drawLine(g, x + 1, y + height, x + width - 2, y + height);   // large bottom to let overwrite by hl below
      // right decoration
      UIUtil.drawLine(g, x + width - 2, y, x + width - 2, y + height - 1);
    }

    public int getX() {
      return myX;
    }

    public int getY() {
      return myY;
    }

    public int getWidth() {
      return myWidth;
    }

    public int getHeight() {
      return myHeight;
    }

    public int getPriority() {
      return myMessage.getPriority();
    }

    public MessageStatus getStatus() {
      return myMessage.getStatus();
    }

    public SimpleEditorMessage getEditorMessage() {
      return myMessage;
    }
  }

  private enum GutterStatus {
    OK,
    WARNING,
    ERROR,
    IN_PROGRESS;

    static GutterStatus getStatus(MessageStatus status) {
      switch (status) {
        case WARNING:
          return WARNING;
        case ERROR:
          return ERROR;
      }
      return OK;
    }
  }

  private class MyErrorsButton extends JButton {
    private MyErrorsButton() {
      super(General.InspectionsEye);
      setFocusable(false);
    }

    @Override
    public void paint(Graphics g) {
      final Rectangle bounds = getBounds();

      g.setColor(getEditorComponent().getBackground());
      g.fillRect(0, 0, bounds.width, bounds.height);

      Icon icon = getIcon();
      if (icon != null) {
        icon.paintIcon(this, g, (getWidth() - icon.getIconWidth()) / 2, (getHeight() - icon.getIconHeight()) / 2);
      }
    }

    @Override
    public Dimension getPreferredSize() {
      return new Dimension(General.InspectionsOK.getIconWidth(), General.InspectionsOK.getIconHeight());
    }
  }
}
