// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.customizepublictransportstop;

import java.awt.Point;
import java.awt.geom.Point2D;
import java.util.AbstractMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.openstreetmap.josm.command.AddCommand;
import org.openstreetmap.josm.command.ChangeCommand;
import org.openstreetmap.josm.data.UndoRedoHandler;
import org.openstreetmap.josm.data.coor.EastNorth;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.osm.BBox;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.IWaySegment;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.data.osm.WaySegment;
import org.openstreetmap.josm.data.projection.ProjectionRegistry;
import org.openstreetmap.josm.gui.MainApplication;
import org.openstreetmap.josm.gui.MapView;
import org.openstreetmap.josm.spi.preferences.Config;
import org.openstreetmap.josm.tools.Geometry;

/**
 * Operation of creation of new stop point
 * 
 * @author Rodion Scherbakov
 */
public class CreateNewStopPointOperation extends StopAreaOperationBase {

    /**
     * Constructor of operation object
     * 
     * @param currentDataSet Current Josm data set
     */
    public CreateNewStopPointOperation(DataSet currentDataSet) {
        super(currentDataSet);
    }

    /**
     * The *result* does not depend on the current map selection state, neither does
     * the result *order*. It solely depends on the distance to point p.
     *
     * This code is coped from JOSM code
     * 
     * @return a sorted map with the keys representing the distance of their
     *         associated nodes to point p.
     */
    private Map<Double, List<Node>> getNearestNodesImpl(Point p) {
        TreeMap<Double, List<Node>> nearestMap = new TreeMap<>();
        DataSet ds = getCurrentDataSet();

        if (ds != null) {
            double dist, snapDistanceSq = 200;
            snapDistanceSq *= snapDistanceSq;

            for (Node n : ds.searchNodes(getBBox(p, 200))) {
                if ((dist = MainApplication.getMap().mapView.getPoint2D(n).distanceSq(p)) < snapDistanceSq) {
                    List<Node> nlist;
                    if (nearestMap.containsKey(dist)) {
                        nlist = nearestMap.get(dist);
                    } else {
                        nlist = new LinkedList<>();
                        nearestMap.put(dist, nlist);
                    }
                    nlist.add(n);
                }
            }
        }

        return nearestMap;
    }

    /**
     * Selection of area for search of roads
     * 
     * @param p Current point
     * @param snapDistance Distance for search
     * @return Area
     */
    private BBox getBBox(Point p, int snapDistance) {
        MapView mapView = MainApplication.getMap().mapView;
        return new BBox(mapView.getLatLon(p.x - snapDistance, p.y - snapDistance),
                mapView.getLatLon(p.x + snapDistance, p.y + snapDistance));
    }

    /**
     * Search of nearest points on ways
     * 
     * @param platformCoord Platform coordinates
     * @param stopArea Stop area object
     * @return Dictionary of founded points and distances from platform
     */
    public AbstractMap.SimpleEntry<Double, Node> getNearestNode(LatLon platformCoord, StopArea stopArea) {
        Point p = MainApplication.getMap().mapView.getPoint(platformCoord);
        Map<Double, List<Node>> dist_nodes = getNearestNodesImpl(p);
        Double[] distances = dist_nodes.keySet().toArray(new Double[0]);
        distances = sort(distances);
        Integer distanceIndex = -1;
        Node nearestNode = null;
        while (++distanceIndex < distances.length && nearestNode == null) {
            List<Node> nodes = dist_nodes.get(distances[distanceIndex]);
            for (Node node : nodes) {
                for (Way way : getCurrentDataSet().getWays()) {
                    if (way.getNodes().contains(node) && testWay(way, stopArea)) {
                        nearestNode = node;
                        return new AbstractMap.SimpleEntry<Double, Node>(distances[distanceIndex], nearestNode);
                    }
                }
                if (nearestNode != null)
                    break;
            }
        }
        return null;
    }

    /**
     * Sorting of founded points by distance
     * 
     * @param distances Array of distances
     * @return Sorted array of distances
     */
    public Double[] sort(Double[] distances) {
        for (Integer i = 0; i < distances.length - 1; i++) {
            for (Integer j = i + 1; j < distances.length; j++) {
                if (distances[i] > distances[j]) {
                    Double d = distances[i];
                    distances[i] = distances[j];
                    distances[j] = d;
                }
            }
        }
        return distances;
    }

