/*
 *   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.
 *
 * FloatPoint.java
 *
 * Created on 2. Februar 2003, 09:14
 */

package eu.mihosoft.freerouting.geometry.planar;

import eu.mihosoft.freerouting.logger.FRLogger;

/**
 *
 * Implements a point in the plane as a touple of double's.
 * Because arithmetic calculations with double's are in general not
 * exact, FloatPoint is not derived from the abstract class Point.
 *
 *
 * @author Alfons Wirtz
 */


public class FloatPoint implements java.io.Serializable
{
    
    public static final FloatPoint ZERO = new FloatPoint(0,0);
    
    /**
     * creates an instance of class FloatPoint from two double's,
     */
    public FloatPoint(double p_x, double p_y)
    {
        x = p_x;
        y = p_y;
    }
    
    public FloatPoint(IntPoint p_pt)
    {
        x = p_pt.x ;
        y = p_pt.y ;
    }
    
    /**
     * returns the square of the distance from this point to the zero point
     */
    public final double size_square()
    {
        return x * x + y * y;
    }
    
    /**
     * returns the distance from this point to the zero point
     */
    public final double size()
    {
        return Math.sqrt(size_square());
    }
    
    /**
     * returns the square of the distance from this Point to the Point p_other
     */
    public final double distance_square(FloatPoint p_other)
    {
        double dx = p_other.x - x;
        double dy = p_other.y - y;
        return dx * dx + dy * dy;
    }
    
    /**
     * returns the distance from this point to the point p_other
     */
    public final double distance(FloatPoint p_other)
    {
        return Math.sqrt(distance_square(p_other));
    }
    
    /**
     * Computes the weighted distance to p_other.
     */
    public double weighted_distance(FloatPoint p_other, double p_horizontal_weight, double p_vertical_weight)
    {
        double delta_x = this.x - p_other.x;
        double delta_y = this.y - p_other.y;
        delta_x *= p_horizontal_weight;
        delta_y *= p_vertical_weight;
        double result = Math.sqrt(delta_x * delta_x + delta_y * delta_y);
        return result;
    }
    
    /**
     * rounds the coordinates from an object of class Point_double to
     * an object of class IntPoint
     */
    public IntPoint round()
    {
        return new IntPoint((int)Math.round(x), (int)Math.round(y));
    }
    
    /**
     * Rounds this point, so that if this point is on the right side
     * of any directed line with direction p_dir, the result
     * point will also be on the right side.
     */
    public IntPoint round_to_the_right(Direction p_dir)
    {
        FloatPoint dir = p_dir.get_vector().to_float();
        int rounded_x;
        
        if (dir.y > 0)
        {
            rounded_x = (int) Math.ceil(x);
        }
        else if (dir.y < 0)
        {
            rounded_x = (int) Math.floor(x);
        }
        else
        {
            rounded_x = (int) Math.round(x);
        }
        
        int rounded_y;
        
        if (dir.x > 0)
        {
            rounded_y = (int) Math.floor(y);
        }
        else if (dir.x < 0)
        {
            rounded_y = (int) Math.ceil(y);
        }
        else
        {
            rounded_y = (int) Math.round(y);
        }
        return new IntPoint(rounded_x, rounded_y);
    }
    
    /**
     * Round this Point so the x coordinate of the result will be a multiple of p_horizontal_grid
     * and the y coordinate a multiple of p_vertical_grid.
     */
    public IntPoint round_to_grid(int p_horizontal_grid, int p_vertical_grid)
    {
        double rounded_x;
        if (p_horizontal_grid > 0)
        {
            rounded_x = Math.rint(this.x / p_horizontal_grid) *  p_horizontal_grid;
        }
        else
        {
            rounded_x = this.x;
        }
        double rounded_y;
        if (p_vertical_grid > 0)
        {
            rounded_y =  Math.rint(this.y / p_vertical_grid) *  p_vertical_grid;
        }
        else
        {
            rounded_y = this.y;
        }
        return new IntPoint((int) rounded_x, (int) rounded_y );
    }
    
    /**
     * Rounds this point, so that if this point is on the left side
     * of any directed line with direction p_dir, the result
     * point will also be on the left side.
     */
    public IntPoint round_to_the_left(Direction p_dir)
    {
        FloatPoint dir = p_dir.get_vector().to_float();
        int rounded_x;
        
        if (dir.y > 0)
        {
            rounded_x = (int) Math.floor(x);
        }
        else if (dir.y < 0)
        {
            rounded_x = (int) Math.ceil(x);
        }
        else
        {
            rounded_x = (int) Math.round(x);
        }
        
        int rounded_y;
        
        if (dir.x > 0)
        {
            rounded_y = (int) Math.ceil(y);
        }
        else if (dir.x < 0)
        {
            rounded_y = (int) Math.floor(y);
        }
        else
        {
            rounded_y = (int) Math.round(y);
        }
        return new IntPoint(rounded_x, rounded_y);
    }
    
