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

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
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.TravelTime;
import org.matsim.core.utils.collections.QuadTree;

import com.google.inject.TypeLiteral;

import amodeus.amodeus.dispatcher.core.DispatcherConfigWrapper;
import amodeus.amodeus.dispatcher.core.RoboTaxi;
import amodeus.amodeus.dispatcher.core.SharedPartitionedDispatcher;
import amodeus.amodeus.dispatcher.shared.OnMenuRequests;
import amodeus.amodeus.dispatcher.shared.SharedMenu;
import amodeus.amodeus.dispatcher.util.DistanceHeuristics;
import amodeus.amodeus.net.MatsimAmodeusDatabase;
import amodeus.amodeus.routing.CachedNetworkTimeDistance;
import amodeus.amodeus.routing.EasyMinDistPathCalculator;
import amodeus.amodeus.routing.EasyMinTimePathCalculator;
import amodeus.amodeus.routing.TimeDistanceProperty;
import amodeus.amodeus.util.geo.FastQuadTree;
import amodeus.amodeus.util.math.SI;
import amodeus.amodeus.util.matsim.SafeConfig;
import amodeus.amodeus.virtualnetwork.core.VirtualNetwork;
import amodeus.amodeus.virtualnetwork.core.VirtualNode;
import ch.ethz.idsc.tensor.Scalar;
import ch.ethz.idsc.tensor.Scalars;
import ch.ethz.idsc.tensor.Tensor;
import ch.ethz.idsc.tensor.Tensors;
import ch.ethz.idsc.tensor.qty.Quantity;

/** Ma, Shuo, Yu Zheng, and Ouri Wolfson. "T-share: A large-scale dynamic taxi ridesharing service."
 * Data Engineering (ICDE), 2013 IEEE 29th International Conference on. IEEE, 2013.
 * 
 * Changes compared to the original version:
 * - The version presented in the publication considers only the addition of 1 trip to a trip which is
 * already being transported by a taxi. In order to operate the policy with taxis with capacity N, in this
 * version the time windows of all requests already in a taxi are checked before the insertion of a
 * new request is allowed.
 * - To limit computation time, a maximum length of the planned {@link SharedMenu} was introduced. */
public class TShareDispatcher extends SharedPartitionedDispatcher {

    /** general */
    private final int dispatchPeriod;
    private final TShareBipartiteMatchingUtils bipartiteMatchingUtils;
    private Tensor printInfo = Tensors.empty();

    /** T-Share specific */
    private final Map<VirtualNode<Link>, GridCell> gridCells = new HashMap<>();
    private final Scalar pickupDelayMax;
    private final Scalar drpoffDelayMax;
    private final DualSideSearch dualSideSearch;
    private final CachedNetworkTimeDistance distanceCashed;
    private final CachedNetworkTimeDistance travelTimeCalculator;

    protected TShareDispatcher(Network network, Config config, AmodeusModeConfig operatorConfig, //
            TravelTime travelTime, AmodeusRouter router, EventsManager eventsManager, //
            MatsimAmodeusDatabase db, VirtualNetwork<Link> virtualNetwork) {
        super(config, operatorConfig, travelTime, router, eventsManager, virtualNetwork, db);
        DispatcherConfigWrapper dispatcherConfig = DispatcherConfigWrapper.wrap(operatorConfig.getDispatcherConfig());
        dispatchPeriod = dispatcherConfig.getDispatchPeriod(30);
        DistanceHeuristics distanceHeuristics = dispatcherConfig.getDistanceHeuristics(DistanceHeuristics.EUCLIDEAN);
        System.out.println("Using DistanceHeuristics: " + distanceHeuristics.name());
        distanceCashed = new CachedNetworkTimeDistance(
                EasyMinDistPathCalculator.prepPathCalculator(network, new FastAStarLandmarksFactory(Runtime.getRuntime().availableProcessors())), //
                180000.0, TimeDistanceProperty.INSTANCE);
        travelTimeCalculator = new CachedNetworkTimeDistance(
                EasyMinTimePathCalculator.prepPathCalculator(network, new FastAStarLandmarksFactory(Runtime.getRuntime().availableProcessors())), //
                180000.0, TimeDistanceProperty.INSTANCE);
        bipartiteMatchingUtils = new TShareBipartiteMatchingUtils();

        /** T-Share specific */
        SafeConfig safeConfig = SafeConfig.wrap(operatorConfig.getDispatcherConfig());
        pickupDelayMax = Quantity.of(safeConfig.getInteger("pickupDelayMax", 10 * 60), SI.SECOND);
        drpoffDelayMax = Quantity.of(safeConfig.getInteger("drpoffDelayMax", 30 * 60), SI.SECOND);

        /** initialize grid with T-cells */
        QuadTree<Link> linkTree = FastQuadTree.of(network);
        for (VirtualNode<Link> virtualNode : virtualNetwork.getVirtualNodes()) {
            System.out.println("preparing grid cell: " + virtualNode.getIndex());
            gridCells.put(virtualNode, new GridCell(virtualNode, virtualNetwork, distanceCashed, travelTimeCalculator, linkTree));
        }
        dualSideSearch = new DualSideSearch(gridCells, virtualNetwork);
        System.out.println("According to the reference, a rectangular {@link VirtualNetwork} should be used.");
        System.out.println("Ensure that VirtualNetworkCreators.RECTANGULAR is used.");
    }

