/*
 * Copyright 2000-2009 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.util.ui;

import javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.TreeMap;

public class GeometryUtil implements SwingConstants {
  private static final int myArrowSize = 9;
  private static final Shape myArrowPolygon = new Polygon(new int[] {0, myArrowSize, 0, 0}, new int[] {0, myArrowSize /2, myArrowSize, 0}, 4);

  public static Point getIntersectionPoint(Line2D aSegment, Rectangle aRectangle) {

    if (segmentOutsideRectangle(aRectangle, aSegment)) {
      throw new IllegalArgumentException("Segment " + toString(aSegment) + " lies out of rectangle " + aRectangle + " or intersects more than one bound");
    }

    if (segmentInsideRectangle(aRectangle, aSegment)) {
      return null;
    }

    Line2D[] bounds = new Line2D[4];
    bounds[0] = getTopOf(aRectangle);
    bounds[1] = getRightOf(aRectangle);
    bounds[2] = getBottomOf(aRectangle);
    bounds[3] = getLeftOf(aRectangle);

    for (Line2D bound : bounds) {
      if (bound.intersectsLine(aSegment)) {
        return getIntersectionPoint(aSegment, bound);
      }
    }

    return null;
  }

  public static Line2D.Double getLeftOf(Rectangle aRectangle) {
    return new Line2D.Double(aRectangle.getX(), aRectangle.getY(), aRectangle.getX(), aRectangle.getY() + aRectangle.getHeight());
  }

  public static Line2D.Double getBottomOf(Rectangle aRectangle) {
    return new Line2D.Double(aRectangle.getX(), aRectangle.getY() + aRectangle.getHeight(), aRectangle.getX() + aRectangle.getWidth(), aRectangle.getY() + aRectangle.getHeight());
  }

  public static Line2D.Double getRightOf(Rectangle aRectangle) {
    return new Line2D.Double(aRectangle.getX() + aRectangle.getWidth(), aRectangle.getY(), aRectangle.getX() + aRectangle.getWidth(), aRectangle.getY() + aRectangle.getHeight());
  }

  public static Line2D.Double getTopOf(Rectangle aRectangle) {
    return new Line2D.Double(aRectangle.getX(), aRectangle.getY(), aRectangle.getX() + aRectangle.getWidth(), aRectangle.getY());
  }


  private static boolean segmentInsideRectangle(Rectangle aRectangle, Line2D aSegment) {
    return isWithin(aRectangle, aSegment.getP1()) && isWithin(aRectangle, aSegment.getP2());
  }

  private static boolean segmentOutsideRectangle(Rectangle aRectangle, Line2D aSegment) {
    return (!isWithin(aRectangle, aSegment.getP1())) && (!isWithin(aRectangle, aSegment.getP2()));
  }

  public static boolean isWithin(Rectangle aRectangle, Point2D aPoint) {
    return
        (aPoint.getX() > aRectangle.getX()) && (aPoint.getX() < aRectangle.getX() + aRectangle.getBounds().width)
        &&
        ((aPoint.getY() > aRectangle.getY()) && (aPoint.getY() < aRectangle.getY() + aRectangle.getBounds().height));
  }

  public static Point getIntersectionPoint(Line2D aFirst, Line2D aSecond) {
    double firstDeltaX = aFirst.getX2() - aFirst.getX1();
    double firstDeltaY = aFirst.getY2() - aFirst.getY1();

    double kFirst = firstDeltaY / firstDeltaX;
    double bFirst = aFirst.getY1() - kFirst * aFirst.getX1();


    double secondDeltaX = aSecond.getX2() - aSecond.getX1();
    double secondDeltaY = aSecond.getY2() - aSecond.getY1();

    double kSecond = secondDeltaY / secondDeltaX;
    double bSecond = aSecond.getY1() - kSecond * aSecond.getX1();


    double xIntersection;
    double yIntersection;


    double deltaK = (kFirst - kSecond);


    if (linesAreAngledAndParallel(kFirst, kSecond)) {
      return null;
    }

    if (Double.isInfinite(deltaK) || (0 == deltaK)) {

      if (firstDeltaX == secondDeltaX && 0 == firstDeltaX) {
        return null;
      }

      if (firstDeltaY == secondDeltaY && 0 == firstDeltaY) {
        return null;
      }

      if ((0 == firstDeltaX) && (0 == secondDeltaY)) {
        xIntersection = aFirst.getX1();
        yIntersection = aSecond.getY1();
      }
      else if ((0 == secondDeltaX) && (0 == firstDeltaY)) {
        xIntersection = aSecond.getX1();
        yIntersection = aFirst.getY1();
      }
      else {

        if (0 == firstDeltaX) {
          xIntersection = aFirst.getX1();
          yIntersection = kSecond * xIntersection + bSecond;
        }
        else {
          xIntersection = aSecond.getX1();
          yIntersection = kFirst * xIntersection + bFirst;
        }
      }


    }
    else {
      xIntersection = (bSecond - bFirst) / deltaK;
      yIntersection = kFirst * xIntersection + bFirst;
    }

    return new Point((int) xIntersection, (int) yIntersection);
  }

  private static boolean linesAreAngledAndParallel(double aKFirst, double aKSecond) {
    return (aKFirst == aKSecond) && (0 != aKFirst);
  }

  public static String toString(Line2D aLine) {
    return aLine.getP1() + ":" + aLine.getP2();
  }

  public static boolean intersects(Rectangle aRectangle, Line2D aLine) {
    if (aLine == null || aRectangle == null) {
      return false;
    }

    return (!segmentOutsideRectangle(aRectangle, aLine)) && (!segmentInsideRectangle(aRectangle, aLine));
  }

  public static int getPointPositionOnRectangle(Rectangle aRectangle, Point aPoint, int aEpsilon) {
    final int ERROR_CODE = Integer.MIN_VALUE;

    if (pointOnBound(getTopOf(aRectangle), aPoint, aEpsilon)) {
      return TOP;
    }
    else if (pointOnBound(getBottomOf(aRectangle), aPoint, aEpsilon)) {
      return BOTTOM;
    }
    else if (pointOnBound(getLeftOf(aRectangle), aPoint, aEpsilon)) {
      return LEFT;
    }
    else if (pointOnBound(getRightOf(aRectangle), aPoint, aEpsilon)) {
      return RIGHT;
    }
    else {
      return ERROR_CODE;
    }
  }

  private static boolean pointOnBound(Line2D aTop, Point aPoint, int aEpsilon) {
    return withinRange(aTop.getX1(), aTop.getX2(), aPoint.getX(), aEpsilon) && withinRange(aTop.getY1(), aTop.getY2(), aPoint.getY(), aEpsilon);
  }

  private static boolean withinRange(double aLeft, double aRight, double aValue, int aEpsilon) {
    return ((aLeft - aEpsilon) <= aValue) && ((aRight + aEpsilon) >= aValue);
  }

//  public static Point shiftByY(Line2D aLine, Point aPoint, int aPointDeltaY) {
//    return new Point((int) (aPoint.getX() + getShiftByY(aLine, aPointDeltaY)), (int) (aPoint.getY() + aPointDeltaY));
//  }
//
//  public static Point shiftByX(Line2D aLine, Point aPoint, int aPointDeltaX) {
//    return new Point((int) (aPoint.getX() + aPointDeltaX), (int) (aPoint.getY() + getShiftByX(aLine, aPointDeltaX)));
//  }

  public static double getShiftByY(Line2D aLine, double aPointDeltaY) {
    return aPointDeltaY * ((aLine.getX2() - aLine.getX1()) / (aLine.getY2() - aLine.getY1()));
  }

  public static double getShiftByX(Line2D aLine, double aPointDeltaX) {
    double width = aLine.getX2() - aLine.getX1();
    double height = aLine.getY2() - aLine.getY1();
    return aPointDeltaX * (height / width);
  }

  public static Shape getArrowShape(Line2D line, Point2D intersectionPoint) {
    final double deltaY = line.getP2().getY() - line.getP1().getY();
    final double length = Math.sqrt(Math.pow(deltaY, 2) + Math.pow(line.getP2().getX() - line.getP1().getX(), 2));

    double theta = Math.asin(deltaY / length);

    if (line.getP1().getX() > line.getP2().getX()) {
      theta = Math.PI - theta;
    }

    AffineTransform rotate = AffineTransform.getRotateInstance(theta, myArrowSize, myArrowSize / 2);
    Shape polygon = rotate.createTransformedShape(myArrowPolygon);

    AffineTransform move = AffineTransform.getTranslateInstance(intersectionPoint.getX() - myArrowSize, intersectionPoint.getY() - myArrowSize /2);
    polygon = move.createTransformedShape(polygon);
    return polygon;
  }

  private static class OrientedPoint extends Point {
    private final int myOrientation;

    OrientedPoint(double x, double y, int aOrientation) {
      super((int) x, (int) y);
      myOrientation = aOrientation;
    }

    public int getOrientation() {
      return myOrientation;
    }
  }

  public static int getClosestToLineRectangleCorner(Rectangle aRectangle, Line2D aSegment) {
    Point northWest = new OrientedPoint(aRectangle.getX(), aRectangle.getY(), NORTH_WEST);
    Point northEast = new OrientedPoint(aRectangle.getMaxX(), aRectangle.getY(), NORTH_EAST);
    Point southEast = new OrientedPoint(aRectangle.getMaxX(), aRectangle.getMaxY(), SOUTH_EAST);
    Point southWest = new OrientedPoint(aRectangle.getX(), aRectangle.getMaxY(), SOUTH_WEST);

    TreeMap sorter = new TreeMap();

    sorter.put(getDistance(aSegment, northWest), northWest);
    sorter.put(getDistance(aSegment, southWest), southWest);
    sorter.put(getDistance(aSegment, southEast), southEast);
    sorter.put(getDistance(aSegment, northEast), northEast);

    return ((OrientedPoint) sorter.get(sorter.firstKey())).getOrientation();

  }

  private static Double getDistance(Line2D aSegment, Point aPoint) {
    double length1 = getLineLength(aSegment.getX1(), aSegment.getY1(), aPoint.getX(), aPoint.getY());
    double length2 = getLineLength(aSegment.getX2(), aSegment.getY2(), aPoint.getX(), aPoint.getY());

    return new Double(length1 + length2);
  }

  public static double getLineLength(double aX1, double aY1, double aX2, double aY2) {
    double deltaX = aX2 - aX1;
    double deltaY = aY2 - aY1;

    return Math.hypot(deltaX, deltaY);
  }

  public static double cos(Line2D aLine) {
    final double length = getLineLength(aLine.getX1(), aLine.getY1(), aLine.getX2(), aLine.getY2());
    if (length == 0) {
      throw new IllegalArgumentException(toString(aLine) + " has a zero length");
    }

    double deltaX = aLine.getX2() - aLine.getX1();
    return deltaX / length;
  }

  public static double sin(Line2D aLine) {
    final double length = getLineLength(aLine.getX1(), aLine.getY1(), aLine.getX2(), aLine.getY2());
    if (length == 0) {
      throw new IllegalArgumentException(toString(aLine) + " has a zero length");
    }

    double deltaY = aLine.getY2() - aLine.getY1();
    return deltaY / length;
  }

}