    /**
     * Adds the coordinates of this FloatPoint and p_other.
     */
    public FloatPoint add(FloatPoint p_other)
    {
        return new FloatPoint(this.x + p_other.x, this.y + p_other.y);
    }
    
    /**
     * Substracts the coordinates of p_other from this FloatPoint.
     */
    public FloatPoint substract(FloatPoint p_other)
    {
        return new FloatPoint(this.x - p_other.x, this.y - p_other.y);
    }
    
    /**
     * Returns an approximation of the perpendicular projection
     * of this point onto p_line
     */
    public FloatPoint projection_approx(Line p_line)
    {
        FloatLine line = new FloatLine(p_line.a.to_float(), p_line.b.to_float());
        return line.perpendicular_projection(this);
    }
    
    /**
     * Calculates the scalar prodct of (p_1 - this). with (p_2 - this).
     */
    public double scalar_product(FloatPoint p_1, FloatPoint p_2)
    {
        if (p_1 == null || p_2 == null)
        {
            FRLogger.warn("FloatPoint.scalar_product: parameter point is null");
            return 0;
        }
        double  dx_1 = p_1.x - this.x;
        double  dx_2 = p_2.x - this.x;
        double  dy_1 = p_1.y - this.y;
        double  dy_2 = p_2.y - this.y;
        return (dx_1 * dx_2 + dy_1 * dy_2);
    }
    
    /**
     * Approximates a FloatPoint on the line from zero to this point
     * with distance p_new_length from zero.
     */
    public FloatPoint change_size(double p_new_size)
    {
        if (x == 0 && y == 0)
        {
            // the size of the zero point cannot be changed
            return this;
        }
        double length = Math.sqrt(x * x + y * y);
        double new_x = (x * p_new_size) / length;
        double new_y = (y * p_new_size) / length;
        return new FloatPoint(new_x, new_y);
    }
    
    /**
     * Approximates a FloatPoint on the line from this point to p_to_point
     * with distance p_new_length from this point.
     */
    public FloatPoint change_length(FloatPoint p_to_point, double p_new_length)
    {
        double dx = p_to_point.x - this.x;
        double dy = p_to_point.y - this.y;
        if (dx == 0 && dy == 0)
        {
            FRLogger.warn("IntPoint.change_length: Points are equal");
            return p_to_point;
        }
        double length = Math.sqrt(dx * dx + dy * dy);
        double new_x = this.x + (dx * p_new_length) / length;
        double new_y = this.y + (dy * p_new_length) / length;
        return new FloatPoint(new_x, new_y);
    }
    
    /**
     * Returns the middle point between this point and p_to_point.
     */
    public FloatPoint middle_point(FloatPoint p_to_point)
    {
        if (p_to_point == this)
        {
            return this;
        }
        double middle_x = 0.5 * (this.x + p_to_point.x);
        double middle_y = 0.5 * (this.y + p_to_point.y);
        return new FloatPoint(middle_x, middle_y);
    }
    
    /**
     * The function returns
     * Side.ON_THE_LEFT, if this Point is on the left of the line from p_1 to p_2;
     * and Side.ON_THE_RIGHT, if this Point is on the right of the line from p_1 to p_2.
     * Collinearity is not defined, becouse numerical calculations ar not exact for FloatPoints.
     */
    public Side side_of(FloatPoint p_1, FloatPoint p_2)
    {
        double d21_x = p_2.x - p_1.x;
        double d21_y = p_2.y - p_1.y;
        double d01_x = this.x - p_1.x;
        double d01_y = this.y - p_1.y;
        double determinant = d21_x * d01_y - d21_y * d01_x;
        return Side.of(determinant);
    }
    
    /**
     *  Rotates this FloatPoints by p_angle ( in radian ) around the p_pole.
     */
    public FloatPoint rotate(double p_angle, FloatPoint p_pole)
    {
        if (p_angle == 0)
        {
            return this;
        }
        double dx = x - p_pole.x;
        double dy = y - p_pole.y;
        double sin_angle = Math.sin(p_angle);
        double cos_angle = Math.cos(p_angle);
        double new_dx = dx * cos_angle - dy * sin_angle;
        double new_dy = dx * sin_angle + dy * cos_angle;
        return new FloatPoint(p_pole.x + new_dx, p_pole.y + new_dy);
    }
    
