package org.opentripplanner.standalone;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;

import org.onebusaway.gtfs.model.AgencyAndId;
import org.onebusaway.gtfs.model.Route;
import org.onebusaway.gtfs.model.Trip;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Objects;

/**
 * A trip planning request. Some parameters may not be honored by the trip planner for some or all
 * itineraries. For example, maxWalkDistance may be relaxed if the alternative is to not provide a
 * route. NOTE this is the result of merging what used to be called a REQUEST and a TRAVERSEOPTIONS.
 * The Lombok Getter/Setter annotations serve to create getter and setter methods on all fields, so
 * defaults can be supplied in a PrototypeRoutingRequest bean via Spring.
 */
public class RoutingRequest implements Cloneable, Serializable {
    
    private static final long serialVersionUID = MavenVersion.VERSION.getUID();
    
    private static final Logger LOG = LoggerFactory.getLogger(RoutingRequest.class);
    
    private static final int CLAMP_ITINERARIES = 3;
    
    /**
     * The model that computes turn/traversal costs. TODO(flamholz): this is a weird place to inject
     * this model. We do it here because, for historical reasons, this is the most reasonable place
     * to inject it.
     */
    public IntersectionTraversalCostModel traversalCostModel = new SimpleIntersectionTraversalCostModel();
    
    /* FIELDS UNIQUELY IDENTIFYING AN SPT REQUEST */
    
    /* TODO no defaults should be set here, they should all be handled in one place (searchresource) */
    /** The complete list of incoming query parameters. */
    public final HashMap<String, String> parameters = new HashMap<String, String>();
    
    /** The router ID -- internal ID to switch between router implementation (or graphs) */
    public String routerId = "";
    
    /** The start location */
    public GenericLocation from;
    
    /** The end location */
    public GenericLocation to;
    
    /** An unordered list of intermediate locations to be visited. */
    public List<GenericLocation> intermediatePlaces;
    
    public boolean intermediatePlacesOrdered;
    
    /** The maximum distance (in meters) the user is willing to walk. Defaults to unlimited. */
    public double maxWalkDistance = Double.MAX_VALUE;
    
    /**
     * The maximum time (in seconds) of pre-transit travel when using drive-to-transit (park and
     * ride or kiss and ride). Defaults to unlimited.
     */
    public int maxPreTransitTime = Integer.MAX_VALUE;
    
    /** The worst possible time (latest for depart-by and earliest for arrive-by) to accept */
    public long worstTime = Long.MAX_VALUE;
    
    /** The worst possible weight that we will accept when planning a trip. */
    public double maxWeight = Double.MAX_VALUE;
    
    /** The set of TraverseModes that a user is willing to use. Defaults to WALK | TRANSIT. */
    public TraverseModeSet modes = new TraverseModeSet("TRANSIT,WALK"); // defaults in constructor
    
    /**
     * The set of characteristics that the user wants to optimize for -- defaults to QUICK, or
     * optimize for transit time.
     */
    public OptimizeType optimize = OptimizeType.QUICK;
    
    /**
     * The epoch date/time that the trip should depart (or arrive, for requests where arriveBy is
     * true)
     */
    public long dateTime = new Date().getTime() / 1000;
    
    /** Whether the trip should depart at dateTime (false, the default), or arrive at dateTime. */
    public boolean arriveBy = false;
    
    /** Whether the trip must be wheelchair accessible. */
    public boolean wheelchairAccessible = false;
    
    /** The maximum number of possible itineraries to return. */
    public int numItineraries = 3;
    
    /** The maximum slope of streets for wheelchair trips. */
    public double maxSlope = 0.0833333333333; // ADA max wheelchair ramp slope is a good default.
    
    /** Whether the planner should return intermediate stops lists for transit legs. */
    public boolean showIntermediateStops = false;
    
    /** max walk/bike speed along streets, in meters per second */
    public double walkSpeed;
    
    public double bikeSpeed;
    
    public double carSpeed;
    
    public Locale locale = new Locale("en", "US");
    
    /**
     * An extra penalty added on transfers (i.e. all boardings except the first one). Not to be
     * confused with bikeBoardCost and walkBoardCost, which are the cost of boarding a vehicle with
     * and without a bicycle. The boardCosts are used to model the 'usual' perceived cost of using a
     * transit vehicle, and the transferPenalty is used when a user requests even less transfers. In
     * the latter case, we don't actually optimize for fewest transfers, as this can lead to absurd
     * results. Consider a trip in New York from Grand Army Plaza (the one in Brooklyn) to
     * Kalustyan's at noon. The true lowest transfers route is to wait until midnight, when the 4
     * train runs local the whole way. The actual fastest route is the 2/3 to the 4/5 at Nevins to
     * the 6 at Union Square, which takes half an hour. Even someone optimizing for fewest transfers
     * doesn't want to wait until midnight. Maybe they would be willing to walk to 7th Ave and take
     * the Q to Union Square, then transfer to the 6. If this takes less than
     * optimize_transfer_penalty seconds, then that's what we'll return.
     */
    public int transferPenalty = 0;
    
    /**
     * A multiplier for how bad walking is, compared to being in transit for equal lengths of time.
     * Defaults to 2. Empirically, values between 10 and 20 seem to correspond well to the concept
     * of not wanting to walk too much without asking for totally ridiculous itineraries, but this
     * observation should in no way be taken as scientific or definitive. Your mileage may vary.
     */
    public double walkReluctance = 2.0;
    
    /** Used instead of walk reluctance for stairs */
    public double stairsReluctance = 2.0;

