/*
 *   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.
 *
 * IntPoint.java
 *
 * Created on 1. Februar 2003, 10:31
 */

package eu.mihosoft.freerouting.geometry.planar;

import eu.mihosoft.freerouting.logger.FRLogger;

import java.math.BigInteger;

/**
 * Implementation of the abstract class Point
 * as a tuple of integers.
 *
 *
 * @author Alfons Wirtz
 */

public class IntPoint extends Point implements java.io.Serializable
{
    
    /**
     * create an  IntPoint from two integer coordinates
     */
    public IntPoint(int p_x, int p_y)
    {
        if (Math.abs(p_x) > Limits.CRIT_INT || Math.abs(p_y) > Limits.CRIT_INT)
        {
            FRLogger.warn("Warning in IntPoint: p_x or p_y to big");
        }
        x = p_x;
        y = p_y;
    }
    
    /**
     * Returns true, if this IntPoint is equal to p_ob
     */
    public final boolean equals( Object p_ob )
    {
        if ( this == p_ob )
        {
            return true;
        }
        if ( p_ob == null )
        {
            return false;
        }
        if ( getClass() != p_ob.getClass() )
        {
            return false ;
        }
        IntPoint other = (IntPoint)p_ob ;
        return ( x == other.x && y == other.y ) ;
    }
    
    public boolean is_infinite()
    {
        return false;
    }
    
    public IntBox surrounding_box()
    {
        return new IntBox(this, this);
    }
    
    public IntOctagon surrounding_octagon()
    {
        int tmp_1 = x - y;
        int tmp_2 = x + y;
        
        return new IntOctagon(x, y, x, y, tmp_1, tmp_1, tmp_2, tmp_2);
    }
    
    public boolean is_contained_in(IntBox p_box)
    {
        return x >= p_box.ll.x && y >= p_box.ll.y
                && x <= p_box.ur.x && y <= p_box.ur.y;
    }
    
    /**
     * returns the translation of this point by p_vector
     */
    public final Point translate_by( Vector p_vector )
    {
        if (p_vector.equals(Vector.ZERO))
        {
            return this;
        }
        return p_vector.add_to(this) ;
    }
    
    Point translate_by( IntVector p_vector )
    {
        return ( new IntPoint( x + p_vector.x, y + p_vector.y ) ) ;
    }
    
    Point translate_by( RationalVector p_vector )
    {
        return p_vector.add_to(this);
    }
    
    
    /**
     * returns the difference vector of this point and p_other
     */
    public Vector difference_by(Point p_other)
    {
        Vector tmp =  p_other.difference_by(this);
        return tmp.negate();
    }
    
    Vector difference_by(RationalPoint p_other)
    {
        Vector tmp = p_other.difference_by(this);
        return tmp.negate();
    }
    
    IntVector difference_by(IntPoint p_other)
    {
        return new IntVector(x - p_other.x, y - p_other.y);
    }
    
    public Side side_of(Line p_line)
    {
        Vector v1 = difference_by(p_line.a);
        Vector v2 = p_line.b.difference_by(p_line.a);
        return v1.side_of(v2);
    }
    
    /**
     * converts this point to a FloatPoint.
     */
    public FloatPoint to_float()
    {
        return new FloatPoint(x, y);
    }
    
    /**
     * returns the determinant of the vectors (x, y) and (p_other.x, p_other.y)
     */
    public final long determinant(IntPoint p_other)
    {
        return  (long)x * p_other.y - (long)y * p_other.x;
    }
    
    
    public Point perpendicular_projection(Line p_line)
    {
        // this function is at the moment only implemented for lines
        // consisting of IntPoints.
        // The general implementation is still missing.
        IntVector v = (IntVector)p_line.b.difference_by(p_line.a);
        BigInteger vxvx = BigInteger.valueOf((long)v.x * v.x);
        BigInteger vyvy = BigInteger.valueOf((long)v.y * v.y);
        BigInteger vxvy = BigInteger.valueOf((long) v.x * v.y);
        BigInteger denominator = vxvx.add(vyvy);
        BigInteger det =
                BigInteger.valueOf(((IntPoint)p_line.a).determinant((IntPoint)p_line.b));
        BigInteger point_x = BigInteger.valueOf(x);
        BigInteger point_y = BigInteger.valueOf(y);
        
        BigInteger tmp1 = vxvx.multiply(point_x);
        BigInteger tmp2 = vxvy.multiply(point_y);
        tmp1 = tmp1.add(tmp2);
        tmp2 = det.multiply(BigInteger.valueOf(v.y));
        BigInteger proj_x = tmp1.add(tmp2);
        
        tmp1 = vxvy.multiply(point_x);
        tmp2 = vyvy.multiply(point_y);
        tmp1 = tmp1.add(tmp2);
        tmp2 = det.multiply(BigInteger.valueOf(v.x));
        BigInteger proj_y = tmp1.subtract(tmp2);
        
        int signum = denominator.signum();
        if (signum != 0)
        {
            if (signum < 0)
            {
                denominator = denominator.negate();
                proj_x = proj_x.negate();
                proj_y = proj_y.negate();
            }
            if ((proj_x.mod(denominator)).signum() == 0 &&
                    (proj_y.mod(denominator)).signum() == 0)
            {
                proj_x = proj_x.divide(denominator);
                proj_y = proj_y.divide(denominator);
                return new IntPoint(proj_x.intValue(), proj_y.intValue());
            }
        }
        return new RationalPoint(proj_x, proj_y, denominator);
    }
    
