/*
 *   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.
 *
 * PullTightAlgo.java
 *
 * Created on 19. Juli 2003, 12:42
 */
package eu.mihosoft.freerouting.board;

import eu.mihosoft.freerouting.geometry.planar.FloatPoint;
import eu.mihosoft.freerouting.geometry.planar.IntOctagon;
import eu.mihosoft.freerouting.geometry.planar.IntPoint;
import eu.mihosoft.freerouting.geometry.planar.Line;
import eu.mihosoft.freerouting.geometry.planar.Point;
import eu.mihosoft.freerouting.geometry.planar.Polyline;
import eu.mihosoft.freerouting.geometry.planar.Side;
import eu.mihosoft.freerouting.geometry.planar.TileShape;

import java.util.Collection;
import java.util.Iterator;
import java.util.Set;

import eu.mihosoft.freerouting.datastructures.Signum;
import eu.mihosoft.freerouting.datastructures.Stoppable;
import eu.mihosoft.freerouting.datastructures.TimeLimit;

import eu.mihosoft.freerouting.autoroute.AutorouteControl.ExpansionCostFactor;
import eu.mihosoft.freerouting.logger.FRLogger;

/**
 * Class with functionality for optimising traces and vias.
 *
 * @author Alfons Wirtz
 */
public abstract class PullTightAlgo
{

    /**
     * Returns a new instance of  PullTightAlgo.
     * If p_only_net_no > 0, only traces with net number p_not_no are optimized.
     * If p_stoppable_thread != null, the agorithm can be requested to be stopped.
     * If p_time_limit > 0; the algorithm will be stopped after p_time_limit Milliseconds.
     */
    static PullTightAlgo get_instance(RoutingBoard p_board,
            int[] p_only_net_no_arr, IntOctagon p_clip_shape, int p_min_translate_dist,
            Stoppable p_stoppable_thread, int p_time_limit, Point p_keep_point, int p_keep_point_layer)
    {
        PullTightAlgo result;
        AngleRestriction angle_restriction = p_board.rules.get_trace_angle_restriction();
        if (angle_restriction == AngleRestriction.NINETY_DEGREE)
        {
            result = new PullTightAlgo90(p_board, p_only_net_no_arr, p_stoppable_thread, p_time_limit,
                    p_keep_point, p_keep_point_layer);
        }
        else if (angle_restriction == AngleRestriction.FORTYFIVE_DEGREE)
        {
            result = new PullTightAlgo45(p_board, p_only_net_no_arr, p_stoppable_thread, p_time_limit,
                    p_keep_point, p_keep_point_layer);
        }
        else
        {
            result = new PullTightAlgoAnyAngle(p_board, p_only_net_no_arr, p_stoppable_thread, p_time_limit,
                    p_keep_point, p_keep_point_layer);
        }
        result.curr_clip_shape = p_clip_shape;
        result.min_translate_dist = Math.max(p_min_translate_dist, 100);
        return result;
    }

    /** Creates a new instance of PullTightAlgo */
    PullTightAlgo(RoutingBoard p_board, int[] p_only_net_no_arr, Stoppable p_stoppable_thread, int p_time_limit,
            Point p_keep_point, int p_keep_point_layer)
    {
        board = p_board;
        only_net_no_arr = p_only_net_no_arr;
        stoppable_thread = p_stoppable_thread;
        if (p_time_limit > 0)
        {
            this.time_limit = new TimeLimit(p_time_limit);
        }
        else
        {
            this.time_limit = null;
        }
        this.keep_point = p_keep_point;
        this.keep_point_layer = p_keep_point_layer;
    }

