/* amod - Copyright (c) 2018, ETH Zurich, Institute for Dynamic Systems and Control */
package amodeus.socket;

import java.io.File;
import java.net.MalformedURLException;
import java.util.Objects;

import amodeus.amodeus.data.LocationSpec;
import amodeus.amodeus.data.ReferenceFrame;
import amodeus.amodeus.generator.RandomDensityGenerator;
import amodeus.amodeus.linkspeed.LinkSpeedDataContainer;
import amodeus.amodeus.linkspeed.LinkSpeedUtils;
import amodeus.amodeus.linkspeed.TaxiTravelTimeRouter;
import amodeus.amodeus.linkspeed.TrafficDataModule;
import amodeus.amodeus.net.MatsimAmodeusDatabase;
import amodeus.amodeus.net.SimulationServer;
import amodeus.amodeus.options.ScenarioOptions;
import amodeus.amodeus.options.ScenarioOptionsBase;
import amodeus.amodeus.util.math.GlobalAssert;
import amodeus.amodeus.util.matsim.AddCoordinatesToActivities;
import amodeus.amodeus.util.net.StringSocket;
import org.matsim.amodeus.AmodeusConfigurator;
import org.matsim.amodeus.config.AmodeusConfigGroup;
import org.matsim.amodeus.framework.AmodeusUtils;
import org.matsim.api.core.v01.Scenario;
import org.matsim.api.core.v01.network.Network;
import org.matsim.api.core.v01.population.Population;
import org.matsim.contrib.dvrp.run.DvrpConfigGroup;
import org.matsim.core.config.Config;
import org.matsim.core.config.ConfigUtils;
import org.matsim.core.config.groups.PlanCalcScoreConfigGroup.ActivityParams;
import org.matsim.core.controler.AbstractModule;
import org.matsim.core.controler.Controler;
import org.matsim.core.scenario.ScenarioUtils;

import amodeus.amod.ext.Static;
import amodeus.socket.core.SocketDispatcherHost;

/** only one ScenarioServer can run at one time, since a fixed network port is
 * reserved to serve the simulation status */
/* package */ class SocketServer {

    private File configFile;
    private File outputDirectory;
    private Network network;
    private ReferenceFrame referenceFrame;
    private ScenarioOptions scenarioOptions;

    /** runs a simulation run using input data from Amodeus.properties, av.xml and MATSim config.xml
     * 
     * @throws MalformedURLException
     * @throws Exception */

    public void simulate(StringSocket stringSocket, int numReqTot, //
            File workingDirectory) throws MalformedURLException, Exception {
        Static.setup();
        /** working directory and options */
        scenarioOptions = new ScenarioOptions(workingDirectory, ScenarioOptionsBase.getDefault());

        /** set to true in order to make server wait for at least 1 client, for
         * instance viewer client, for fals the ScenarioServer starts the simulation
         * immediately */
        boolean waitForClients = scenarioOptions.getBoolean("waitForClients");
        configFile = new File(scenarioOptions.getSimulationConfigName());
        /** geographic information */
        LocationSpec locationSpec = scenarioOptions.getLocationSpec();
        referenceFrame = locationSpec.referenceFrame();

        /** open server port for clients to connect to */
        SimulationServer.INSTANCE.startAcceptingNonBlocking();
        SimulationServer.INSTANCE.setWaitForClients(waitForClients);

        /** load MATSim configs - including av.xml configurations, load routing packages */
        GlobalAssert.that(configFile.exists());
        DvrpConfigGroup dvrpConfigGroup = new DvrpConfigGroup();
        dvrpConfigGroup.setTravelTimeEstimationAlpha(0.05);
        Config config = ConfigUtils.loadConfig(configFile.toString(), new AmodeusConfigGroup(), dvrpConfigGroup);
        config.planCalcScore().addActivityParams(new ActivityParams("activity"));
        // TODO @Sebastian fix this to meaningful values, remove, or add comment
        // this was added because there are sometimes problems, is there a more elegant option?
        for (ActivityParams activityParams : config.planCalcScore().getActivityParams()) {
            activityParams.setTypicalDuration(3600.0);
        }

        /** load MATSim scenario for simulation */
        Scenario scenario = ScenarioUtils.loadScenario(config);
        AddCoordinatesToActivities.run(scenario);
        network = scenario.getNetwork();
        Population population = scenario.getPopulation();
        GlobalAssert.that(Objects.nonNull(network));
        GlobalAssert.that(Objects.nonNull(population));

        Objects.requireNonNull(network);
        MatsimAmodeusDatabase db = MatsimAmodeusDatabase.initialize(network, referenceFrame);
        Controler controller = new Controler(scenario);
        AmodeusConfigurator.configureController(controller, db, scenarioOptions);

        /** try to load link speed data and use for speed adaption in network */
        try {
            File linkSpeedDataFile = new File(scenarioOptions.getLinkSpeedDataName());
            System.out.println(linkSpeedDataFile.toString());
            LinkSpeedDataContainer lsData = LinkSpeedUtils.loadLinkSpeedData(linkSpeedDataFile);
            controller.addOverridingQSimModule(new TrafficDataModule(lsData));
        } catch (Exception exception) {
            System.err.println("Unable to load linkspeed data, freeflow speeds will be used in the simulation.");
            exception.printStackTrace();
        }

        controller.addOverridingModule(new SocketModule(stringSocket, numReqTot));

        /** Custom router that ensures same network speeds as taxis in original data set. */
        controller.addOverridingModule(new AbstractModule() {
            @Override
            public void install() {
                bind(TaxiTravelTimeRouter.Factory.class);
                AmodeusUtils.bindRouterFactory(binder(), TaxiTravelTimeRouter.class.getSimpleName()).to(TaxiTravelTimeRouter.Factory.class);
            }
        });

        /** adding the dispatcher to receive and process string fleet commands */
        controller.addOverridingModule(new AbstractModule() {
            @Override
            public void install() {
                AmodeusUtils.registerDispatcherFactory(binder(), "SocketDispatcherHost", SocketDispatcherHost.Factory.class);
            }
        });

        /** adding an initial vehicle placer */
        controller.addOverridingModule(new AbstractModule() {
            @Override
            public void install() {
                AmodeusUtils.bindGeneratorFactory(binder(), RandomDensityGenerator.class.getSimpleName()).//
                to(RandomDensityGenerator.Factory.class);
            }
        });

        /** run simulation */
        controller.run();

        /** close port for visualizaiton */
        SimulationServer.INSTANCE.stopAccepting();

        /** perform analysis of simulation */
        /** output directory for saving results */
        outputDirectory = new File(config.controler().getOutputDirectory());

    }

    /* package */ File getOutputDirectory() {
        return outputDirectory;
    }

    /* package */ File getConfigFile() {
        return configFile;
    }

    /* package */ Network getNetwork() {
        return network;
    }

    /* package */ ReferenceFrame getReferenceFrame() {
        return referenceFrame;
    }

    /* package */ ScenarioOptions getScenarioOptions() {
        return scenarioOptions;
    }
}