/*
 *   Copyright (C) 2014  Alfons Wirtz
 *   website www.freerouting.net
 *
 *   Copyright (C) 2017 Michael Hoffer <info@michaelhoffer.de>
 *   Website www.freerouting.mihosoft.eu
*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License at <http://www.gnu.org/licenses/> 
 *   for more details.
 */

package eu.mihosoft.freerouting.geometry.planar;

import java.math.BigInteger;

import eu.mihosoft.freerouting.datastructures.Signum;
import eu.mihosoft.freerouting.logger.FRLogger;

/**
 *
 * Implements functionality for lines in the plane.
 *
 * @author Alfons Wirtz
 */

public class Line implements Comparable<Line>, java.io.Serializable
{
    
    /**
     * creates a directed Line from two Points
     */
    public Line(Point p_a, Point p_b)
    {
        a = p_a;
        b = p_b;
        dir = null;
        if (!(a instanceof IntPoint && b instanceof IntPoint))
        {
            FRLogger.warn("Line(p_a, p_b) only implemented for IntPoints till now");
        }
    }
    
    /**
     * creates a directed Line from four integer Coordinates
     */
    public Line(int p_a_x, int p_a_y, int p_b_x, int p_b_y)
    {
        a = new IntPoint(p_a_x, p_a_y);
        b = new IntPoint(p_b_x, p_b_y);
        dir = null;
    }
    
    /**
     * creates a directed Line from a Point and a Direction
     */
    public Line(Point p_a, Direction p_dir)
    {
        a = p_a;
        b = p_a.translate_by(p_dir.get_vector());
        dir = p_dir;
        if (!(a instanceof IntPoint && b instanceof IntPoint))
        {
            FRLogger.warn("Line(p_a, p_dir) only implemented for IntPoints till now");
        }
    }
    
    /**
     * create a directed line from an IntPoint and an IntDirection
     */
    public static Line get_instance(Point p_a, Direction p_dir)
    {
        Point b = p_a.translate_by(p_dir.get_vector());
        return new Line(p_a, b);
    }
    
    /**
     * returns true, if this and p_ob define the same line
     */
    public final boolean equals( Object p_ob )
    {
        if ( this == p_ob )
        {
            return true;
        }
        if ( p_ob == null )
        {
            return false;
        }
        if (!(p_ob instanceof Line))
        {
            return false;
        }
        Line other = (Line)p_ob ;
        if (side_of(other.a) != Side.COLLINEAR)
        {
            return false;
        }
        return direction().equals(other.direction());
    }
    
    /**
     * Returns true, if this and p_other define the same line.
     * Is designed for good performance, but
     * works only for lines consisting of IntPoints.
     */
    public final boolean fast_equals(Line p_other)
    {
        IntPoint this_a = (IntPoint)a;
        IntPoint this_b = (IntPoint)b;
        IntPoint other_a = (IntPoint)p_other.a;
        double dx1 = other_a.x - this_a.x;
        double dy1 = other_a.y - this_a.y;
        double dx2 = this_b.x - this_a.x;
        double dy2 = this_b.y - this_a.y;
        double det = dx1 * dy2 - dx2 * dy1;
        if (det != 0)
        {
            return false;
        }
        return direction().equals(p_other.direction());
    }
    
    
    /**
     * get the direction of this directed line
     */
    public Direction direction()
    {
        if (dir == null)
        {
            Vector d = b.difference_by(a);
            dir = Direction.get_instance(d);
        }
        return dir;
    }
    
    /**
     * The function returns
     *         Side.ON_THE_LEFT, if this Line is on the left of p_point,
     *         Side.ON_THE_RIGHT, if this Line is on the right of p_point
     *     and Side.COLLINEAR, if this Line contains p_point.
     */
    public Side side_of(Point p_point)
    {
        Side result = p_point.side_of(this);
        return result.negate();
    }
    
    
    /**
     * Returns Side.COLLINEAR, if p_point is on the line with tolerance p_tolerance.
     * Otherwise Side.ON_THE_LEFT, if this line is on the left of p_point,
     * or Side.ON_THE_RIGHT, if this line is on the right of p_point,
     */
    public Side side_of(FloatPoint p_point, double p_tolerance)
    {
        // only implemented for IntPoint lines for performance reasons
        IntPoint this_a = (IntPoint)a;
        IntPoint this_b = (IntPoint)b;
        double det =
                (this_b.y - this_a.y) * (p_point.x - this_a.x) -
                (this_b.x - this_a.x) * (p_point.y - this_a.y);
        Side result;
        if (det - p_tolerance > 0)
        {
            result = Side.ON_THE_LEFT;
        }
        else if (det + p_tolerance < 0)
        {
            result = Side.ON_THE_RIGHT;
        }
        else
        {
            result = Side.COLLINEAR;
        }
        
        return result;
    }
    
