/*
 *   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.
 *
 * RouteState.java
 *
 * Created on 8. November 2003, 08:22
 */
package eu.mihosoft.freerouting.interactive;

import eu.mihosoft.freerouting.geometry.planar.FloatPoint;
import eu.mihosoft.freerouting.geometry.planar.IntPoint;
import eu.mihosoft.freerouting.geometry.planar.Point;

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

import eu.mihosoft.freerouting.board.Trace;
import eu.mihosoft.freerouting.board.Via;
import eu.mihosoft.freerouting.board.PolylineTrace;
import eu.mihosoft.freerouting.board.ConductionArea;
import eu.mihosoft.freerouting.board.DrillItem;
import eu.mihosoft.freerouting.board.Item;
import eu.mihosoft.freerouting.board.ItemSelectionFilter;
import eu.mihosoft.freerouting.logger.FRLogger;

/**
 * Interactive routing state.
 *
 * @author Alfons Wirtz
 */
public class RouteState extends InteractiveState
{

    /**
     * Returns a new instance of this class or null,
     * if starting a new route was not possible at p_location.
     * If p_logfile != null, the creation of the route is stored
     * in the logfile.
     **/
    public static RouteState get_instance(FloatPoint p_location, InteractiveState p_parent_state, BoardHandling p_board_handling, ActivityReplayFile p_activityReplayFile)
    {
        if (!(p_parent_state instanceof MenuState))
        {
            FRLogger.warn("RouteState.get_instance: unexpected parent state");
        }
        p_board_handling.display_layer_messsage();
        IntPoint location = p_location.round();
        Item picked_item = start_ok(location, p_board_handling);
        if (picked_item == null)
        {
            return null;
        }
        int net_count = picked_item.net_count();
        if (net_count <= 0)
        {
            return null;
        }
        int[] route_net_no_arr;
        if (picked_item instanceof eu.mihosoft.freerouting.board.Pin && net_count > 1)
        {
            // tie pin, remove nets, which are already conneccted to this pin on the current layer.
            route_net_no_arr = get_route_net_numbers_at_tie_pin((eu.mihosoft.freerouting.board.Pin) picked_item, p_board_handling.settings.layer);
        }
        else
        {
            route_net_no_arr = new int[net_count];
            for (int i = 0; i < net_count; ++i)
            {
                route_net_no_arr[i] = picked_item.get_net_no(i);
            }
        }
        if (route_net_no_arr.length <= 0)
        {
            return null;
        }
        eu.mihosoft.freerouting.board.RoutingBoard routing_board = p_board_handling.get_routing_board();
        int[] trace_half_widths = new int[routing_board.get_layer_count()];
        boolean[] layer_active_arr = new boolean[trace_half_widths.length];
        for (int i = 0; i < trace_half_widths.length; ++i)
        {
            trace_half_widths[i] = p_board_handling.get_trace_halfwidth(route_net_no_arr[0], i);
            layer_active_arr[i] = false;
            for (int j = 0; j < route_net_no_arr.length; ++j)
            {
                if (p_board_handling.is_active_routing_layer(route_net_no_arr[j], i))
                {
                    layer_active_arr[i] = true;
                }
            }
        }

        int trace_clearance_class = p_board_handling.get_trace_clearance_class(route_net_no_arr[0]);
        boolean start_ok = true;
        if (picked_item instanceof Trace)
        {
            Trace picked_trace = (Trace) picked_item;
            Point picked_corner = picked_trace.nearest_end_point(location);
            if (picked_corner instanceof IntPoint &&
                    p_location.distance(picked_corner.to_float()) < 5 * picked_trace.get_half_width())
            {
                location = (IntPoint) picked_corner;
            }
            else
            {
                if (picked_trace instanceof PolylineTrace)
                {
                    FloatPoint nearest_point = ((PolylineTrace) picked_trace).polyline().nearest_point_approx(p_location);
                    location = nearest_point.round();
                }
                if (!routing_board.connect_to_trace(location, picked_trace,
                        picked_trace.get_half_width(), picked_trace.clearance_class_no()))
                {
                    start_ok = false;
                }
            }
            if (start_ok && !p_board_handling.settings.manual_rule_selection)
            {
                // Pick up the half with and the clearance class of the found trace.
                int[] new_trace_half_widths = new int[trace_half_widths.length];
                System.arraycopy(trace_half_widths, 0, new_trace_half_widths, 0, trace_half_widths.length);
                new_trace_half_widths[picked_trace.get_layer()] = picked_trace.get_half_width();
                trace_half_widths = new_trace_half_widths;
                trace_clearance_class = picked_trace.clearance_class_no();

            }
        }
        else if (picked_item instanceof DrillItem)
        {
            DrillItem drill_item = (DrillItem) picked_item;
            Point center = drill_item.get_center();
            if (center instanceof IntPoint)
            {
                location = (IntPoint) center;
            }
        }
        if (!start_ok)
        {
            return null;
        }


        eu.mihosoft.freerouting.rules.Net curr_net = routing_board.rules.nets.get(route_net_no_arr[0]);
        if (curr_net == null)
        {
            return null;
        }
        // Switch to stitch mode for nets, which are shove fixed.
        boolean is_stitch_route = p_board_handling.settings.is_stitch_route || curr_net.get_class().is_shove_fixed() || !curr_net.get_class().get_pull_tight();
        routing_board.generate_snapshot();
        RouteState new_instance;
        if (is_stitch_route)
        {
            new_instance = new StitchRouteState(p_parent_state, p_board_handling, p_activityReplayFile);
        }
        else
        {
            new_instance = new DynamicRouteState(p_parent_state, p_board_handling, p_activityReplayFile);
        }
        new_instance.routing_target_set = picked_item.get_unconnected_set(-1);

        new_instance.route = new Route(location, p_board_handling.settings.layer, trace_half_widths, layer_active_arr, route_net_no_arr,
                trace_clearance_class, p_board_handling.get_via_rule(route_net_no_arr[0]),
                p_board_handling.settings.push_enabled, p_board_handling.settings.trace_pull_tight_region_width,
                p_board_handling.settings.trace_pull_tight_accuracy, picked_item, new_instance.routing_target_set, routing_board,
                is_stitch_route, p_board_handling.settings.automatic_neckdown, p_board_handling.settings.via_snap_to_smd_center,
                p_board_handling.settings.hilight_routing_obstacle);
        new_instance.observers_activated = !routing_board.observers_active();
        if (new_instance.observers_activated)
        {
            routing_board.start_notify_observers();
        }
        p_board_handling.repaint();
        if (new_instance != null)
        {
            if (new_instance.activityReplayFile != null)
            {
                new_instance.activityReplayFile.start_scope(ActivityReplayFileScope.CREATING_TRACE, p_location);
                p_board_handling.hide_ratsnest();
            }
            new_instance.display_default_message();
        }
        return new_instance;
    }

