/*
 *   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.
 *
 * LocateFoundConnectionAlgo.java
 *
 * Created on 14. Februar 2004, 07:55
 */

package eu.mihosoft.freerouting.autoroute;

import eu.mihosoft.freerouting.geometry.planar.FloatLine;
import eu.mihosoft.freerouting.geometry.planar.FloatPoint;
import eu.mihosoft.freerouting.geometry.planar.Side;
import eu.mihosoft.freerouting.geometry.planar.TileShape;

import java.util.Collection;
import java.util.SortedSet;
import java.util.LinkedList;

import eu.mihosoft.freerouting.board.ShapeSearchTree;
import eu.mihosoft.freerouting.board.AngleRestriction;
import eu.mihosoft.freerouting.board.Item;
import eu.mihosoft.freerouting.board.TestLevel;
import eu.mihosoft.freerouting.logger.FRLogger;


/**
 * Calculates from the backtrack list the location of the traces and vias,
 * which realize a connection found by the maze search algorithm.
 *
 * @author Alfons Wirtz
 */
class LocateFoundConnectionAlgoAnyAngle extends LocateFoundConnectionAlgo
{
    
    /** Creates a new instance of LocateFoundConnectionAlgo */
    protected LocateFoundConnectionAlgoAnyAngle(MazeSearchAlgo.Result p_maze_search_result, AutorouteControl p_ctrl,
            ShapeSearchTree p_search_tree, AngleRestriction p_angle_restriction,
            SortedSet<Item> p_ripped_item_list, TestLevel p_test_level)
    {
        super(p_maze_search_result, p_ctrl, p_search_tree, p_angle_restriction, p_ripped_item_list, p_test_level);
    }
    
