/*
 *   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.
 *
 * MazeShoveTraceAlgo.java
 *
 * Created on 10. Mai 2006, 06:41
 *
 */

package eu.mihosoft.freerouting.autoroute;

import java.util.Collection;

import eu.mihosoft.freerouting.geometry.planar.TileShape;
import eu.mihosoft.freerouting.geometry.planar.Line;
import eu.mihosoft.freerouting.geometry.planar.Polyline;
import eu.mihosoft.freerouting.geometry.planar.Point;
import eu.mihosoft.freerouting.geometry.planar.FloatPoint;
import eu.mihosoft.freerouting.geometry.planar.FloatLine;
import eu.mihosoft.freerouting.geometry.planar.Side;
import eu.mihosoft.freerouting.geometry.planar.Direction;
import eu.mihosoft.freerouting.geometry.planar.LineSegment;

import eu.mihosoft.freerouting.board.Item;
import eu.mihosoft.freerouting.board.RoutingBoard;
import eu.mihosoft.freerouting.board.ShoveTraceAlgo;
import eu.mihosoft.freerouting.logger.FRLogger;

/**
 * Auxiliary functions used in MazeSearchAlgo.
 *
 * @author Alfons Wirtz
 */
public class MazeShoveTraceAlgo
{
    
    /**
     * Returns false, if the algorithm did not succeed and trying to shove from another door section
     * may be more successful.
     */
    public static boolean check_shove_trace_line(MazeListElement p_list_element,
            ObstacleExpansionRoom p_obstacle_room, RoutingBoard p_board, AutorouteControl p_ctrl,
            boolean p_shove_to_the_left, Collection<DoorSection> p_to_door_list)
    {
        if (!(p_list_element.door instanceof ExpansionDoor))
        {
            return true;
        }
        ExpansionDoor from_door = (ExpansionDoor) p_list_element.door;
        if (!(p_obstacle_room.get_item() instanceof eu.mihosoft.freerouting.board.PolylineTrace))
        {
            return true;
        }
        eu.mihosoft.freerouting.board.PolylineTrace obstacle_trace = (eu.mihosoft.freerouting.board.PolylineTrace)p_obstacle_room.get_item();
        int trace_layer = p_obstacle_room.get_layer();
        // only traces with the same halfwidth and the same clearance class can be shoved.
        if (obstacle_trace.get_half_width() != p_ctrl.trace_half_width[trace_layer]
                || obstacle_trace.clearance_class_no() != p_ctrl.trace_clearance_class_no)
        {
            return true;
        }
        double compensated_trace_half_width = p_ctrl.compensated_trace_half_width[trace_layer];
        TileShape from_door_shape = from_door.get_shape();
        if (from_door_shape.max_width() < 2 * compensated_trace_half_width)
        {
            return true;
        }
        int trace_corner_no = p_obstacle_room.get_index_in_item();
        
        Polyline trace_polyline = obstacle_trace.polyline();
        
        if (trace_corner_no >= trace_polyline.arr.length - 1)
        {
            FRLogger.warn("MazeShoveTraceAlgo.check_shove_trace_line: trace_corner_no to big");
            return false;
        }
        Collection<ExpansionDoor> room_doors = p_obstacle_room.get_doors();
        // The side of the trace line seen from the doors to expand.
        // Used to determine, if a door is on the right side to put it into the p_door_list.
        LineSegment shove_line_segment;
        if (from_door.dimension == 2)
        {
            // shove from a link door into the direction of the other link door.
            CompleteExpansionRoom other_room = from_door.other_room(p_obstacle_room);
            if (!(other_room instanceof ObstacleExpansionRoom))
            {
                return false;
            }
            if (!end_points_matching(obstacle_trace, ((ObstacleExpansionRoom)other_room).get_item()))
            {
                return false;
            }
            FloatPoint door_center = from_door_shape.centre_of_gravity();
            FloatPoint corner_1 = trace_polyline.corner_approx(trace_corner_no);
            FloatPoint corner_2 = trace_polyline.corner_approx(trace_corner_no + 1);
            if (corner_1.distance_square(corner_2) < 1)
            {
                // shove_line_segment may be reduced to a point
                return false;
            }
            boolean shove_into_direction_of_trace_start =
                    door_center.distance_square(corner_2) < door_center.distance_square(corner_1);
            shove_line_segment = new LineSegment(trace_polyline, trace_corner_no + 1);
            if (shove_into_direction_of_trace_start)
            {
                
                // shove from the endpoint to the start point of the line segment
                shove_line_segment = shove_line_segment.opposite();
            }
        }
        else
        {
            CompleteExpansionRoom from_room = from_door.other_room(p_obstacle_room);
            FloatPoint from_point = from_room.get_shape().centre_of_gravity();
            Line shove_trace_line = trace_polyline.arr[trace_corner_no + 1];
            FloatLine door_line_segment = from_door_shape.diagonal_corner_segment();
            Side side_of_trace_line = shove_trace_line.side_of(door_line_segment.a, 0);
            
            FloatLine polar_line_segment = from_door_shape.polar_line_segment(from_point);
            
            boolean door_line_swapped =
                    polar_line_segment.b.distance_square(door_line_segment.a) <
                    polar_line_segment.a.distance_square(door_line_segment.a);
            
            boolean section_ok;
            // shove only from the right most section to the right or from the left most section to the left.
            
            double shape_entry_check_distance  = compensated_trace_half_width + 5;
            double check_dist_square = shape_entry_check_distance * shape_entry_check_distance;
            
            if (p_shove_to_the_left && !door_line_swapped || !p_shove_to_the_left && door_line_swapped)
            {
                section_ok =
                        p_list_element.section_no_of_door == p_list_element.door.maze_search_element_count() - 1
                        && (p_list_element.shape_entry.a.distance_square(door_line_segment.b) <= check_dist_square
                        || p_list_element.shape_entry.b.distance_square(door_line_segment.b) <= check_dist_square);
            }
            else
            {
                section_ok =
                        p_list_element.section_no_of_door == 0
                        && (p_list_element.shape_entry.a.distance_square(door_line_segment.a) <= check_dist_square
                        || p_list_element.shape_entry.b.distance_square(door_line_segment.a) <= check_dist_square);
            }
            if (!section_ok)
            {
                return false;
            }
            
            
            // create the line segment for shoving
            
            FloatLine shrinked_line_segment = polar_line_segment.shrink_segment(compensated_trace_half_width);
            Direction perpendicular_direction = shove_trace_line.direction().turn_45_degree(2);
            if (side_of_trace_line == Side.ON_THE_LEFT)
            {
                if (p_shove_to_the_left)
                {
                    Line start_closing_line = new Line(shrinked_line_segment.b.round(), perpendicular_direction);
                    shove_line_segment =
                            new LineSegment(start_closing_line, trace_polyline.arr[trace_corner_no + 1],
                            trace_polyline.arr[trace_corner_no + 2]);
                }
                else
                {
                    Line start_closing_line = new Line(shrinked_line_segment.a.round(), perpendicular_direction);
                    shove_line_segment =
                            new LineSegment(start_closing_line, trace_polyline.arr[trace_corner_no + 1].opposite(),
                            trace_polyline.arr[trace_corner_no].opposite());
                }
            }
            else
            {
                if (p_shove_to_the_left)
                {
                    Line start_closing_line = new Line(shrinked_line_segment.b.round(), perpendicular_direction);
                    shove_line_segment =
                            new LineSegment(start_closing_line, trace_polyline.arr[trace_corner_no + 1].opposite(),
                            trace_polyline.arr[trace_corner_no].opposite());
                }
                else
                {
                    Line start_closing_line = new Line(shrinked_line_segment.a.round(), perpendicular_direction);
                    shove_line_segment =
                            new LineSegment(start_closing_line, trace_polyline.arr[trace_corner_no + 1],
                            trace_polyline.arr[trace_corner_no + 2]);
                }
            }
        }
        int trace_half_width = p_ctrl.trace_half_width[trace_layer];
        int [] net_no_arr = new int[1];
        net_no_arr[0] = p_ctrl.net_no;
        
        double shove_width =
                p_board.check_trace_segment(shove_line_segment, trace_layer, net_no_arr, trace_half_width,
                p_ctrl.trace_clearance_class_no, true);
        boolean segment_shortened = false;
        if (shove_width < Integer.MAX_VALUE)
        {
            // shorten shove_line_segment
            shove_width = shove_width - 1;
            if (shove_width <= 0)
            {
                return true;
            }
            shove_line_segment = shove_line_segment.change_length_approx(shove_width);
            segment_shortened = true;
        }
        
        FloatPoint from_corner = shove_line_segment.start_point_approx();
        FloatPoint to_corner = shove_line_segment.end_point_approx();
        boolean segment_ist_point = from_corner.distance_square(to_corner) < 0.1;
        
        if (!segment_ist_point)
        {
            shove_width = ShoveTraceAlgo.check(p_board, shove_line_segment, p_shove_to_the_left, trace_layer, net_no_arr, trace_half_width,
                    p_ctrl.trace_clearance_class_no, p_ctrl.max_shove_trace_recursion_depth, p_ctrl.max_shove_via_recursion_depth);
            
            if (shove_width <= 0)
            {
                return true;
            }
        }
        
        // Put the doors on this side of the room into p_to_door_list with
        if (segment_shortened)
        {
            shove_width = Math.min(shove_width, from_corner.distance(to_corner));
        }
        
        Line shove_line = shove_line_segment.get_line();
        
        // From_door_compare_distance is used to check, that a door is between from_door and the end point
        // of the shove line.
        double from_door_compare_distance;
        if (from_door.dimension == 2 || segment_ist_point)
        {
            from_door_compare_distance = Double.MAX_VALUE;
        }
        else
        {
            from_door_compare_distance = to_corner.distance_square(from_door_shape.corner_approx(0));
        }
        
        for (ExpansionDoor curr_door : room_doors)
        {
            if (curr_door == from_door)
            {
                continue;
            }
            if (curr_door.first_room instanceof ObstacleExpansionRoom &&
                    curr_door.second_room instanceof ObstacleExpansionRoom)
            {
                Item first_room_item = ((ObstacleExpansionRoom)curr_door.first_room).get_item();
                Item second_room_item = ((ObstacleExpansionRoom)curr_door.second_room).get_item();
                if (first_room_item != second_room_item)
                {
                    // there may be topological problems at a trace fork
                    continue;
                }
            }
            TileShape curr_door_shape = curr_door.get_shape();
            if (curr_door.dimension == 2 && shove_width >= Integer.MAX_VALUE)
            {
                boolean add_link_door = curr_door_shape.contains(to_corner);
                
                
                if (add_link_door)
                {
                    FloatLine[] line_sections = curr_door.get_section_segments(compensated_trace_half_width);
                    p_to_door_list.add(new DoorSection(curr_door, 0, line_sections[0]));
                }
                continue;
            }
            else if (!segment_ist_point)
            {
                // now curr_door is 1-dimensional
                
                // check, that curr_door is on the same border_line as p_from_door.
                FloatLine curr_door_segment = curr_door_shape.diagonal_corner_segment();
                if (curr_door_segment == null)
                {
                    if (p_board.get_test_level() == eu.mihosoft.freerouting.board.TestLevel.ALL_DEBUGGING_OUTPUT)
                    {
                        FRLogger.warn("MazeShoveTraceAlgo.check_shove_trace_line: door shape is empty");
                    }
                    continue;
                }
                Side start_corner_side_of_trace_line = shove_line.side_of(curr_door_segment.a, 0);
                Side end_corner_side_of_trace_line = shove_line.side_of(curr_door_segment.b, 0);
                if (p_shove_to_the_left)
                {
                    if (start_corner_side_of_trace_line != Side.ON_THE_LEFT || end_corner_side_of_trace_line != Side.ON_THE_LEFT)
                    {
                        continue;
                    }
                }
                else
                {
                    if (start_corner_side_of_trace_line != Side.ON_THE_RIGHT || end_corner_side_of_trace_line != Side.ON_THE_RIGHT)
                    {
                        continue;
                    }
                }
                FloatLine curr_door_line = curr_door_shape.polar_line_segment(from_corner);
                FloatPoint curr_door_nearest_corner;
                if (curr_door_line.a.distance_square(from_corner) <= curr_door_line.b.distance_square(from_corner))
                {
                    curr_door_nearest_corner = curr_door_line.a;
                }
                else
                {
                    curr_door_nearest_corner = curr_door_line.b;
                }
                if (to_corner.distance_square(curr_door_nearest_corner) >= from_door_compare_distance)
                {
                    // curr_door is not located into the direction of to_corner.
                    continue;
                }
                FloatPoint curr_door_projection = curr_door_nearest_corner.projection_approx(shove_line);
                
                if (curr_door_projection.distance(from_corner) + compensated_trace_half_width <= shove_width)
                {
                    FloatLine[] line_sections = curr_door.get_section_segments(compensated_trace_half_width);
                    for (int i = 0; i < line_sections.length; ++i)
                    {
                        FloatLine curr_line_section = line_sections[i];
                        FloatPoint curr_section_nearest_corner;
                        if (curr_line_section.a.distance_square(from_corner) <= curr_line_section.b.distance_square(from_corner))
                        {
                            curr_section_nearest_corner = curr_line_section.a;
                        }
                        else
                        {
                            curr_section_nearest_corner = curr_line_section.b;
                        }
                        FloatPoint curr_section_projection = curr_section_nearest_corner.projection_approx(shove_line);
                        if (curr_section_projection.distance(from_corner) <= shove_width)
                        {
                            p_to_door_list.add(new DoorSection(curr_door, i, curr_line_section));
                        }
                    }
                }
            }
        }
        return true;
    }
    