    /**
     * Function for optimizing the route in an internal marked area.
     * If p_clip_shape != null, the optimizing area is restricted to p_clip_shape.
     *  p_trace_cost_arr is used for optimizing vias and may be null.
     */
    void opt_changed_area(ExpansionCostFactor[] p_trace_cost_arr)
    {
        if (board.changed_area == null)
        {
            return;
        }
        boolean something_changed = true;
        // starting with curr_min_translate_dist big is a try to
        // avoid fine approximation at the beginning to avoid
        // problems with dog ears
        while (something_changed)
        {
            something_changed = false;
            for (int i = 0; i < board.get_layer_count(); ++i)
            {
                IntOctagon changed_region = board.changed_area.get_area(i);
                if (changed_region.is_empty())
                {
                    continue;
                }
                board.changed_area.set_empty(i);
                board.join_graphics_update_box(changed_region.bounding_box());
                double changed_area_offset =
                        1.5 * (board.rules.clearance_matrix.max_value(i) + 2 * board.rules.get_max_trace_half_width());
                changed_region = changed_region.enlarge(changed_area_offset);
                // search in the ShapeSearchTree for all overlapping traces
                // with clip_shape on layer i
                Collection<SearchTreeObject> items = board.overlapping_objects(changed_region, i);
                Iterator<SearchTreeObject> it = items.iterator();
                while (it.hasNext())
                {
                    if (this.is_stop_requested())
                    {
                        return;
                    }
                    SearchTreeObject curr_ob = it.next();
                    if (curr_ob instanceof PolylineTrace)
                    {
                        PolylineTrace curr_trace = (PolylineTrace) curr_ob;
                        if (curr_trace.pull_tight(this))
                        {
                            something_changed = true;
                            if (this.split_traces_at_keep_point())
                            {
                                break;
                            }
                        }
                        else if (smoothen_end_corners_at_trace_1(curr_trace))
                        {
                            something_changed = true;
                            break; // because items may be removed
                        }
                    }
                    else if (curr_ob instanceof Via && p_trace_cost_arr != null)
                    {
                        if (OptViaAlgo.opt_via_location(this.board, (Via) curr_ob,
                                p_trace_cost_arr, this.min_translate_dist, 10))
                        {
                            something_changed = true;
                        }
                    }
                }
            }
        }
    }

    /**
     * Function for optimizing a single trace polygon
     * p_contact_pins are the pins at the end corners of p_polyline.
     * Other pins are regarded as obstacles, even if they are of the own net.
     */
    Polyline pull_tight(Polyline p_polyline, int p_layer, int p_half_width,
            int[] p_net_no_arr, int p_cl_type, Set<Pin> p_contact_pins)
    {
        curr_layer = p_layer;
        ShapeSearchTree search_tree = this.board.search_tree_manager.get_default_tree();
        curr_half_width = p_half_width + search_tree.clearance_compensation_value(p_cl_type, p_layer);
        curr_net_no_arr = p_net_no_arr;
        curr_cl_type = p_cl_type;
        contact_pins = p_contact_pins;
        return pull_tight(p_polyline);
    }

    /**
     * Termitates the pull tight algorithm, if the user has made a stop request.
     */
    protected boolean is_stop_requested()
    {
        if (this.stoppable_thread != null && this.stoppable_thread.is_stop_requested())
        {
            return true;
        }
        if (this.time_limit == null)
        {
            return false;
        }
        boolean time_limit_exceeded = this.time_limit.limit_exceeded();
        if (time_limit_exceeded && this.board.get_test_level().ordinal() >= TestLevel.CRITICAL_DEBUGGING_OUTPUT.ordinal())
        {
            FRLogger.warn("PullTightAlgo.is_stop_requested: time limit exceeded");
        }
        return time_limit_exceeded;
    }

    /**
     * tries to shorten p_polyline by relocating its lines
     */
    Polyline reposition_lines(Polyline p_polyline)
    {
        if (p_polyline.arr.length < 5)
        {
            return p_polyline;
        }
        for (int i = 2; i < p_polyline.arr.length - 2; ++i)
        {
            Line new_line = reposition_line(p_polyline.arr, i);
            if (new_line != null)
            {
                Line[] line_arr = new Line[p_polyline.arr.length];
                System.arraycopy(p_polyline.arr, 0, line_arr, 0, line_arr.length);
                line_arr[i] = new_line;
                Polyline result = new Polyline(line_arr);
                return skip_segments_of_length_0(result);
            }
        }
        return p_polyline;
    }

