package org.opentripplanner.standalone;

import static com.google.common.base.Preconditions.checkNotNull;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map.Entry;

import org.onebusaway.gtfs.model.AgencyAndId;
import org.onebusaway.gtfs.model.Route;
import org.onebusaway.gtfs.model.Stop;
import org.onebusaway.gtfs.model.Trip;

/**
 * This class represents all transfer information in the graph. Transfers are grouped by
 * stop-to-stop pairs. Each transfer may consist of multiple specific transfers. See
 * https://developers.google.com/transit/gtfs/reference#transfers_fields and
 * https://support.google.com/transitpartners/answer/2450962 (heading Route-to-route and
 * trip-to-trip transfers) for more details about the specifications.
 * 
 * @see StopTransfer, SpecificTransfer
 */
public class TransferTable implements Serializable {
    
    private static final long serialVersionUID = 9160765220742241406L;

    /**
     * Table which contains transfers between two stops
     */
    protected HashMap<P2<AgencyAndId>, StopTransfer> table = new HashMap<P2<AgencyAndId>, StopTransfer>();

    /**
     * Preferred transfers (or timed transfers, which are preferred as well) are present if true
     */
    protected boolean preferredTransfers = false;

    public boolean hasPreferredTransfers() {
        return this.preferredTransfers;
    }

    /**
     * Get the transfer time that should be used when transferring from a trip to another trip. Note
     * that this function does not check whether another specific transfer exists with the same
     * specificity, what is forbidden by the specifications.
     * 
     * @param fromStop is the arriving stop
     * @param toStop is the departing stop
     * @param fromTrip is the arriving trip
     * @param toTrip is the departing trip
     * @param forwardInTime is true when moving forward in time; false when moving backwards in time
     *        (usually this will be the variable "boarding")
     * @return the transfer time in seconds. May contain special (negative) values which meaning can
     *         be found in the StopTransfer.*_TRANSFER constants. If no transfer is found,
     *         StopTransfer.UNKNOWN_TRANSFER is returned.
     */
    public int getTransferTime(Stop fromStop, Stop toStop, Trip fromTrip, Trip toTrip, boolean forwardInTime) {
        checkNotNull(fromStop);
        checkNotNull(toStop);

        // Reverse from and to if we are moving backwards in time
        if (!forwardInTime) {
            Stop tempStop = fromStop;
            fromStop = toStop;
            toStop = tempStop;
            Trip tempTrip = fromTrip;
            fromTrip = toTrip;
            toTrip = tempTrip;
        }

        // Get transfer time between the two stops
        int transferTime = getTransferTime(fromStop.getId(), toStop.getId(), fromTrip, toTrip);

        // Check parents of stops if no transfer was found
        if (transferTime == StopTransfer.UNKNOWN_TRANSFER) {
            // Find parent ids
            AgencyAndId fromStopParentId = null;
            AgencyAndId toStopParentId = null;
            if ((fromStop.getParentStation() != null) && !fromStop.getParentStation().isEmpty()) {
                // From stop has a parent
                fromStopParentId = new AgencyAndId(fromStop.getId().getAgencyId(), fromStop.getParentStation());
            }
            if ((toStop.getParentStation() != null) && !toStop.getParentStation().isEmpty()) {
                // To stop has a parent
                toStopParentId = new AgencyAndId(toStop.getId().getAgencyId(), toStop.getParentStation());
            }

            // Check parent of from stop if no transfer was found
            if (fromStopParentId != null) {
                transferTime = getTransferTime(fromStopParentId, toStop.getId(), fromTrip, toTrip);
            }
            
            // Check parent of to stop if still no transfer was found
            if ((transferTime == StopTransfer.UNKNOWN_TRANSFER) && (toStopParentId != null)) {
                transferTime = getTransferTime(fromStop.getId(), toStopParentId, fromTrip, toTrip);
            }
            
            // Check parents of both stops if still no transfer was found
            if ((transferTime == StopTransfer.UNKNOWN_TRANSFER) && (fromStopParentId != null) && (toStopParentId != null)) {
                transferTime = getTransferTime(fromStopParentId, toStopParentId, fromTrip, toTrip);
            }
        }

        return transferTime;
    }