    /**
     * Check if the endpoints of p_trace and p_from_item are maching, so that the
     * shove can continue through a link door.
     */
    private static boolean end_points_matching(eu.mihosoft.freerouting.board.PolylineTrace p_trace, Item p_from_item)
    {
        if (p_from_item == p_trace)
        {
            return true;
        }
        if (!p_trace.shares_net(p_from_item))
        {
            return false;
        }
        boolean points_matching;
        if (p_from_item instanceof eu.mihosoft.freerouting.board.DrillItem)
        {
            Point from_center = ((eu.mihosoft.freerouting.board.DrillItem) p_from_item).get_center();
            points_matching =  from_center.equals(p_trace.first_corner()) || from_center.equals(p_trace.last_corner());
        }
        else if (p_from_item instanceof eu.mihosoft.freerouting.board.PolylineTrace)
        {
            eu.mihosoft.freerouting.board.PolylineTrace from_trace = (eu.mihosoft.freerouting.board.PolylineTrace) p_from_item;
            points_matching = p_trace.first_corner().equals(from_trace.first_corner()) ||
                    p_trace.first_corner().equals(from_trace.last_corner()) ||
                    p_trace.last_corner().equals(from_trace.first_corner()) ||
                    p_trace.last_corner().equals(from_trace.last_corner());
        }
        else
        {
            points_matching = false;
        }
        return points_matching;
    }
    
    public static class DoorSection
    {
        DoorSection(ExpansionDoor p_door, int p_section_no, FloatLine p_section_line)
        {
            door = p_door;
            section_no = p_section_no;
            section_line = p_section_line;
            
        }
        final ExpansionDoor door;
        final int section_no;
        final FloatLine section_line;
    }
}