    /**
     * Tries to reposition the line with index p_no to make the polyline consisting
     * of p_line_arr shorter.
     */
    protected Line reposition_line(Line[] p_line_arr, int p_no)
    {
        if (p_line_arr.length - p_no < 3)
        {
            return null;
        }
        if (curr_clip_shape != null)
        // check, that the corners of the line to translate are inside
        // the clip shape
        {
            for (int i = -1; i < 1; ++i)
            {
                Point curr_corner =
                        p_line_arr[p_no + i].intersection(p_line_arr[p_no + i + 1]);
                if (curr_clip_shape.is_outside(curr_corner))
                {
                    return null;
                }
            }
        }
        Line translate_line = p_line_arr[p_no];
        Point prev_corner =
                p_line_arr[p_no - 2].intersection(p_line_arr[p_no - 1]);
        Point next_corner =
                p_line_arr[p_no + 1].intersection(p_line_arr[p_no + 2]);
        double prev_dist = translate_line.signed_distance(prev_corner.to_float());
        double next_dist = translate_line.signed_distance(next_corner.to_float());
        if (Signum.of(prev_dist) != Signum.of(next_dist))
        {
            // the 2 corners are at different sides of translate_line
            return null;
        }
        Point nearest_point;
        double max_translate_dist;
        if (Math.abs(prev_dist) < Math.abs(next_dist))
        {
            nearest_point = prev_corner;
            max_translate_dist = prev_dist;
        }
        else
        {
            nearest_point = next_corner;
            max_translate_dist = next_dist;
        }
        double translate_dist = max_translate_dist;
        double delta_dist = max_translate_dist;
        Side side_of_nearest_point = translate_line.side_of(nearest_point);
        int sign = Signum.as_int(max_translate_dist);
        Line new_line = null;
        Line[] check_lines = new Line[3];
        check_lines[0] = p_line_arr[p_no - 1];
        check_lines[2] = p_line_arr[p_no + 1];
        boolean first_time = true;
        while (first_time || Math.abs(delta_dist) > min_translate_dist)
        {
            boolean check_ok = false;

            if (first_time && nearest_point instanceof IntPoint)
            {
                check_lines[1] = Line.get_instance(nearest_point, translate_line.direction());
            }
            else
            {
                check_lines[1] = translate_line.translate(-translate_dist);
            }
            if (check_lines[1].equals(translate_line))
            {
                // may happen at first time if nearest_point is not an IntPoint
                return null;
            }
            Side new_line_side_of_nearest_point = check_lines[1].side_of(nearest_point);
            if (new_line_side_of_nearest_point != side_of_nearest_point && new_line_side_of_nearest_point != Side.COLLINEAR)
            {
                // moved a little bit to far at the first time
                // because of numerical inaccuracy;
                // may happen if nearest_point is not an IntPoint
                double shorten_value = sign * 0.5;
                max_translate_dist -= shorten_value;
                translate_dist -= shorten_value;
                delta_dist -= shorten_value;
                continue;
            }
            Polyline tmp = new Polyline(check_lines);

            if (tmp.arr.length == 3)
            {
                TileShape shape_to_check =
                        tmp.offset_shape(curr_half_width, 0);
                check_ok = board.check_trace_shape(shape_to_check,
                        curr_layer, curr_net_no_arr, curr_cl_type, this.contact_pins);

            }
            delta_dist /= 2;
            if (check_ok)
            {
                new_line = check_lines[1];
                if (first_time)
                {
                    // biggest possible change
                    break;
                }
                translate_dist += delta_dist;
            }
            else
            {
                translate_dist -= delta_dist;
            }
            first_time = false;
        }
        if (new_line != null && board.changed_area != null)
        {
            // mark the changed area
            board.changed_area.join(check_lines[0].intersection_approx(new_line), curr_layer);
            board.changed_area.join(check_lines[2].intersection_approx(new_line), curr_layer);
            board.changed_area.join(p_line_arr[p_no - 1].intersection_approx(p_line_arr[p_no]), curr_layer);
            board.changed_area.join(p_line_arr[p_no].intersection_approx(p_line_arr[p_no + 1]), curr_layer);

        }
        return new_line;
    }