    /**
     * Creates a new instance of RouteState
     * If p_logfile != null, the creation of the route is stored
     * in the logfile.
     */
    protected RouteState(InteractiveState p_parent_state, BoardHandling p_board_handling, ActivityReplayFile p_activityReplayFile)
    {
        super(p_parent_state, p_board_handling, p_activityReplayFile);
    }

    /**
     * Checks starting an interactive route at p_location.
     * Returns the picked start item of the routing at p_location,
     * or null, if no such item was found.
     */
    static protected Item start_ok(IntPoint p_location, BoardHandling p_hdlg)
    {
        eu.mihosoft.freerouting.board.RoutingBoard routing_board = p_hdlg.get_routing_board();

        /**
         * look if an already exististing trace ends at p_start_corner
         * and pick it up in this case.
         */
        Item picked_item = routing_board.pick_nearest_routing_item(p_location, p_hdlg.settings.layer, null);
        int layer_count = routing_board.get_layer_count();
        if (picked_item == null && p_hdlg.settings.select_on_all_visible_layers)
        {
            // Nothing found on preferred layer, try the other visible layers.
            // Prefer the outer layers.
            picked_item = pick_routing_item(p_location, 0, p_hdlg);
            if (picked_item == null)
            {
                picked_item = pick_routing_item(p_location, layer_count - 1, p_hdlg);
            }
            // prefer signal layers
            if (picked_item == null)
            {
                for (int i = 1; i < layer_count - 1; ++i)
                {
                    if (routing_board.layer_structure.arr[i].is_signal)
                    {
                        picked_item = pick_routing_item(p_location, i, p_hdlg);
                        if (picked_item != null)
                        {
                            break;
                        }
                    }
                }
            }
            if (picked_item == null)
            {
                for (int i = 1; i < layer_count - 1; ++i)
                {
                    if (!routing_board.layer_structure.arr[i].is_signal)
                    {
                        picked_item = pick_routing_item(p_location, i, p_hdlg);
                        if (picked_item != null)
                        {
                            break;
                        }
                    }
                }
            }
        }
        return picked_item;
    }

    static private Item pick_routing_item(IntPoint p_location, int p_layer_no, BoardHandling p_hdlg)
    {

        if (p_layer_no == p_hdlg.settings.layer || (p_hdlg.graphics_context.get_layer_visibility(p_layer_no) <= 0))
        {
            return null;
        }
        Item picked_item = p_hdlg.get_routing_board().pick_nearest_routing_item(p_location, p_layer_no, null);
        if (picked_item != null)
        {
            p_hdlg.set_layer(picked_item.first_layer());
        }
        return picked_item;
    }