    /**
     * Returns the signed area of the parallelogramm spanned by the vectors
     * p_2 - p_1 and this - p_1
     */
    public double signed_area( IntPoint p_1, IntPoint p_2 )
    {
        IntVector d21 = (IntVector) p_2.difference_by(p_1) ;
        IntVector d01 = (IntVector) this.difference_by(p_1) ;
        return d21.determinant(d01) ;
    }
    
    /**
     * calculates the square of the distance between this point and p_to_point
     */
    public double distance_square(IntPoint p_to_point)
    {
        double dx = p_to_point.x - this.x;
        double dy = p_to_point.y - this.y;
        return dx * dx + dy * dy;
    }
    
    /**
     * calculates the distance between this point and p_to_point
     */
    public double distance(IntPoint p_to_point)
    {
        return Math.sqrt(distance_square(p_to_point));
    }
    
    /**
     * Calculates the nearest point to this point on the horizontal or
     * vertical line through p_other (Snaps this point to on ortogonal line
     * through p_other).
     */
    public IntPoint orthogonal_projection(IntPoint p_other)
    {
        IntPoint result;
        int horizontal_distance = Math.abs(this.x -p_other.x);
        int vertical_distance = Math.abs(this.y -p_other.y);
        if (horizontal_distance <= vertical_distance)
        {
            // projection onto the vertical line through p_other
            result = new IntPoint(p_other.x, this.y);
        }
        else
        {
            // projection onto the horizontal line through p_other
            result = new IntPoint(this.x, p_other.y);
        }
        return result;
    }
    
    /**
     * Calculates the nearest point to this point on an orthogonal or
     * diagonal line through p_other (Snaps this point to on 45 degree line
     * through p_other).
     */
    public IntPoint fortyfive_degree_projection(IntPoint p_other)
    {
        int dx = this.x -p_other.x;
        int dy = this.y -p_other.y;
        double[] dist_arr = new double[4];
        dist_arr[0] = Math.abs(dx);
        dist_arr[1] = Math.abs(dy);
        double diagonal_1 = ((double)dy - (double)dx) / 2;
        double diagonal_2 = ((double)dy + (double)dx) / 2;
        dist_arr[2] = Math.abs(diagonal_1);
        dist_arr[3] = Math.abs(diagonal_2);
        double min_dist = dist_arr[0];
        for (int i = 1; i < 4; ++i)
        {
            if (dist_arr[i] < min_dist)
            {
                min_dist = dist_arr[i];
            }
        }
        IntPoint result;
        if (min_dist == dist_arr[0])
        {
            // projection onto the vertical line through p_other
            result = new IntPoint(p_other.x, this.y);
        }
        else if (min_dist == dist_arr[1])
        {
            // projection onto the horizontal line through p_other
            result = new IntPoint(this.x, p_other.y);
        }
        else if (min_dist == dist_arr[2])
        {
            // projection onto the right diagonal line through p_other
            int diagonal_value = (int)diagonal_2;
            result = new IntPoint(p_other.x + diagonal_value, p_other.y + diagonal_value);
        }
        else
        {
            // projection onto the left diagonal line through p_other
            int diagonal_value = (int)diagonal_1;
            result = new IntPoint(p_other.x - diagonal_value, p_other.y + diagonal_value);
        }
        return result;
    }
    
