// License: GPL. For details, see LICENSE file.
package com.innovant.josm.plugin.routing;

import static org.openstreetmap.josm.tools.I18n.marktr;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;

import javax.swing.Action;
import javax.swing.Icon;

import org.apache.log4j.Logger;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.RenameLayerAction;
import org.openstreetmap.josm.data.Bounds;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
import org.openstreetmap.josm.gui.MapView;
import org.openstreetmap.josm.gui.NavigatableComponent;
import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
import org.openstreetmap.josm.gui.layer.Layer;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.tools.ColorHelper;
import org.openstreetmap.josm.tools.ImageProvider;

import com.innovant.josm.jrt.osm.OsmEdge;


/**
 * A JOSM layer that encapsulates the representation of the shortest path.
 * @author juangui
 * @author Jose Vidal
 */
public class RoutingLayer extends Layer {

    public enum PreferencesKeys {
        KEY_ACTIVE_ROUTE_COLOR(marktr("routing active route")),
        KEY_INACTIVE_ROUTE_COLOR(marktr("routing inactive route")),
        KEY_ROUTE_WIDTH("routing.route.width"),
        KEY_ROUTE_SELECT("routing.route.select");

        public final String key;
        PreferencesKeys(String key) {
            this.key = key;
        }

        public String getKey() {
            return key;
        }
    }

    /**
     * Logger
     */
    static Logger logger = Logger.getLogger(RoutingLayer.class);

    /**
     * Constant
     */
    private static final double ARROW_PHI = Math.toRadians(20);

    /**
     * Routing Model
     */
    private RoutingModel routingModel;

    /**
     * Start, Middle and End icons
     */
    private Icon startIcon, middleIcon, endIcon;

    /**
     * Associated OSM layer
     */
    private OsmDataLayer dataLayer;

    /**
     * Default constructor
     * @param name Layer name.
     */
    public RoutingLayer(String name, OsmDataLayer dataLayer) {
        super(name);
        logger.debug("Creating Routing Layer...");
        if (startIcon == null) startIcon = ImageProvider.get("routing", "startflag");
        if (middleIcon == null) middleIcon = ImageProvider.get("routing", "middleflag");
        if (endIcon == null) endIcon = ImageProvider.get("routing", "endflag");
        this.dataLayer = dataLayer;
        this.routingModel = new RoutingModel(dataLayer.data);
        logger.debug("Routing Layer created.");


        this.routingModel.routingGraph.createGraph();    /* construct the graph right after we we create the layer */
        Main.map.repaint();                            /* update MapView */
    }

    /**
     * Getter Routing Model.
     * @return the routingModel
     */
    public RoutingModel getRoutingModel() {
        return this.routingModel;
    }

    /**
     * Gets associated data layer
     * @return OsmDataLayer associated to the RoutingLayer
     */
    public OsmDataLayer getDataLayer() {
        return dataLayer;
    }

    /**
     * Gets nearest node belonging to a highway tagged way
     * @param p Point on the screen
     * @return The nearest highway node, in the range of the snap distance
     */
    public final Node getNearestHighwayNode(Point p) {
        Node nearest = null;
        int snapDistance = NavigatableComponent.PROP_SNAP_DISTANCE.get();
        double minDist = 0;
        for (Way w : dataLayer.data.getWays()) {
            if (w.isDeleted() || w.isIncomplete() || w.get("highway") == null) continue;
            for (Node n : w.getNodes()) {
                if (n.isDeleted() || n.isIncomplete()) continue;

                Point P = Main.map.mapView.getPoint(n);
                double dist = p.distance(P);
                if (dist < snapDistance) {
                    if ((nearest == null) || (dist < minDist)) {
                        nearest = n;
                        minDist = dist;
                    }
                }
            }
        }
        return nearest;
    }

    @Override
    public Icon getIcon() {
        Icon icon = ImageProvider.get("layer", "routing_small");
        return icon;
    }

    @Override
    public Object getInfoComponent() {
        String info = "<html>"
                + "<body>"
                +"Graph Vertex: "+this.routingModel.routingGraph.getVertexCount()+"<br/>"
                +"Graph Edges: "+this.routingModel.routingGraph.getEdgeCount()+"<br/>"
                + "</body>"
                + "</html>";
        return info;
    }

    @Override
    public Action[] getMenuEntries() {
        Collection<Action> components = new ArrayList<>();
        components.add(LayerListDialog.getInstance().createShowHideLayerAction());
        //        components.add(new JMenuItem(new LayerListDialog.ShowHideMarkerText(this)));
        components.add(LayerListDialog.getInstance().createDeleteLayerAction());
        components.add(SeparatorLayerAction.INSTANCE);
        components.add(new RenameLayerAction(getAssociatedFile(), this));
        components.add(SeparatorLayerAction.INSTANCE);
        components.add(new LayerListPopup.InfoAction(this));
        return components.toArray(new Action[0]);
    }

    @Override
    public String getToolTipText() {
        String tooltip = this.routingModel.routingGraph.getVertexCount() + " vertices, "
                + this.routingModel.routingGraph.getEdgeCount() + " edges";
        return tooltip;
    }

    @Override
    public boolean isMergable(Layer other) {
        return false;
    }

