/*
 *   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.
 *
 * BoxShapeSearchTree.java
 *
 * Created on 20. Mai 2007, 07:33
 *
 */

package eu.mihosoft.freerouting.board;

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

import eu.mihosoft.freerouting.geometry.planar.OrthogonalBoundingDirections;
import eu.mihosoft.freerouting.geometry.planar.TileShape;
import eu.mihosoft.freerouting.geometry.planar.Shape;
import eu.mihosoft.freerouting.geometry.planar.IntBox;
import eu.mihosoft.freerouting.geometry.planar.Polyline;

import eu.mihosoft.freerouting.autoroute.IncompleteFreeSpaceExpansionRoom;
import eu.mihosoft.freerouting.autoroute.CompleteFreeSpaceExpansionRoom;
import eu.mihosoft.freerouting.logger.FRLogger;

/**
 * A special simple ShapeSearchtree, where the shapes are of class IntBox.
 * It is used in the 90-degree autorouter algorithm.
 *
 * @author Alfons Wirtz
 */
public class ShapeSearchTree90Degree extends ShapeSearchTree
{
    
    /** Creates a new instance of ShapeSearchTree90Degree */
    public ShapeSearchTree90Degree(BasicBoard p_board, int p_compensated_clearance_class_no)
    {
        super(OrthogonalBoundingDirections.INSTANCE, p_board, p_compensated_clearance_class_no);
    }
    
    /**
     * Calculates a new incomplete room with a maximal TileShape contained in the shape of p_room,
     * which may overlap only with items of the input net on the input layer.
     * p_room.get_contained_shape() will be contained in the shape of the result room.
     * If that is not possible, several rooms are returned with shapes,
     * which intersect with p_room.get_contained_shape().
     * The result room is not yet complete, because its doors are not yet calculated.
     */
    public Collection<IncompleteFreeSpaceExpansionRoom> complete_shape(IncompleteFreeSpaceExpansionRoom p_room,
            int p_net_no, SearchTreeObject p_ignore_object, TileShape p_ignore_shape)
    {
        if (!(p_room.get_contained_shape() instanceof IntBox))
        {
            FRLogger.warn("BoxShapeSearchTree.complete_shape: unexpected p_shape_to_be_contained");
            return new LinkedList<IncompleteFreeSpaceExpansionRoom>();
        }
        IntBox shape_to_be_contained = (IntBox) p_room.get_contained_shape();
        if (this.root == null)
        {
            return new LinkedList<IncompleteFreeSpaceExpansionRoom>();
        }
        IntBox start_shape = board.get_bounding_box();
        if (p_room.get_shape() != null)
        {
            if (!(p_room.get_shape() instanceof IntBox))
            {
                FRLogger.warn("BoxShapeSearchTree.complete_shape: p_start_shape of type IntBox expected");
                return new LinkedList<IncompleteFreeSpaceExpansionRoom>();
            }
            start_shape = ((IntBox)p_room.get_shape()).intersection(start_shape);
        }
        IntBox bounding_shape = start_shape;
        int room_layer = p_room.get_layer();
        Collection<IncompleteFreeSpaceExpansionRoom> result = new LinkedList<IncompleteFreeSpaceExpansionRoom>();
        result.add(new IncompleteFreeSpaceExpansionRoom(start_shape, room_layer, shape_to_be_contained));
        this.node_stack.reset();
        this.node_stack.push(this.root);
        TreeNode curr_node;
        
        for (;;)
        {
            curr_node = this.node_stack.pop();
            if (curr_node == null)
            {
                break;
            }
            if (curr_node.bounding_shape.intersects(bounding_shape))
            {
                if (curr_node instanceof Leaf)
                {
                    Leaf curr_leaf = (Leaf) curr_node;
                    SearchTreeObject curr_object = (SearchTreeObject) curr_leaf.object;
                    int shape_index = curr_leaf.shape_index_in_object;
                    if (curr_object.is_trace_obstacle(p_net_no) && curr_object.shape_layer(shape_index) == room_layer
                            && curr_object != p_ignore_object)
                    {
                        
                        IntBox curr_object_shape = curr_object.get_tree_shape(this, shape_index).bounding_box();
                        Collection<IncompleteFreeSpaceExpansionRoom> new_result = new LinkedList<IncompleteFreeSpaceExpansionRoom>();
                        IntBox new_bounding_shape = IntBox.EMPTY;
                        for (IncompleteFreeSpaceExpansionRoom curr_room : result)
                        {
                            IntBox curr_shape = (IntBox) curr_room.get_shape();
                            if (curr_shape.overlaps(curr_object_shape))
                            {
                                if (curr_object instanceof CompleteFreeSpaceExpansionRoom
                                        && p_ignore_shape != null )
                                {
                                    IntBox intersection = curr_shape.intersection(curr_object_shape);
                                    if (p_ignore_shape.contains(intersection))
                                    {
                                        // ignore also all objects, whose intersection is contained in the
                                        // 2-dim overlap-door with the from_room.
                                        continue;
                                    }
                                }
                                Collection <IncompleteFreeSpaceExpansionRoom> new_restrained_shapes =
                                        restrain_shape(curr_room, curr_object_shape);
                                new_result.addAll(new_restrained_shapes);
                                
                                
                                for (IncompleteFreeSpaceExpansionRoom tmp_shape : new_result)
                                {
                                    new_bounding_shape = new_bounding_shape.union(tmp_shape.get_shape().bounding_box());
                                }
                            }
                            else
                            {
                                new_result.add(curr_room);
                                new_bounding_shape = new_bounding_shape.union(curr_shape.bounding_box());
                            }
                        }
                        result = new_result;
                        bounding_shape = new_bounding_shape;
                    }
                }
                else
                {
                    this.node_stack.push(((InnerNode)curr_node).first_child);
                    this.node_stack.push(((InnerNode)curr_node).second_child);
                }
            }
        }
        return result;
    }
    