    /**
     * Calculates a list with the next  point  of the trace under construction.
     * If the trace is completed, the result list will be empty.
     */
    protected Collection<FloatPoint> calculate_next_trace_corners()
    {
        Collection<FloatPoint> result = new LinkedList<FloatPoint>();
        if (this.current_to_door_index >= this.current_target_door_index)
        {
            if (this.current_to_door_index == this.current_target_door_index)
            {
                FloatPoint nearest_point = this.current_target_shape.nearest_point(this.current_from_point.round()).to_float();
                ++this.current_to_door_index;
                result.add(nearest_point);
            }
            return result;
        }
        
        double trace_halfwidth_exact = this.ctrl.compensated_trace_half_width[this.current_trace_layer];
        double trace_halfwidth_max = trace_halfwidth_exact + AutorouteEngine.TRACE_WIDTH_TOLERANCE;
        double trace_halfwidth_middle = trace_halfwidth_exact + c_tolerance;
        
        BacktrackElement curr_to_info = this.backtrack_array[this.current_to_door_index];
        FloatPoint door_left_corner = calc_door_left_corner(curr_to_info);
        FloatPoint door_right_corner = calc_door_right_corner(curr_to_info);
        if (this.current_from_point.side_of(door_left_corner, door_right_corner) != Side.ON_THE_RIGHT)
        {
            // the door is already crossed at this.from_point
            if (this.current_from_point.scalar_product(this.previous_from_point, door_left_corner) >= 0)
            {
                // Also the left corner of the door is passed.
                // That may not be the case if the door line is crossed almost parallel.
                door_left_corner = null;
            }
            if(this.current_from_point.scalar_product(this.previous_from_point, door_right_corner) >= 0)
            {
                // Also the right corner of the door is passed.
                door_right_corner = null;
            }
            if (door_left_corner == null && door_right_corner == null)
            {
                // The door is completely passed.
                ++this.current_to_door_index;
                result.add(this.current_from_point);
                return result;
            }
        }
        
        // Calculate the visibility range for a trace line from current_from_point
        // through the interval from left_most_visible_point to right_most_visible_point,
        // by advancing the door index as far as possible, so that still somthing is visible.
        
        boolean end_of_trace = false;
        FloatPoint left_tangent_point = null;
        FloatPoint right_tangent_point = null;
        int new_door_ind = this.current_to_door_index;
        int left_ind = new_door_ind;
        int right_ind = new_door_ind;
        int curr_door_ind =  this.current_to_door_index + 1;
        FloatPoint result_corner = null;
        
        // construct a maximum lenght straight line through the doors
        
        for (;;)
        {
            left_tangent_point = this.current_from_point.right_tangential_point(door_left_corner, trace_halfwidth_max);
            if (door_left_corner != null && left_tangent_point == null)
            {
                if (this.test_level.ordinal() >=  TestLevel.ALL_DEBUGGING_OUTPUT.ordinal())
                {
                    FRLogger.warn("LocateFoundConnectionAlgo.calculate_next_trace_corner: left tangent point is null");
                }
                left_tangent_point = door_left_corner;
            }
            right_tangent_point = this.current_from_point.left_tangential_point(door_right_corner, trace_halfwidth_max);
            if (door_right_corner != null && right_tangent_point == null)
            {
                if (this.test_level.ordinal() >=  TestLevel.ALL_DEBUGGING_OUTPUT.ordinal())
                {
                    FRLogger.warn("LocateFoundConnectionAlgo.calculate_next_trace_corner: right tangent point is null");
                }
                right_tangent_point = door_right_corner;
            }
            if (left_tangent_point != null && right_tangent_point != null &&
                    right_tangent_point.side_of(this.current_from_point, left_tangent_point) != Side.ON_THE_RIGHT)
            {
                // The gap between  left_most_visible_point and right_most_visible_point ist to small
                // for a trace with the current half width.
                
                double left_corner_distance = door_left_corner.distance(this.current_from_point);
                double right_corner_distance = door_right_corner.distance(this.current_from_point);
                if ( left_corner_distance <= right_corner_distance)
                {
                    new_door_ind = left_ind;
                    result_corner = left_turn_next_corner(this.current_from_point, trace_halfwidth_max, door_left_corner, door_right_corner);
                }
                else
                {
                    new_door_ind = right_ind;
                    result_corner = right_turn_next_corner(this.current_from_point, trace_halfwidth_max, door_right_corner, door_left_corner);
                }
                break;
            }
            if (curr_door_ind >= this.current_target_door_index)
            {
                end_of_trace = true;
                break;
            }
            BacktrackElement next_to_info = this.backtrack_array[curr_door_ind];
            FloatPoint next_left_corner = calc_door_left_corner(next_to_info);
            FloatPoint next_right_corner = calc_door_right_corner(next_to_info);
            if (this.current_from_point.side_of(next_left_corner, next_right_corner) != Side.ON_THE_RIGHT)
            {
                // the door may be already crossed at this.from_point
                if (door_left_corner == null && this.current_from_point.scalar_product(this.previous_from_point, next_left_corner) >= 0)
                {
                    // Also the left corner of the door is passed.
                    // That may not be the case if the door line is crossed almost parallel.
                    next_left_corner = null;
                }
                if(door_right_corner == null && this.current_from_point.scalar_product(this.previous_from_point, next_right_corner) >= 0)
                {
                    // Also the right corner of the door is passed.
                    next_right_corner = null;
                }
                if (next_left_corner == null && next_right_corner == null)
                {
                    // The door is completely passed.
                    // Should not happen because the previous door was not passed compledtely.
                    if (this.test_level.ordinal() >=  TestLevel.ALL_DEBUGGING_OUTPUT.ordinal())
                    {
                        FRLogger.warn("LocateFoundConnectionAlgo.calculate_next_trace_corner: next door passed unexpected");
                    }
                    ++this.current_to_door_index;
                    result.add(this.current_from_point);
                    return result;
                }
            }
            if (door_left_corner != null && door_right_corner != null)
                // otherwise the following side_of conditions may not be correct
                // even if all parameter points are defined
            {
                if (next_left_corner.side_of(this.current_from_point, door_right_corner) == Side.ON_THE_RIGHT)
                {
                    // bend to the right
                    new_door_ind = right_ind + 1;
                    result_corner = right_turn_next_corner(this.current_from_point, trace_halfwidth_max, door_right_corner, next_left_corner);
                    break;
                }
                
                if (next_right_corner.side_of(this.current_from_point, door_left_corner) == Side.ON_THE_LEFT)
                {
                    // bend to the left
                    new_door_ind = left_ind + 1;
                    result_corner = left_turn_next_corner(this.current_from_point, trace_halfwidth_max, door_left_corner, next_right_corner);
                    break;
                }
            }
            boolean visability_range_gets_smaller_on_the_right_side = (door_right_corner == null);
            if (door_right_corner != null && next_right_corner.side_of(this.current_from_point, door_right_corner) != Side.ON_THE_RIGHT)
            {
                FloatPoint curr_tangential_point = this.current_from_point.left_tangential_point(next_right_corner, trace_halfwidth_max);
                if (curr_tangential_point != null)
                {
                    FloatLine check_line = new FloatLine(this.current_from_point, curr_tangential_point);
                    if ( check_line.segment_distance(door_right_corner) >= trace_halfwidth_max)
                    {
                        visability_range_gets_smaller_on_the_right_side = true;
                    }
                }
            }
            if (visability_range_gets_smaller_on_the_right_side)
            {
                // The visibility range gets smaller on the right side.
                door_right_corner = next_right_corner;
                right_ind = curr_door_ind;
            }
            boolean visability_range_gets_smaller_on_the_left_side = (door_left_corner == null);
            if (door_left_corner != null && next_left_corner.side_of(this.current_from_point,  door_left_corner) != Side.ON_THE_LEFT)
            {
                FloatPoint curr_tangential_point = this.current_from_point.right_tangential_point(next_left_corner, trace_halfwidth_max);
                if (curr_tangential_point != null)
                {
                    FloatLine check_line = new FloatLine(this.current_from_point, curr_tangential_point);
                    if ( check_line.segment_distance(door_left_corner) >= trace_halfwidth_max)
                    {
                        visability_range_gets_smaller_on_the_left_side = true;
                    }
                }
            }
            if (visability_range_gets_smaller_on_the_left_side)
            {
                // The visibility range gets smaller on the left side.
                door_left_corner = next_left_corner;
                left_ind = curr_door_ind;
            }
            ++curr_door_ind;
        }
        
        if (end_of_trace)
        {
            FloatPoint nearest_point = this.current_target_shape.nearest_point(this.current_from_point.round()).to_float();
            result_corner = nearest_point;
            if (left_tangent_point != null && nearest_point.side_of(this.current_from_point, left_tangent_point) == Side.ON_THE_LEFT)
            {
                // The nearest target point is to the left of the visible range, add another corner
                new_door_ind = left_ind + 1;
                FloatPoint target_right_corner = this.current_target_shape.corner_approx(this.current_target_shape.index_of_right_most_corner(this.current_from_point));
                FloatPoint curr_corner = right_left_tangential_point(this.current_from_point,  target_right_corner, door_left_corner, trace_halfwidth_max);
                if (curr_corner != null)
                {
                    result_corner = curr_corner;
                    end_of_trace = false;
                }
            }
            else if (right_tangent_point != null && nearest_point.side_of(this.current_from_point, right_tangent_point) == Side.ON_THE_RIGHT)
            {
                // The nearest target point is to the right of the visible range, add another corner
                FloatPoint target_left_corner = this.current_target_shape.corner_approx(this.current_target_shape.index_of_left_most_corner(this.current_from_point));
                new_door_ind = right_ind + 1;
                FloatPoint curr_corner = left_right_tangential_point(this.current_from_point,  target_left_corner, door_right_corner, trace_halfwidth_max);
                if (curr_corner != null)
                {
                    result_corner = curr_corner;
                    end_of_trace = false;
                }
            }
        }
        if (end_of_trace)
        {
            new_door_ind = this.current_target_door_index;
        }
        
        // Check clearance violation with the previous door shapes
        // and correct them in this case.
        
        FloatLine check_line = new FloatLine(this.current_from_point, result_corner);
        int check_from_door_index = Math.max( this.current_to_door_index - 5 , this.current_from_door_index + 1);
        FloatPoint corrected_result = null;
        int corrected_door_ind = 0;
        for (int i = check_from_door_index; i < new_door_ind; ++i)
        {
            FloatPoint curr_left_corner = calc_door_left_corner(this.backtrack_array[i]);
            double curr_dist = check_line.segment_distance(curr_left_corner);
            if (Math.abs(curr_dist) < trace_halfwidth_middle)
            {
                FloatPoint curr_corrected_result = right_left_tangential_point(check_line.a, check_line.b, curr_left_corner,  trace_halfwidth_max);
                if (curr_corrected_result != null)
                {
                    if(corrected_result == null ||
                            curr_corrected_result.side_of(this.current_from_point, corrected_result) == Side.ON_THE_RIGHT)
                    {
                        corrected_door_ind = i;
                        corrected_result = curr_corrected_result;
                    }
                }
            }
            FloatPoint curr_right_corner = calc_door_right_corner(this.backtrack_array[i]);
            curr_dist = check_line.segment_distance(curr_right_corner);
            if (Math.abs(curr_dist) < trace_halfwidth_middle)
            {
                FloatPoint curr_corrected_result = left_right_tangential_point(check_line.a, check_line.b, curr_right_corner,  trace_halfwidth_max);
                if (curr_corrected_result != null)
                {
                    if(corrected_result == null ||
                            curr_corrected_result.side_of(this.current_from_point, corrected_result) == Side.ON_THE_LEFT)
                    {
                        corrected_door_ind = i;
                        corrected_result = curr_corrected_result;
                    }
                }
            }
        }
        if (corrected_result != null)
        {
            result_corner = corrected_result;
            new_door_ind = Math.max(corrected_door_ind, this.current_to_door_index);
        }
        
        this.current_to_door_index = new_door_ind;
        if(result_corner != null && result_corner != this.current_from_point)
        {
            result.add(result_corner);
        }
        return result;
    }
    
    
    /**
     * Calculates the left most corner of the shape of p_to_info.door
     * seen from the center of the common room with the previous door.
     */
    private static FloatPoint calc_door_left_corner(BacktrackElement p_to_info)
    {
        CompleteExpansionRoom from_room = p_to_info.door.other_room(p_to_info.next_room);
        FloatPoint pole = from_room.get_shape().centre_of_gravity();
        TileShape curr_to_door_shape = p_to_info.door.get_shape();
        int left_most_corner_no = curr_to_door_shape.index_of_left_most_corner(pole);
        return curr_to_door_shape.corner_approx(left_most_corner_no);
    }
    