    /**
     * returns Side.ON_THE_LEFT, if this line is on the left of p_point,
     *         Side.ON_THE_RIGHT, if this line is on the right of p_point,
     *         Side.COLLINEAR otherwise.
     */
    public Side side_of(FloatPoint p_point)
    {
        return side_of(p_point, 0);
    }
    
    /**
     * Returns Side.ON_THE_LEFT, if this line is on the left of the intersection
     * of p_1 and p_2, Side.ON_THE_RIGHT, if this line is on the right of the intersection,
     *  and Side.COLLINEAR, if all 3 lines intersect in exacly 1 point.
     */
    public Side side_of_intersection(Line p_1, Line p_2)
    {
        
        FloatPoint intersection_approx = p_1.intersection_approx(p_2);
        Side result = this.side_of(intersection_approx, 1.0);
        if (result == Side.COLLINEAR)
        {
            // Previous calculation was with FloatPoints and a tolerance
            // for performance reasons. Make an exact check for
            // collinearity now with class Point instead of FloatPoint.
            Point intersection = p_1.intersection(p_2);
            result = this.side_of(intersection);
        }
        return result;
    }
    
    /**
     * Looks, if all interiour points of p_tile are on the right side of this line.
     */
    public boolean is_on_the_left(TileShape p_tile)
    {
        for ( int i = 0; i < p_tile.border_line_count(); ++i)
        {
            if (this.side_of(p_tile.corner(i)) == Side.ON_THE_RIGHT)
            {
                return false;
            }
        }
        return true;
    }
    
    /**
     * Looks, if all interiour points of p_tile are on the left side of this line.
     */
    public boolean is_on_the_right(TileShape p_tile)
    {
        for ( int i = 0; i < p_tile.border_line_count(); ++i)
        {
            if (this.side_of(p_tile.corner(i)) == Side.ON_THE_LEFT)
            {
                return false;
            }
        }
        return true;
    }
    
    /**
     * Returns the signed distance of this line from p_point.
     * The result will be positive, if the line is on the left of p_point,
     * else negative.
     */
    public double signed_distance(FloatPoint p_point)
    {
        // only implemented for IntPoint lines for performance reasons
        IntPoint this_a = (IntPoint)a;
        IntPoint this_b = (IntPoint)b;
        double dx = this_b.x - this_a.x;
        double dy = this_b.y - this_a.y;
        double det =
                dy * (p_point.x - this_a.x) -
                dx * (p_point.y - this_a.y);
        // area of the parallelogramm spanned by the 3 points
        double length = Math.sqrt(dx * dx + dy * dy);
        return det / length;
    }
    
    /**
     * returns true, if the 2 lines defins the same set of points, but may
     * have opposite directions
     */
    public boolean overlaps(Line p_other)
    {
        return side_of(p_other.a) == Side.COLLINEAR
                && side_of(p_other.b) == Side.COLLINEAR;
    }
    
    /**
     * Returns the line defining the same set of points, but
     * with opposite direction
     */
    public Line opposite()
    {
        return new Line(b, a);
    }
    