    /** Multiplicative factor on expected turning time. */
    public double turnReluctance = 1.0;
    
    /**
     * How long does it take to get an elevator, on average (actually, it probably should be a bit
     * *more* than average, to prevent optimistic trips)? Setting it to "seems like forever," while
     * accurate, will probably prevent OTP from working correctly.
     */
    // TODO: how long does it /really/ take to get an elevator?
    public int elevatorBoardTime = 90;
    
    /** What is the cost of boarding an elevator? */
    public int elevatorBoardCost = 90;
    
    /** How long does it take to advance one floor on an elevator? */
    public int elevatorHopTime = 20;
    
    /** What is the cost of travelling one floor on an elevator? */
    public int elevatorHopCost = 20;
    
    // it is assumed that getting off an elevator is completely free
    
    /** Time to get on and off your own bike */
    public int bikeSwitchTime;
    
    /** Cost of getting on and off your own bike */
    public int bikeSwitchCost;
    
    /** Time to rent a bike */
    public int bikeRentalPickupTime = 60;
    
    /**
     * Cost of renting a bike. The cost is a bit more than actual time to model the associated cost
     * and trouble.
     */
    public int bikeRentalPickupCost = 120;
    
    /** Time to drop-off a rented bike */
    public int bikeRentalDropoffTime = 30;
    
    /** Cost of dropping-off a rented bike */
    public int bikeRentalDropoffCost = 30;
    
    /**
     * Time to park a car in a park and ride, w/o taking into account driving and walking cost (time
     * to park, switch off, pick your stuff, lock the car, etc...)
     */
    public int carDropoffTime = 120;
    
    /**
     * How much worse is waiting for a transit vehicle than being on a transit vehicle, as a
     * multiplier. The default value treats wait and on-vehicle time as the same. It may be tempting
     * to set this higher than walkReluctance (as studies often find this kind of preferences among
     * riders) but the planner will take this literally and walk down a transit line to avoid
     * waiting at a stop. This used to be set less than 1 (0.95) which would make waiting offboard
     * preferable to waiting onboard in an interlined trip. That is also undesirable. If we only
     * tried the shortest possible transfer at each stop to neighboring stop patterns, this problem
     * could disappear.
     */
    public double waitReluctance = 1.0;
    
    /** How much less bad is waiting at the beginning of the trip (replaces waitReluctance) */
    public double waitAtBeginningFactor = 0.2;
    
    /** This prevents unnecessary transfers by adding a cost for boarding a vehicle. */
    public int walkBoardCost = 60 * 10;
    
    /** Separate cost for boarding a vehicle with a bicycle, which is more difficult than on foot. */
    public int bikeBoardCost = 60 * 10;
    
    /** Do not use certain named routes */
    public RouteMatcher bannedRoutes = RouteMatcher.emptyMatcher();
    
    /** Do not use certain named agencies */
    public HashSet<String> bannedAgencies = new HashSet<String>();
    
    /** Do not use certain trips */
    public HashMap<AgencyAndId, BannedStopSet> bannedTrips = new HashMap<AgencyAndId, BannedStopSet>();
    
    /**
     * Do not use certain stops. See for more information the bannedStops property in the
     * RoutingResource class.
     */
    public StopMatcher bannedStops = StopMatcher.emptyMatcher();
    
    /**
     * Do not use certain stops. See for more information the bannedStopsHard property in the
     * RoutingResource class.
     */
    public StopMatcher bannedStopsHard = StopMatcher.emptyMatcher();
    
    /** Set of preferred routes by user. */
    public RouteMatcher preferredRoutes = RouteMatcher.emptyMatcher();

    /** Set of preferred agencies by user. */
    public HashSet<String> preferredAgencies = new HashSet<String>();

    /**
     * Penalty added for using every route that is not preferred if user set any route as preferred.
     * We return number of seconds that we are willing to wait for preferred route.
     */
    public int otherThanPreferredRoutesPenalty = 300;
    
    /** Set of unpreferred routes for given user. */
    public RouteMatcher unpreferredRoutes = RouteMatcher.emptyMatcher();

    /** Set of unpreferred agencies for given user. */
    public HashSet<String> unpreferredAgencies = new HashSet<String>();
    
    /**
     * Penalty added for using every unpreferred route. We return number of seconds that we are
     * willing to wait for preferred route.
     */
    public int useUnpreferredRoutesPenalty = 300;
    
    /**
     * A global minimum transfer time (in seconds) that specifies the minimum amount of time that
     * must pass between exiting one transit vehicle and boarding another. This time is in addition
     * to time it might take to walk between transit stops. This time should also be overridden by
     * specific transfer timing information in transfers.txt
     */
    // initialize to zero so this does not inadvertently affect tests, and let Planner handle
    // defaults
    public int transferSlack = 0;
    
    /** Invariant: boardSlack + alightSlack <= transferSlack. */
    public int boardSlack = 0;
    
    public int alightSlack = 0;
    
    public int maxTransfers = 2;
    
    /**
     * Extensions to the trip planner will require additional traversal options beyond the default
     * set. We provide an extension point for adding arbitrary parameters with an extension-specific
     * key.
     */
    public Map<Object, Object> extensions = new HashMap<Object, Object>();
    
    /** Penalty for using a non-preferred transfer */
    public int nonpreferredTransferPenalty = 180;
    
    /**
     * For the bike triangle, how important time is.
     * triangleTimeFactor+triangleSlopeFactor+triangleSafetyFactor == 1
     */
    public double triangleTimeFactor;
    
    /** For the bike triangle, how important slope is */
    public double triangleSlopeFactor;
    
