package ch.epfl.isochrone.timetable;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * Classe servant à modélise un arc de graphe.
 * 
 * @author Charles Thiebaut (238935)
 * @author Arnaud Delamare (234841)
 * 
 */
public final class GraphEdge {

    private final Stop destination;
    private final int walkingTime;
    private final Set<Integer> packedTrips;

    /**
     * encode en entier un trajet donné par son heure de départ et son heure
     * d'arrivée
     * 
     * @throw IllegalArgumentException si l'heure de dépard ou d'arrivée est
     *        négative ou si elle est supérieure à la limite fixe.
     * @param departureTime
     *            heure de dépard.
     * @param arrivalTime
     *            heure d'arrivée.
     * @return la date encodé en entier.
     */
    public static int packTrip(int departureTime, int arrivalTime) {
        if (departureTime < 0 || departureTime > 107999
                || arrivalTime - departureTime < 0
                || arrivalTime - departureTime > 9999) {
            throw new IllegalArgumentException();
        }

        return departureTime * 10000 + (arrivalTime - departureTime);
    }

    /**
     * extrait l'heure de départ d'un trajet encodé
     * 
     * @param packedTrip
     *            l'entier du trajet encodé duquel on veut extraire l'heure de
     *            dépard.
     * @return l'heure de dépard extraite.
     */
    public static int unpackTripDepartureTime(int packedTrip) {
        return packedTrip / 10000;
    }

    /**
     * extrait la durée du trajet encodé (en secondes).
     * 
     * @param packedTrip
     *            l'entier du trajet encodé duquel on veut extraire la durée du
     *            trajet.
     * @return la durée du trajet extraite du trajet encodée (en seconde).
     * 
     */
    public static int unpackTripDuration(int packedTrip) {
        return packedTrip % 10000;
    }

    /**
     * extrait l'heure d'arrivée du trajet encodé passé en argument.
     * 
     * @param packedTrip
     *            l'entier du trajet encodé duquel on veut extraire l'heure
     *            d'arrivée.
     * @return l'heure d'arrivée à partir du trajet encodé passé en argument.
     */
    public static int unpackTripArrivalTime(int packedTrip) {
        return unpackTripDepartureTime(packedTrip)
                + unpackTripDuration(packedTrip);
    }

    /**
     * @param destination
     *            l'arrêt (stop) de destination.
     * @param walkingTime
     *            le temps de marche en secondes, vaut -1 si le trajet est
     *            impossible ou trop long a effectue à pied.
     * @param packedTrips
     *            les trajets disponibles.
     * 
     * @throw IllegalArgumentException si le temps de marche est inférieure à
     *        -1.
     */
    public GraphEdge(Stop destination, int walkingTime, Set<Integer> packedTrips) {
        if (walkingTime < -1) {
            throw new IllegalArgumentException();
        }

        this.destination = destination;
        this.walkingTime = walkingTime;
        this.packedTrips = new HashSet<Integer>(packedTrips);
    }

    /**
     * @return l'arrêt de la destination.
     */
    public Stop destination() {
        return destination;
    }

    /**
     * @param departureTime
     *            l'heure à laquelle on souhaite vérifier la disponibilité des
     *            transport.
     * 
     * @return la première heure d'arrivee possible à la destination voulue
     *         étant donnée une heure de départ.
     */
    public int earliestArrivalTime(int departureTime) {
        int walkingArrivalTime;

        if (walkingTime == -1) {
            walkingArrivalTime = SecondsPastMidnight.INFINITE;
        } else {
            walkingArrivalTime = departureTime + walkingTime;
        }

        int minTransportTime = SecondsPastMidnight.INFINITE;

        Integer[] tabDeparture = new Integer[packedTrips.size()];
        Integer[] tabArrival = new Integer[packedTrips.size()];

        Iterator<Integer> iteratorPackedTrips = packedTrips.iterator();
        Integer i;

        int j = 0;
        while (iteratorPackedTrips.hasNext()) {
            i = iteratorPackedTrips.next();
            tabDeparture[j] = unpackTripDepartureTime(i);
            tabArrival[j] = unpackTripArrivalTime(i);
            j++;
        }

        java.util.Arrays.sort(tabDeparture);
        java.util.Arrays.sort(tabArrival);
        int index = java.util.Arrays.binarySearch(tabDeparture, departureTime);

        if (index >= 0) {
            minTransportTime = tabArrival[index];
        } else {
            if (-index - 1 >= tabArrival.length) {
                minTransportTime = SecondsPastMidnight.INFINITE;
            } else {
                minTransportTime = tabArrival[-index - 1];
            }
        }

        return java.lang.Math.min(walkingArrivalTime, minTransportTime);

    }

    /**
     * class qui sert de Builder pour un GraphEdge
     */
    public static class Builder {
        private final Stop destination;
        private int walkingTime;
        private Set<Integer> packedTrips;

        /**
         * builder pour un arc ayant l'arrêt donné comme destination, aucun
         * trajet et un temps de marche de -1 (infiniment grand).
         * 
         * @param destination
         *            l'arrêt de destination.
         */
        public Builder(Stop destination) {
            this.destination = destination;
            this.walkingTime = -1;
            this.packedTrips = new HashSet<Integer>();
        }

        /**
         * méthode qui change le temps de marche la remplaçant par la valeur
         * passé en argument.
         * 
         * @param newWalkingTime
         *            le nouveau temps de marche
         * @throw IllegalArgumentException si le temps de marche n'est pas
         *        valide (<-1)
         * @return le builder modifié.
         */
        public Builder setWalkingTime(int newWalkingTime) {
            if (newWalkingTime < -1) {
                throw new IllegalArgumentException();
            }
            this.walkingTime = newWalkingTime;
            return this;
        }

        /**
         * méthode qui un trajet avec une heure de départ et d'arrivée passé en
         * argument et tout deux en secondes depuis minuit.
         * 
         * @param departureTime
         *            l'heure de départ du nouveau trajet
         * @param arrivalTime
         *            l'heure d'arrivée du nouveau trajet.
         * @throw IllegalArgumentException si l'heure d'arrivée est antérieure à
         *        l'heure de départ.
         * @return le builder modifié.
         */
        public Builder addTrip(int departureTime, int arrivalTime) {
            if (departureTime < 0 || departureTime > 107999
                    || arrivalTime - departureTime < 0
                    || arrivalTime - departureTime > 9999) {
                throw new IllegalArgumentException();
            }

            packedTrips.add(packTrip(departureTime, arrivalTime));

            return this;
        }

        /**
         * @return le GraphEdge construit via le builder.
         */
        public GraphEdge build() {
            return new GraphEdge(destination, walkingTime, packedTrips);
        }

    }
}