    /**
     * Returns the intersection point of the 2 lines.
     * If the lines are parallel result.is_infinite() will be true.
     */
    public Point intersection(Line p_other)
    {
        // this function is at the moment only implemented for lines
        // consisting of IntPoints.
        // The general implementation is still missing.
        IntVector delta_1 = (IntVector)b.difference_by(a);
        IntVector delta_2 = (IntVector)p_other.b.difference_by(p_other.a);
        // Separate handling for orthogonal and 45 degree lines for better perpormance
        if (delta_1.x == 0 ) // this line is vertical
        {
            if (delta_2.y == 0) // other line is horizontal
            {
                return new IntPoint(((IntPoint)this.a).x, ((IntPoint)p_other.a).y);
            }
            if (delta_2.x == delta_2.y) // other line is right diagonal
            {
                int this_x = ((IntPoint)this.a).x;
                IntPoint other_a = (IntPoint) p_other.a;
                return new IntPoint(this_x, other_a.y + this_x - other_a.x);
            }
            if (delta_2.x == -delta_2.y) // other line is left diagonal
            {
                int this_x = ((IntPoint)this.a).x;
                IntPoint other_a = (IntPoint) p_other.a;
                return new IntPoint(this_x, other_a.y + other_a.x - this_x);
            }
        }
        else if (delta_1.y == 0) // this line is horizontal
        {
            if (delta_2.x == 0) // other line is vertical
            {
                return new IntPoint(((IntPoint)p_other.a).x, ((IntPoint)this.a).y);
            }
            if (delta_2.x == delta_2.y) // other line is right diagonal
            {
                int this_y = ((IntPoint)this.a).y;
                IntPoint other_a = (IntPoint) p_other.a;
                return new IntPoint(other_a.x + this_y - other_a.y, this_y);
            }
            if (delta_2.x == -delta_2.y) // other line is left diagonal
            {
                int this_y = ((IntPoint)this.a).y;
                IntPoint other_a = (IntPoint) p_other.a;
                return new IntPoint(other_a.x + other_a.y - this_y, this_y);
            }
        }
        else if (delta_1.x == delta_1.y) // this line is right diagonal
        {
            if (delta_2.x == 0) // other line is vertical
            {
                int other_x = ((IntPoint)p_other.a).x;
                IntPoint this_a = (IntPoint) this.a;
                return new IntPoint(other_x, this_a.y + other_x - this_a.x);
            }
            if (delta_2.y == 0) // other line is horizontal
            {
                int other_y = ((IntPoint)p_other.a).y;
                IntPoint this_a = (IntPoint) this.a;
                return new IntPoint(this_a.x + other_y - this_a.y, other_y);
            }
        }
        else if (delta_1.x == -delta_1.y) // this line is left diagonal
        {
            if (delta_2.x == 0) // other line is vertical
            {
                int other_x = ((IntPoint)p_other.a).x;
                IntPoint this_a = (IntPoint) this.a;
                return new IntPoint(other_x, this_a.y + this_a.x - other_x);
            }
            if (delta_2.y == 0) // other line is horizontal
            {
                int other_y = ((IntPoint)p_other.a).y;
                IntPoint this_a = (IntPoint) this.a;
                return new IntPoint(this_a.x + this_a.y - other_y, other_y);
            }
        }
        
        BigInteger det_1 =
                BigInteger.valueOf(((IntPoint)a).determinant((IntPoint)b));
        BigInteger det_2 =
                BigInteger.valueOf(((IntPoint)p_other.a).determinant((IntPoint)p_other.b));
        BigInteger det =  BigInteger.valueOf(delta_2.determinant(delta_1));
        BigInteger tmp_1 = det_1.multiply(BigInteger.valueOf(delta_2.x));
        BigInteger tmp_2 = det_2.multiply(BigInteger.valueOf(delta_1.x));
        BigInteger is_x = tmp_1.subtract(tmp_2);
        tmp_1 = det_1.multiply(BigInteger.valueOf(delta_2.y));
        tmp_2 = det_2.multiply(BigInteger.valueOf(delta_1.y));
        BigInteger is_y = tmp_1.subtract(tmp_2);
        int signum = det.signum();
        if (signum != 0)
        {
            if (signum < 0)
            {
                det = det.negate();
                is_x = is_x.negate();
                is_y = is_y.negate();
            }
            if ((is_x.mod(det)).signum() == 0 && (is_y.mod(det)).signum() == 0)
            {
                is_x = is_x.divide(det);
                is_y = is_y.divide(det);
                if (Math.abs(is_x.doubleValue()) <= Limits.CRIT_INT
                        && Math.abs(is_y.doubleValue()) <= Limits.CRIT_INT)
                {
                    return new IntPoint(is_x.intValue(), is_y.intValue());
                }
                det = BigInteger.ONE;
            }
        }
        return new RationalPoint(is_x, is_y, det);
    }
    
    /**
     * Returns an approximation of the intersection of the 2 lines by a
     * FloatPoint. If the lines are parallel the result coordinates will be
     * Integer.MAX_VALUE. Useful in situations ehere performance is
     * more important than accuracy.
     */
    public FloatPoint intersection_approx(Line p_other)
    {
        // this function is at the moment only implemented for lines
        // consisting of IntPoints.
        // The general implementation is still missing.
        IntPoint this_a = (IntPoint) a;
        IntPoint this_b = (IntPoint) b;
        IntPoint other_a = (IntPoint) p_other.a;
        IntPoint other_b = (IntPoint) p_other.b;
        double d1x = this_b.x - this_a.x;
        double d1y = this_b.y - this_a.y;
        double d2x = other_b.x - other_a.x;
        double d2y = other_b.y - other_a.y;
        double det_1 = (double)this_a.x * this_b.y - (double)this_a.y * this_b.x;
        double det_2 = (double)other_a.x * other_b.y - (double)other_a.y * other_b.x;
        double det = d2x * d1y - d2y * d1x;
        double is_x;
        double is_y;
        if(det == 0)
        {
            is_x = Integer.MAX_VALUE;
            is_y = Integer.MAX_VALUE;
        }
        else
        {
            is_x = (d2x * det_1 - d1x * det_2) / det;
            is_y = (d2y * det_1 - d1y * det_2) / det;
        }
        return new FloatPoint(is_x, is_y);
    }
    