    /** For the bike triangle, how important safety is */
    public double triangleSafetyFactor;
    
    /** Options specifically for the case that you are walking a bicycle. */
    public RoutingRequest bikeWalkingOptions;
    
    /** This is true when a GraphPath is being traversed in reverse for optimization purposes. */
    public boolean reverseOptimizing = false;
    
    /** when true, do not use goal direction or stop at the target, build a full SPT */
    public boolean batch = false;
    
    /**
     * Whether or not bike rental availability information will be used to plan bike rental trips
     */
    public boolean useBikeRentalAvailabilityInformation = false;
    
    /**
     * The maximum wait time in seconds the user is willing to delay trip start. Only effective in
     * Analyst.
     */
    public long clampInitialWait;
    
    /**
     * When true, reverse optimize this search on the fly whenever needed, rather than
     * reverse-optimizing the entire path when it's done.
     */
    public boolean reverseOptimizeOnTheFly = false;
    
    /**
     * If true, cost turns as they would be in a country where driving occurs on the right;
     * otherwise, cost them as they would be in a country where driving occurs on the left.
     */
    public boolean driveOnRight = true;
    
    /**
     * The deceleration speed of an automobile, in meters per second per second.
     */
    // 2.9 m/s/s: 65 mph - 0 mph in 10 seconds
    public double carDecelerationSpeed = 2.9;
    
    /**
     * The acceleration speed of an automobile, in meters per second per second.
     */
    // 2.9 m/s/s: 0 mph to 65 mph in 10 seconds
    public double carAccelerationSpeed = 2.9;

    /**
     * When true, realtime updates are ignored during this search.
     */
    public boolean ignoreRealtimeUpdates = false;
    
    /**
     * If true, the remaining weight heuristic is disabled. Currently only implemented for the long
     * distance path service.
     */
    public boolean disableRemainingWeightHeuristic = false;
    
    /**
     * The routing context used to actually carry out this search. It is important to build States
     * from TraverseOptions rather than RoutingContexts, and just keep a reference to the context in
     * the TraverseOptions, rather than using RoutingContexts for everything because in some testing
     * and graph building situations we need to build a bunch of initial states with different times
     * and vertices from a single TraverseOptions, without setting all the transit context or
     * building temporary vertices (with all the exception-throwing checks that entails). While they
     * are conceptually separate, TraverseOptions does maintain a reference to its accompanying
     * RoutingContext (and vice versa) so that both do not need to be passed/injected separately
     * into tight inner loops within routing algorithms. These references should be set to null when
     * the request scope is torn down -- the routing context becomes irrelevant at that point, since
     * temporary graph elements have been removed and the graph may have been reloaded.
     */
    public RoutingContext rctx;
    
    /** A transit stop that this trip must start from */
    public AgencyAndId startingTransitStopId;

    /** A trip where this trip must start from (depart-onboard routing) */
    public AgencyAndId startingTransitTripId;
    
    public boolean walkingBike;
    
    public double heuristicWeight = 1.0;
    
    public boolean softWalkLimiting = true;
    public boolean softPreTransitLimiting = true;
    
    public double softWalkPenalty = 60.0; // a jump in cost when stepping over the walking limit
    public double softWalkOverageRate = 5.0; // a jump in cost for every meter over the walking
    // limit
    
    public double preTransitPenalty = 300.0; // a jump in cost when stepping over the pre-transit
    // time limit
    public double preTransitOverageRate = 10.0; // a jump in cost for every second over the
    // pre-transit time limit
    
    /*
     * Additional flags affecting mode transitions. This is a temporary solution, as it only covers
     * parking and rental at the beginning of the trip.
     */
    public boolean allowBikeRental = false;
    public boolean bikeParkAndRide = false;
    public boolean parkAndRide = false;
    public boolean kissAndRide = false;
    
    /* CONSTRUCTORS */
    
    /** Constructor for options; modes defaults to walk and transit */
    public RoutingRequest() {
        // http://en.wikipedia.org/wiki/Walking
        this.walkSpeed = 1.33; // 1.33 m/s ~ 3mph, avg. human speed
        this.bikeSpeed = 5; // 5 m/s, ~11 mph, a random bicycling speed
        // http://en.wikipedia.org/wiki/Speed_limit
        this.carSpeed = 40; // 40 m/s, 144 km/h, above the maximum (finite) driving speed limit
        // worldwide
        setModes(new TraverseModeSet(TraverseMode.WALK, TraverseMode.TRANSIT));
        this.bikeWalkingOptions = this;
        
        // So that they are never null.
        this.from = new GenericLocation();
        this.to = new GenericLocation();
    }
    
    public RoutingRequest(TraverseModeSet modes) {
        this();
        this.setModes(modes);
    }
    
    public RoutingRequest(QualifiedModeSetSequence qmodes) {
        this();
        qmodes.applyToRequest(this);
    }
    
    public RoutingRequest(String qmodes) {
        this();
        new QualifiedModeSetSequence(qmodes).applyToRequest(this);
    }
    
    public RoutingRequest(TraverseMode mode) {
        this();
        this.setModes(new TraverseModeSet(mode));
    }
    
    public RoutingRequest(TraverseMode mode, OptimizeType optimize) {
        this(new TraverseModeSet(mode), optimize);
    }
    
    public RoutingRequest(TraverseModeSet modeSet, OptimizeType optimize) {
        this();
        this.optimize = optimize;
        this.setModes(modeSet);
    }
    
    /* ACCESSOR/SETTER METHODS */
    
    public boolean transitAllowed() {
        return this.modes.isTransit();
    }
    