    /**
     * tries to skip linesegments of length 0.
     * A check is nessesary before skipping because new dog ears
     * may occur.
     */
    Polyline skip_segments_of_length_0(Polyline p_polyline)
    {
        boolean polyline_changed = false;
        Polyline curr_polyline = p_polyline;
        for (int i = 1; i < curr_polyline.arr.length - 1; ++i)
        {
            boolean try_skip;
            if (i == 1 || i == curr_polyline.arr.length - 2)
            // the position of the first corner and the last corner
            //  must be retaimed exactly
            {
                Point prev_corner = curr_polyline.corner(i - 1);
                Point curr_corner = curr_polyline.corner(i);
                try_skip = curr_corner.equals(prev_corner);
            }
            else
            {
                FloatPoint prev_corner = curr_polyline.corner_approx(i - 1);
                FloatPoint curr_corner = curr_polyline.corner_approx(i);
                try_skip = curr_corner.distance_square(prev_corner) <
                        c_min_corner_dist_square;
            }

            if (try_skip)
            {
                // check, if skipping the line of length 0 does not
                // result in a clearance violation
                Line[] curr_lines = new Line[curr_polyline.arr.length - 1];
                System.arraycopy(curr_polyline.arr, 0, curr_lines, 0, i);
                System.arraycopy(curr_polyline.arr, i + 1, curr_lines,
                        i, curr_lines.length - i);
                Polyline tmp = new Polyline(curr_lines);
                boolean check_ok = (tmp.arr.length == curr_lines.length);
                if (check_ok && !curr_polyline.arr[i].is_multiple_of_45_degree())
                {
                    // no check necessary for skipping 45 degree lines, because the check is
                    // performance critical and the line shapes
                    // are intersected with the bounding octagon anyway.
                    if (i > 1)
                    {
                        TileShape shape_to_check =
                                tmp.offset_shape(curr_half_width, i - 2);
                        check_ok = board.check_trace_shape(shape_to_check,
                                curr_layer, curr_net_no_arr, curr_cl_type, this.contact_pins);
                    }
                    if (check_ok && (i < curr_polyline.arr.length - 2))
                    {
                        TileShape shape_to_check = tmp.offset_shape(curr_half_width, i - 1);
                        check_ok = board.check_trace_shape(shape_to_check,
                                curr_layer, curr_net_no_arr, curr_cl_type, this.contact_pins);
                    }
                }
                if (check_ok)
                {
                    polyline_changed = true;
                    curr_polyline = tmp;
                    --i;
                }
            }
        }
        if (!polyline_changed)
        {
            return p_polyline;
        }
        return curr_polyline;
    }

    /**
     * Smoothens acute angles with contact traces.
     * Returns true, if something was changed.
     */
    boolean smoothen_end_corners_at_trace(PolylineTrace p_trace)
    {
        curr_layer = p_trace.get_layer();
        curr_half_width = p_trace.get_half_width();
        curr_net_no_arr = p_trace.net_no_arr;
        curr_cl_type = p_trace.clearance_class_no();
        return smoothen_end_corners_at_trace_1(p_trace);
    }

    /**
     * Smoothens acute angles with contact traces.
     * Returns true, if something was changed.
     */
    private boolean smoothen_end_corners_at_trace_1(PolylineTrace p_trace)
    {
        // try to improve the connection to other traces
        if (p_trace.is_shove_fixed())
        {
            return false;
        }
        Set<Pin> saved_contact_pins = this.contact_pins;
        // to allow the trace to slide to the end point of a contact trace, if the contact trace ends at a pin.
        this.contact_pins = null;
        boolean result = false;
        boolean connection_to_trace_improved = true;
        PolylineTrace curr_trace = p_trace;
        while (connection_to_trace_improved)
        {
            connection_to_trace_improved = false;
            Polyline adjusted_polyline = smoothen_end_corners_at_trace_2(curr_trace);
            if (adjusted_polyline != null)
            {
                result = true;
                connection_to_trace_improved = true;
                int trace_layer = curr_trace.get_layer();
                int curr_cl_class = curr_trace.clearance_class_no();
                FixedState curr_fixed_state = curr_trace.get_fixed_state();
                board.remove_item(curr_trace);
                curr_trace = board.insert_trace_without_cleaning(adjusted_polyline, trace_layer, curr_half_width, curr_trace.net_no_arr,
                        curr_cl_class, curr_fixed_state);
                for (int curr_net_no : curr_trace.net_no_arr)
                {
                    board.split_traces(adjusted_polyline.first_corner(), trace_layer, curr_net_no);
                    board.split_traces(adjusted_polyline.last_corner(), trace_layer, curr_net_no);
                    board.normalize_traces(curr_net_no);

                    if (split_traces_at_keep_point())
                    {
                        return true;
                    }
                }
            }
        }
        this.contact_pins = saved_contact_pins;
        return result;
    }

