package org.matsim.amodeus.drt;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.apache.commons.io.FileUtils;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.matsim.amodeus.config.AmodeusConfigGroup;
import org.matsim.amodeus.config.AmodeusModeConfig;
import org.matsim.amodeus.drt.AmodeusDrtModule;
import org.matsim.amodeus.drt.AmodeusDrtQSimModule;
import org.matsim.amodeus.drt.MultiModeDrtModuleForAmodeus;
import org.matsim.amodeus.framework.AmodeusModule;
import org.matsim.amodeus.framework.VirtualNetworkModeModule;
import org.matsim.amodeus.scenario.TestScenarioGenerator;
import org.matsim.api.core.v01.Id;
import org.matsim.api.core.v01.Scenario;
import org.matsim.api.core.v01.network.Link;
import org.matsim.api.core.v01.network.Network;
import org.matsim.contrib.drt.routing.DrtRoute;
import org.matsim.contrib.drt.routing.DrtRouteFactory;
import org.matsim.contrib.drt.run.DrtConfigGroup;
import org.matsim.contrib.drt.run.DrtConfigGroup.OperationalScheme;
import org.matsim.contrib.drt.run.DrtConfigs;
import org.matsim.contrib.drt.run.MultiModeDrtConfigGroup;
import org.matsim.contrib.drt.run.MultiModeDrtModule;
import org.matsim.contrib.dvrp.fleet.DvrpVehicle;
import org.matsim.contrib.dvrp.fleet.FleetWriter;
import org.matsim.contrib.dvrp.fleet.ImmutableDvrpVehicleSpecification;
import org.matsim.contrib.dvrp.run.DvrpConfigGroup;
import org.matsim.contrib.dvrp.run.DvrpModule;
import org.matsim.contrib.dvrp.run.DvrpQSimComponents;
import org.matsim.core.config.Config;
import org.matsim.core.config.ConfigUtils;
import org.matsim.core.config.groups.PlansConfigGroup.HandlingOfPlansWithoutRoutingMode;
import org.matsim.core.config.groups.QSimConfigGroup.StarttimeInterpretation;
import org.matsim.core.controler.Controler;

public class RunDrtTest {
    private final static String DRT_MODE = "av";

    @BeforeClass
    public static void doYourOneTimeSetup() {
        new File("test_output").mkdir();
        new File("test_data").mkdir();
    }

    @AfterClass
    public static void doYourOneTimeTeardown() throws IOException {
        FileUtils.deleteDirectory(new File("test_output"));
        FileUtils.deleteDirectory(new File("test_data"));
    }

    @Test
    public void testDefaultDrt() {
        Config config = ConfigUtils.createConfig(new MultiModeDrtConfigGroup(), new DvrpConfigGroup());
        Scenario scenario = TestScenarioGenerator.generateWithAVLegs(config);
        run(config, scenario, false);
    }

    @Test
    public void testAmodeusDrt() {
        Config config = ConfigUtils.createConfig(new MultiModeDrtConfigGroup(), new DvrpConfigGroup(), new AmodeusConfigGroup());
        Scenario scenario = TestScenarioGenerator.generateWithAVLegs(config);
        run(config, scenario, true);

        // Currently, Amodeus does not know where to put the auto-generated ScenarioOptions file.
        // We can make this a config option for AmodeusConfigGroup.
        FileUtils.deleteQuietly(new File("AmodeusOptions.properties"));
    }