    public long getSecondsSinceEpoch() {
        return this.dateTime;
    }
    
    public void setArriveBy(boolean arriveBy) {
        this.arriveBy = arriveBy;
        this.bikeWalkingOptions.arriveBy = arriveBy;
        if ((this.worstTime == Long.MAX_VALUE) || (this.worstTime == 0)) {
            this.worstTime = arriveBy ? 0 : Long.MAX_VALUE;
        }
    }
    
    public void setMode(TraverseMode mode) {
        setModes(new TraverseModeSet(mode));
    }
    
    public void setModes(TraverseModeSet modes) {
        this.modes = modes;
        if (modes.getBicycle()) {
            this.bikeWalkingOptions = new RoutingRequest();
            this.bikeWalkingOptions.setArriveBy(this.arriveBy);
            this.bikeWalkingOptions.maxWalkDistance = this.maxWalkDistance;
            this.bikeWalkingOptions.maxPreTransitTime = this.maxPreTransitTime;
            this.bikeWalkingOptions.walkSpeed = this.walkSpeed * 0.8; // walking bikes is slow
            this.bikeWalkingOptions.walkReluctance = this.walkReluctance * 2.7; // and painful
            this.bikeWalkingOptions.optimize = this.optimize;
            this.bikeWalkingOptions.modes = modes.clone();
            this.bikeWalkingOptions.modes.setBicycle(false);
            this.bikeWalkingOptions.modes.setWalk(true);
            this.bikeWalkingOptions.walkingBike = true;
            this.bikeWalkingOptions.bikeSwitchTime = this.bikeSwitchTime;
            this.bikeWalkingOptions.bikeSwitchCost = this.bikeSwitchCost;
        } else if (modes.getDriving()) {
            this.bikeWalkingOptions = new RoutingRequest();
            this.bikeWalkingOptions.setArriveBy(this.arriveBy);
            this.bikeWalkingOptions.maxWalkDistance = this.maxWalkDistance;
            this.bikeWalkingOptions.maxPreTransitTime = this.maxPreTransitTime;
            this.bikeWalkingOptions.modes = modes.clone();
            this.bikeWalkingOptions.modes.setBicycle(false);
            this.bikeWalkingOptions.modes.setWalk(true);
        }
    }
    
    public void setOptimize(OptimizeType optimize) {
        this.optimize = optimize;
        this.bikeWalkingOptions.optimize = optimize;
    }
    
    public void setWheelchairAccessible(boolean wheelchairAccessible) {
        this.wheelchairAccessible = wheelchairAccessible;
    }
    
    /**
     * only allow traversal by the specified mode; don't allow walking bikes. This is used during
     * contraction to reduce the number of possible paths.
     */
    public void freezeTraverseMode() {
        this.bikeWalkingOptions = clone();
        this.bikeWalkingOptions.bikeWalkingOptions = new RoutingRequest(new TraverseModeSet());
    }
    
    /**
     * Add an extension parameter with the specified key. Extensions allow you to add arbitrary
     * traversal options.
     */
    public void putExtension(Object key, Object value) {
        this.extensions.put(key, value);
    }
    
    /** Determine if a particular extension parameter is present for the specified key. */
    public boolean containsExtension(Object key) {
        return this.extensions.containsKey(key);
    }
    
    /** Get the extension parameter with the specified key. */
    @SuppressWarnings("unchecked")
    public <T> T getExtension(Object key) {
        return (T) this.extensions.get(key);
    }
    
    /** Returns the model that computes the cost of intersection traversal. */
    public IntersectionTraversalCostModel getIntersectionTraversalCostModel() {
        return this.traversalCostModel;
    }

    /** @return the (soft) maximum walk distance */
    // If transit is not to be used, disable walk limit.
    public double getMaxWalkDistance() {
        if (!this.modes.isTransit()) {
            return Double.MAX_VALUE;
        } else {
            return this.maxWalkDistance;
        }
    }

    public void setWalkBoardCost(int walkBoardCost) {
        if (walkBoardCost < 0) {
            this.walkBoardCost = 0;
        } else {
            this.walkBoardCost = walkBoardCost;
        }
    }

    public void setBikeBoardCost(int bikeBoardCost) {
        if (bikeBoardCost < 0) {
            this.bikeBoardCost = 0;
        } else {
            this.bikeBoardCost = bikeBoardCost;
        }
    }

    public void setPreferredAgencies(String s) {
        if ((s != null) && !s.equals("")) {
            this.preferredAgencies = new HashSet<String>(Arrays.asList(s.split(",")));
        }
    }
    
    public void setPreferredRoutes(String s) {
        if ((s != null) && !s.equals("")) {
            this.preferredRoutes = RouteMatcher.parse(s);
        } else {
            this.preferredRoutes = RouteMatcher.emptyMatcher();
        }
    }

    public void setOtherThanPreferredRoutesPenalty(int penalty) {
        if (penalty < 0) {
            penalty = 0;
        }
        this.otherThanPreferredRoutesPenalty = penalty;
    }

    public void setUnpreferredAgencies(String s) {
        if ((s != null) && !s.equals("")) {
            this.unpreferredAgencies = new HashSet<String>(Arrays.asList(s.split(",")));
        }
    }

    public void setUnpreferredRoutes(String s) {
        if ((s != null) && !s.equals("")) {
            this.unpreferredRoutes = RouteMatcher.parse(s);
        } else {
            this.unpreferredRoutes = RouteMatcher.emptyMatcher();
        }
    }
    
