/*
 *   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.
 */

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.Point;
import eu.mihosoft.freerouting.geometry.planar.TileShape;
import eu.mihosoft.freerouting.logger.FRLogger;

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


/**
 *
 * Class describing functionality required for traces in the plane.
 *
 * @author Alfons Wirtz
 */

public abstract class Trace extends Item implements Connectable, java.io.Serializable
{
    
    Trace(int p_layer, int p_half_width, int[] p_net_no_arr, int p_clearance_type,
            int p_id_no, int p_group_no, FixedState p_fixed_state, BasicBoard p_board)
    {
        super(p_net_no_arr, p_clearance_type, p_id_no, p_group_no, p_fixed_state, p_board);
        half_width = p_half_width ;
        p_layer = Math.max(p_layer, 0);
        if (p_board != null)
        {
            p_layer = Math.min(p_layer, p_board.get_layer_count() - 1);
        }
        layer = p_layer;
    }
    
    /**
     * returns the first corner of the trace
     */
    public abstract Point first_corner();
    
    /**
     * returns the last corner of the trace
     */
    public abstract Point last_corner();
    
    public int first_layer()
    {
        return this.layer;
    }
    
    public int last_layer()
    {
        return this.layer;
    }
    
    public int get_layer()
    {
        return this.layer;
    }
    
    public void set_layer(int p_layer)
    {
        this.layer = p_layer;
    }
    
    public int get_half_width()
    {
        return half_width;
    }
    
    /**
     * Returns the length of this trace.
     */
    public abstract double get_length();
    
    /**
     * Returns the half with enlarged by the clearance compensation value for the tree
     * with id number p_ttree_id_no
     * Equals get_half_width(), if no clearance compensation is used in this tree.
     */
    public int get_compensated_half_width(ShapeSearchTree p_search_tree)
    {
        int result = this.half_width + p_search_tree.clearance_compensation_value(clearance_class_no(), this.layer);
        return result;
    }
    
    public boolean is_obstacle(Item p_other)
    {
        if (p_other == this || p_other instanceof ViaObstacleArea || p_other instanceof ComponentObstacleArea)
        {
            return false;
        }
        if (p_other instanceof ConductionArea && !((ConductionArea) p_other).get_is_obstacle())
        {
            return false;
        }
        if (!p_other.shares_net(this))
        {
            return true;
        }
        return false;
    }
    
    /**
     * Get a list of all items with a connection point on the layer
     * of this trace equal to its first corner.
     */
    public Set<Item> get_start_contacts()
    {
        return get_normal_contacts(first_corner(), false);
    }
    
    /**
     * Get a list of all items with a connection point on the layer
     * of this trace equal to its last corner.
     */
    public Set<Item> get_end_contacts()
    {
        return get_normal_contacts(last_corner(), false);
    }
    
    public Point normal_contact_point(Item p_other)
    {
        return p_other.normal_contact_point(this);
    }
    
    public Set<Item> get_normal_contacts()
    {
        Set<Item> result = new TreeSet<Item>();
        Point start_corner = this.first_corner();
        if (start_corner != null)
        {
            result.addAll(get_normal_contacts(start_corner, false));
        }
        Point end_corner = this.last_corner();
        if (end_corner != null)
        {
            result.addAll(get_normal_contacts(end_corner, false));
        }
        return result;
    }
    
    public boolean is_route()
    {
        return !is_user_fixed() && this.net_count() > 0;
    }
    
    /**
     * Returns true, if this trace is not contacted at its first or at its last point.
     */
    public boolean is_tail()
    {
        Collection<Item> contact_list = this.get_start_contacts();
        if (contact_list.size() == 0)
        {
            return true;
        }
        contact_list = this.get_end_contacts();
        return (contact_list.size() == 0);
    }
    
    
    public java.awt.Color[] get_draw_colors(eu.mihosoft.freerouting.boardgraphics.GraphicsContext p_graphics_context)
    {
        return p_graphics_context.get_trace_colors(this.is_user_fixed());
    }
    
    public int get_draw_priority()
    {
        return eu.mihosoft.freerouting.boardgraphics.Drawable.MAX_DRAW_PRIORITY;
    }
    
    public double get_draw_intensity(eu.mihosoft.freerouting.boardgraphics.GraphicsContext p_graphics_context)
    {
        return p_graphics_context.get_trace_color_intensity();
    }
    