    /**
     * Selection of ways for stop position by type of way and type of stop
     * 
     * @param way The way
     * @param stopArea Stop area
     * @return true, if way can contain stop position
     */
    public Boolean testWay(Way way, StopArea stopArea) {
        if (stopArea.isTrainStation || stopArea.isTrainStop) {
            if (OSMTags.RAIL_TAG_VALUE.equals(way.getKeys().get(OSMTags.RAILWAY_TAG))
                    && OSMTags.MAIN_TAG_VALUE.equals(way.getKeys().get(OSMTags.USAGE_TAG)))
                return true;
            return false;
        }

        if (stopArea.isTram) {
            if (OSMTags.TRAM_TAG_VALUE.equals(way.getKeys().get(OSMTags.RAILWAY_TAG)))
                return true;
            return false;
        }

        String[] highwayValues = {OSMTags.TRUNK_TAG_VALUE, OSMTags.PRIMARY_TAG_VALUE, OSMTags.SECONDARY_TAG_VALUE,
                OSMTags.TERTIARY_TAG_VALUE, OSMTags.UNCLASSIFIED_TAG_VALUE, OSMTags.RESIDENTIAL_TAG_VALUE,
                OSMTags.SERVICE_TAG_VALUE, OSMTags.BUS_GUIDEWAY_TAG_VALUE, OSMTags.ROAD_TAG_VALUE,
                OSMTags.TRUNK_LINK_TAG_VALUE, OSMTags.PRIMARY_LINK_TAG_VALUE, OSMTags.SECONDARY_LINK_TAG_VALUE,
                OSMTags.TERTIARY_LINK_TAG_VALUE};
        if (stopArea.isBus || stopArea.isTrolleybus || stopArea.isShareTaxi) {
            String highway = way.getKeys().get(OSMTags.HIGHWAY_TAG);
            if (highway != null)
                for (Integer i = 0; i < highwayValues.length; i++) {
                    if (highwayValues[i].equals(highway))
                        return true;
                }
        }
        return false;
    }

    /**
     * The *result* does not depend on the current map selection state, neither does
     * the result *order*. It solely depends on the distance to point p.
     *
     * This code is coped from JOSM code
     * 
     * @return a sorted map with the keys representing the perpendicular distance of
     *         their associated way segments to point p.
     */
    private Map<Double, List<IWaySegment<Node, Way>>> getNearestWaySegmentsImpl(Point p) {
        Map<Double, List<IWaySegment<Node, Way>>> nearestMap = new TreeMap<>();
        DataSet ds = getCurrentDataSet();

        if (ds != null) {
            double snapDistanceSq = Config.getPref().getInt("mappaint.segment.snap-distance", 200);
            snapDistanceSq *= snapDistanceSq;

            for (Way w : ds.searchWays(getBBox(p, Config.getPref().getInt("mappaint.segment.snap-distance", 200)))) {
                Node lastN = null;
                int i = -2;
                for (Node n : w.getNodes()) {
                    i++;
                    if (n.isDeleted() || n.isIncomplete()) { // FIXME: This shouldn't happen, raise exception?
                        continue;
                    }
                    if (lastN == null) {
                        lastN = n;
                        continue;
                    }

                    Point2D A = MainApplication.getMap().mapView.getPoint2D(lastN);
                    Point2D B = MainApplication.getMap().mapView.getPoint2D(n);
                    double c = A.distanceSq(B);
                    double a = p.distanceSq(B);
                    double b = p.distanceSq(A);

                    /*
                     * perpendicular distance squared loose some precision to account for possible
                     * deviations in the calculation above e.g. if identical (A and B) come about
                     * reversed in another way, values may differ -- zero out least significant 32
                     * dual digits of mantissa..
                     */
                    double perDistSq = Double.longBitsToDouble(
                            // resolution in numbers with large exponent not needed here..
                            Double.doubleToLongBits(a - (a - b + c) * (a - b + c) / 4 / c) >> 32 << 32);

                    if (perDistSq < snapDistanceSq && a < c + snapDistanceSq && b < c + snapDistanceSq) {
                        List<IWaySegment<Node, Way>> wslist;
                        if (nearestMap.containsKey(perDistSq)) {
                            wslist = nearestMap.get(perDistSq);
                        } else {
                            wslist = new LinkedList<>();
                            nearestMap.put(perDistSq, wslist);
                        }
                        wslist.add(new IWaySegment<>(w, i));
                    }

                    lastN = n;
                }
            }
        }

        return nearestMap;
    }