    public void setBannedRoutes(String s) {
        if ((s != null) && !s.equals("")) {
            this.bannedRoutes = RouteMatcher.parse(s);
        } else {
            this.bannedRoutes = RouteMatcher.emptyMatcher();
        }
    }
    
    public void setBannedStops(String s) {
        if ((s != null) && !s.equals("")) {
            this.bannedStops = StopMatcher.parse(s);
        } else {
            this.bannedStops = StopMatcher.emptyMatcher();
        }
    }
    
    public void setBannedStopsHard(String s) {
        if ((s != null) && !s.equals("")) {
            this.bannedStopsHard = StopMatcher.parse(s);
        } else {
            this.bannedStopsHard = StopMatcher.emptyMatcher();
        }
    }

    public void setBannedAgencies(String s) {
        if ((s != null) && !s.equals("")) {
            this.bannedAgencies = new HashSet<String>(Arrays.asList(s.split(",")));
        }
    }
    
    public final static int MIN_SIMILARITY = 1000;
    
    public int similarity(RoutingRequest options) {
        int s = 0;
        
        // TODO Check this: perfect equality between non-transit modes.
        // For partial equality, should we return a smaller similarity score?
        if (this.modes.getNonTransitSet().equals(options.modes.getNonTransitSet())) {
            s += 1000;
        }
        if (this.optimize == options.optimize) {
            s += 700;
        }
        if (this.wheelchairAccessible == options.wheelchairAccessible) {
            s += 500;
        }
        
        return s;
    }
    
    public void setFromString(String from) {
        this.from = GenericLocation.fromOldStyleString(from);
    }
    
    public void setToString(String to) {
        this.to = GenericLocation.fromOldStyleString(to);
    }
    
    /**
     * Clear the allowed modes.
     */
    public void clearModes() {
        this.modes.clear();
    }
    
    /**
     * Add a TraverseMode to the set of allowed modes.
     */
    public void addMode(TraverseMode mode) {
        this.modes.setMode(mode, true);
    }
    
    /**
     * Add multiple modes to the set of allowed modes.
     */
    public void addMode(List<TraverseMode> mList) {
        for (TraverseMode m : mList) {
            addMode(m);
        }
    }
    
    public Date getDateTime() {
        return new Date(this.dateTime * 1000);
    }
    
    public void setDateTime(Date dateTime) {
        this.dateTime = dateTime.getTime() / 1000;
    }
    
    public void setDateTime(String date, String time, TimeZone tz) {
        Date dateObject = DateUtils.toDate(date, time, tz);
        setDateTime(dateObject);
    }
    
    public int getNumItineraries() {
        if (this.modes.isTransit()) {
            return this.numItineraries;
        } else {
            // If transit is not to be used, only search for one itinerary.
            return 1;
        }
    }
    
    public void setNumItineraries(int numItineraries) {
        if (numItineraries > CLAMP_ITINERARIES) {
            numItineraries = CLAMP_ITINERARIES;
        } else if (numItineraries < 1) {
            numItineraries = 1;
        }
        this.numItineraries = numItineraries;
    }
    
    public String toHtmlString() {
        return toString("<br/>");
    }
    
    @Override
    public String toString() {
        return toString(" ");
    }
    
    public String toString(String sep) {
        return this.from + sep + this.to + sep + getMaxWalkDistance() + sep + getDateTime() + sep + this.arriveBy + sep
                + this.optimize + sep + this.modes.getAsStr() + sep + getNumItineraries();
    }
    
    public void removeMode(TraverseMode mode) {
        this.modes.setMode(mode, false);
    }
    
    /**
     * Sets intermediatePlaces by parsing GenericLocations from a list of string.
     */
    public void setIntermediatePlacesFromStrings(List<String> intermediates) {
        this.intermediatePlaces = new ArrayList<GenericLocation>(intermediates.size());
        for (String place : intermediates) {
            this.intermediatePlaces.add(GenericLocation.fromOldStyleString(place));
        }
    }

    /** Clears any intermediate places from this request. */
    public void clearIntermediatePlaces() {
        if (this.intermediatePlaces != null) {
            this.intermediatePlaces.clear();
        }
    }

    /**
     * Returns true if there are any intermediate places set.
     */
    public boolean hasIntermediatePlaces() {
        return (this.intermediatePlaces != null) && (this.intermediatePlaces.size() > 0);
    }
    
    /**
     * Adds a GenericLocation to the end of the intermediatePlaces list. Will initialize
     * intermediatePlaces if it is null.
     */
    public void addIntermediatePlace(GenericLocation location) {
        if (this.intermediatePlaces == null) {
            this.intermediatePlaces = new ArrayList<GenericLocation>();
        }
        this.intermediatePlaces.add(location);
    }

    /**
     * Returns true if intermediate places are marked ordered, or there is only one of them.
     */
    public boolean intermediatesEffectivelyOrdered() {
        boolean exactlyOneIntermediate = ((this.intermediatePlaces != null) && (this.intermediatePlaces.size() == 1));
        return this.intermediatePlacesOrdered || exactlyOneIntermediate;
    }
    
    public void setTriangleSafetyFactor(double triangleSafetyFactor) {
        this.triangleSafetyFactor = triangleSafetyFactor;
        this.bikeWalkingOptions.triangleSafetyFactor = triangleSafetyFactor;
    }
    
    public void setTriangleSlopeFactor(double triangleSlopeFactor) {
        this.triangleSlopeFactor = triangleSlopeFactor;
        this.bikeWalkingOptions.triangleSlopeFactor = triangleSlopeFactor;
    }
    