    /**
     * Turns this FloatPoint by p_factor times 90 degree around ZERO.
     */
    public FloatPoint turn_90_degree(int p_factor)
    {
        int n = p_factor;
        while (n < 0)
        {
            n += 4;
        }
        while (n >= 4)
        {
            n -= 4;
        }
        double new_x ;
        double new_y ;
        switch (n)
        {
            case 0: // 0 degree
                new_x = x;
                new_y = y;
                break;
            case 1: // 90 degree
                new_x = -y ;
                new_y = x ;
                break;
            case 2: // 180 degree
                new_x = -x ;
                new_y = -y ;
                break;
            case 3: // 270 degree
                new_x = y ;
                new_y = -x ;
                break;
            default:
                new_x = 0 ;
                new_y = 0 ;
        }
        return new FloatPoint(new_x, new_y);
    }
    
    /**
     * Turns this FloatPoint by p_factor times 90 degree around p_pole.
     */
    public FloatPoint turn_90_degree(int p_factor, FloatPoint p_pole)
    {
        FloatPoint v = this.substract(p_pole);
        v = v.turn_90_degree(p_factor);
        return p_pole.add(v);
    }
    /**
     * Checks, if this point is contained in the box spanned by p_1 and p_2 with the input tolerance.
     */
    public boolean is_contained_in_box(FloatPoint p_1, FloatPoint p_2, double p_tolerance)
    {
        double min_x;
        double max_x;
        if (p_1.x < p_2.x)
        {
            min_x = p_1.x;
            max_x = p_2.x;
        }
        else
        {
            min_x = p_2.x;
            max_x = p_1.x;
        }
        if (this.x < min_x - p_tolerance || this.x > max_x + p_tolerance)
        {
            return false;
        }
        double min_y;
        double max_y;
        if (p_1.y < p_2.y)
        {
            min_y = p_1.y;
            max_y = p_2.y;
        }
        else
        {
            min_y = p_2.y;
            max_y = p_1.y;
        }
        return (this.y >= min_y - p_tolerance && this.y <= max_y + p_tolerance);
    }
    
    /**
     * Creates the smallest IntBox containing this point.
     */
    public IntBox bounding_box()
    {
        IntPoint lower_left = new IntPoint((int)Math.floor(this.x),(int)Math.floor(this.y));
        IntPoint upper_right = new IntPoint((int)Math.ceil(this.x),(int)Math.ceil(this.y));
        return new IntBox(lower_left, upper_right);
    }
    
    /**
     * Calculates the touching points of the tangents from this point to a circle
     * around p_to_point with radius p_distance.
     * Solves the quadratic equation, which results by substituting x by the
     * term in y from the equation of the polar line of a circle with center
     * p_to_point and radius p_distance and putting it into the circle
     * equation. The polar line is the line through the 2 tangential points
     * of the circle looked at from from this point and
     * has the equation
     *  (this.x - p_to_point.x) * (x - p_to_point.x)
     *   + (this.y - p_to_point.y) * (y - p_to_point.y) = p_distance **2
     */
    public FloatPoint[] tangential_points(FloatPoint p_to_point, double p_distance)
    {
        // turn the situation 90 degree if the x difference is smaller
        // than the y difference for better numerical stability
        
        double dx = Math.abs(this.x - p_to_point.x);
        double dy = Math.abs(this.y - p_to_point.y);
        boolean situation_turned = (dy > dx);
        FloatPoint pole;
        FloatPoint circle_center;
        
        if (situation_turned)
        {
            // turn the situation by 90 degree
            pole = new FloatPoint(-this.y, this.x);
            circle_center = new FloatPoint(-p_to_point.y, p_to_point.x);
        }
        else
        {
            pole = this;
            circle_center = p_to_point;
        }
        
        dx = pole.x - circle_center.x;
        dy = pole.y - circle_center.y;
        double dx_square = dx * dx;
        double dy_square = dy * dy;
        double dist_square = dx_square + dy_square;
        double radius_square = p_distance * p_distance;
        double discriminant = radius_square * dy_square - (radius_square - dx_square) * dist_square;
        
        if (discriminant <= 0)
        {
            // pole is inside the circle.
            return new FloatPoint[0];
        }
        double square_root = Math.sqrt(discriminant);
        
        FloatPoint[] result = new FloatPoint[2];
        
        double     a1 = radius_square * dy;
        double     dy1 = (a1 + p_distance * square_root) / dist_square;
        double     dy2 = (a1 - p_distance * square_root) / dist_square;
        
        double first_point_y = dy1 + circle_center.y;
        double first_point_x = (radius_square - dy * dy1) / dx + circle_center.x;
        double second_point_y = dy2 + circle_center.y;
        double second_point_x = (radius_square - dy * dy2) / dx + circle_center.x;
        
        if (situation_turned)
        {
            // turn the result by 270 degree
            result[0] = new FloatPoint(first_point_y, -first_point_x);
            result[1] = new FloatPoint(second_point_y, -second_point_x);
        }
        else
        {
            result[0] = new FloatPoint(first_point_x, first_point_y);
            result[1] = new FloatPoint(second_point_x, second_point_y);
        }
        return result;
    }
    