    /**
     * Calculates a corner point p so that the lines through this point and p and from
     * p to p_to_point are multiples of 45 degree, and that the angle at p will be
     * 45 degree. If p_left_turn, p_to_point will be on the left of the line
     * from this point to p, else on the right.
     * Returns null, if the line from this point to p_to_point is already a multiple
     * of 45 degree.
     */
    public IntPoint fortyfive_degree_corner( IntPoint p_to_point, boolean p_left_turn)
    {
        int dx = p_to_point.x - this.x;
        int dy = p_to_point.y - this.y;
        IntPoint result;
        
        // handle the 8 sections between the 45 degree lines
        
        if (dy > 0 && dy < dx)
        {
            if (p_left_turn)
            {
                result = new IntPoint(p_to_point.x - dy, this.y);
            }
            else
            {
                result = new IntPoint(this.x + dy, p_to_point.y);
            }
        }
        else if (dx > 0 && dy > dx)
        {
            if (p_left_turn)
            {
                result = new IntPoint(p_to_point.x, this.y + dx);
            }
            else
            {
                result = new IntPoint(this.x, p_to_point.y - dx);
            }
        }
        else if (dx < 0 && dy > -dx)
        {
            if (p_left_turn)
            {
                result = new IntPoint(this.x, p_to_point.y + dx);
            }
            else
            {
                result = new IntPoint(p_to_point.x, this.y - dx);
            }
        }
        else if (dy > 0 && dy < -dx)
        {
            if (p_left_turn)
            {
                result = new IntPoint(this.x - dy, p_to_point.y);
            }
            else
            {
                result = new IntPoint(p_to_point.x + dy, this.y);
            }
        }
        else if (dy < 0 && dy > dx)
        {
            if (p_left_turn)
            {
                result = new IntPoint(p_to_point.x - dy, this.y);
            }
            else
            {
                result = new IntPoint(this.x + dy, p_to_point.y);
            }
        }
        else if (dx < 0 && dy < dx)
        {
            if (p_left_turn)
            {
                result = new IntPoint(p_to_point.x, this.y + dx);
            }
            else
            {
                result = new IntPoint(this.x, p_to_point.y - dx);
            }
        }
        else if (dx > 0 && dy < -dx)
        {
            if (p_left_turn)
            {
                result = new IntPoint(this.x, p_to_point.y + dx);
            }
            else
            {
                result = new IntPoint(p_to_point.x, this.y - dx);
            }
        }
        else if (dy < 0 && dy > -dx)
        {
            if (p_left_turn)
            {
                result = new IntPoint(this.x - dy, p_to_point.y);
            }
            else
            {
                result = new IntPoint(p_to_point.x + dy, this.y);
            }
        }
        else
        {
            // the line from this point to p_to_point is already a multiple of 45 degree
            result = null;
        }
        return result;
    }
    
    /**
     * Calculates a corner point p so that the lines through this point and p and from
     * p to p_to_point are hprizontal or vertical, and that the angle at p will be
     * 90 degree. If p_left_turn, p_to_point will be on the left of the line
     * from this point to p, else on the right.
     * Returns null, if the line from this point to p_to_point is already orthogonal.
     */
    public IntPoint ninety_degree_corner( IntPoint p_to_point, boolean p_left_turn)
    {
        int dx = p_to_point.x - this.x;
        int dy = p_to_point.y - this.y;
        IntPoint result;
        
        // handle the 4 quadrants
        
        if (dx > 0 && dy > 0 || dx < 0 && dy < 0)
        {
            if (p_left_turn)
            {
                result = new IntPoint(p_to_point.x, this.y);
            }
            else
            {
                result = new IntPoint(this.x, p_to_point.y);
            }
        }
        else if (dx < 0 && dy > 0 || dx > 0 && dy < 0)
        {
            if (p_left_turn)
            {
                result = new IntPoint(this.x, p_to_point.y);
            }
            else
            {
                result = new IntPoint(p_to_point.x, this.y);
            }
        }
        else
        {
            //the line from this point to p_to_point is already orthogonal
            result = null;
        }
        return result;
    }
    
    public int compare_x(Point p_other)
    {
        return -p_other.compare_x(this);
    }
    
    public int compare_y(Point p_other)
    {
        return -p_other.compare_y(this);
    }
    
    
    int compare_x(IntPoint p_other)
    {
        int result;
        if (this.x > p_other.x)
        {
            result = 1;
        }
        else if (this.x == p_other.x)
        {
            result = 0;
        }
        else
        {
            result = -1;
        }
        return result;
    }
    
    int compare_y(IntPoint p_other)
    {
        int result;
        if (this.y > p_other.y)
        {
            result = 1;
        }
        else if (this.y == p_other.y)
        {
            result = 0;
        }
        else
        {
            result = -1;
        }
        return result;
    }
    
    int compare_x(RationalPoint p_other)
    {
        return -p_other.compare_x(this);
    }
    
    int compare_y(RationalPoint p_other)
    {
        return -p_other.compare_y(this);
    }
    
    /**
     * the x coordinate of this point
     */
    public final int x;
    
    /**
     * the y coordinate of this point
     */
    public final int y;
}