    public void setTriangleTimeFactor(double triangleTimeFactor) {
        this.triangleTimeFactor = triangleTimeFactor;
        this.bikeWalkingOptions.triangleTimeFactor = triangleTimeFactor;
    }
    
    public void setMaxTransfers(int maxTransfers) {
        // if (maxTransfers > CLAMP_TRANSFERS) {
        // maxTransfers = CLAMP_TRANSFERS;
        // }
        this.maxTransfers = maxTransfers;
    }
    
    public NamedPlace getFromPlace() {
        return this.from.getNamedPlace();
    }
    
    public NamedPlace getToPlace() {
        return this.to.getNamedPlace();
    }
    
    /* INSTANCE METHODS */
    
    @SuppressWarnings("unchecked")
    @Override
    public RoutingRequest clone() {
        try {
            RoutingRequest clone = (RoutingRequest) super.clone();
            clone.bannedRoutes = this.bannedRoutes.clone();
            clone.bannedTrips = (HashMap<AgencyAndId, BannedStopSet>) this.bannedTrips.clone();
            clone.bannedStops = this.bannedStops.clone();
            clone.bannedStopsHard = this.bannedStopsHard.clone();
            if (this.bikeWalkingOptions != this) {
                clone.bikeWalkingOptions = this.bikeWalkingOptions.clone();
            } else {
                clone.bikeWalkingOptions = clone;
            }
            return clone;
        } catch (CloneNotSupportedException e) {
            /* this will never happen since our super is the cloneable object */
            throw new RuntimeException(e);
        }
    }
    
    public RoutingRequest reversedClone() {
        RoutingRequest ret = this.clone();
        ret.setArriveBy(!ret.arriveBy);
        ret.reverseOptimizing = !ret.reverseOptimizing; // this is not strictly correct
        ret.useBikeRentalAvailabilityInformation = false;
        return ret;
    }
    
    public void setRoutingContext(Graph graph) {
        if (this.rctx == null) {
            // graphService.getGraph(routerId)
            this.rctx = new RoutingContext(this, graph);
            // check after back reference is established, to allow temp edge cleanup on exceptions
            this.rctx.check();
        } else {
            if (this.rctx.graph == graph) {
                LOG.debug("keeping existing routing context");
                return;
            } else {
                LOG.error("attempted to reset routing context using a different graph");
                return;
            }
        }
    }
    
    /** For use in tests. Force RoutingContext to specific vertices rather than making temp edges. */
    public void setRoutingContext(Graph graph, Edge fromBackEdge, Vertex from, Vertex to) {
        // normally you would want to tear down the routing context...
        // but this method is mostly used in tests, and teardown interferes with testHalfEdges
        // FIXME here, or in test, and/or in other places like TSP that use this method
        // if (rctx != null)
        // this.rctx.destroy();
        this.rctx = new RoutingContext(this, graph, from, to);
        this.rctx.originBackEdge = fromBackEdge;
    }

    public void setRoutingContext(Graph graph, Vertex from, Vertex to) {
        setRoutingContext(graph, null, from, to);
    }
    
    /** For use in tests. Force RoutingContext to specific vertices rather than making temp edges. */
    public void setRoutingContext(Graph graph, String from, String to) {
        this.setRoutingContext(graph, graph.getVertex(from), graph.getVertex(to));
    }
    
    /**
     * Used in internals API. Make a RoutingContext with no origin or destination vertices
     * specified.
     */
    public void setDummyRoutingContext(Graph graph) {
        this.setRoutingContext(graph, "", "");
    }
    
    public RoutingContext getRoutingContext() {
        return this.rctx;
    }
    
