/*
 *   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.
 *
 * FloatLine.java
 *
 * Created on 19. Februar 2004, 07:22
 */

package eu.mihosoft.freerouting.geometry.planar;

import eu.mihosoft.freerouting.logger.FRLogger;

/**
 * Defines a line in the plane by to FloatPoints.
 * Calculations with FloatLines are generally not exact.
 * For that reason collinearity for example is not defined for FloatLines.
 * If exactnesss is needed, use the class Line instead.
 *
 * @author Alfons Wirtz
 */
public class FloatLine
{
    
    /**
     * Creates a line from two FloatPoints.
     */
    public FloatLine(FloatPoint p_a, FloatPoint p_b)
    {
        if (p_a == null || p_b == null)
        {
            FRLogger.warn("FloatLine: Parameter is null");
        }
        a = p_a;
        b = p_b;
    }
    
    /**
     * Returns the FloatLine with swapped end points.
     */
    public FloatLine opposite()
    {
        return new FloatLine(this.b, this.a);
    }
    
    public FloatLine adjust_direction(FloatLine p_other)
    {
        if (this.b.side_of(this.a, p_other.a )== p_other.b.side_of(this.a, p_other.a ))
        {
            return this;
        }
        return this.opposite();
    }
    
    /**
     * Calculates the intersection of this line with p_other.
     * Returns null, if the lines are parallel.
     */
    public FloatPoint intersection(FloatLine p_other)
    {
        double d1x = this.b.x - this.a.x;
        double d1y = this.b.y - this.a.y;
        double d2x = p_other.b.x - p_other.a.x;
        double d2y = p_other.b.y - p_other.a.y;
        double det_1 = this.a.x * this.b.y - this.a.y * this.b.x;
        double det_2 = p_other.a.x * p_other.b.y - p_other.a.y * p_other.b.x;
        double det = d2x * d1y - d2y * d1x;
        double is_x;
        double is_y;
        if(det == 0)
        {
            return null;
        }
        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);
    }
    
    /**
     * 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 FloatLine translate(double p_dist)
    {
        double dx = b.x - a.x;
        double dy = b.y - a.y;
        double dxdx = dx * dx;
        double dydy = dy * dy;
        double lenght = Math.sqrt(dxdx + dydy);
        FloatPoint new_a;
        if (dxdx <= dydy)
        {
            // translate along the x axis
            double rel_x = (p_dist * lenght) / dy;
            new_a = new FloatPoint(this.a.x - rel_x, this.a.y);
        }
        else
        {
            // translate along the  y axis
            double rel_y = (p_dist * lenght) / dx;
            new_a = new FloatPoint(this.a.x, this.a.y + rel_y);
        }
        FloatPoint new_b = new FloatPoint(new_a.x + dx, new_a.y + dy);
        return new FloatLine(new_a, new_b);
    }
    
    /**
     * 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)
    {
        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 an approximation of the perpensicular projection
     * of p_point onto this line.
     */
    public FloatPoint perpendicular_projection(FloatPoint p_point)
    {
        
        double dx = b.x - a.x;
        double dy = b.y - a.y;
        if (dx == 0 && dy == 0)
        {
            return this.a;
        }
        
        double dxdx = dx * dx;
        double dydy = dy * dy;
        double dxdy = dx * dy;
        double denominator = dxdx + dydy;
        double  det = a.x * b.y - b.x * a.y;
        
        double x = (p_point.x * dxdx + p_point.y * dxdy + det * dy) / denominator;
        double y = (p_point.x * dxdy + p_point.y * dydy - det * dx) / denominator;
        
        return new FloatPoint(x, y);
    }
    
    /**
     * Returns the distance of p_point to the nearest point of this line
     * betweem this.a and this.b.
     */
    public double segment_distance(FloatPoint p_point)
    {
        FloatPoint projection = perpendicular_projection(p_point);
        double result;
        if (projection.is_contained_in_box(this.a, this.b, 0.01))
        {
            result = p_point.distance(projection);
        }
        else
        {
            result = Math.min(p_point.distance(a), p_point.distance(b));
        }
        return result;
    }
    
    /**
     * Returns the perpendicular projection of p_line_segment onto this oriented line segment,
     * Returns null, if the projection is empty.
     */
    public FloatLine segment_projection(FloatLine p_line_segment)
    {
        if (this.b.scalar_product(this.a, p_line_segment.a) < 0)
        {
            return null;
        }
        if (this.a.scalar_product(this.b, p_line_segment.b) < 0)
        {
            return null;
        }
        FloatPoint projected_a;
        if (this.a.scalar_product(this.b, p_line_segment.a) < 0)
        {
            projected_a = this.a;
        }
        else
        {
            projected_a  = this.perpendicular_projection(p_line_segment.a);
            if (Math.abs(projected_a.x) >= Limits.CRIT_INT || Math.abs(projected_a.y) >= Limits.CRIT_INT)
            {
                return null;
            }
        }
        FloatPoint projected_b;
        if (this.b.scalar_product(this.a, p_line_segment.b) < 0)
        {
            projected_b = this.b;
        }
        else
        {
            projected_b  = this.perpendicular_projection(p_line_segment.b);
        }
        if (Math.abs(projected_b.x) >= Limits.CRIT_INT || Math.abs(projected_b.y) >= Limits.CRIT_INT)
        {
            return null;
        }
        return new FloatLine(projected_a, projected_b);
    }
    
    /**
     * Returns the projection of p_line_segment onto this oriented line segment
     * by moving p_line_segment perpendicular into the direction of this  line segmant
     * Returns null, if the projection is empty or p_line_segment.a  == p_line_segment.b
     */
    public FloatLine segment_projection_2(FloatLine p_line_segment)
    {
        if (p_line_segment.a.scalar_product(p_line_segment.b, this.b) <= 0)
        {
            return null;
        }
        if ( p_line_segment.b.scalar_product(p_line_segment.a, this.a) <= 0)
        {
            return null;
        }
        FloatPoint projected_a;
        if (p_line_segment.a.scalar_product(p_line_segment.b, this.a) < 0)
        {
            FloatLine curr_perpendicular_line =
                    new FloatLine(p_line_segment.a, p_line_segment.b.turn_90_degree(1, p_line_segment.a));
            projected_a = curr_perpendicular_line.intersection(this);
            if (projected_a == null || Math.abs(projected_a.x) >= Limits.CRIT_INT || Math.abs(projected_a.y) >= Limits.CRIT_INT)
            {
                return null;
            }
        }
        else
        {
            projected_a = this.a;
        }
        
        FloatPoint projected_b;
        
        if (p_line_segment.b.scalar_product(p_line_segment.a, this.b) < 0)
        {
            FloatLine curr_perpendicular_line =
                    new FloatLine(p_line_segment.b, p_line_segment.a.turn_90_degree(1, p_line_segment.b));
            projected_b = curr_perpendicular_line.intersection(this);
            if (projected_b == null || Math.abs(projected_b.x) >= Limits.CRIT_INT || Math.abs(projected_b.y) >= Limits.CRIT_INT)
            {
                return null;
            }
        }
        else
        {
            projected_b = this.b;
        }
        return new FloatLine(projected_a, projected_b);
    }
    
    /**
     * Shrinks this line on both sides by p_value.
     * The result will contain at least the gravity point of the line.
     */
    public FloatLine shrink_segment(double p_offset)
    {
        double dx = b.x - a.x;
        double dy = b.y - a.y;
        if (dx == 0 && dy == 0)
        {
            return this;
        }
        double length = Math.sqrt(dx * dx + dy * dy);
        double offset = Math.min(p_offset, length/2);
        FloatPoint new_a = new FloatPoint(a.x + (dx * offset) / length, a.y + (dy * offset) / length);
        double new_length = length - offset;
        FloatPoint new_b = new FloatPoint(a.x + (dx * new_length) / length, a.y + (dy * new_length) / length);
        return new FloatLine(new_a, new_b);
    }
    
    /**
     * Calculates the nearest point on this line to p_from_point between
     * this.a and this.b.
     */
    public FloatPoint nearest_segment_point(FloatPoint p_from_point)
    {
        FloatPoint projection = this.perpendicular_projection(p_from_point);
        if (projection.is_contained_in_box(this.a, this.b, 0.01))
        {
            return projection;
        }
        // Now the projection is outside the line segment.
        FloatPoint result;
        if (p_from_point.distance_square(this.a) <= p_from_point.distance_square(this.b))
        {
            result = this.a;
        }
        else
        {
            result = this.b;
        }
        return result;
    }
    
    /**
     * Divides this line segment into p_count line segments of nearly equal length.
     * and at most p_max_section_length.
     */
    public FloatLine[] divide_segment_into_sections(int p_count)
    {
        if (p_count == 0)
        {
            return new FloatLine[0];
        }
        if (p_count == 1)
        {
            FloatLine []  result = new FloatLine[1];
            result[0] = this;
            return result;
        }
        double line_length = this.b.distance(this.a);
        FloatLine[] result = new FloatLine[p_count];
        double section_length = line_length / p_count;
        double dx = b.x - a.x;
        double dy = b.y - a.y;
        FloatPoint curr_a = this.a;
        for (int i = 0; i < p_count; ++i)
        {
            FloatPoint curr_b;
            if (i == p_count - 1)
            {
                curr_b = this.b;
            }
            else
            {
                double curr_b_dist = (i + 1) * section_length;
                double curr_b_x = a.x + (dx * curr_b_dist)/line_length;
                double curr_b_y = a.y + (dy * curr_b_dist)/line_length;
                curr_b = new FloatPoint(curr_b_x, curr_b_y);
            }
            result[i] = new FloatLine(curr_a, curr_b);
            curr_a = curr_b;
        }
        return result;
    }
    
    public final FloatPoint a;
    public final FloatPoint b;
}
