package com.graphhopper.gtfs;

import com.carrotsearch.hppc.IntHashSet;
import com.carrotsearch.hppc.cursors.IntCursor;
import com.conveyal.gtfs.GTFSFeed;
import com.conveyal.gtfs.model.Stop;
import com.graphhopper.routing.querygraph.QueryGraph;
import com.graphhopper.routing.util.EdgeFilter;
import com.graphhopper.storage.GraphHopperStorage;
import com.graphhopper.storage.index.LocationIndex;
import com.graphhopper.storage.index.Snap;
import com.graphhopper.util.PointList;
import com.graphhopper.util.exceptions.PointNotFoundException;
import com.graphhopper.util.shapes.GHPoint;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;

public class PtLocationSnapper {

    public static class Result {
        public final QueryGraph queryGraph;
        public final List<Label.NodeId> nodes;
        public final PointList points;

        public Result(QueryGraph queryGraph, List<Label.NodeId> nodes, PointList points) {
            this.queryGraph = queryGraph;
            this.nodes = nodes;
            this.points = points;
        }
    }

    GraphHopperStorage graphHopperStorage;
    LocationIndex locationIndex;
    GtfsStorage gtfsStorage;

    public PtLocationSnapper(GraphHopperStorage graphHopperStorage, LocationIndex locationIndex, GtfsStorage gtfsStorage) {
        this.graphHopperStorage = graphHopperStorage;
        this.locationIndex = locationIndex;
        this.gtfsStorage = gtfsStorage;
    }

    public Result snapAll(List<GHLocation> locations, List<EdgeFilter> snapFilters) {
        PointList points = new PointList(2, false);
        ArrayList<Snap> pointSnaps = new ArrayList<>();
        ArrayList<Supplier<Label.NodeId>> allSnaps = new ArrayList<>();
        for (int i = 0; i < locations.size(); i++) {
            GHLocation location = locations.get(i);
            if (location instanceof GHPointLocation) {
                GHPoint point = ((GHPointLocation) location).ghPoint;
                final Snap closest = locationIndex.findClosest(point.lat, point.lon, snapFilters.get(i));
                if (!closest.isValid()) {
                    IntHashSet result = new IntHashSet();
                    gtfsStorage.getStopIndex().findEdgeIdsInNeighborhood(point.lat, point.lon, 0, result::add);
                    gtfsStorage.getStopIndex().findEdgeIdsInNeighborhood(point.lat, point.lon, 1, result::add);
                    if (result.isEmpty()) {
                        throw new PointNotFoundException("Cannot find point: " + point, i);
                    }
                    IntCursor stopNodeId = result.iterator().next();
                    for (Map.Entry<GtfsStorage.FeedIdWithStopId, Integer> e : gtfsStorage.getStationNodes().entrySet()) {
                        if (e.getValue() == stopNodeId.value) {
                            Stop stop = gtfsStorage.getGtfsFeeds().get(e.getKey().feedId).stops.get(e.getKey().stopId);
                            final Snap stopSnap = new Snap(stop.stop_lat, stop.stop_lon);
                            stopSnap.setClosestNode(stopNodeId.value);
                            int streetNode = Optional.ofNullable(gtfsStorage.getPtToStreet().get(stopSnap.getClosestNode())).orElse(-1);
                            allSnaps.add(() -> new Label.NodeId(streetNode, stopSnap.getClosestNode()));
                            points.add(stopSnap.getQueryPoint().lat, stopSnap.getQueryPoint().lon);
                        }
                    }
                } else {
                    pointSnaps.add(closest);
                    int ptNode = Optional.ofNullable(gtfsStorage.getStreetToPt().get(closest.getClosestNode())).orElse(-1);
                    allSnaps.add(() -> new Label.NodeId(closest.getClosestNode(), ptNode));
                    points.add(closest.getSnappedPoint());
                }
            } else if (location instanceof GHStationLocation) {
                final Snap stopSnap = findByStopId((GHStationLocation) location, i);
                int streetNode = Optional.ofNullable(gtfsStorage.getPtToStreet().get(stopSnap.getClosestNode())).orElse(-1);
                allSnaps.add(() -> new Label.NodeId(streetNode, stopSnap.getClosestNode()));
                points.add(stopSnap.getQueryPoint().lat, stopSnap.getQueryPoint().lon);
            }
        }
        QueryGraph queryGraph = QueryGraph.create(graphHopperStorage, pointSnaps); // modifies pointSnaps!

        List<Label.NodeId> nodes = new ArrayList<>();
        for (Supplier<Label.NodeId> supplier : allSnaps) {
            nodes.add(supplier.get());
        }
        return new Result(queryGraph, nodes, points);
    }

    private Snap findByStopId(GHStationLocation station, int indexForErrorMessage) {
        for (Map.Entry<String, GTFSFeed> entry : gtfsStorage.getGtfsFeeds().entrySet()) {
            final Integer node = gtfsStorage.getStationNodes().get(new GtfsStorage.FeedIdWithStopId(entry.getKey(), station.stop_id));
            if (node != null) {
                Stop stop = gtfsStorage.getGtfsFeeds().get(entry.getKey()).stops.get(station.stop_id);
                final Snap stationSnap = new Snap(stop.stop_lat, stop.stop_lon);
                stationSnap.setClosestNode(node);
                return stationSnap;
            }
        }
        throw new PointNotFoundException("Cannot find station: " + station.stop_id, indexForErrorMessage);
    }


}

@@ -3154,33 +3154,52 @@
         
-int street
+allSnaps.add(() -%3E new Label.
 Node
- = 
+Id(
 Optional
@@ -3284,84 +3284,8 @@
 (-1)
-;%0A                            allSnaps.add(() -%3E new Label.NodeId(streetNode
 , st
@@ -3553,20 +3553,69 @@
     
-int p
+allSnaps.add(() -%3E new Label.NodeId(closest.getCloses
 tNode
- =
+(),
  Opt
@@ -3704,98 +3704,8 @@
 (-1)
-;%0A                    allSnaps.add(() -%3E new Label.NodeId(closest.getClosestNode(), ptNode
 ));%0A
@@ -3950,25 +3950,44 @@
     
-int street
+allSnaps.add(() -%3E new Label.
 Node
- = 
+Id(
 Opti
@@ -4076,72 +4076,8 @@
 (-1)
-;%0A                allSnaps.add(() -%3E new Label.NodeId(streetNode
 , st

fix #2518