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

import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.matsim.amodeus.components.AmodeusDispatcher;
import org.matsim.amodeus.components.AmodeusRouter;
import org.matsim.amodeus.config.AmodeusModeConfig;
import org.matsim.api.core.v01.network.Link;
import org.matsim.api.core.v01.network.Network;
import org.matsim.contrib.dvrp.passenger.PassengerRequest;
import org.matsim.contrib.dvrp.run.ModalProviders.InstanceGetter;
import org.matsim.core.api.experimental.events.EventsManager;
import org.matsim.core.config.Config;
import org.matsim.core.router.FastAStarLandmarksFactory;
import org.matsim.core.router.util.LeastCostPathCalculator;
import org.matsim.core.router.util.TravelTime;

import amodeus.amodeus.dispatcher.core.DispatcherConfigWrapper;
import amodeus.amodeus.dispatcher.core.RoboTaxi;
import amodeus.amodeus.dispatcher.core.SharedRebalancingDispatcher;
import amodeus.amodeus.dispatcher.shared.SharedCourse;
import amodeus.amodeus.net.MatsimAmodeusDatabase;
import amodeus.amodeus.routing.CachedNetworkTimeDistance;
import amodeus.amodeus.routing.EasyMinTimePathCalculator;
import amodeus.amodeus.routing.TimeDistanceProperty;
import amodeus.amodeus.util.math.GlobalAssert;
import amodeus.amodeus.util.matsim.SafeConfig;

/** Implementation of the ride sharing strategy used ind:
 * Fagnant, D. J., & Kockelman, K. M. (2015). Dynamic ride-sharing and optimal fleet sizing for a
 * system of shared autonomous vehicles (No. 15-1962).
 * 
 * The strategy goes through the Requests in the order of the submission time. For each request it
 * is first checked if a valid ride sharing possibility is present within a radius of 5 min (MAXWAITTIME).
 * If that is not the cse it is checked if another vehicle is available within 5 minutes which is currently
 * without designates requests. If this is also not possible then the vehicle is put on a wait list which
 * increases the search radius in the next dispatching step.
 * 
 * Ride Sharing is considered valid if 5 constraints are fulfilled:
 * 1. Current passengers’ trip duration increases ≤ 20% (total trip duration with ride-sharing vs. without ride-sharing);
 * 2. Current passengers’ remaining trip time increases ≤ 40%;
 * 3. New traveler’s total trip time increase grows by ≤ Max(20% total trip without ride-sharing, or 3 minutes);
 * 4. New travelers will be picked up at least within the next 5 minutes;
 * 5. Total planned trip time to serve all passengers ≤ remaining time to serve
 * the current trips + time to serve the new trip + drop-off time, if not pooled. */
public class DynamicRideSharingStrategy extends SharedRebalancingDispatcher {

    /** general Dispatcher Settings */
    private final int dispatchPeriod; // [s]

    /** unassigned Robo Taxis in the Scenario sorted by its coordinates in a Tree Structure */
    private final RoboTaxiHandler roboTaxiHandler;

    /** Normal: 300 [s], Time after which a request is put on to the wait list */
    private static final double WAITLISTTIME = 300.0;
    /** Normal is 600 [s] */
    private static final double MAXWAITTIME = 600.0;
    /** Unit: [s] The extreme wait list is used here as in AMoDeus requests should not be rejected.
     * This list guarantees for requests waiting for more than MaxWaitTime that a taxi can be found */
    private static final double EXTREEMWAITTIME = 3600.0 * 24;

    /** Maintains All the Information about the Requests. keeps track of Assignements, Pickups, ... */
    private final RequestHandler requestHandler = new RequestHandler(MAXWAITTIME, WAITLISTTIME, EXTREEMWAITTIME);

    /** Rebalancing Class to make use of a Grid Rebalancing. And its Parameters */
    private final BlockRebalancing rebalancing;
    private static final double BINSIZETRAVELDEMAND = 3600.0; // Assumption made for the Request records required for rebalancing
    private static final double REBALANCINGGRIDDISTANCE = 3218.69; // 2.0 miles in [m]
    private static final int MINNUMBERROBOTAXISINBLOCKTOREBALANCE = 5;