    /**
     * Calculates the left tangential point of the line from this point
     * to a circle around p_to_point with radius p_distance.
     * Returns null, if this point is inside this circle.
     */
    public FloatPoint left_tangential_point(FloatPoint p_to_point, double p_distance)
    {
        if (p_to_point == null)
        {
            return null;
        }
        FloatPoint[] tangent_points = tangential_points(p_to_point, p_distance);
        if (tangent_points.length < 2)
        {
            return null;
        }
        FloatPoint result;
        if (p_to_point.side_of(this,  tangent_points[0]) == Side.ON_THE_RIGHT)
        {
            result = tangent_points[0];
        }
        else
        {
            result = tangent_points[1];
        }
        return result;
    }
    
    /**
     * Calculates the right tangential point of the line from this point
     * to a circle around p_to_point with radius p_distance.
     * Returns null, if this point is inside this circle.
     */
    public FloatPoint right_tangential_point(FloatPoint p_to_point, double p_distance)
    {
        if (p_to_point == null)
        {
            return null;
        }
        FloatPoint[] tangent_points = tangential_points(p_to_point, p_distance);
        if (tangent_points.length < 2)
        {
            return null;
        }
        FloatPoint result;
        if (p_to_point.side_of(this,  tangent_points[0]) == Side.ON_THE_LEFT)
        {
            result = tangent_points[0];
        }
        else
        {
            result = tangent_points[1];
        }
        return result;
    }
    
    /**
     * Calculates the center of the circle through this point, p_1 and p_2
     * by calculating the intersection of the two lines perpendicular to and passing through
     * the midpoints of the lines (this, p_1) and (p_1, p_2).
     */
    public FloatPoint circle_center(FloatPoint p_1, FloatPoint p_2)
    {
        double slope_1 = (p_1.y - this.y)/(p_1.x - this.x);
        double slope_2 = (p_2.y - p_1.y)/(p_2.x - p_1.x);
        double x_center =
                (slope_1 * slope_2 * (this.y -p_2.y) + slope_2 * (this.x + p_1.x) - slope_1 *(p_1.x + p_2.x))
                /(2 * (slope_2 - slope_1));
        double y_center = (0.5 * (this.x + p_1.x) - x_center)/slope_1 + 0.5 * (this.y + p_1.y);
        return new FloatPoint(x_center, y_center);
    }
    
    /**
     * Returns true, if this point is contained in the circle through p_1, p_2 and p_3.
     */
    public boolean inside_circle(FloatPoint p_1, FloatPoint p_2, FloatPoint p_3)
    {
        FloatPoint center =  p_1.circle_center(p_2, p_3);
        double radius_square = center.distance_square(p_1);
        return (this.distance_square(center) < radius_square - 1); // - 1 is a tolerance for numerical stability.
    }
    
    public String to_string(java.util.Locale p_locale)
    {
        java.text.NumberFormat nf =  java.text.NumberFormat.getInstance(p_locale);
        nf.setMaximumFractionDigits(4);
        return (" (" + nf.format(x) + " , " + nf.format(y) + ") ");
    }
    
    public String toString()
    {
        return to_string(java.util.Locale.ENGLISH);
    }
    
    /**
     * Calculates the smallest IntOctagon containing all the input points
     */
    public static IntOctagon bounding_octagon(FloatPoint [] p_point_arr)
    {
        double lx = Integer.MAX_VALUE;
        double ly = Integer.MAX_VALUE;
        double rx = Integer.MIN_VALUE;
        double uy = Integer.MIN_VALUE;
        double ulx = Integer.MAX_VALUE;
        double lrx = Integer.MIN_VALUE;
        double llx = Integer.MAX_VALUE;
        double urx = Integer.MIN_VALUE;
        for (int i = 0; i < p_point_arr.length; ++i)
        {
            FloatPoint curr = p_point_arr[i];
            lx = Math.min(lx, curr.x);
            ly = Math.min(ly, curr.y);
            rx = Math.max(rx, curr.x);
            uy = Math.max(uy, curr.y);
            double tmp = curr.x - curr.y;
            ulx = Math.min(ulx, tmp);
            lrx = Math.max(lrx, tmp);
            tmp = curr.x + curr.y;
            llx = Math.min(llx, tmp);
            urx = Math.max(urx, tmp);
        }
        IntOctagon surrounding_octagon = new
                IntOctagon((int)Math.floor(lx), (int)Math.floor(ly),
                (int)Math.ceil(rx), (int)Math.ceil(uy),
                (int)Math.floor(ulx), (int)Math.ceil(lrx),
                (int)Math.floor(llx), (int)Math.ceil(urx));
        return surrounding_octagon;
    }
    
    /**
     * the x coordinate of this point
     */
    public final double x;
    
    /**
     * the y coordinate of this point
     */
    public final double y;
}