    /**
     * returns the perpendicular projection of p_point onto this line
     */
    public Point perpendicular_projection(Point p_point)
    {
        return p_point.perpendicular_projection(this);
    }
    
    /**
     * translates the line perpendicular at about p_dist.
     * If p_dist {@literal >} 0, the line will be translated to the left, else to the right
     */
    public Line translate(double p_dist)
    {
        // this function is at the moment only implemented for lines
        // consisting of IntPoints.
        // The general implementation is still missing.
        IntPoint ai = (IntPoint) a;
        IntVector v = (IntVector)direction().get_vector();
        double vxvx = (double)v.x * v.x;
        double vyvy = (double)v.y * v.y;
        double lenght = Math.sqrt(vxvx + vyvy);
        IntPoint new_a;
        if (vxvx <= vyvy)
        {
            // translate along the x axis
            int rel_x = (int) Math.round((p_dist * lenght) / v.y);
            new_a = new IntPoint(ai.x - rel_x, ai.y);
        }
        else
        {
            // translate along the  y axis
            int rel_y = (int) Math.round((p_dist * lenght) / v.x);
            new_a = new IntPoint(ai.x, ai.y + rel_y);
        }
        return Line.get_instance(new_a, direction());
    }
    
    /**
     * translates the line by p_vector
     */
    public Line translate_by(Vector p_vector)
    {
        if (p_vector.equals(Vector.ZERO))
        {
            return this;
        }
        Point new_a = a.translate_by(p_vector);
        Point new_b = b.translate_by(p_vector);
        return new Line(new_a, new_b);
    }
    
    
    /**
     * returns true, if the line is axis_parallel
     */
    public boolean is_orthogonal()
    {
        return direction().is_orthogonal();
    }
    
    /**
     * returns true, if this line is diagonal
     */
    public boolean is_diagonal()
    {
        return direction().is_diagonal();
    }
    
    /**
     * returns true, if the direction of this line is a multiple of 45 degree
     */
    public boolean is_multiple_of_45_degree()
    {
        return direction().is_multiple_of_45_degree();
    }
    
    /**
     * checks, if this Line and p_other are parallel
     */
    public boolean is_parallel(Line p_other)
    {
        return this.direction().side_of(p_other.direction()) == Side.COLLINEAR;
    }
    
    /**
     * checks, if this Line and p_other are perpendicular
     */
    public boolean is_perpendicular(Line p_other)
    {
        Vector v1 = direction().get_vector();
        Vector v2 = p_other.direction().get_vector();
        return v1.projection(v2) == Signum.ZERO;
    }
    
    /**
     * returns true, if this and p_ob define the same line
     */
    public boolean is_equal_or_opposite(Line p_other )
    {
        
        return(side_of(p_other.a) == Side.COLLINEAR &&
                side_of(p_other.b) == Side.COLLINEAR);
    }
    
    
    /**
     * calculates the cosinus of the angle between this line and p_other
     */
    public double cos_angle( Line p_other)
    {
        Vector v1 = b.difference_by(a);
        Vector v2 = p_other.b.difference_by(p_other.a);
        return v1.cos_angle(v2);
    }
    