    /**
     * Calculates the right most corner of the shape of p_to_info.door
     * seen from the center of the common room with the previous door.
     */
    private static FloatPoint calc_door_right_corner(BacktrackElement p_to_info)
    {
        CompleteExpansionRoom from_room = p_to_info.door.other_room(p_to_info.next_room);
        FloatPoint pole = from_room.get_shape().centre_of_gravity();
        TileShape curr_to_door_shape = p_to_info.door.get_shape();
        int right_most_corner_no = curr_to_door_shape.index_of_right_most_corner(pole);
        return curr_to_door_shape.corner_approx(right_most_corner_no);
    }
    
    /**
     * Calculates as first line the left side tangent from p_from_corner to
     * the circle with center p_to_corner and radius p_dist.
     * As second line the right side tangent from p_to_corner to the circle
     * with center p_next_corner and radius 2 * p_dist is constructed.
     * The second line is than translated by the distance p_dist to the left.
     * Returned is the intersection of the first and the second line.
     */
    private FloatPoint right_turn_next_corner(FloatPoint p_from_corner, double p_dist, FloatPoint p_to_corner, FloatPoint p_next_corner)
    {
        FloatPoint curr_tangential_point = p_from_corner.left_tangential_point(p_to_corner, p_dist);
        if (curr_tangential_point == null)
        {
            if (this.test_level.ordinal() >=  TestLevel.ALL_DEBUGGING_OUTPUT.ordinal())
            {
                FRLogger.warn("LocateFoundConnectionAlgo.right_turn_next_corner: left tangential point is null");
            }
            return p_from_corner;
        }
        FloatLine first_line = new FloatLine(p_from_corner, curr_tangential_point);
        curr_tangential_point = p_to_corner.right_tangential_point(p_next_corner, 2 * p_dist + c_tolerance);
        if (curr_tangential_point == null)
        {
            if (this.test_level.ordinal() >=  TestLevel.ALL_DEBUGGING_OUTPUT.ordinal())
            {
                FRLogger.warn("LocateFoundConnectionAlgo.right_turn_next_corner: right tangential point is null");
            }
            return p_from_corner;
        }
        FloatLine second_line = new FloatLine(p_to_corner, curr_tangential_point);
        second_line = second_line.translate(p_dist);
        return first_line.intersection(second_line);
    }
    