    /** Class which handles The Validation of routes. Afterwards The Constraints */
    private final RouteValidation routeValidation;
    private static final String MAXWAITTIMEID = "maxWaitTime";
    private static final String MAXDRIVETIMEINCREASEID = "maxDriveTimeIncrease";
    private static final String MAXREMAININGTIMEINCREASEID = "maxRemainingTimeIncrease";
    private static final String MAXABSOLUTETRAVELTIMEINCREASEID = "maxAbsolutDriveTimeIncrease";

    /** Travel Time Calculation */
    private final CachedNetworkTimeDistance timeDb;
    private static final double MAXLAGTRAVELTIMECALCULATION = 180000.0;

    protected DynamicRideSharingStrategy(Network network, //
            Config config, AmodeusModeConfig operatorConfig, //
            TravelTime travelTime, AmodeusRouter router, EventsManager eventsManager, //
            MatsimAmodeusDatabase db) {
        super(config, operatorConfig, travelTime, router, eventsManager, db);
        DispatcherConfigWrapper dispatcherConfig = DispatcherConfigWrapper.wrap(operatorConfig.getDispatcherConfig());
        dispatchPeriod = dispatcherConfig.getDispatchPeriod(300);

        SafeConfig safeConfig = SafeConfig.wrap(operatorConfig.getDispatcherConfig());
        double maxWaitTime = safeConfig.getInteger(MAXWAITTIMEID, 300); // normal is 300
        double maxDriveTimeIncrease = safeConfig.getDouble(MAXDRIVETIMEINCREASEID, 1.2); // normal is 1.2
        double maxRemainingTimeIncrease = safeConfig.getDouble(MAXREMAININGTIMEINCREASEID, 1.4); // normal is 1.4
        double newTravelTimeIncreaseAllowed = safeConfig.getInteger(MAXABSOLUTETRAVELTIMEINCREASEID, 180); // normal is 180 (=3min);

        roboTaxiHandler = new RoboTaxiHandler(network);

        FastAStarLandmarksFactory factory = new FastAStarLandmarksFactory(Runtime.getRuntime().availableProcessors());
        LeastCostPathCalculator calculator = EasyMinTimePathCalculator.prepPathCalculator(network, factory);
        timeDb = new CachedNetworkTimeDistance(calculator, MAXLAGTRAVELTIMECALCULATION, TimeDistanceProperty.INSTANCE);

        rebalancing = new BlockRebalancing(network, timeDb, MINNUMBERROBOTAXISINBLOCKTOREBALANCE, BINSIZETRAVELDEMAND, dispatchPeriod, REBALANCINGGRIDDISTANCE);

        routeValidation = new RouteValidation(maxWaitTime, maxDriveTimeIncrease, maxRemainingTimeIncrease, //
                dropoffDurationPerStop, pickupDurationPerStop, newTravelTimeIncreaseAllowed);
    }