    public InteractiveState process_logfile_point(FloatPoint p_point)
    {
        return add_corner(p_point);
    }

    /**
     * Action to be taken when a key is pressed (Shortcut).
     */
    public InteractiveState key_typed(char p_key_char)
    {
        InteractiveState curr_return_state = this;
        if (Character.isDigit(p_key_char))
        {
            // change to the p_key_char-ths signal layer
            eu.mihosoft.freerouting.board.LayerStructure layer_structure = hdlg.get_routing_board().layer_structure;
            int d = Character.digit(p_key_char, 10);
            d = Math.min(d, layer_structure.signal_layer_count());
            // Board layers start at 0, keyboard input for layers starts at 1.
            d = Math.max(d - 1, 0);
            eu.mihosoft.freerouting.board.Layer new_layer = layer_structure.get_signal_layer(d);
            d = layer_structure.get_no(new_layer);

            if (d >= 0)
            {
                change_layer_action(d);
            }
        }
        else if (p_key_char == '+')
        {
            // change to the next signal layer
            eu.mihosoft.freerouting.board.LayerStructure layer_structure = hdlg.get_routing_board().layer_structure;
            int current_layer_no = hdlg.settings.layer;
            for (;;)
            {
                ++current_layer_no;
                if (current_layer_no >= layer_structure.arr.length || layer_structure.arr[current_layer_no].is_signal)
                {
                    break;
                }
            }
            if (current_layer_no < layer_structure.arr.length)
            {
                change_layer_action(current_layer_no);
            }
        }
        else if (p_key_char == '-')
        {
            // change to the to the previous signal layer
            eu.mihosoft.freerouting.board.LayerStructure layer_structure = hdlg.get_routing_board().layer_structure;
            int current_layer_no = hdlg.settings.layer;
            for (;;)
            {
                --current_layer_no;
                if (current_layer_no < 0 || layer_structure.arr[current_layer_no].is_signal)
                {
                    break;
                }
            }
            if (current_layer_no >= 0)
            {
                change_layer_action(current_layer_no);
            }

        }
        else
        {
            curr_return_state = super.key_typed(p_key_char);
        }
        return curr_return_state;
    }

    /**
     * Append a line to p_location to the trace routed so far.
     * Returns from state, if the route is completed by connecting
     * to a target.
     */
    public InteractiveState add_corner(FloatPoint p_location)
    {
        boolean route_completed = route.next_corner(p_location);
        String layer_string = hdlg.get_routing_board().layer_structure.arr[route.nearest_target_layer()].name;
        hdlg.screen_messages.set_target_layer(layer_string);
        if (this.activityReplayFile != null)
        {
            this.activityReplayFile.add_corner(p_location);
        }
        if (route_completed)
        {
            if (this.observers_activated)
            {
                hdlg.get_routing_board().end_notify_observers();
                this.observers_activated = false;
            }
        }
        InteractiveState result;
        if (route_completed)
        {
            result = this.return_state;
            hdlg.screen_messages.clear();
            for (int curr_net_no : this.route.net_no_arr)
            {
                hdlg.update_ratsnest(curr_net_no);
            }
        }
        else
        {
            result = this;
        }
        hdlg.recalculate_length_violations();
        hdlg.repaint(hdlg.get_graphics_update_rectangle());
        return result;
    }

    public InteractiveState cancel()
    {
        Trace tail = hdlg.get_routing_board().get_trace_tail(route.get_last_corner(), hdlg.settings.layer, route.net_no_arr);
        if (tail != null)
        {
            Collection<Item> remove_items = tail.get_connection_items(Item.StopConnectionOption.VIA);
            if (hdlg.settings.push_enabled)
            {
                hdlg.get_routing_board().remove_items_and_pull_tight(remove_items,
                        hdlg.settings.trace_pull_tight_region_width, hdlg.settings.trace_pull_tight_accuracy, false);
            }
            else
            {
                hdlg.get_routing_board().remove_items(remove_items, false);
            }
        }
        if (this.observers_activated)
        {
            hdlg.get_routing_board().end_notify_observers();
            this.observers_activated = false;
        }
        if (activityReplayFile != null)
        {
            activityReplayFile.start_scope(ActivityReplayFileScope.CANCEL_SCOPE);
        }
        hdlg.screen_messages.clear();
        for (int curr_net_no : this.route.net_no_arr)
        {
            hdlg.update_ratsnest(curr_net_no);
        }
        return this.return_state;
    }

