/*
 * Copyright 2000-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 com.intellij.vcs.log.paint;

import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Pair;
import com.intellij.ui.JBColor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.vcs.log.graph.EdgePrintElement;
import com.intellij.vcs.log.graph.NodePrintElement;
import com.intellij.vcs.log.graph.PrintElement;
import com.intellij.vcs.log.graph.impl.print.elements.TerminalEdgePrintElement;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.awt.*;
import java.awt.geom.Ellipse2D;
import java.util.Collection;
import java.util.List;

/**
 * @author erokhins
 */
public class SimpleGraphCellPainter implements GraphCellPainter {
  private static final Color MARK_COLOR = JBColor.BLACK;
  private static final double ARROW_ANGLE_COS2 = 0.7;
  private static final double ARROW_LENGTH = 0.3;

  @NotNull private final ColorGenerator myColorGenerator;

  public SimpleGraphCellPainter(@NotNull ColorGenerator colorGenerator) {
    myColorGenerator = colorGenerator;
  }

  protected int getRowHeight() {
    return PaintParameters.ROW_HEIGHT;
  }

  private float[] getDashLength(int edgeLength) {
    int space = getRowHeight() / 4 - 1;
    int dash = getRowHeight() / 4 + 1;
    int count = edgeLength / (2 * (dash + space));
    assert count != 0;
    int dashApprox = (edgeLength / 2 - count * space) / count;

    return new float[]{2 * dashApprox, 2 * space};
  }

  @NotNull
  private BasicStroke getOrdinaryStroke() {
    return new BasicStroke(PaintParameters.getLineThickness(getRowHeight()), BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL);
  }

  @NotNull
  private BasicStroke getSelectedStroke() {
    return new BasicStroke(PaintParameters.getSelectedLineThickness(getRowHeight()), BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL);
  }

  @NotNull
  private Stroke getDashedStroke(float[] dash) {
    return new BasicStroke(PaintParameters.getLineThickness(getRowHeight()), BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL, 0, dash,
                           dash[0] / 2);
  }

  @NotNull
  private Stroke getSelectedDashedStroke(float[] dash) {
    return new BasicStroke(PaintParameters.getSelectedLineThickness(getRowHeight()), BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL, 0, dash,
                           dash[0] / 2);
  }

  private void paintUpLine(@NotNull Graphics2D g2,
                           @NotNull Color color,
                           int from,
                           int to,
                           boolean hasArrow,
                           boolean isUsual,
                           boolean isSelected,
                           boolean isTerminal) {
    // paint vertical lines normal size
    // paint non-vertical lines twice the size to make them dock with each other well
    int nodeWidth = PaintParameters.getNodeWidth(getRowHeight());
    if (from == to) {
      int x = nodeWidth * from + nodeWidth / 2;
      int y1 = getRowHeight() / 2 - 1;
      int y2 = isTerminal ? PaintParameters.getCircleRadius(getRowHeight()) / 2 + 1 : 0;
      paintLine(g2, color, hasArrow, x, y1, x, y2, x, y2, isUsual, isSelected);
    }
    else {
      assert !isTerminal;
      int x1 = nodeWidth * from + nodeWidth / 2;
      int y1 = getRowHeight() / 2;
      int x2 = nodeWidth * to + nodeWidth / 2;
      int y2 = -getRowHeight() / 2;
      paintLine(g2, color, hasArrow, x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2, isUsual, isSelected);
    }
  }

  private void paintDownLine(@NotNull Graphics2D g2,
                             @NotNull Color color,
                             int from,
                             int to,
                             boolean hasArrow,
                             boolean isUsual,
                             boolean isSelected,
                             boolean isTerminal) {
    int nodeWidth = PaintParameters.getNodeWidth(getRowHeight());
    if (from == to) {
      int y2 = getRowHeight() - 1 - (isTerminal ? PaintParameters.getCircleRadius(getRowHeight()) / 2 + 1 : 0);
      int y1 = getRowHeight() / 2;
      int x = nodeWidth * from + nodeWidth / 2;
      paintLine(g2, color, hasArrow, x, y1, x, y2, x, y2, isUsual, isSelected);
    }
    else {
      assert !isTerminal;
      int x1 = nodeWidth * from + nodeWidth / 2;
      int y1 = getRowHeight() / 2;
      int x2 = nodeWidth * to + nodeWidth / 2;
      int y2 = getRowHeight() + getRowHeight() / 2;
      paintLine(g2, color, hasArrow, x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2, isUsual, isSelected);
    }
  }

