package com.conveyal.r5.analyst.scenario;

import com.conveyal.gtfs.GTFSFeed;
import com.conveyal.gtfs.model.Agency;
import com.conveyal.gtfs.model.Calendar;
import com.conveyal.gtfs.model.FeedInfo;
import com.conveyal.gtfs.model.Route;
import com.conveyal.gtfs.model.Service;
import com.conveyal.gtfs.model.StopTime;
import com.conveyal.gtfs.model.Trip;
import com.conveyal.r5.transit.TransportNetwork;
import com.google.common.io.ByteStreams;
import org.mapdb.Fun;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Get fake graphs.
 */
public class FakeGraph {
    public static final Logger LOG = LoggerFactory.getLogger(FakeGraph.class);

    /** frequency of trips generated by addTransit, seconds */
    public static final int FREQUENCY = 600;

    /** Travel time between stops of trips generated by addTransit, seconds */
    public static final int TRAVEL_TIME = 500;
    public static final int DWELL_TIME = 30;

    /** Build a graph in Columbus, OH using requested transit feeds */
    public static TransportNetwork buildNetwork (TransitNetwork... networks) {
        try {
            File osmFile = File.createTempFile("columbus", ".osm.pbf");

            InputStream is = new BufferedInputStream(FakeGraph.class.getResourceAsStream("columbus.osm.pbf"));
            OutputStream os = new BufferedOutputStream(new FileOutputStream(osmFile));
            ByteStreams.copy(is, os);
            is.close();
            os.close();

            List<File> filesToDelete = new ArrayList<>();
            filesToDelete.add(osmFile);

            List<String> gtfsFiles = new ArrayList<>();

            for (TransitNetwork network : networks) {
                File gtfsFile = File.createTempFile(network.toString(), ".gtfs.zip");
                network.get().toFile(gtfsFile.getAbsolutePath());
                filesToDelete.add(gtfsFile);
                gtfsFiles.add(gtfsFile.getAbsolutePath());
            }

            TransportNetwork net = TransportNetwork.fromFiles(osmFile.getAbsolutePath(), gtfsFiles);
            net.transitLayer.buildDistanceTables(null);

            // clean up
            filesToDelete.forEach(f -> f.delete());

            return net;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /** Add transit (not just stops) to a Columbus graph */
    public static GTFSFeed getTransit () throws Exception {
        // using conveyal GTFS lib to build GTFS so a lot of code does not have to be rewritten later
        // once we're using the conveyal GTFS lib for everything we ought to be able to do this
        // without even writing out the GTFS to a file.
        GTFSFeed feed = TransitNetwork.SINGLE_LINE.getBlankFeed();
        Agency a = new Agency();
        a.agency_id = "agency";
        a.agency_name = "Agency";
        a.agency_timezone = "America/New_York";
        a.agency_url = new URL("http://www.example.com");
        feed.agency.put("agency", a);

        Route r = new Route();
        r.route_short_name = "1";
        r.route_long_name = "High Street";
        r.route_type = 3;
        r.agency_id = a.agency_id;
        r.route_id = "route";
        feed.routes.put(r.route_id, r);

        Service s = new Service("service");
        s.calendar = new Calendar();
        s.calendar.service_id = s.service_id;
        s.calendar.monday = s.calendar.tuesday = s.calendar.wednesday = s.calendar.thursday = s.calendar.friday =
                s.calendar.saturday = s.calendar.sunday = 1;
        s.calendar.start_date = 19991231;
        s.calendar.end_date = 21001231;
        feed.services.put(s.service_id, s);

        com.conveyal.gtfs.model.Stop s1 = new com.conveyal.gtfs.model.Stop();
        s1.stop_id = s1.stop_name = "s1";
        s1.stop_lat = 40.0182;
        s1.stop_lon = -83.0889;
        feed.stops.put(s1.stop_id, s1);

        com.conveyal.gtfs.model.Stop s2 = new com.conveyal.gtfs.model.Stop();
        s2.stop_id = s2.stop_name = "s2";
        s2.stop_lat = 40.01;
        s2.stop_lon = -83.0007;
        feed.stops.put(s2.stop_id, s2);

        com.conveyal.gtfs.model.Stop s3 = new com.conveyal.gtfs.model.Stop();
        s3.stop_id = s3.stop_name = "s3";
        s3.stop_lat = 39.969;
        s3.stop_lon = -83.0007;
        feed.stops.put(s3.stop_id, s3);

        com.conveyal.gtfs.model.Stop s4 = new com.conveyal.gtfs.model.Stop();
        s4.stop_id = s4.stop_name = "s4";
        s4.stop_lat = 39.9621;
        s4.stop_lon = -83.0007;
        feed.stops.put(s4.stop_id, s4);

        com.conveyal.gtfs.model.Stop s5 = new com.conveyal.gtfs.model.Stop();
        s5.stop_id = s5.stop_name = "s5";
        s5.stop_lat = 39.9631;
        s5.stop_lon = -83.0017;
        feed.stops.put(s5.stop_id, s5);

        // make timetabled trips
        for (int departure = 7 * 3600; departure < 20 * 3600; departure += FREQUENCY) {
            Trip t = new Trip();
            t.trip_id = "trip" + departure;
            t.service_id = s.service_id;
            t.route_id = r.route_id;
            feed.trips.put(t.trip_id, t);

            StopTime st1 = new StopTime();
            st1.trip_id = t.trip_id;
            st1.arrival_time = departure;
            st1.departure_time = departure;
            st1.stop_id = s1.stop_id;
            st1.stop_sequence = 1;
            feed.stop_times.put(new Fun.Tuple2(st1.trip_id, st1.stop_sequence), st1);

            StopTime st2 = new StopTime();
            st2.trip_id = t.trip_id;
            st2.arrival_time = st1.departure_time + TRAVEL_TIME;
            st2.departure_time = st2.arrival_time + DWELL_TIME;
            st2.stop_sequence = 2;
            st2.stop_id = s2.stop_id;
            feed.stop_times.put(new Fun.Tuple2(st2.trip_id, st2.stop_sequence), st2);

            StopTime st3 = new StopTime();
            st3.trip_id = t.trip_id;
            st3.arrival_time = st2.departure_time + TRAVEL_TIME;
            st3.departure_time = st3.arrival_time + DWELL_TIME;
            st3.stop_sequence = 3;
            st3.stop_id = s3.stop_id;
            feed.stop_times.put(new Fun.Tuple2(st3.trip_id, st3.stop_sequence), st3);

            StopTime st4 = new StopTime();
            st4.trip_id = t.trip_id;
            st4.arrival_time = st3.departure_time + TRAVEL_TIME;
            st4.departure_time = st4.arrival_time;
            st4.stop_sequence = 4;
            st4.stop_id = s4.stop_id;
            feed.stop_times.put(new Fun.Tuple2(st4.trip_id, st4.stop_sequence), st4);
        }

        feed.findPatterns();
        return feed;
    }

    /** create two transit lines */
    public static GTFSFeed getMultipleLines () throws Exception {
        // using conveyal GTFS lib to build GTFS so a lot of code does not have to be rewritten later
        // once we're using the conveyal GTFS lib for everything we ought to be able to do this
        // without even writing out the GTFS to a file.
        GTFSFeed feed = TransitNetwork.MULTIPLE_LINES.getBlankFeed();

        Agency a = new Agency();
        a.agency_id = "agency";
        a.agency_name = "Agency";
        a.agency_timezone = "America/New_York";
        a.agency_url = new URL("http://www.example.com");
        feed.agency.put("agency", a);

        Route r = new Route();
        r.route_short_name = "1";
        r.route_long_name = "High Street";
        r.route_type = 3;
        r.agency_id = a.agency_id;
        r.route_id = "route";
        feed.routes.put(r.route_id, r);

        Service s = new Service("service");
        s.calendar = new Calendar();
        s.calendar.service_id = s.service_id;
        s.calendar.monday = s.calendar.tuesday = s.calendar.wednesday = s.calendar.thursday = s.calendar.friday =
                s.calendar.saturday = s.calendar.sunday = 1;
        s.calendar.start_date = 19991231;
        s.calendar.end_date = 21001231;
        feed.services.put(s.service_id, s);

        com.conveyal.gtfs.model.Stop s1 = new com.conveyal.gtfs.model.Stop();
        s1.stop_id = s1.stop_name = "s1";
        s1.stop_lat = 40.02182;
        s1.stop_lon = -83.0889;
        feed.stops.put(s1.stop_id, s1);

        com.conveyal.gtfs.model.Stop s2 = new com.conveyal.gtfs.model.Stop();
        s2.stop_id = s2.stop_name = "s2";
        s2.stop_lat = 39.9621;
        s2.stop_lon = -83.0007;
        feed.stops.put(s2.stop_id, s2);

        // make timetabled trips
        for (int departure = 7 * 3600; departure < 20 * 3600; departure += FREQUENCY) {
            Trip t = new Trip();
            t.trip_id = "trip" + departure;
            t.service_id = s.service_id;
            t.route_id = r.route_id;
            feed.trips.put(t.trip_id, t);

            StopTime st1 = new StopTime();
            st1.trip_id = t.trip_id;
            st1.arrival_time = departure;
            st1.departure_time = departure;
            st1.stop_id = s1.stop_id;
            st1.stop_sequence = 1;
            feed.stop_times.put(new Fun.Tuple2(st1.trip_id, st1.stop_sequence), st1);

            StopTime st2 = new StopTime();
            st2.trip_id = t.trip_id;
            st2.arrival_time = departure + TRAVEL_TIME;
            st2.departure_time = departure + TRAVEL_TIME;
            st2.stop_sequence = 2;
            st2.stop_id = s2.stop_id;
            feed.stop_times.put(new Fun.Tuple2(st2.trip_id, st2.stop_sequence), st2);
        }

        r = new Route();
        r.route_short_name = "2";
        r.route_long_name = "High Street";
        r.route_type = 3;
        r.agency_id = a.agency_id;
        r.route_id = "route2";
        feed.routes.put(r.route_id, r);

        s = new Service("serviceb");
        s.calendar = new Calendar();
        s.calendar.service_id = s.service_id;
        s.calendar.monday = s.calendar.tuesday = s.calendar.wednesday = s.calendar.thursday = s.calendar.friday =
                s.calendar.saturday = s.calendar.sunday = 1;
        s.calendar.start_date = 19991231;
        s.calendar.end_date = 21001231;
        feed.services.put(s.service_id, s);

        s1 = new com.conveyal.gtfs.model.Stop();
        s1.stop_id = s1.stop_name = "s1b";
        s1.stop_lat = 40.02184;
        s1.stop_lon = -83.0889;
        feed.stops.put(s1.stop_id, s1);

        s2 = new com.conveyal.gtfs.model.Stop();
        s2.stop_id = s2.stop_name = "s2b";
        s2.stop_lat = 39.9623;
        s2.stop_lon = -83.0007;
        feed.stops.put(s2.stop_id, s2);

        // make timetabled trips
        for (int departure = 7 * 3600; departure < 20 * 3600; departure += FREQUENCY) {
            Trip t = new Trip();
            t.trip_id = "tripb" + departure;
            t.service_id = s.service_id;
            t.route_id = r.route_id;
            feed.trips.put(t.trip_id, t);

            StopTime st1 = new StopTime();
            st1.trip_id = t.trip_id;
            st1.arrival_time = departure;
            st1.departure_time = departure;
            st1.stop_id = s1.stop_id;
            st1.stop_sequence = 1;
            feed.stop_times.put(new Fun.Tuple2(st1.trip_id, st1.stop_sequence), st1);

            StopTime st2 = new StopTime();
            st2.trip_id = t.trip_id;
            st2.arrival_time = departure + TRAVEL_TIME;
            st2.departure_time = departure + TRAVEL_TIME;
            st2.stop_sequence = 2;
            st2.stop_id = s2.stop_id;
            feed.stop_times.put(new Fun.Tuple2(st2.trip_id, st2.stop_sequence), st2);
        }

        feed.findPatterns();
        return feed;
    }

    /** Add a transit line with multiple patterns to a Columbus graph. Most trips serve stops s1, s2, s3 but some serve only s1, s3 */
    public static GTFSFeed getMultiplePatterns () throws Exception {
        // using conveyal GTFS lib to build GTFS so a lot of code does not have to be rewritten later
        // once we're using the conveyal GTFS lib for everything we ought to be able to do this
        // without even writing out the GTFS to a file.
        GTFSFeed feed = TransitNetwork.MULTIPLE_PATTERNS.getBlankFeed();
        Agency a = new Agency();
        a.agency_id = "agency";
        a.agency_name = "Agency";
        a.agency_timezone = "America/New_York";
        a.agency_url = new URL("http://www.example.com");
        feed.agency.put("agency", a);

        Route r = new Route();
        r.route_short_name = "1";
        r.route_long_name = "High Street";
        r.route_type = 3;
        r.agency_id = a.agency_id;
        r.route_id = "route";
        feed.routes.put(r.route_id, r);

        Service s = new Service("service");
        s.calendar = new Calendar();
        s.calendar.service_id = s.service_id;
        s.calendar.monday = s.calendar.tuesday = s.calendar.wednesday = s.calendar.thursday = s.calendar.friday =
                s.calendar.saturday = s.calendar.sunday = 1;
        s.calendar.start_date = 19991231;
        s.calendar.end_date = 21001231;
        feed.services.put(s.service_id, s);

        com.conveyal.gtfs.model.Stop s1 = new com.conveyal.gtfs.model.Stop();
        s1.stop_id = s1.stop_name = "s1";
        s1.stop_lat = 40.02182;
        s1.stop_lon = -83.0889;
        feed.stops.put(s1.stop_id, s1);

        com.conveyal.gtfs.model.Stop s2 = new com.conveyal.gtfs.model.Stop();
        s2.stop_id = s2.stop_name = "s2";
        s2.stop_lat = 39.9621;
        s2.stop_lon = -83.0007;
        feed.stops.put(s2.stop_id, s2);

        com.conveyal.gtfs.model.Stop s3 = new com.conveyal.gtfs.model.Stop();
        s3.stop_id = s3.stop_name = "s3";
        s3.stop_lat = 39.9510;
        s3.stop_lon = -83.0007;
        feed.stops.put(s3.stop_id, s3);

        // make timetabled trips
        for (int departure = 7 * 3600, dcount = 0; departure < 20 * 3600; departure += FREQUENCY) {
            Trip t = new Trip();
            t.trip_id = "trip" + departure;
            t.service_id = s.service_id;
            t.route_id = r.route_id;
            feed.trips.put(t.trip_id, t);

            int currentTime = departure;

            StopTime st1 = new StopTime();
            st1.trip_id = t.trip_id;
            st1.arrival_time = currentTime;
            st1.departure_time = currentTime;
            st1.stop_id = s1.stop_id;
            st1.stop_sequence = 1;
            feed.stop_times.put(new Fun.Tuple2(st1.trip_id, st1.stop_sequence), st1);

            // occasionally skip second stop
            boolean secondStop = dcount++ % 10 != 0;
            if (secondStop) {
                StopTime st2 = new StopTime();
                st2.trip_id = t.trip_id;
                currentTime += TRAVEL_TIME;
                st2.arrival_time = currentTime;
                currentTime += DWELL_TIME;
                st2.departure_time = currentTime;
                st2.stop_sequence = 2;
                st2.stop_id = s2.stop_id;
                feed.stop_times.put(new Fun.Tuple2(st2.trip_id, st2.stop_sequence), st2);
            }

            StopTime st3 = new StopTime();
            st3.trip_id = t.trip_id;
            currentTime += TRAVEL_TIME;
            st3.arrival_time = currentTime;
            st3.departure_time = currentTime;
            st3.stop_sequence = 3;
            st3.stop_id = s3.stop_id;
            feed.stop_times.put(new Fun.Tuple2(st3.trip_id, st3.stop_sequence), st3);
        }

        feed.findPatterns();
        return feed;
    }

    /** Add transit (in both directions) to a Columbus graph */
    public static GTFSFeed getTransitBidirectional () throws Exception {
        // using conveyal GTFS lib to build GTFS so a lot of code does not have to be rewritten later
        // once we're using the conveyal GTFS lib for everything we ought to be able to do this
        // without even writing out the GTFS to a file.
        GTFSFeed feed = GTFSFeed.newWritableInMemory();
        Agency a = new Agency();
        a.agency_id = "agency";
        a.agency_name = "Agency";
        a.agency_timezone = "America/New_York";
        a.agency_url = new URL("http://www.example.com");
        feed.agency.put("agency", a);

        Route r = new Route();
        r.route_short_name = "1";
        r.route_long_name = "High Street";
        r.route_type = 3;
        r.agency_id = a.agency_id;
        r.route_id = "route";
        feed.routes.put(r.route_id, r);

        Service s = new Service("service");
        s.calendar = new Calendar();
        s.calendar.service_id = s.service_id;
        s.calendar.monday = s.calendar.tuesday = s.calendar.wednesday = s.calendar.thursday = s.calendar.friday =
                s.calendar.saturday = s.calendar.sunday = 1;
        s.calendar.start_date = 19991231;
        s.calendar.end_date = 21001231;
        feed.services.put(s.service_id, s);

        com.conveyal.gtfs.model.Stop s1 = new com.conveyal.gtfs.model.Stop();
        s1.stop_id = s1.stop_name = "s1";
        s1.stop_lat = 40.02182;
        s1.stop_lon = -83.0889;
        feed.stops.put(s1.stop_id, s1);

        com.conveyal.gtfs.model.Stop s2 = new com.conveyal.gtfs.model.Stop();
        s2.stop_id = s2.stop_name = "s2";
        s2.stop_lat = 39.9621;
        s2.stop_lon = -83.0007;
        feed.stops.put(s2.stop_id, s2);

        // make timetabled trips
        for (int departure = 7 * 3600; departure < 20 * 3600; departure += FREQUENCY) {
            Trip t = new Trip();
            t.trip_id = "trip" + departure;
            t.service_id = s.service_id;
            t.route_id = r.route_id;
            t.direction_id = 0;
            feed.trips.put(t.trip_id, t);

            StopTime st1 = new StopTime();
            st1.trip_id = t.trip_id;
            st1.arrival_time = departure;
            st1.departure_time = departure;
            st1.stop_id = s1.stop_id;
            st1.stop_sequence = 1;
            feed.stop_times.put(new Fun.Tuple2(st1.trip_id, st1.stop_sequence), st1);

            StopTime st2 = new StopTime();
            st2.trip_id = t.trip_id;
            st2.arrival_time = departure + TRAVEL_TIME;
            st2.departure_time = departure + TRAVEL_TIME;
            st2.stop_sequence = 2;
            st2.stop_id = s2.stop_id;
            feed.stop_times.put(new Fun.Tuple2(st2.trip_id, st2.stop_sequence), st2);

            // opposite direction
            t = new Trip();
            t.trip_id = "trip_back" + departure;
            t.service_id = s.service_id;
            t.route_id = r.route_id;
            t.direction_id = 1;
            feed.trips.put(t.trip_id, t);

            st1 = new StopTime();
            st1.trip_id = t.trip_id;
            st1.arrival_time = departure;
            st1.departure_time = departure;
            st1.stop_id = s2.stop_id;
            st1.stop_sequence = 1;
            feed.stop_times.put(new Fun.Tuple2(st1.trip_id, st1.stop_sequence), st1);

            st2 = new StopTime();
            st2.trip_id = t.trip_id;
            st2.arrival_time = departure + TRAVEL_TIME;
            st2.departure_time = departure + TRAVEL_TIME;
            st2.stop_sequence = 2;
            st2.stop_id = s1.stop_id;
            feed.stop_times.put(new Fun.Tuple2(st2.trip_id, st2.stop_sequence), st2);
        }

        feed.findPatterns();
        return feed;
    }

    public enum TransitNetwork {
        // Single line on High Street, Columbus, OH.
        SINGLE_LINE,

        // Same but bidirectional
        BIDIRECTIONAL,

        // Same as single line but with an extra stop pattern
        MULTIPLE_PATTERNS,

        // same with multiple lines
        MULTIPLE_LINES;

        public GTFSFeed get () throws Exception {
            switch (this) {

                case SINGLE_LINE:
                    return getTransit();
                case BIDIRECTIONAL:
                    return getTransitBidirectional();
                case MULTIPLE_PATTERNS:
                    return getMultiplePatterns();
                case MULTIPLE_LINES:
                    return getMultipleLines();
            }

            throw new RuntimeException("can't happen");
        }

        public GTFSFeed getBlankFeed() {
            GTFSFeed feed = GTFSFeed.newWritableInMemory();
            feed.feedId = this.toString();

            FeedInfo info = new FeedInfo();
            info.feed_id = this.toString();

            feed.feedInfo.put("NONE", info);

            return feed;
        }
    }

    /** convenience function to make a set */
    public static <T> Set<T> set(T... value) {
        return new HashSet<T>(Arrays.asList(value));
    }
}