    @Override
    public void mergeFrom(Layer from) {
        // This layer is not mergable, so do nothing
    }

    @Override
    public void paint(Graphics2D g, MapView mv, Bounds bounds) {
        boolean isActiveLayer = (mv.getLayerManager().getActiveLayer().equals(this));
        // Get routing nodes (start, middle, end)
        List<Node> nodes = routingModel.getSelectedNodes();

        // Get path stroke color from preferences
        // Color is different for active and inactive layers
        Color color;
        if (isActiveLayer) {
            color = Main.pref.getColor(PreferencesKeys.KEY_ACTIVE_ROUTE_COLOR.key, Color.RED);
        } else {
            color = Main.pref.getColor(PreferencesKeys.KEY_INACTIVE_ROUTE_COLOR.key, Color.decode("#dd2222"));
        }

        // Get path stroke width from preferences
        String widthString = Main.pref.get(PreferencesKeys.KEY_ROUTE_WIDTH.key);
        if (widthString.length() == 0) {
            widthString = "2";                        /* I think 2 is better  */
            // FIXME add after good width is found: Main.pref.put(KEY_ROUTE_WIDTH, widthString);
        }
        int width = Integer.parseInt(widthString);


        // draw our graph
        if (isActiveLayer) {
            if (routingModel != null) {
                if (routingModel.routingGraph != null && routingModel.routingGraph.getGraph() != null) {
                    Set<OsmEdge> graphEdges = routingModel.routingGraph.getGraph().edgeSet();
                    if (!graphEdges.isEmpty()) {
                        Color color2 = ColorHelper.html2color("#00ff00");        /* just green for now  */
                        OsmEdge firstedge = (OsmEdge) graphEdges.toArray()[0];
                        Point from = mv.getPoint(firstedge.fromEastNorth());
                        g.drawRect(from.x-4, from.y+4, from.x+4, from.y-4);
                        for (OsmEdge edge : graphEdges) {
                            drawGraph(g, mv, edge, color2, width);
                        }
                    }
                }
            }
        }


        if (nodes == null || nodes.size() == 0) return;

        // Paint routing path
        List<OsmEdge> routeEdges = routingModel.getRouteEdges();
        if (routeEdges != null) {
            for (OsmEdge edge : routeEdges) {
                drawEdge(g, mv, edge, color, width, true);
            }
        }

        // paint start icon
        Node node = nodes.get(0);
        Point screen = mv.getPoint(node);
        startIcon.paintIcon(mv, g, screen.x - startIcon.getIconWidth()/2,
                screen.y - startIcon.getIconHeight());

        // paint middle icons
        for (int index = 1; index < nodes.size() - 1; ++index) {
            node = nodes.get(index);
            screen = mv.getPoint(node);
            middleIcon.paintIcon(mv, g, screen.x - startIcon.getIconWidth()/2,
                    screen.y - middleIcon.getIconHeight());
        }
        // paint end icon
        if (nodes.size() > 1) {
            node = nodes.get(nodes.size() - 1);
            screen = mv.getPoint(node);
            endIcon.paintIcon(mv, g, screen.x - startIcon.getIconWidth()/2,
                    screen.y - endIcon.getIconHeight());
        }
    }

    @Override
    public void visitBoundingBox(BoundingXYVisitor v) {
        for (Node node : routingModel.getSelectedNodes()) {
            v.visit(node);
        }
    }

    @Override
    public void destroy() {
        routingModel.reset();
        //      layerAdded = false;
    }

    /**
     * Draw a line with the given color.
     */
    private void drawEdge(Graphics g, MapView mv, OsmEdge edge, Color col, int width,
            boolean showDirection) {
        g.setColor(col);
        Point from;
        Point to;
        from = mv.getPoint(edge.fromEastNorth());
        to = mv.getPoint(edge.toEastNorth());

        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Anti-alias!
        Stroke oldStroke = g2d.getStroke();
        g2d.setStroke(new BasicStroke(width)); // thickness
        g2d.drawLine(from.x, from.y, to.x, to.y);
        if (showDirection) {
            double t = Math.atan2(to.y-from.y, to.x-from.x) + Math.PI;
            g.drawLine(to.x, to.y, (int) (to.x + 10*Math.cos(t-ARROW_PHI)), (int) (to.y + 10*Math.sin(t-ARROW_PHI)));
            g.drawLine(to.x, to.y, (int) (to.x + 10*Math.cos(t+ARROW_PHI)), (int) (to.y + 10*Math.sin(t+ARROW_PHI)));
        }
        g2d.setStroke(oldStroke);
    }

    private void drawGraph(Graphics g, MapView mv, OsmEdge edge, Color col, int width) {
        g.setColor(col);
        Point from;
        Point to;
        from = mv.getPoint(edge.fromEastNorth());
        to = mv.getPoint(edge.toEastNorth());

        Graphics2D g2d = (Graphics2D) g;
        Stroke oldStroke = g2d.getStroke();
        g2d.setStroke(new BasicStroke(width)); // thickness
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Anti-alias!
        g2d.drawLine(from.x, from.y, to.x, to.y);
        g2d.drawRect(to.x- 4, to.y+4, 4, 4);

        g2d.setStroke(oldStroke);
    }

}