    /**
     *  Splits the traces containing this.keep_point if this.keep_point != null.
     *  Returns true, if something was split.
     */
    boolean split_traces_at_keep_point()
    {
        if (this.keep_point == null)
        {
            return false;
        }
        ItemSelectionFilter filter = new ItemSelectionFilter(ItemSelectionFilter.SelectableChoices.TRACES);
        Collection<Item> picked_items = this.board.pick_items(this.keep_point, this.keep_point_layer, filter);
        for (Item curr_item : picked_items)
        {
            Trace[] split_pieces = ((Trace) curr_item).split(this.keep_point);
            if (split_pieces != null)
            {
                return true;
            }
        }
        return false;
    }

    /**
     * Smoothens acute angles with contact traces.
     * Returns null, if something was changed.
     */
    private Polyline smoothen_end_corners_at_trace_2(PolylineTrace p_trace)
    {
        if (p_trace == null || !p_trace.is_on_the_board())
        {
            return null;
        }
        Polyline result = smoothen_start_corner_at_trace(p_trace);
        if (result == null)
        {
            result = smoothen_end_corner_at_trace(p_trace);
            if (result != null && board.changed_area != null)
            {
                // mark the changed area
                board.changed_area.join(result.corner_approx(result.corner_count() - 1), curr_layer);
            }
        }
        else if (board.changed_area != null)
        {
            // mark the changed area
            board.changed_area.join(result.corner_approx(0), curr_layer);
        }
        if (result != null)
        {
            this.contact_pins = p_trace.touching_pins_at_end_corners();
            result = skip_segments_of_length_0(result);
        }
        return result;
    }

    /**
     * Wraps around pins of the own net to avoid acid traps.
     */
    protected Polyline avoid_acid_traps(Polyline p_polyline)
    {
        if (true)
        {
            return p_polyline;
        }
        Polyline result = p_polyline;
        ShoveTraceAlgo shove_trace_algo = new ShoveTraceAlgo(this.board);
        Polyline new_polyline = shove_trace_algo.spring_over_obstacles(p_polyline,
                curr_half_width, curr_layer, curr_net_no_arr, curr_cl_type, contact_pins);
        if (new_polyline != null && new_polyline != p_polyline)
        {
            if (this.board.check_polyline_trace(new_polyline, curr_layer, curr_half_width,
                    curr_net_no_arr, curr_cl_type))
            {
                result = new_polyline;
            }
        }
        return result;
    }

    abstract Polyline pull_tight(Polyline p_polyline);

    abstract Polyline smoothen_start_corner_at_trace(PolylineTrace p_trace);

    abstract Polyline smoothen_end_corner_at_trace(PolylineTrace p_trace);
    protected final RoutingBoard board;
    /** If only_net_no {@literal >} 0, only nets with this net numbers are optimized. */
    protected final int[] only_net_no_arr;
    protected int curr_layer;
    protected int curr_half_width;
    protected int[] curr_net_no_arr;
    protected int curr_cl_type;
    protected IntOctagon curr_clip_shape;
    protected Set<Pin> contact_pins;
    protected int min_translate_dist;
    protected static final double c_max_cos_angle = 0.999;
    // with angles to close to 180 degree the algorithm becomes numerically
    // unstable
    protected static final double c_min_corner_dist_square = 0.9;
    /**
     * If stoppable_thread != null, the agorithm can be requested to be stopped.
     */
    private final Stoppable stoppable_thread;
    private final TimeLimit time_limit;
    /**
     *  If keep_point != null, traces containing the keep_point must also 
     *  contain the keep_point after optimizing.
     */
    private final Point keep_point;
    private final int keep_point_layer;
}