    public boolean change_layer_action(int p_new_layer)
    {
        boolean result = true;
        if (p_new_layer >= 0 && p_new_layer < hdlg.get_routing_board().get_layer_count())
        {
            if (this.route != null && !this.route.is_layer_active(p_new_layer))
            {
                String layer_name = hdlg.get_routing_board().layer_structure.arr[p_new_layer].name;
                hdlg.screen_messages.set_status_message(resources.getString("layer_not_changed_because_layer") + " " + layer_name + " " + resources.getString("is_not_active_for_the_current_net"));
            }
            boolean change_layer_succeeded = route.change_layer(p_new_layer);
            if (change_layer_succeeded)
            {
                boolean connected_to_plane = false;
                // check, if the layer change resulted in a connection to a power plane.
                int old_layer = hdlg.settings.get_layer();
                ItemSelectionFilter selection_filter = new ItemSelectionFilter(ItemSelectionFilter.SelectableChoices.VIAS);
                Collection<Item> picked_items =
                        hdlg.get_routing_board().pick_items(route.get_last_corner(), old_layer, selection_filter);
                Via new_via = null;
                for (Item curr_via : picked_items)
                {
                    if (curr_via.shares_net_no(route.net_no_arr))
                    {
                        new_via = (Via) curr_via;
                        break;
                    }
                }
                if (new_via != null)
                {
                    int from_layer;
                    int to_layer;
                    if (old_layer < p_new_layer)
                    {
                        from_layer = old_layer + 1;
                        to_layer = p_new_layer;
                    }
                    else
                    {
                        from_layer = p_new_layer;
                        to_layer = old_layer - 1;
                    }
                    Collection<Item> contacts = new_via.get_normal_contacts();
                    for (Item curr_item : contacts)
                    {
                        if (curr_item instanceof ConductionArea)
                        {
                            ConductionArea curr_area = (ConductionArea) curr_item;
                            if (curr_area.get_layer() >= from_layer && curr_area.get_layer() <= to_layer)
                            {
                                connected_to_plane = true;
                                break;
                            }
                        }
                    }
                }

                if (connected_to_plane)
                {
                    hdlg.set_interactive_state(this.return_state);
                    for (int curr_net_no : this.route.net_no_arr)
                    {
                        hdlg.update_ratsnest(curr_net_no);
                    }
                }
                else
                {
                    hdlg.set_layer(p_new_layer);
                    String layer_name = hdlg.get_routing_board().layer_structure.arr[p_new_layer].name;
                    hdlg.screen_messages.set_status_message(resources.getString("layer_changed_to") + " " + layer_name);
                    // make the current situation restorable by undo
                    hdlg.get_routing_board().generate_snapshot();
                }
                if (activityReplayFile != null)
                {
                    activityReplayFile.start_scope(ActivityReplayFileScope.CHANGE_LAYER, p_new_layer);
                }
            }
            else
            {
                int shove_failing_layer = hdlg.get_routing_board().get_shove_failing_layer();
                if (shove_failing_layer >= 0)
                {
                    String layer_name = hdlg.get_routing_board().layer_structure.arr[hdlg.get_routing_board().get_shove_failing_layer()].name;
                    hdlg.screen_messages.set_status_message(resources.getString("layer_not_changed_because_of_obstacle_on_layer") + " " + layer_name);
                }
                else
                {
                    FRLogger.warn("RouteState.change_layer_action: shove_failing_layer not set");
                }
                result = false;
            }
            hdlg.repaint();
        }
        return result;
    }

    /**
     * get nets of p_tie_pin except nets of traces, which are already conneccted to this pin on p_layer.
     */
    static int[] get_route_net_numbers_at_tie_pin(eu.mihosoft.freerouting.board.Pin p_pin, int p_layer)
    {
        Set<Integer> net_number_list = new java.util.TreeSet<Integer>();
        for (int i = 0; i < p_pin.net_count(); ++i)
        {
            net_number_list.add(p_pin.get_net_no(i));
        }
        Set<Item> contacts = p_pin.get_normal_contacts();
        for (Item curr_contact : contacts)
        {
            if (curr_contact.first_layer() <= p_layer && curr_contact.last_layer() >= p_layer)
            {
                for (int i = 0; i < curr_contact.net_count(); ++i)
                {
                    net_number_list.remove(curr_contact.get_net_no(i));
                }
            }
        }
        int[] result = new int[net_number_list.size()];
        int curr_ind = 0;
        for (Integer curr_net_number : net_number_list)
        {
            result[curr_ind] = curr_net_number;
            ++curr_ind;
        }
        return result;
    }

    public void draw(java.awt.Graphics p_graphics)
    {
        if (route != null)
        {
            route.draw(p_graphics, hdlg.graphics_context);
        }
    }

    public void display_default_message()
    {
        if (route != null)
        {
            eu.mihosoft.freerouting.rules.Net curr_net = hdlg.get_routing_board().rules.nets.get(route.net_no_arr[0]);
            hdlg.screen_messages.set_status_message(resources.getString("routing_net") + " " + curr_net.name);
        }
    }
    protected Route route = null;
    private Set<Item> routing_target_set = null;
    protected boolean observers_activated = false;
}