    /**
     * Equality does not mean that the fields of the two RoutingRequests are identical, but that
     * they will produce the same SPT. This is particularly important when the batch field is set to
     * 'true'. Does not consider the RoutingContext, to allow SPT caching. Intermediate places are
     * also not included because the TSP solver will factor a single intermediate places routing
     * request into several routing requests without intermediates before searching.
     */
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof RoutingRequest)) { return false; }
        RoutingRequest other = (RoutingRequest) o;
        if (this.batch != other.batch) { return false; }
        boolean endpointsMatch;
        if (this.batch) {
            if (this.arriveBy) {
                endpointsMatch = this.to.equals(other.to);
            } else {
                endpointsMatch = this.from.equals(other.from);
            }
        } else {
            endpointsMatch = (((this.from == null) && (other.from == null)) || this.from.equals(other.from))
                    && (((this.to == null) && (other.to == null)) || this.to.equals(other.to));
        }
        return endpointsMatch
                && (this.dateTime == other.dateTime)
                && (this.arriveBy == other.arriveBy)
                && (this.numItineraries == other.numItineraries // should only apply in non-batch?
                ) && (this.walkSpeed == other.walkSpeed) && (this.bikeSpeed == other.bikeSpeed)
                        && (this.carSpeed == other.carSpeed) && (this.maxWeight == other.maxWeight)
                        && (this.worstTime == other.worstTime) && (this.maxTransfers == other.maxTransfers)
                        && this.modes.equals(other.modes) && (this.wheelchairAccessible == other.wheelchairAccessible)
                        && this.optimize.equals(other.optimize) && (this.maxWalkDistance == other.maxWalkDistance)
                        && (this.maxPreTransitTime == other.maxPreTransitTime) && (this.transferPenalty == other.transferPenalty)
                        && (this.maxSlope == other.maxSlope) && (this.walkReluctance == other.walkReluctance)
                        && (this.waitReluctance == other.waitReluctance) && (this.waitAtBeginningFactor == other.waitAtBeginningFactor)
                        && (this.walkBoardCost == other.walkBoardCost) && (this.bikeBoardCost == other.bikeBoardCost)
                        && this.bannedRoutes.equals(other.bannedRoutes) && this.bannedTrips.equals(other.bannedTrips)
                        && this.preferredRoutes.equals(other.preferredRoutes) && this.unpreferredRoutes.equals(other.unpreferredRoutes)
                        && (this.transferSlack == other.transferSlack) && (this.boardSlack == other.boardSlack)
                        && (this.alightSlack == other.alightSlack)
                        && (this.nonpreferredTransferPenalty == other.nonpreferredTransferPenalty)
                        && (this.otherThanPreferredRoutesPenalty == other.otherThanPreferredRoutesPenalty)
                        && (this.useUnpreferredRoutesPenalty == other.useUnpreferredRoutesPenalty)
                        && (this.triangleSafetyFactor == other.triangleSafetyFactor)
                        && (this.triangleSlopeFactor == other.triangleSlopeFactor)
                        && (this.triangleTimeFactor == other.triangleTimeFactor) && (this.stairsReluctance == other.stairsReluctance)
                        && (this.elevatorBoardTime == other.elevatorBoardTime) && (this.elevatorBoardCost == other.elevatorBoardCost)
                        && (this.elevatorHopTime == other.elevatorHopTime) && (this.elevatorHopCost == other.elevatorHopCost)
                        && (this.bikeSwitchTime == other.bikeSwitchTime) && (this.bikeSwitchCost == other.bikeSwitchCost)
                        && (this.bikeRentalPickupTime == other.bikeRentalPickupTime)
                        && (this.bikeRentalPickupCost == other.bikeRentalPickupCost)
                        && (this.bikeRentalDropoffTime == other.bikeRentalDropoffTime)
                        && (this.bikeRentalDropoffCost == other.bikeRentalDropoffCost)
                        && (this.useBikeRentalAvailabilityInformation == other.useBikeRentalAvailabilityInformation)
                        && this.extensions.equals(other.extensions) && (this.clampInitialWait == other.clampInitialWait)
                        && (this.reverseOptimizeOnTheFly == other.reverseOptimizeOnTheFly)
                        && (this.ignoreRealtimeUpdates == other.ignoreRealtimeUpdates)
                        && (this.disableRemainingWeightHeuristic == other.disableRemainingWeightHeuristic)
                        && Objects.equal(this.startingTransitTripId, other.startingTransitTripId);
    }
    
    /**
     * Equality and hashCode should not consider the routing context, to allow SPT caching. When
     * adding fields to the hash code, pick a random large prime number that's not yet in use.
     */
    @Override
    public int hashCode() {
        int hashCode = new Double(this.walkSpeed).hashCode() + new Double(this.bikeSpeed).hashCode()
                + new Double(this.carSpeed).hashCode() + new Double(this.maxWeight).hashCode()
                + (int) (this.worstTime & 0xffffffff) + this.modes.hashCode() + (this.arriveBy ? 8966786 : 0)
                + (this.wheelchairAccessible ? 731980 : 0) + this.optimize.hashCode()
                + new Double(this.maxWalkDistance).hashCode() + new Double(this.transferPenalty).hashCode()
                + new Double(this.maxSlope).hashCode() + new Double(this.walkReluctance).hashCode()
                + new Double(this.waitReluctance).hashCode() + (new Double(this.waitAtBeginningFactor).hashCode() * 15485863)
                + this.walkBoardCost + this.bikeBoardCost + this.bannedRoutes.hashCode() + (this.bannedTrips.hashCode() * 1373)
                + (this.transferSlack * 20996011) + this.nonpreferredTransferPenalty + (this.transferPenalty * 163013803)
                + (new Double(this.triangleSafetyFactor).hashCode() * 195233277)
                + (new Double(this.triangleSlopeFactor).hashCode() * 136372361)
                + (new Double(this.triangleTimeFactor).hashCode() * 790052899)
                + (new Double(this.stairsReluctance).hashCode() * 315595321) + (this.maxPreTransitTime * 63061489)
                + (new Long(this.clampInitialWait).hashCode() * 209477)
                + (new Boolean(this.reverseOptimizeOnTheFly).hashCode() * 95112799)
                + (new Boolean(this.ignoreRealtimeUpdates).hashCode() * 154329)
                + (new Boolean(this.disableRemainingWeightHeuristic).hashCode() * 193939);
        if (this.batch) {
            hashCode *= -1;
            // batch mode, only one of two endpoints matters
            if (this.arriveBy) {
                hashCode += this.to.hashCode() * 1327144003;
            } else {
                hashCode += this.from.hashCode() * 524287;
            }
            hashCode += this.numItineraries; // why is this only present here?
        } else {
            // non-batch, both endpoints matter
            hashCode += this.from.hashCode() * 524287;
            hashCode += this.to.hashCode() * 1327144003;
        }
        return hashCode;
    }
    
    /** Tear down any routing context (remove temporary edges from edge lists) */
    public void cleanup() {
        if (this.rctx == null) {
            LOG.warn("routing context was not set, cannot destroy it.");
        } else {
            int nRemoved = this.rctx.destroy();
            LOG.debug("routing context destroyed ({} temporary edges removed)", nRemoved);
        }
    }
    
    /**
     * @param mode
     * @return The road speed for a specific traverse mode.
     */
    public double getSpeed(TraverseMode mode) {
        switch (mode) {
            case WALK:
                return this.walkSpeed;
            case BICYCLE:
                return this.bikeSpeed;
            case CAR:
                return this.carSpeed;
            case CUSTOM_MOTOR_VEHICLE:
                return this.carSpeed;
            default:
                break;
        }
        throw new IllegalArgumentException("getSpeed(): Invalid mode " + mode);
    }
    
    /** @return The highest speed for all possible road-modes. */
    public double getStreetSpeedUpperBound() {
        // Assume carSpeed > bikeSpeed > walkSpeed
        if (this.modes.getDriving()) { return this.carSpeed; }
        if (this.modes.getBicycle()) { return this.bikeSpeed; }
        return this.walkSpeed;
    }
    
    /**
     * @param mode
     * @return The board cost for a specific traverse mode.
     */
    public int getBoardCost(TraverseMode mode) {
        if (mode == TraverseMode.BICYCLE) { return this.bikeBoardCost; }
        // I assume you can't bring your car in the bus
        return this.walkBoardCost;
    }
    
    /** @return The lower boarding cost for all possible road-modes. */
    public int getBoardCostLowerBound() {
        // Assume walkBoardCost < bikeBoardCost
        if (this.modes.getWalk()) { return this.walkBoardCost; }
        return this.bikeBoardCost;
    }

    private String getRouteOrAgencyStr(HashSet<String> strings) {
        StringBuilder builder = new StringBuilder();
        for (String agency : strings) {
            builder.append(agency);
            builder.append(",");
        }
        if (builder.length() > 0) {
            // trim trailing comma
            builder.setLength(builder.length() - 1);
        }
        return builder.toString();
    }
    
    public String getPreferredRouteStr() {
        return this.preferredRoutes.asString();
    }

    public String getPreferredAgenciesStr() {
        return getRouteOrAgencyStr(this.preferredAgencies);
    }
    
    public String getUnpreferredRouteStr() {
        return this.unpreferredRoutes.asString();
    }

    public String getUnpreferredAgenciesStr() {
        return getRouteOrAgencyStr(this.unpreferredAgencies);
    }
    
    public String getBannedRouteStr() {
        return this.bannedRoutes.asString();
    }
    
    public String getBannedStopsStr() {
        return this.bannedStops.asString();
    }

    public String getBannedStopsHardStr() {
        return this.bannedStopsHard.asString();
    }

    public String getBannedAgenciesStr() {
        return getRouteOrAgencyStr(this.bannedAgencies);
    }
    
    public void setMaxWalkDistance(double maxWalkDistance) {
        if (maxWalkDistance > 0) {
            this.maxWalkDistance = maxWalkDistance;
            this.bikeWalkingOptions.maxWalkDistance = maxWalkDistance;
        }
    }
    
    public void setMaxPreTransitTime(int maxPreTransitTime) {
        if (maxPreTransitTime > 0) {
            this.maxPreTransitTime = maxPreTransitTime;
            this.bikeWalkingOptions.maxPreTransitTime = maxPreTransitTime;
        }
    }
    
    public void setWalkReluctance(double walkReluctance) {
        if (walkReluctance > 0) {
            this.walkReluctance = walkReluctance;
            // Do not set bikeWalkingOptions.walkReluctance here, because that needs a higher value.
        }
    }
    
    public void setWaitReluctance(double waitReluctance) {
        if (waitReluctance > 0) {
            this.waitReluctance = waitReluctance;
        }
    }
    
    public void setWaitAtBeginningFactor(double waitAtBeginningFactor) {
        if (waitAtBeginningFactor > 0) {
            this.waitAtBeginningFactor = waitAtBeginningFactor;
        }
    }
    
    public void banTrip(AgencyAndId trip) {
        this.bannedTrips.put(trip, BannedStopSet.ALL);
    }

    /**
     * tripIsBanned is a misnomer: this checks whether the agency or route are banned. banning of
     * individual trips is actually performed inside the trip search, in TripTimes.tripAcceptable.
     */
    public boolean tripIsBanned(Trip trip) {
        /* check if agency is banned for this plan */
        if (this.bannedAgencies != null) {
            if (this.bannedAgencies.contains(trip.getId().getAgencyId())) { return true; }
        }
        
        /* check if route banned for this plan */
        if (this.bannedRoutes != null) {
            Route route = trip.getRoute();
            if (this.bannedRoutes.matches(route)) { return true; }
        }
        
        return false;
    }
    
    /** Check if route is preferred according to this request. */
    public long preferencesPenaltyForRoute(Route route) {
        long preferences_penalty = 0;
        String agencyID = route.getId().getAgencyId();
        if (((this.preferredRoutes != null) && !this.preferredRoutes.equals(RouteMatcher.emptyMatcher()))
                || ((this.preferredAgencies != null) && !this.preferredAgencies.isEmpty())) {
            boolean isPreferedRoute = (this.preferredRoutes != null) && this.preferredRoutes.matches(route);
            boolean isPreferedAgency = (this.preferredAgencies != null) && this.preferredAgencies.contains(agencyID);
            if (!isPreferedRoute && !isPreferedAgency) {
                preferences_penalty += this.otherThanPreferredRoutesPenalty;
            } else {
                preferences_penalty = 0;
            }
        }
        boolean isUnpreferedRoute = (this.unpreferredRoutes != null) && this.unpreferredRoutes.matches(route);
        boolean isUnpreferedAgency = (this.unpreferredAgencies != null) && this.unpreferredAgencies.contains(agencyID);
        if (isUnpreferedRoute || isUnpreferedAgency) {
            preferences_penalty += this.useUnpreferredRoutesPenalty;
        }
        return preferences_penalty;
    }
    
}