    /**
     * Restrains the shape of p_incomplete_room to a box shape, which does not intersect with the interiour
     * of p_obstacle_shape. p_incomplete_room.get_contained_shape() must be contained in the shape of
     * the result room.
     */
    private Collection<IncompleteFreeSpaceExpansionRoom> restrain_shape(IncompleteFreeSpaceExpansionRoom p_incomplete_room, IntBox p_obstacle_shape)
    {
        // Search the edge line of p_obstacle_shape, so that p_shape_to_be_contained
        // are on the right side of this line, and that the line segment
        // intersects with the interiour of p_shape.
        // If there are more than 1 such lines take the line which is
        // furthest away from the shape_to_be_contained
        // Then insersect p_shape with the halfplane defined by the
        // opposite of this line.
        
        Collection<IncompleteFreeSpaceExpansionRoom> result = new LinkedList<IncompleteFreeSpaceExpansionRoom>();
        if (p_incomplete_room.get_contained_shape().is_empty())
        {
            if (this.board.get_test_level().ordinal() >=  TestLevel.ALL_DEBUGGING_OUTPUT.ordinal())
            {
                FRLogger.warn("BoxShapeSearchTree.restrain_shape: p_shape_to_be_contained is empty");
            }
            return result;
        }
        IntBox room_shape = p_incomplete_room.get_shape().bounding_box();
        IntBox shape_to_be_contained = p_incomplete_room.get_contained_shape().bounding_box();
        int cut_line_distance = 0;
        IntBox restrained_shape = null;
        
        if (room_shape.ll.x  < p_obstacle_shape.ur.x && room_shape.ur.x > p_obstacle_shape.ur.x
                && room_shape.ur.y > p_obstacle_shape.ll.y && room_shape.ll.y < p_obstacle_shape.ur.y )
        {
            // The right line segment of the obstacle_shape intersects the interiour of p_shape
            int curr_distance = shape_to_be_contained.ll.x - p_obstacle_shape.ur.x;
            if (curr_distance > cut_line_distance)
            {
                cut_line_distance = curr_distance;
                restrained_shape = new IntBox(p_obstacle_shape.ur.x, room_shape.ll.y, room_shape.ur.x, room_shape.ur.y);
            }
        }
        if (room_shape.ll.x  < p_obstacle_shape.ll.x && room_shape.ur.x > p_obstacle_shape.ll.x
                && room_shape.ur.y > p_obstacle_shape.ll.y && room_shape.ll.y < p_obstacle_shape.ur.y )
        {
            // The left line segment of the obstacle_shape intersects the interiour of p_shape
            int curr_distance = p_obstacle_shape.ll.x - shape_to_be_contained.ur.x;
            if (curr_distance > cut_line_distance)
            {
                cut_line_distance = curr_distance;
                restrained_shape = new IntBox(room_shape.ll.x, room_shape.ll.y, p_obstacle_shape.ll.x, room_shape.ur.y);
            }
        }
        if (room_shape.ll.y  < p_obstacle_shape.ll.y && room_shape.ur.y > p_obstacle_shape.ll.y
                && room_shape.ur.x > p_obstacle_shape.ll.x && room_shape.ll.x < p_obstacle_shape.ur.x )
        {
            // The lower  line segment of the obstacle_shape intersects the interiour of p_shape
            int curr_distance = p_obstacle_shape.ll.y - shape_to_be_contained.ur.y;
            if (curr_distance > cut_line_distance)
            {
                cut_line_distance = curr_distance;
                restrained_shape = new IntBox(room_shape.ll.x, room_shape.ll.y, room_shape.ur.x, p_obstacle_shape.ll.y);
            }
        }
        if (room_shape.ll.y  < p_obstacle_shape.ur.y && room_shape.ur.y > p_obstacle_shape.ur.y
                && room_shape.ur.x > p_obstacle_shape.ll.x && room_shape.ll.x < p_obstacle_shape.ur.x )
        {
            // The upper line segment of the obstacle_shape intersects the interiour of p_shape
            int curr_distance = shape_to_be_contained.ll.y - p_obstacle_shape.ur.y;
            if (curr_distance > cut_line_distance)
            {
                cut_line_distance = curr_distance;
                restrained_shape = new IntBox(room_shape.ll.x, p_obstacle_shape.ur.y, room_shape.ur.x, room_shape.ur.y);
            }
        }
        if (restrained_shape != null)
        {
            result.add(new IncompleteFreeSpaceExpansionRoom(restrained_shape,
                    p_incomplete_room.get_layer(), shape_to_be_contained));
            return result;
        }
        
        // Now shape_to_be_contained intersects with the obstacle_shape.
        // shape_to_be_contained and p_shape evtl. need to be divided in two.
        IntBox is = shape_to_be_contained.intersection(p_obstacle_shape);
        if (is.is_empty())
        {
            FRLogger.warn("BoxShapeSearchTree.restrain_shape: Intersection between obstacle_shape and shape_to_be_contained expected");
            return result;
        }
        IntBox new_shape_1 = null;
        IntBox new_shape_2 = null;
        if (is.ll.x > room_shape.ll.x && is.ll.x == p_obstacle_shape.ll.x && is.ll.x < room_shape.ur.x)
        {
            new_shape_1 = new IntBox(room_shape.ll.x, room_shape.ll.y, is.ll.x, room_shape.ur.y);
            new_shape_2 = new IntBox(is.ll.x, room_shape.ll.y, room_shape.ur.x, room_shape.ur.y);
        }
        else if (is.ur.x > room_shape.ll.x  && is.ur.x == p_obstacle_shape.ur.x && is.ur.x < room_shape.ur.x)
        {
            new_shape_2 = new IntBox(room_shape.ll.x, room_shape.ll.y, is.ur.x, room_shape.ur.y);
            new_shape_1 = new IntBox(is.ur.x, room_shape.ll.y, room_shape.ur.x, room_shape.ur.y);
        }
        else if (is.ll.y > room_shape.ll.y && is.ll.y == p_obstacle_shape.ll.y && is.ll.y < room_shape.ur.y)
        {
            new_shape_1 = new IntBox(room_shape.ll.x, room_shape.ll.y, room_shape.ur.x, is.ll.y);
            new_shape_2 = new IntBox(room_shape.ll.x, is.ll.y, room_shape.ur.x, room_shape.ur.y);
        }
        else if (is.ur.y > room_shape.ll.y && is.ur.y == p_obstacle_shape.ur.y && is.ur.y < room_shape.ur.y)
        {
            new_shape_2 = new IntBox(room_shape.ll.x, room_shape.ll.y, room_shape.ur.x, is.ur.y);
            new_shape_1 = new IntBox(room_shape.ll.x, is.ur.y, room_shape.ur.x, room_shape.ur.y);
        }
        if (new_shape_1 != null)
        {
            IntBox new_shape_to_be_contained = shape_to_be_contained.intersection(new_shape_1);
            if (new_shape_to_be_contained.dimension() > 0)
            {
                result.add(new IncompleteFreeSpaceExpansionRoom(new_shape_1,
                        p_incomplete_room.get_layer(), new_shape_to_be_contained));
                IncompleteFreeSpaceExpansionRoom new_incomplete_room =
                        new IncompleteFreeSpaceExpansionRoom( new_shape_2, p_incomplete_room.get_layer(),
                        shape_to_be_contained.intersection(new_shape_2));
                result.addAll(restrain_shape(new_incomplete_room, p_obstacle_shape));
            }
        }
        return result;
    }
    