    /**
     * Selection of nearest way for stop position
     * 
     * @param platformCoord Platform coordinates
     * @param stopArea Stop area
     * @return Nearest way segment
     */
    protected NearestWaySegment getNearestWaySegment(LatLon platformCoord, StopArea stopArea) {
        MapView mapView = MainApplication.getMap().mapView;
        Point p = mapView.getPoint(platformCoord);
        Map<Double, List<IWaySegment<Node, Way>>> dist_waySegments = getNearestWaySegmentsImpl(p);
        for (Map.Entry<Double, List<IWaySegment<Node, Way>>> entry : dist_waySegments.entrySet()) {
            for (IWaySegment<Node, Way> waySegment : entry.getValue()) {
                if (testWay(waySegment.getWay(), stopArea)) {
                    Node n = waySegment.getFirstNode();
                    Node lastN = waySegment.getSecondNode();

                    EastNorth newPosition = Geometry.closestPointToSegment(n.getEastNorth(), lastN.getEastNorth(),
                            ProjectionRegistry.getProjection().latlon2eastNorth(platformCoord));
                    LatLon newNodePosition = ProjectionRegistry.getProjection().eastNorth2latlon(newPosition);
                    Point2D lastN2D = mapView.getPoint2D(lastN);
                    Point2D n2D = mapView.getPoint2D(n);
                    Point2D newNodePosition2D = mapView.getPoint2D(newNodePosition);
                    Double distCurrenNodes = lastN2D.distance(n2D);
                    if ((newNodePosition2D.distance(lastN2D) < distCurrenNodes)
                            && (newNodePosition2D.distance(n2D) < distCurrenNodes)) {
                        return new NearestWaySegment(entry.getKey(), waySegment, new Node(newNodePosition));
                    }
                }
            }
        }
        return null;
    }

    /**
     * Creation of stop position node on nearest way
     * 
     * @param newStopNode New stop position node
     * @param waySegment Way segment including stop position node
     * @return Stop position node
     */
    protected Node createNodeOnWay(Node newStopNode, IWaySegment<Node, Way> waySegment) {
        UndoRedoHandler.getInstance().add(new AddCommand(MainApplication.getLayerManager().getEditDataSet(), newStopNode));
        List<Node> wayNodes = waySegment.getWay().getNodes();
        wayNodes.add(waySegment.getUpperIndex(), newStopNode);
        Way newWay = new Way(waySegment.getWay());
        newWay.setNodes(wayNodes);
        UndoRedoHandler.getInstance().add(new ChangeCommand(waySegment.getWay(), newWay));
        return newStopNode;
    }

    /**
     * Creation of stop position
     * 
     * @param stopArea Stop Area
     */
    @Override
    public StopArea performCustomizing(StopArea stopArea) {
        LatLon platformCoord = null;
        if (stopArea.selectedObject instanceof Node) {
            platformCoord = ((Node) stopArea.selectedObject).getCoor();
        } else
            platformCoord = getCenterOfWay(stopArea.selectedObject);
        if (platformCoord == null)
            return stopArea;
        AbstractMap.SimpleEntry<Double, Node> nearestNode = getNearestNode(platformCoord, stopArea);
        NearestWaySegment nearestWaySegment = getNearestWaySegment(platformCoord, stopArea);
        Node newStopPointNode = null;
        if (nearestNode != null && nearestWaySegment != null) {
            MapView mapView = MainApplication.getMap().mapView;
            Double segmentDist = mapView.getPoint2D(platformCoord)
                    .distanceSq(mapView.getPoint2D(nearestWaySegment.newNode));
            Double nodeDistSq = nearestNode.getKey();
            // nodeDistSq *= nodeDistSq - 2;
            if (segmentDist < nodeDistSq - 2) {
                // MessageBox.ok("new stop node v2 " + segmentDist.toString() + " " +
                // nodeDistSq.toString());
                newStopPointNode = createNodeOnWay(nearestWaySegment.newNode, nearestWaySegment.waySegment);
            } else {
                // MessageBox.ok("new stop node v3 " + segmentDist.toString() + " " +
                // nodeDistSq.toString());
                newStopPointNode = nearestNode.getValue();
            }
        } else if (nearestNode != null && nearestWaySegment == null) {
            newStopPointNode = nearestNode.getValue();
        } else if (nearestNode == null && nearestWaySegment != null) {
            // MessageBox.ok("new stop node2");
            newStopPointNode = createNodeOnWay(nearestWaySegment.newNode, nearestWaySegment.waySegment);
        }
        if (newStopPointNode != null) {
            stopArea.stopPoints.add(newStopPointNode);
        }
        return stopArea;
    }

}