    /**
     * Calculates as first line the right side tangent from p_from_corner to
     * the circle with center p_to_corner and radius p_dist.
     * As second line the left side tangent from p_to_corner to the circle
     * with center p_next_corner and radius 2 * p_dist is constructed.
     * The second line is than translated by the distance p_dist to the right.
     * Returned is the intersection of the first and the second line.
     */
    private FloatPoint left_turn_next_corner(FloatPoint p_from_corner, double p_dist, FloatPoint p_to_corner, FloatPoint p_next_corner)
    {
        FloatPoint curr_tangential_point = p_from_corner.right_tangential_point(p_to_corner, p_dist);
        if (curr_tangential_point == null)
        {
            if (this.test_level.ordinal() >=  TestLevel.ALL_DEBUGGING_OUTPUT.ordinal())
            {
                FRLogger.warn("LocateFoundConnectionAlgo.left_turn_next_corner: right tangential point is null");
            }
            return p_from_corner;
        }
        FloatLine first_line = new FloatLine( p_from_corner, curr_tangential_point);
        curr_tangential_point = p_to_corner.left_tangential_point(p_next_corner, 2 * p_dist + c_tolerance);
        if (curr_tangential_point == null)
        {
            if (this.test_level.ordinal() >=  TestLevel.ALL_DEBUGGING_OUTPUT.ordinal())
            {
                FRLogger.warn("LocateFoundConnectionAlgo.left_turn_next_corner: left tangential point is null");
            }
            return p_from_corner;
        }
        FloatLine second_line = new FloatLine(p_to_corner, curr_tangential_point);
        second_line = second_line.translate(-p_dist);
        return first_line.intersection(second_line);
    }
    