    /**
     * Get a list of all items having a connection point at p_point
     * on the layer of this trace.
     * If p_ignore_net is false, only contacts to items sharing a net with this trace
     * are calculated. This is the normal case.
     */
    public Set<Item> get_normal_contacts(Point p_point, boolean p_ignore_net)
    {
        if (p_point == null || !(p_point.equals(this.first_corner()) || p_point.equals(this.last_corner())))
        {
            return new TreeSet<Item>();
        }
        TileShape search_shape = TileShape.get_instance(p_point);
        Set<SearchTreeObject> overlaps = board.overlapping_objects(search_shape, this.layer);
        Set<Item> result = new TreeSet<Item> ();
        for (SearchTreeObject curr_ob : overlaps)
        {
            if (!(curr_ob instanceof Item))
            {
                continue;
            }
            Item curr_item = (Item) curr_ob;
            if (curr_item != this && curr_item.shares_layer(this) && (p_ignore_net || curr_item.shares_net(this)))
            {
                if (curr_item instanceof Trace)
                {
                    Trace curr_trace = (Trace) curr_item;
                    if (p_point.equals(curr_trace.first_corner())
                    || p_point.equals(curr_trace.last_corner()))
                    {
                        result.add(curr_item);
                    }
                }
                else if (curr_item instanceof DrillItem)
                {
                    DrillItem curr_drill_item = (DrillItem) curr_item;
                    if(p_point.equals(curr_drill_item.get_center()))
                    {
                        result.add(curr_item);
                    }
                }
                else if (curr_item instanceof ConductionArea)
                {
                    ConductionArea curr_area = (ConductionArea) curr_item;
                    if (curr_area.get_area().contains(p_point))
                    {
                        result.add(curr_item);
                    }
                }
            }
        }
        return result;
    }
    
    Point normal_contact_point(DrillItem p_drill_item)
    {
        return p_drill_item.normal_contact_point(this);
    }
    
    Point normal_contact_point(Trace p_other)
    {
        if (this.layer != p_other.layer)
        {
            return null;
        }
        boolean contact_at_first_corner =
                this.first_corner().equals(p_other.first_corner())
                || this.first_corner().equals(p_other.last_corner());
        boolean contact_at_last_corner =
                this.last_corner().equals(p_other.first_corner())
                || this.last_corner().equals(p_other.last_corner());
        Point result;
        if (!(contact_at_first_corner || contact_at_last_corner)
        || contact_at_first_corner && contact_at_last_corner)
        {
            // no contact point or more than 1 contact point
            result = null;
        }
        else if (contact_at_first_corner)
        {
            result = this.first_corner();
        }
        else // contact at last corner
        {
            result = this.last_corner();
        }
        return result;
    }
    
    public boolean is_drillable(int p_net_no)
    {
        return this.contains_net(p_net_no);
    }
    
    /**
     * looks, if this trace is connectet to the same object
     * at its start and its end point
     */
    public boolean is_overlap()
    {
        Set<Item> start_contacts = this.get_start_contacts();
        Set<Item> end_contacts = this.get_end_contacts();
        Iterator<Item> it = end_contacts.iterator();
        while (it.hasNext())
        {
            if (start_contacts.contains(it.next()))
            {
                return true;
            }
        }
        return false;
    }
    
    /**
     * Returns true, if it is not allowed to change the location of this item by the push algorithm.
     */
    public boolean is_shove_fixed()
    {
        if (super.is_shove_fixed())
        {
            return true;
        }
        
        // check, if the trace  belongs to a net, which is not shovable.
        eu.mihosoft.freerouting.rules.Nets nets = this.board.rules.nets;
        for (int curr_net_no : this.net_no_arr)
        {
            if (eu.mihosoft.freerouting.rules.Nets.is_normal_net_no(curr_net_no))
            {
                if (nets.get(curr_net_no).get_class().is_shove_fixed())
                {
                    return true;
                }
            }
        }
        return false;
    }
    
    /**
     * returns the endpoint of this trace with the shortest distance
     * to p_from_point
     */
    public Point nearest_end_point(Point p_from_point)
    {
        Point p1 = first_corner();
        Point p2 = last_corner();
        FloatPoint from_point = p_from_point.to_float();
        double d1 = from_point.distance(p1.to_float());
        double d2 = from_point.distance(p2.to_float());
        Point result;
        if (d1 < d2)
        {
            result = p1;
        }
        else
        {
            result = p2;
        }
        return result;
    }
    
    /**
     * Checks, if this trace can be reached by other items via more than one path
     */
    public boolean is_cycle()
    {
        if (this.is_overlap())
        {
            return true;
        }
        Set<Item> visited_items = new TreeSet<Item>();
        Collection<Item> start_contacts = this.get_start_contacts();
        // a cycle exists if through expanding the start contact we reach
        // this trace again via an end contact
        for (Item curr_contact : start_contacts)
        {
            // make shure, that all direct neighbours are
            // expanded from here, to block coming back to
            // this trace via a start contact.
            visited_items.add(curr_contact);
        }
        boolean ignore_areas = false;
        if (this.net_no_arr.length > 0)
        {
            eu.mihosoft.freerouting.rules.Net curr_net = this.board.rules.nets.get(this.net_no_arr[0]);
            if (curr_net != null && curr_net.get_class() != null)
            {
                ignore_areas = curr_net.get_class().get_ignore_cycles_with_areas();
            }
        }
        for (Item curr_contact : start_contacts)
        {
            if (curr_contact.is_cycle_recu(visited_items, this, this, ignore_areas))
            {
                return true;
            }
        }
        return false;
    }
    