    /**
     * A line l_1 is defined bigger than a line l_2, if the direction of l_1
     * is bigger than the direction of l_2.
     * Implements the comparable interface.
     * Throws a cast exception, if p_other is not a Line.
     * Fast implementation only for lines consisting of IntPoints
     * because of critical performance
     */
    public int compareTo(Line p_other)
    {
        IntPoint this_a = (IntPoint) a;
        IntPoint this_b = (IntPoint) b;
        IntPoint other_a = (IntPoint) p_other.a;
        IntPoint other_b = (IntPoint) p_other.b;
        int dx1 = this_b.x - this_a.x;
        int dy1 = this_b.y - this_a.y;
        int dx2 = other_b.x - other_a.x;
        int dy2 = other_b.y - other_a.y;
        if (dy1 > 0)
        {
            if (dy2 < 0)
            {
                return -1 ;
            }
            if (dy2 == 0)
            {
                if (dx2 > 0)
                {
                    return 1 ;
                }
                return -1 ;
            }
        }
        else if (dy1 < 0)
        {
            if (dy2 >= 0)
            {
                return 1 ;
            }
        }
        else // dy1 == 0
        {
            if (dx1 > 0)
            {
                if (dy2 != 0 || dx2 < 0)
                {
                    return -1 ;
                }
                return 0 ;
            }
            // dx1 < 0
            if (dy2 > 0 || dy2 == 0 && dx2 > 0)
            {
                return 1 ;
            }
            if (dy2 < 0)
            {
                return -1 ;
            }
            return 0;
        }
        
        // now this direction and p_other are located in the same
        // open horizontal half plane
        
        double determinant = (double) dx2 * dy1 - (double) dy2 * dx1;
        return Signum.as_int(determinant);
    }
    
    /**
     * Calculates an approximation of the function value of this line at p_x,
     * if the line is not vertical.
     */
    public double function_value_approx(double p_x)
    {
        FloatPoint p1 = a.to_float();
        FloatPoint p2 = b.to_float();
        double dx = p2.x - p1.x;
        if (dx == 0)
        {
            FRLogger.warn("function_value_approx: line is vertical");
            return 0;
        }
        double dy = p2.y - p1.y;
        double  det = p1.x * p2.y - p2.x * p1.y;
        double result = (dy * p_x - det) /dx;
        return result;
    }
    
    /**
     * Calculates an approximation of the function value in y of this line at p_y,
     * if the line is not horizontal.
     */
    public double function_in_y_value_approx(double p_y)
    {
        FloatPoint p1 = a.to_float();
        FloatPoint p2 = b.to_float();
        double dy = p2.y - p1.y;
        if (dy == 0)
        {
            FRLogger.warn("function_in_y_value_approx: line is horizontal");
            return 0;
        }
        double dx = p2.x - p1.x;
        double  det = p1.x * p2.y - p2.x * p1.y;
        double result = (dx * p_y + det) /dy;
        return result;
    }
    
    /**
     * Calculates the direction from p_from_point to the nearest point on
     * this line to p_fro_point.
     * Returns null, if p_from_point is contained in this line.
     */
    public Direction perpendicular_direction(Point p_from_point)
    {
        Side line_side = this.side_of(p_from_point);
        if (line_side == Side.COLLINEAR)
        {
            return null;
        }
        Direction dir1 = this.direction().turn_45_degree(2);
        Direction dir2 = this.direction().turn_45_degree(6);
        
        Point check_point_1 = p_from_point.translate_by(dir1.get_vector());
        if (this.side_of(check_point_1) != line_side)
        {
            return dir1;
        }
        Point check_point_2 = p_from_point.translate_by(dir2.get_vector());
        if (this.side_of(check_point_2) != line_side)
        {
            return dir2;
        }
        FloatPoint nearest_line_point = p_from_point.to_float().projection_approx(this);
        Direction result;
        if (nearest_line_point.distance_square(check_point_1.to_float()) <=
                nearest_line_point.distance_square(check_point_2.to_float()))
        {
            result = dir1;
        }
        else
        {
            result = dir2;
        }
        return result;
    }
    
    /**
     * Turns this line by p_factor times 90 degree around p_pole.
     */
    public Line turn_90_degree(int p_factor, IntPoint p_pole)
    {
        Point new_a = a.turn_90_degree(p_factor, p_pole);
        Point new_b = b.turn_90_degree(p_factor, p_pole);
        return new Line(new_a, new_b);
    }
    
    /** Mirrors this line at the vertical line through p_pole */
    public Line mirror_vertical(IntPoint p_pole)
    {
        Point new_a = b.mirror_vertical(p_pole);
        Point new_b = a.mirror_vertical(p_pole);
        return new Line(new_a, new_b);
    }
    
    /** Mirrors this line at the horizontal line through p_pole */
    public Line mirror_horizontal(IntPoint p_pole)
    {
        Point new_a = b.mirror_horizontal(p_pole);
        Point new_b = a.mirror_horizontal(p_pole);
        return new Line(new_a, new_b);
    }
    
    
    public final Point a;
    public final Point b;
    transient private Direction dir; // should only be accessed from get_direction().
}