    TileShape[] calculate_tree_shapes(DrillItem p_drill_item)
    {
        if (this.board == null)
        {
            return new TileShape[0];
        }
        TileShape[] result = new TileShape [p_drill_item.tile_shape_count()];
        for (int i = 0; i < result.length; ++i)
        {
            Shape curr_shape = p_drill_item.get_shape(i);
            if (curr_shape == null)
            {
                result[i] = null;
            }
            else
            {
                IntBox curr_tile_shape = curr_shape.bounding_box();
                int offset_width = this.clearance_compensation_value(p_drill_item.clearance_class_no(), p_drill_item.shape_layer(i));
                if (curr_tile_shape == null)
                {
                    FRLogger.warn("BoxShapeSearchTree.calculate_tree_shapes: shape is null");
                }
                else
                {
                    curr_tile_shape = curr_tile_shape.offset(offset_width);
                }
                result [i] = curr_tile_shape;
            }
        }
        return  result;
    }
    
    TileShape[] calculate_tree_shapes(ObstacleArea p_obstacle_area)
    {
        TileShape[] result = super.calculate_tree_shapes(p_obstacle_area);
        for (int i  = 0; i < result.length; ++i)
        {
            result[i] = result[i].bounding_box();
        }
        return result;
    }
    
    TileShape[] calculate_tree_shapes(BoardOutline p_outline)
    {
        TileShape[] result = super.calculate_tree_shapes(p_outline);
        for (int i  = 0; i < result.length; ++i)
        {
            result[i] = result[i].bounding_box();
        }
        return result;
    }
    
    /**
     * Used for creating the shapes of a polyline_trace for this tree.
     */
    TileShape offset_shape(Polyline p_polyline, int p_half_width, int p_no)
    {
        return p_polyline.offset_box(p_half_width, p_no);
    }
    
    /**
     * Used for creating the shapes of a polyline_trace for this tree.
     */
    public TileShape[] offset_shapes(Polyline p_polyline, int p_half_width,
            int p_from_no, int p_to_no)
    {
        int from_no = Math.max(p_from_no, 0);
        int to_no = Math.min(p_to_no, p_polyline.arr.length -1);
        int shape_count = Math.max(to_no - from_no -1, 0);
        TileShape[] shape_arr = new TileShape[shape_count];
        for (int j = from_no; j < to_no - 1; ++j)
        {
            shape_arr [j - from_no] = p_polyline.offset_box(p_half_width, j);
        }
        return shape_arr;
    }
}