    @Override
    protected void redispatch(double now) {
        final long round_now = Math.round(now);
        requestHandler.updatePickupTimes(getPassengerRequests(), now);

        if (round_now % dispatchPeriod == 0) {
            /** prepare the registers for the dispatching */
            roboTaxiHandler.update(getRoboTaxis(), getDivertableUnassignedRoboTaxis());
            requestHandler.addUnassignedRequests(getUnassignedPassengerRequests(), timeDb, now);
            requestHandler.updateLastHourRequests(now, BINSIZETRAVELDEMAND);

            /** calculate Rebalance before (!) dispatching */
            Set<Link> lastHourRequests = requestHandler.getRequestLinksLastHour();
            RebalancingDirectives rebalanceDirectives = rebalancing.getRebalancingDirectives(round_now, lastHourRequests, requestHandler.getCopyOfUnassignedPassengerRequests(),
                    roboTaxiHandler.getUnassignedRoboTaxis());

            /** for all AV Requests in the order of their submision, try to find the closest
             * vehicle and assign */
            for (PassengerRequest avRequest : requestHandler.getInOrderOffSubmissionTime()) {
                Set<RoboTaxi> robotaxisWithMenu = getRoboTaxis().stream()//
                        .filter(StaticHelper::plansPickupsOrDropoffs)//
                        .collect(Collectors.toSet());

                /** THIS IS WHERE WE CALCULATE THE SHARING POSSIBILITIES */
                Optional<Entry<RoboTaxi, List<SharedCourse>>> rideSharingRoboTaxi = routeValidation.getClosestValidSharingRoboTaxi(robotaxisWithMenu, avRequest, now, timeDb, //
                        requestHandler, roboTaxiHandler);

                if (rideSharingRoboTaxi.isPresent()) {
                    /** in Case we have a sharing possibility we assign */
                    RoboTaxi roboTaxi = rideSharingRoboTaxi.get().getKey();
                    GlobalAssert.that(routeValidation.menuFulfillsConstraints(roboTaxi, rideSharingRoboTaxi.get().getValue(), avRequest, now, timeDb, requestHandler));
                    addSharedRoboTaxiPickup(roboTaxi, avRequest);
                    requestHandler.removeFromUnasignedRequests(avRequest);
                    rebalanceDirectives.removefromDirectives(roboTaxi);
                    roboTaxi.updateMenu(rideSharingRoboTaxi.get().getValue());

                } else {
                    /** in Case No sharing possibility is present, try to find a close enough vehicle */
                    Optional<RoboTaxi> emptyRoboTaxi = RoboTaxiUtilsFagnant.getClosestUnassignedRoboTaxiWithinMaxTime(roboTaxiHandler, avRequest,
                            requestHandler.calculateWaitTime(avRequest), now, timeDb);
                    if (emptyRoboTaxi.isPresent()) {
                        /** In case we have a close vehicle which is free lets assign it */
                        addSharedRoboTaxiPickup(emptyRoboTaxi.get(), avRequest); // give directive
                        roboTaxiHandler.assign(emptyRoboTaxi.get()); // the assigned RoboTaxi is not unassigned anymore
                        rebalanceDirectives.removefromDirectives(emptyRoboTaxi.get()); // this taxi can not be rebalanced anymore
                        requestHandler.removeFromUnasignedRequests(avRequest); // the request is not unassigned anymore
                    } else {
                        /** Assignement was not possible as no Taxi was able to fulfill the constraints -> wait list! */
                        if (!requestHandler.isOnWaitList(avRequest))
                            requestHandler.addToWaitList(avRequest);
                        else // and if it was already on the wait list put it to the extreme wait list
                            requestHandler.addToExtreemWaitList(avRequest);
                    }
                }

            }

            /** execute New Rebalance Directives */
            for (Entry<RoboTaxi, Link> entry : rebalanceDirectives.getDirectives().entrySet()) {
                roboTaxiHandler.assign(entry.getKey());
                setRoboTaxiRebalance(entry.getKey(), entry.getValue());
            }

            /** For all robotaxis which were on rebalance and did not receive a new directive
             * stop on current link */
            getRebalancingRoboTaxis().stream() //
                    .filter(rt -> !rebalanceDirectives.getDirectives().containsKey(rt)) //
                    .forEach(rt -> setRoboTaxiRebalance(rt, rt.getDivertableLocation()));
            roboTaxiHandler.clear();
        }
    }

    public static class Factory implements AVDispatcherFactory {
        @Override
        public AmodeusDispatcher createDispatcher(InstanceGetter inject) {
            Config config = inject.get(Config.class);
            MatsimAmodeusDatabase db = inject.get(MatsimAmodeusDatabase.class);
            EventsManager eventsManager = inject.get(EventsManager.class);

            AmodeusModeConfig operatorConfig = inject.getModal(AmodeusModeConfig.class);
            Network network = inject.getModal(Network.class);
            AmodeusRouter router = inject.getModal(AmodeusRouter.class);
            TravelTime travelTime = inject.getModal(TravelTime.class);

            return new DynamicRideSharingStrategy(network, config, operatorConfig, travelTime, router, eventsManager, db);
        }
    }
}