    static public void run(Config config, Scenario scenario, boolean useAmodeus) {
        // CONFIG PART

        // Set up MATSim configuration to be compatible with DRT
        config.plans().setHandlingOfPlansWithoutRoutingMode(HandlingOfPlansWithoutRoutingMode.useMainModeIdentifier);
        config.qsim().setStartTime(0.0);
        config.qsim().setSimStarttimeInterpretation(StarttimeInterpretation.onlyUseStarttime);

        config.qsim().setNumberOfThreads(1);

        // Set up missing scoring parameters
        config.planCalcScore().getOrCreateModeParams(DRT_MODE);

        // Set up DRT mode
        DrtConfigGroup drtModeConfig = new DrtConfigGroup();
        drtModeConfig.setMode(DRT_MODE);

        drtModeConfig.setMaxTravelTimeBeta(600.0);
        drtModeConfig.setMaxTravelTimeAlpha(1.4);
        drtModeConfig.setMaxWaitTime(600.0);
        drtModeConfig.setStopDuration(60);
        drtModeConfig.setRejectRequestIfMaxWaitOrTravelTimeViolated(true);
        drtModeConfig.setOperationalScheme(OperationalScheme.door2door);

        MultiModeDrtConfigGroup drtConfig = MultiModeDrtConfigGroup.get(config);
        drtConfig.addParameterSet(drtModeConfig);
        DrtConfigs.adjustDrtConfig(drtModeConfig, config.planCalcScore(), config.plansCalcRoute());

        // Create a fleet on the fly
        String vehiclesFile = new File("test_data/drt_vehicles.xml.gz").getAbsolutePath();
        drtModeConfig.setVehiclesFile(vehiclesFile);
        createFleet(vehiclesFile, 100, scenario.getNetwork());

        // Set up DVRP
        DvrpConfigGroup dvrpConfig = DvrpConfigGroup.get(config);
        dvrpConfig.setTravelTimeEstimationAlpha(1.0);
        dvrpConfig.setTravelTimeEstimationBeta(900);

        // SCENARIO PART
        scenario.getPopulation().getFactory().getRouteFactories().setRouteFactory(DrtRoute.class, new DrtRouteFactory());

        // CONTROLLER PART
        Controler controller = new Controler(scenario);

        // Add DVRP and activate modes
        controller.addOverridingModule(new DvrpModule());
        controller.configureQSimComponents(DvrpQSimComponents.activateModes(drtModeConfig.getMode()));

        if (!useAmodeus) {
            // No Amodeus, so we use standard MultiModeDrtModule
            controller.addOverridingModule(new MultiModeDrtModule());
        } else {
            // Add DRT, but NOT with MultiModeDrtModule, but with MultiModeDrtModuleForAmodeus
            // because right now we remove DRT's analysis components as they are not compatible yet
            controller.addOverridingModule(new MultiModeDrtModuleForAmodeus());
        }

        // Here we start overriding things of DRT with Amodeus
        if (useAmodeus) {

            // This is a per-mode config, which usually is contained in a AmodeusConfigGroup,
            // here we only use it to set up a small portion of Amodeus (the dispatching part),
            // and not scoring, waiting times, etc.
            AmodeusModeConfig amodeusModeConfig = new AmodeusModeConfig(drtModeConfig.getMode());

            // We can choose the dispatcher and set additional options. Note that some dispatchers
            // rely heavily on GLPK. You need to install it and then tell JAVA where to find it
            // via -Djava.library.path=/path/to/glpk/lib/jni on the command line.
            amodeusModeConfig.getDispatcherConfig().setType("FeedforwardFluidicRebalancingPolicy");

            // Change, for instance, to "GlobalBipartiteMatchingDispatcher" if you want to
            // test without GLPK!

            // Disable Amodeus-specific output (e.g., for the viewer)
            amodeusModeConfig.getDispatcherConfig().setPublishPeriod(0);

            // Path where to generate or read a VirtualNetwork and TravelData for rebalancing.
            // Note that not all dispatchers need this.
            amodeusModeConfig.getDispatcherConfig().setVirtualNetworkPath(new File("test_data/virtualNetwork").getAbsolutePath());
            amodeusModeConfig.getDispatcherConfig().setTravelDataPath(new File("test_data/travelData").getAbsolutePath());

            // Add a subset of Amodeus modules which usually would be added automatically
            // in the upper-level AmodeusModule.
            controller.addOverridingModule(new VirtualNetworkModeModule(amodeusModeConfig));
            controller.addOverridingModule(new AmodeusModule());

            // Add overriding modules for the Drt <-> Amodeus integration, which override some
            // components of DRT. Later on, we would only override DrtOptimizer, but we are
            // not there yet, because Amodeus internally still works with AmodeusStayTask, etc.
            // and does not understand DrtStayTask, etc.
            controller.addOverridingModule(new AmodeusDrtModule(amodeusModeConfig));
            controller.addOverridingQSimModule(new AmodeusDrtQSimModule(drtModeConfig.getMode()));
        }

        controller.run();
    }

    static public void createFleet(String path, int numberOfVehicles, Network network) {
        Random random = new Random(0);

        List<Link> links = network.getLinks().values().stream().filter(link -> link.getAllowedModes().contains("car")).collect(Collectors.toList());

        new FleetWriter(IntStream.range(0, 100).mapToObj(i -> {
            return ImmutableDvrpVehicleSpecification.newBuilder() //
                    .id(Id.create("drt" + i, DvrpVehicle.class)) //
                    .startLinkId(links.get(random.nextInt(links.size())).getId()) //
                    .capacity(4) //
                    .serviceBeginTime(0.0) //
                    .serviceEndTime(30.0 * 3600.0) //
                    .build();
        })).write(path);
    }
}