    /**
     * Calculates the right tangential line from p_from_point and the
     * left tangential line from p_to_point to the circle
     * with center p_center and radius p_dist.
     * Returns the intersection of the 2 lines.
     */
    private FloatPoint right_left_tangential_point(FloatPoint p_from_point, FloatPoint p_to_point, FloatPoint p_center, double p_dist)
    {
        FloatPoint curr_tangential_point = p_from_point.right_tangential_point(p_center, p_dist);
        if (curr_tangential_point == null)
        {
            if (this.test_level.ordinal() >=  TestLevel.ALL_DEBUGGING_OUTPUT.ordinal())
            {
                FRLogger.warn("LocateFoundConnectionAlgo. right_left_tangential_point: right tangential point is null");
            }
            return null;
        }
        FloatLine first_line = new FloatLine(p_from_point, curr_tangential_point);
        curr_tangential_point = p_to_point.left_tangential_point(p_center, p_dist);
        if (curr_tangential_point == null)
        {
            if (this.test_level.ordinal() >=  TestLevel.ALL_DEBUGGING_OUTPUT.ordinal())
            {
                FRLogger.warn("LocateFoundConnectionAlgo. right_left_tangential_point: left tangential point is null");
            }
            return null;
        }
        FloatLine second_line = new FloatLine(p_to_point, curr_tangential_point);
        return first_line.intersection(second_line);
    }
    
    /**
     * Calculates the left tangential line from p_from_point and the
     * right tangential line from p_to_point to the circle
     * with center p_center and radius p_dist.
     * Returns the intersection of the 2 lines.
     */
    private FloatPoint left_right_tangential_point(FloatPoint p_from_point, FloatPoint p_to_point, FloatPoint p_center, double p_dist)
    {
        FloatPoint curr_tangential_point = p_from_point.left_tangential_point(p_center, p_dist);
        if (curr_tangential_point == null)
        {
            if (this.test_level.ordinal() >=  TestLevel.ALL_DEBUGGING_OUTPUT.ordinal())
            {
                FRLogger.warn("LocateFoundConnectionAlgo. left_right_tangential_point: left tangential point is null");
            }
            return null;
        }
        FloatLine first_line = new FloatLine(p_from_point, curr_tangential_point);
        curr_tangential_point = p_to_point.right_tangential_point(p_center, p_dist);
        if (curr_tangential_point == null)
        {
            if (this.test_level.ordinal() >=  TestLevel.ALL_DEBUGGING_OUTPUT.ordinal())
            {
                FRLogger.warn("LocateFoundConnectionAlgo. left_right_tangential_point: right tangential point is null");
            }
            return null;
        }
        FloatLine second_line = new FloatLine(p_to_point, curr_tangential_point);
        return first_line.intersection(second_line);
    }
    
    static private final double c_tolerance = 1.0;
    
}