  private void paintLine(@NotNull Graphics2D g2,
                         @NotNull Color color,
                         boolean hasArrow,
                         int x1,
                         int y1,
                         int x2,
                         int y2,
                         int startArrowX,
                         int startArrowY,
                         boolean isUsual,
                         boolean isSelected) {
    g2.setColor(color);

    int length = (x1 == x2) ? getRowHeight() : (int)Math.ceil(Math.hypot(x1 - x2, y1 - y2));
    setStroke(g2, isUsual || hasArrow, isSelected, length);

    g2.drawLine(x1, y1, x2, y2);
    if (hasArrow) {
      Pair<Integer, Integer> rotate1 =
        rotate(x1, y1, startArrowX, startArrowY, Math.sqrt(ARROW_ANGLE_COS2), Math.sqrt(1 - ARROW_ANGLE_COS2),
               ARROW_LENGTH * getRowHeight());
      Pair<Integer, Integer> rotate2 =
        rotate(x1, y1, startArrowX, startArrowY, Math.sqrt(ARROW_ANGLE_COS2), -Math.sqrt(1 - ARROW_ANGLE_COS2),
               ARROW_LENGTH * getRowHeight());
      g2.drawLine(startArrowX, startArrowY, rotate1.first, rotate1.second);
      g2.drawLine(startArrowX, startArrowY, rotate2.first, rotate2.second);
    }
  }

  @NotNull
  private static Pair<Integer, Integer> rotate(double x,
                                               double y,
                                               double centerX,
                                               double centerY,
                                               double cos,
                                               double sin,
                                               double arrowLength) {
    double translateX = (x - centerX);
    double translateY = (y - centerY);

    double d = Math.hypot(translateX, translateY);
    double scaleX = arrowLength * translateX / d;
    double scaleY = arrowLength * translateY / d;

    double rotateX = scaleX * cos - scaleY * sin;
    double rotateY = scaleX * sin + scaleY * cos;

    return Pair.create((int)Math.round(rotateX + centerX), (int)Math.round(rotateY + centerY));
  }

  private void paintCircle(@NotNull Graphics2D g2, int position, @NotNull Color color, boolean select) {
    int nodeWidth = PaintParameters.getNodeWidth(getRowHeight());
    int circleRadius = PaintParameters.getCircleRadius(getRowHeight());
    int selectedCircleRadius = PaintParameters.getSelectedCircleRadius(getRowHeight());

    int x0 = nodeWidth * position + nodeWidth / 2;
    int y0 = getRowHeight() / 2;
    int r = circleRadius;
    if (select) {
      r = selectedCircleRadius;
    }
    Ellipse2D.Double circle = new Ellipse2D.Double(x0 - r + 0.5, y0 - r + 0.5, 2 * r, 2 * r);
    g2.setColor(color);
    g2.fill(circle);
  }

  private void setStroke(@NotNull Graphics2D g2, boolean usual, boolean select, int edgeLength) {
    if (usual) {
      if (select) {
        g2.setStroke(getSelectedStroke());
      }
      else {
        g2.setStroke(getOrdinaryStroke());
      }
    }
    else {
      if (select) {
        g2.setStroke(getSelectedDashedStroke(getDashLength(edgeLength)));
      }
      else {
        g2.setStroke(getDashedStroke(getDashLength(edgeLength)));
      }
    }
  }

  @NotNull
  private Color getColor(@NotNull PrintElement printElement) {
    return myColorGenerator.getColor(printElement.getColorId());
  }