    public int shape_layer(int p_index)
    {
        return layer;
    }
    
    public Point[] get_ratsnest_corners()
    {
        // Use only uncontacted enpoints of the trace.
        // Otherwise the allocated memory in the calculation of the incompletes might become very big.
        int stub_count = 0;
        boolean stub_at_start = false;
        boolean stub_at_end = false;
        if (get_start_contacts().isEmpty())
        {
            ++stub_count;
            stub_at_start = true;
        }
        if (get_end_contacts().isEmpty())
        {
            ++stub_count;
            stub_at_end = true;
        }
        Point[] result = new Point[stub_count];
        int stub_no = 0;
        if (stub_at_start)
        {
            result[stub_no] = first_corner();
            ++stub_no;
        }
        if (stub_at_end)
        {
            result[stub_no] = last_corner();
        }
        for (int i = 0; i < result.length; ++i)
        {
            if (result[i] == null)
            {
                return new Point[0];// Trace is inconsistent
            }
        }
        return result;
    }
    
    
    /**
     * checks, that the connection restrictions to the contact pins
     * are satisfied. If p_at_start, the start of this trace is checked,
     * else the end. Returns false, if a pin is at that end, where
     * the connection is checked and the connection is not ok.
     */
    public abstract  boolean check_connection_to_pin(boolean p_at_start);
    
    public boolean is_selected_by_filter(ItemSelectionFilter p_filter)
    {
        if (!this.is_selected_by_fixed_filter(p_filter))
        {
            return false;
        }
        return p_filter.is_selected(ItemSelectionFilter.SelectableChoices.TRACES);
    }
    
    
    /**
     * Looks up touching pins at the first corner and the last corner of the trace.
     * Used to avoid acid traps.
     */
    Set<Pin> touching_pins_at_end_corners()
    {
        Set<Pin> result = new TreeSet<Pin>();
        if (this.board == null)
        {
            return result;
        }
        Point curr_end_point = this.first_corner();
        for (int i = 0; i < 2; ++i)
        {
            IntOctagon curr_oct = curr_end_point.surrounding_octagon();
            curr_oct = curr_oct.enlarge(this.half_width);
            Set<Item> curr_overlaps = this.board.overlapping_items_with_clearance(curr_oct, this.layer, new int[0], this.clearance_class_no());
            for (Item curr_item : curr_overlaps)
            {
                if ((curr_item instanceof Pin) && curr_item.shares_net(this))
                {
                    result.add((Pin)curr_item);
                }
            }
            curr_end_point = this.last_corner();
        }
        return result;
    }
    
    public void print_info(ObjectInfoPanel p_window, java.util.Locale p_locale)
    {
        java.util.ResourceBundle resources =
                java.util.ResourceBundle.getBundle("eu.mihosoft.freerouting.board.ObjectInfoPanel", p_locale);
        p_window.append_bold(resources.getString("trace"));
        p_window.append(" " + resources.getString("from"));
        p_window.append(this.first_corner().to_float());
        p_window.append(resources.getString("to"));
        p_window.append(this.last_corner().to_float());
        p_window.append(resources.getString("on_layer") + " ");
        p_window.append(this.board.layer_structure.arr[this.layer].name);
        p_window.append(", " + resources.getString("width") + " ");
        p_window.append(2 * this.half_width);
        p_window.append(", " + resources.getString("length") + " ");
        p_window.append(this.get_length());
        this.print_connectable_item_info(p_window, p_locale);
        p_window.newline();
    }
    
    public boolean validate()
    {
        boolean result = super.validate();
        
        if (this.first_corner().equals(this.last_corner()))
        {
            FRLogger.warn("Trace.validate: first and last corner are equal");
            result = false;
        }
        return result;
    }
    
    
    /**
     * looks, if this trace can be combined with other traces .
     * Returns true, if somthing has been combined.
     */
    abstract boolean combine();
    
    /**
     * Looks up traces intersecting with this trace and splits them at the intersection points.
     * In case of an overlaps, the traces are split at their first and their last common point.
     * Returns the pieces resulting from splitting.
     * If nothing is split, the result will contain just this Trace.
     * If p_clip_shape != null, the split may be resticted to p_clip_shape.
     */
    public abstract Collection<PolylineTrace> split(IntOctagon p_clip_shape);
    
    /**
     * Splits this trace into two at p_point.
     * Returns the 2 pieces of the splitted trace, or null if nothing was splitted because for example 
     * p_point is not located on this trace.
     */
    public abstract Trace[] split(Point p_point);
    
    /**
     * Tries to make this trace shorter according to its rules.
     * Returns true if the geometry of the trace was changed.
     */
    public abstract boolean pull_tight(PullTightAlgo p_pull_tight_algo);
    
    
    private final int half_width ; // half width of the trace pen
    private int layer ; // board layer of the trace
}