    /**
     * Get the transfer time that should be used when transferring from a trip to another trip. Note
     * that this function does not check whether another specific transfer exists with the same
     * specificity, what is forbidden by the specifications.
     * 
     * @param fromStopId is the id of the arriving stop
     * @param toStopId is the id of the departing stop
     * @param fromTrip is the arriving trip
     * @param toTrip is the departing trip
     * @return the transfer time in seconds. May contain special (negative) values which meaning can
     *         be found in the StopTransfer.*_TRANSFER constants. If no transfer is found,
     *         StopTransfer.UNKNOWN_TRANSFER is returned.
     */
    private int getTransferTime(AgencyAndId fromStopId, AgencyAndId toStopId, Trip fromTrip, Trip toTrip) {
        checkNotNull(fromStopId);
        checkNotNull(toStopId);

        // Define transfer time to return
        int transferTime = StopTransfer.UNKNOWN_TRANSFER;
        // Lookup transfer between two stops
        StopTransfer stopTransfer = this.table.get(new P2<AgencyAndId>(fromStopId, toStopId));
        if (stopTransfer != null) {
            // Lookup correct transfer time between two stops and two trips
            transferTime = stopTransfer.getTransferTime(fromTrip, toTrip);
        }
        return transferTime;
    }

    /**
     * Add a transfer time to the transfer table.
     * 
     * @param fromStop is the arriving stop
     * @param toStop is the departing stop
     * @param fromRoute is the arriving route; is allowed to be null
     * @param toRoute is the departing route; is allowed to be null
     * @param fromTrip is the arriving trip; is allowed to be null
     * @param toTrip is the departing trip; is allowed to be null
     * @param transferTime is the transfer time in seconds. May contain special (negative) values
     *        which meaning can be found in the StopTransfer.*_TRANSFER constants. If no transfer is
     *        found, StopTransfer.UNKNOWN_TRANSFER is returned.
     */
    public void addTransferTime(Stop fromStop, Stop toStop, Route fromRoute, Route toRoute, Trip fromTrip, Trip toTrip,
            int transferTime) {
        checkNotNull(fromStop);
        checkNotNull(toStop);
        
        // Check whether this transfer is preferred (or timed)
        if ((transferTime == StopTransfer.PREFERRED_TRANSFER) || (transferTime == StopTransfer.TIMED_TRANSFER)) {
            this.preferredTransfers = true;
        }

        // Lookup whether a transfer between the two stops already exists
        P2<AgencyAndId> stopIdPair = new P2<AgencyAndId>(fromStop.getId(), toStop.getId());
        StopTransfer stopTransfer = this.table.get(stopIdPair);
        if (stopTransfer == null) {
            // If not, create one and add to table
            stopTransfer = new StopTransfer();
            this.table.put(stopIdPair, stopTransfer);
        }
        assert (stopTransfer != null);

        // Create and add a specific transfer to the stop transfer
        SpecificTransfer specificTransfer = new SpecificTransfer(fromRoute, toRoute, fromTrip, toTrip, transferTime);
        stopTransfer.addSpecificTransfer(specificTransfer);
    }

    /**
     * Determines the transfer penalty given a transfer time and a penalty for non-preferred
     * transfers.
     * 
     * @param transferTime is the transfer time
     * @param nonpreferredTransferPenalty is the penalty for non-preferred transfers
     * @return the transfer penalty
     */
    public int determineTransferPenalty(int transferTime, int nonpreferredTransferPenalty) {
        int transferPenalty = 0;

        if (hasPreferredTransfers()) {
            // Only penalize transfers if there are some that will be depenalized
            transferPenalty = nonpreferredTransferPenalty;
            
            if ((transferTime == StopTransfer.PREFERRED_TRANSFER) || (transferTime == StopTransfer.TIMED_TRANSFER)) {
                // Depenalize preferred transfers
                // Timed transfers are assumed to be preferred as well
                // TODO: verify correctness of this method (AMB)
                transferPenalty = 0;
            }
        }

        return transferPenalty;
    }

    /**
     * Internal class for testing purposes only.
     * 
     * @see TransferGraphLinker
     */
    @Deprecated
    public static class Transfer {
        public AgencyAndId fromStopId, toStopId;
        public int seconds;
        
        public Transfer(AgencyAndId fromStopId, AgencyAndId toStopId, int seconds) {
            this.fromStopId = fromStopId;
            this.toStopId = toStopId;
            this.seconds = seconds;
        }
    }

    /**
     * Public function for testing purposes only. Returns only the first specific transfers.
     * 
     * @see TransferGraphLinker
     */
    @Deprecated
    public Iterable<Transfer> getAllFirstSpecificTransfers() {
        ArrayList<Transfer> transfers = new ArrayList<Transfer>(this.table.size());
        for (Entry<P2<AgencyAndId>, StopTransfer> entry : this.table.entrySet()) {
            P2<AgencyAndId> p2 = entry.getKey();
            int transferTime = entry.getValue().getFirstSpecificTransferTime();
            if (transferTime != StopTransfer.UNKNOWN_TRANSFER) {
                transfers.add(new Transfer(p2.getFirst(), p2.getSecond(), transferTime));
            }
        }
        return transfers;
    }
}