    @Override
    protected void redispatch(double now) {
        final long round_now = Math.round(now);
        if (round_now % dispatchPeriod == 0) {

            /** STEP 1: search for options to to T-Share strategy ride sharing */
            doTShareRidesharing(now);

            /** STEP 2: unit capacity dispatching,in this implementation, global bipartite matching is used.
             * The following sets are used:
             * - vehicles: all divertable vehicles with zero passengers on board,
             * - requests: all requests not assigned to any vehicle */
            Collection<RoboTaxi> divertableAndEmpty = getDivertableRoboTaxis().stream()//
                    .filter(rt -> (rt.getUnmodifiableViewOfCourses().size() == 0)) //
                    .collect(Collectors.toList());
            printInfo = bipartiteMatchingUtils.executePickup(this, this::getCurrentPickupTaxi, divertableAndEmpty, //
                    getUnassignedPassengerRequests(), distanceCashed, now);
        }
    }

    private void doTShareRidesharing(double now) {
        /** update the roboTaxi planned locations */
        Collection<RoboTaxi> occupiedNotFull = getDivertableRoboTaxis().stream() //
                .filter(rt -> rt.getOnBoardPassengers() >= 1) // at least 1 passenger on board
                .filter(OnMenuRequests::canPickupAdditionalCustomer) // still capacity left
                .collect(Collectors.toList());

        Map<VirtualNode<Link>, Set<RoboTaxi>> plannedLocs = //
                RoboTaxiPlannedLocations.of(occupiedNotFull, virtualNetwork);

        /** do T-share ridesharing */
        List<PassengerRequest> sortedRequests = getPassengerRequests().stream() //
                .filter(avr -> !getCurrentPickupAssignements().keySet().contains(avr)) // requests are not scheduled to be picked up
                .sorted(RequestWaitTimeComparator.INSTANCE) // sort such that earliest submission is first
                .collect(Collectors.toList());

        for (PassengerRequest avr : sortedRequests) {

            // compute times left until pickup or dropoff window closed
            Scalar timeLeftForPickup = LatestPickup.timeTo(avr, pickupDelayMax, now);
            Scalar timeLeftUntilArrival = LatestArrival.timeTo(avr, drpoffDelayMax, travelTimeCalculator, now);

            /** if still available time, find ridesharing opportunity */
            if (Scalars.lessThan(Quantity.of(0, SI.SECOND), timeLeftForPickup) && //
                    Scalars.lessThan(Quantity.of(0, SI.SECOND), timeLeftForPickup)) {

                /** dual side search */
                Collection<RoboTaxi> potentialTaxis = dualSideSearch.apply(avr, plannedLocs, timeLeftForPickup, timeLeftUntilArrival);

                /** insertion feasibility check, compute possible insertions into schedules
                 * of all {@link RoboTaxi}s, find the insertion with smallest additional distance */
                NavigableMap<Scalar, InsertionChecker> insertions = new TreeMap<>();
                for (RoboTaxi taxi : potentialTaxis) {
                    InsertionChecker checker = //
                            new InsertionChecker(distanceCashed, travelTimeCalculator, taxi, avr, //
                                    pickupDelayMax, drpoffDelayMax, now);
                    if (Objects.nonNull(checker.getAddDistance()))
                        insertions.put(checker.getAddDistance(), checker);
                }

                /** plan update: insert the request into the plan of the {@link RoboTaxi} */
                if (Objects.nonNull(insertions.firstEntry()))
                    insertions.firstEntry().getValue().executeBest(this::addSharedRoboTaxiPickup);
            }
        }
    }

    @Override
    protected String getInfoLine() {
        return String.format("%s H=%s", //
                super.getInfoLine(), //
                printInfo.toString() /** This is where Dispatcher@ V... R... MR.. H is printed on console */
        );
    }

    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);

            VirtualNetwork<Link> virtualNetwork = inject.getModal(new TypeLiteral<VirtualNetwork<Link>>() {
            });

            return new TShareDispatcher(network, config, operatorConfig, travelTime, router, eventsManager, //
                    db, virtualNetwork);
        }
    }
}