  private static boolean isUsual(@NotNull PrintElement printElement) {
    if (!(printElement instanceof EdgePrintElement)) return true;
    EdgePrintElement.LineStyle lineStyle = ((EdgePrintElement)printElement).getLineStyle();
    return lineStyle == EdgePrintElement.LineStyle.SOLID;
  }

  @Override
  public void draw(@NotNull Graphics2D g2, @NotNull Collection<? extends PrintElement> printElements) {
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

    for (PrintElement printElement : printElements) {
      if (!printElement.isSelected()) {
        drawElement(g2, printElement, false);
      }
    }

    List<PrintElement> selected = ContainerUtil.filter(printElements, new Condition<PrintElement>() {
      @Override
      public boolean value(PrintElement printElement) {
        return printElement.isSelected();
      }
    });
    for (PrintElement printElement : selected) {
      drawElement(g2, printElement, true);
    }

    for (PrintElement printElement : selected) {
      drawElement(g2, printElement, false);
    }
  }

  protected void drawElement(@NotNull Graphics2D g2, @NotNull PrintElement printElement, boolean isSelected) {
    if (printElement instanceof EdgePrintElement) {
      if (isSelected) {
        printEdge(g2, MARK_COLOR, true, (EdgePrintElement)printElement);
      }
      else {
        printEdge(g2, getColor(printElement), false, (EdgePrintElement)printElement);
      }
    }

    if (printElement instanceof NodePrintElement) {
      int position = printElement.getPositionInCurrentRow();
      if (isSelected) {
        paintCircle(g2, position, MARK_COLOR, true);
      }
      else {
        paintCircle(g2, position, getColor(printElement), false);
      }
    }
  }

  private void printEdge(@NotNull Graphics2D g2, @NotNull Color color, boolean isSelected, @NotNull EdgePrintElement edgePrintElement) {
    int from = edgePrintElement.getPositionInCurrentRow();
    int to = edgePrintElement.getPositionInOtherRow();
    boolean isUsual = isUsual(edgePrintElement);

    if (edgePrintElement.getType() == EdgePrintElement.Type.DOWN) {
      paintDownLine(g2, color, from, to, edgePrintElement.hasArrow(), isUsual, isSelected,
                    edgePrintElement instanceof TerminalEdgePrintElement);
    }
    else {
      paintUpLine(g2, color, from, to, edgePrintElement.hasArrow(), isUsual, isSelected,
                  edgePrintElement instanceof TerminalEdgePrintElement);
    }
  }

  @Nullable
  @Override
  public PrintElement getElementUnderCursor(@NotNull Collection<? extends PrintElement> printElements, int x, int y) {
    int nodeWidth = PaintParameters.getNodeWidth(getRowHeight());
    for (PrintElement printElement : printElements) {
      if (printElement instanceof NodePrintElement) {
        int circleRadius = PaintParameters.getCircleRadius(getRowHeight());
        if (PositionUtil.overNode(printElement.getPositionInCurrentRow(), x, y, getRowHeight(), nodeWidth, circleRadius)) {
          return printElement;
        }
      }
    }

    for (PrintElement printElement : printElements) {
      if (printElement instanceof EdgePrintElement) {
        EdgePrintElement edgePrintElement = (EdgePrintElement)printElement;
        float lineThickness = PaintParameters.getLineThickness(getRowHeight());
        if (edgePrintElement.getType() == EdgePrintElement.Type.DOWN) {
          if (PositionUtil
            .overDownEdge(edgePrintElement.getPositionInCurrentRow(), edgePrintElement.getPositionInOtherRow(), x, y, getRowHeight(),
                          nodeWidth, lineThickness)) {
            return printElement;
          }
        }
        else {
          if (PositionUtil
            .overUpEdge(edgePrintElement.getPositionInOtherRow(), edgePrintElement.getPositionInCurrentRow(), x, y, getRowHeight(),
                        nodeWidth, lineThickness)) {
            return printElement;
          }
        }
      }
    }
    return null;
  }
}
