package org.opentripplanner.standalone;

import java.util.Arrays;
import java.util.Date;
import java.util.Set;

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

public class State implements Cloneable {
    /* Data which is likely to change at most traversals */

    // the current time at this state, in milliseconds
    protected long time;
    
    // accumulated weight up to this state
    public double weight;
    
    // associate this state with a vertex in the graph
    protected Vertex vertex;
    
    // allow path reconstruction from states
    protected State backState;
    
    public Edge backEdge;
    
    // allow traverse result chaining (multiple results)
    protected State next;
    
    /* StateData contains data which is unlikely to change as often */
    public StateData stateData;
    
    // how far have we walked
    // TODO(flamholz): this is a very confusing name as it actually applies to all non-transit
    // modes.
    // we should DEFINITELY rename this variable and the associated methods.
    public double walkDistance;
    
    // The time traveled pre-transit, for park and ride or kiss and ride searches
    int preTransitTime;
    
    // track the states of all path parsers -- probably changes frequently
    protected int[] pathParserStates;

    private static final Logger LOG = LoggerFactory.getLogger(State.class);
    
    /* CONSTRUCTORS */
    
    /**
     * Create an initial state representing the beginning of a search for the given routing context.
     * Initial "parent-less" states can only be created at the beginning of a trip. elsewhere, all
     * states must be created from a parent and associated with an edge.
     */
    public State(RoutingRequest opt) {
        this(opt.rctx.origin, opt.rctx.originBackEdge, opt.getSecondsSinceEpoch(), opt);
    }
    
    /**
     * Create an initial state, forcing vertex to the specified value. Useful for reusing a
     * RoutingContext in TransitIndex, tests, etc.
     */
    public State(Vertex vertex, RoutingRequest opt) {
        // Since you explicitly specify, the vertex, we don't set the backEdge.
        this(vertex, opt.getSecondsSinceEpoch(), opt);
    }
    
    /**
     * Create an initial state, forcing vertex and time to the specified values. Useful for reusing
     * a RoutingContext in TransitIndex, tests, etc.
     */
    public State(Vertex vertex, long timeSeconds, RoutingRequest options) {
        // Since you explicitly specify, the vertex, we don't set the backEdge.
        this(vertex, null, timeSeconds, options);
    }

    /**
     * Create an initial state, forcing vertex, back edge and time to the specified values. Useful
     * for reusing a RoutingContext in TransitIndex, tests, etc.
     */
    public State(Vertex vertex, Edge backEdge, long timeSeconds, RoutingRequest options) {
        this.weight = 0;
        this.vertex = vertex;
        this.backEdge = backEdge;
        this.backState = null;
        this.stateData = new StateData(options);
        // note that here we are breaking the circular reference between rctx and options
        // this should be harmless since reversed clones are only used when routing has finished
        this.stateData.opt = options;
        this.stateData.startTime = timeSeconds;
        this.stateData.usingRentedBike = false;
        /*
         * If the itinerary is to begin with a car that is left for transit, the initial state of
         * arriveBy searches is with the car already "parked" and in WALK mode. Otherwise, we are in
         * CAR mode and "unparked".
         */
        if (options.parkAndRide || options.kissAndRide) {
            this.stateData.carParked = options.arriveBy;
            this.stateData.nonTransitMode = this.stateData.carParked ? TraverseMode.WALK : TraverseMode.CAR;
        }
        this.walkDistance = 0;
        this.preTransitTime = 0;
        this.time = timeSeconds * 1000;
        if (options.rctx != null) {
            this.pathParserStates = new int[options.rctx.pathParsers.length];
            Arrays.fill(this.pathParserStates, AutomatonState.START);
        }
        this.stateData.routeSequence = new AgencyAndId[0];
    }
    
    /**
     * Create a state editor to produce a child of this state, which will be the result of
     * traversing the given edge.
     *
     * @param e
     * @return
     */
    public StateEditor edit(Edge e) {
        return new StateEditor(this, e);
    }
    
    @Override
    protected State clone() {
        State ret;
        try {
            ret = (State) super.clone();
        } catch (CloneNotSupportedException e1) {
            throw new IllegalStateException("This is not happening");
        }
        return ret;
    }
    
    /*
     * FIELD ACCESSOR METHODS States are immutable, so they have only get methods. The corresponding
     * set methods are in StateEditor.
     */
    
    /**
     * Retrieve a State extension based on its key.
     *
     * @param key - An Object that is a key in this State's extension map
     * @return - The extension value for the given key, or null if not present
     */
    public Object getExtension(Object key) {
        if (this.stateData.extensions == null) { return null; }
        return this.stateData.extensions.get(key);
    }
    
    @Override
    public String toString() {
        return "<State " + new Date(getTimeInMillis()) + " [" + this.weight + "] " + (isBikeRenting() ? "BIKE_RENT " : "")
                + (isCarParked() ? "CAR_PARKED " : "") + this.vertex + ">";
    }

    public String toStringVerbose() {
        return "<State " + new Date(getTimeInMillis()) + " w=" + this.getWeight() + " t=" + this.getElapsedTimeSeconds() + " d="
                + this.getWalkDistance() + " p=" + this.getPreTransitTime() + " b=" + this.getNumBoardings() + " br="
                + this.isBikeRenting() + " pr=" + this.isCarParked() + ">";
    }

    /** Returns time in seconds since epoch */
    public long getTimeSeconds() {
        return this.time / 1000;
    }
    
    /** returns the length of the trip in seconds up to this state */
    public long getElapsedTimeSeconds() {
        return Math.abs(getTimeSeconds() - this.stateData.startTime);
    }
    
    public TripTimes getTripTimes() {
        return this.stateData.tripTimes;
    }
    
    /**
     * Returns the length of the trip in seconds up to this time, not including the initial wait. It
     * subtracts out the initial wait, up to a clamp value specified in the request. If the clamp
     * value is set to -1, no clamping will occur. If the clamp value is set to 0, the initial wait
     * time will not be subtracted out (i.e. it will be clamped to zero). This is used in lieu of
     * reverse optimization in Analyst.
     */
    public long getActiveTime() {
        long clampInitialWait = this.stateData.opt.clampInitialWait;
        
        long initialWait = this.stateData.initialWaitTime;
        
        // only subtract up the clamp value
        if ((clampInitialWait >= 0) && (initialWait > clampInitialWait)) {
            initialWait = clampInitialWait;
        }
        
        long activeTime = getElapsedTimeSeconds() - initialWait;
        
        // TODO: what should be done here? (Does this ever happen?)
        if (activeTime < 0) {
            LOG.warn("initial wait was greater than elapsed time.");
            activeTime = getElapsedTimeSeconds();
        }
        
        return activeTime;
    }
    
    public AgencyAndId getTripId() {
        return this.stateData.tripId;
    }
    
    public Trip getPreviousTrip() {
        return this.stateData.previousTrip;
    }

    public String getZone() {
        return this.stateData.zone;
    }
    
    public AgencyAndId getRoute() {
        return this.stateData.route;
    }
    
    public int getNumBoardings() {
        return this.stateData.numBoardings;
    }
    
    /**
     * Whether this path has ever previously boarded (or alighted from, in a reverse search) a
     * transit vehicle
     */
    public boolean isEverBoarded() {
        return this.stateData.everBoarded;
    }
    
    public boolean isBikeRenting() {
        return this.stateData.usingRentedBike;
    }

    public boolean isCarParked() {
        return this.stateData.carParked;
    }
    
    /**
     * @return True if the state at vertex can be the end of path.
     */
    public boolean isFinal() {
        // When drive-to-transit is enabled, we need to check whether the car has been parked (or
        // whether it has been picked up in reverse).
        boolean checkPark = this.stateData.opt.parkAndRide || this.stateData.opt.kissAndRide;
        if (this.stateData.opt.arriveBy) {
            return !isBikeRenting() && !(checkPark && isCarParked());
        } else {
            return !isBikeRenting() && !(checkPark && !isCarParked());
        }
    }
    
    public Stop getPreviousStop() {
        return this.stateData.previousStop;
    }
    
    public long getLastAlightedTimeSeconds() {
        return this.stateData.lastAlightedTime;
    }
    
    public double getWalkDistance() {
        return this.walkDistance;
    }
    
    public int getPreTransitTime() {
        return this.preTransitTime;
    }
    
    public Vertex getVertex() {
        return this.vertex;
    }
    
    public int getLastNextArrivalDelta() {
        return this.stateData.lastNextArrivalDelta;
    }
    
    /**
     * Returns true if this state's weight is lower than the other one. Considers only weight and
     * not time or other criteria.
     */
    public boolean betterThan(State other) {
        return this.weight < other.weight;
    }
    
    public double getWeight() {
        return this.weight;
    }
    
    public int getTimeDeltaSeconds() {
        return this.backState != null ? (int) (getTimeSeconds() - this.backState.getTimeSeconds()) : 0;
    }
    
    public int getAbsTimeDeltaSeconds() {
        return Math.abs(getTimeDeltaSeconds());
    }
    
    public double getWalkDistanceDelta() {
        if (this.backState != null) {
            return Math.abs(this.walkDistance - this.backState.walkDistance);
        } else {
            return 0.0;
        }
    }
    
    public int getPreTransitTimeDelta() {
        if (this.backState != null) {
            return Math.abs(this.preTransitTime - this.backState.preTransitTime);
        } else {
            return 0;
        }
    }
    
    public double getWeightDelta() {
        return this.weight - this.backState.weight;
    }
    
    public void checkNegativeWeight() {
        double dw = this.weight - this.backState.weight;
        if (dw < 0) { throw new NegativeWeightException(String.valueOf(dw) + " on edge " + this.backEdge); }
    }
    
    public boolean isOnboard() {
        return this.backEdge instanceof OnboardEdge;
    }
    
    public State getBackState() {
        return this.backState;
    }

    public TraverseMode getBackMode() {
        return this.stateData.backMode;
    }

    public boolean isBackWalkingBike() {
        return this.stateData.backWalkingBike;
    }
    
    public Set<Alert> getBackAlerts() {
        return this.stateData.notes;
    }

    /**
     * Get the name of the direction used to get to this state. For transit, it is the headsign,
     * while for other things it is what you would expect.
     */
    public String getBackDirection() {
        // This can happen when stop_headsign says different things at two trips on the same
        // pattern and at the same stop.
        if (this.backEdge instanceof TablePatternEdge) {
            return this.stateData.tripTimes.getHeadsign(((TablePatternEdge) this.backEdge).getStopIndex());
        } else {
            return this.backEdge.getDirection();
        }
    }

    /**
     * Get the back trip of the given state. For time dependent transit, State will find the right
     * thing to do.
     */
    public Trip getBackTrip() {
        if (this.backEdge instanceof TablePatternEdge) {
            return this.stateData.tripTimes.trip;
        } else {
            return this.backEdge.getTrip();
        }
    }
    
    public Edge getBackEdge() {
        return this.backEdge;
    }
    
    public boolean exceedsWeightLimit(double maxWeight) {
        return this.weight > maxWeight;
    }
    
    public long getStartTimeSeconds() {
        return this.stateData.startTime;
    }
    
    /**
     * Optional next result that allows {@link Edge} to return multiple results.
     *
     * @return the next additional result from an edge traversal, or null if no more results
     */
    public State getNextResult() {
        return this.next;
    }
    
    /**
     * Extend an exiting result chain by appending this result to the existing chain. The usage
     * model looks like this: <code>
     * TraverseResult result = null;
     *
     * for( ... ) {
     *   TraverseResult individualResult = ...;
     *   result = individualResult.addToExistingResultChain(result);
     * }
     *
     * return result;
     * </code>
     *
     * @param existingResultChain the tail of an existing result chain, or null if the chain has not
     *        been started
     * @return
     */
    public State addToExistingResultChain(State existingResultChain) {
        if (this.getNextResult() != null) { throw new IllegalStateException("this result already has a next result set"); }
        this.next = existingResultChain;
        return this;
    }
    
    public State detachNextResult() {
        State ret = this.next;
        this.next = null;
        return ret;
    }
    
    public RoutingContext getContext() {
        return this.stateData.opt.rctx;
    }
    
    public RoutingRequest getOptions() {
        return this.stateData.opt;
    }

    /**
     * This method is on State rather than RoutingRequest because we care whether the user is in
     * possession of a rented bike.
     *
     * @return BICYCLE if routing with an owned bicycle, or if at this state the user is holding on
     *         to a rented bicycle.
     */
    public TraverseMode getNonTransitMode() {
        return this.stateData.nonTransitMode;
    }
    
    // TODO: There is no documentation about what this means. No one knows precisely.
    // Needs to be replaced with clearly defined fields.
    
    public State reversedClone() {
        // We no longer compensate for schedule slack (minTransferTime) here.
        // It is distributed symmetrically over all preboard and prealight edges.
        State newState = new State(this.vertex, getTimeSeconds(), this.stateData.opt.reversedClone());
        newState.stateData.tripTimes = this.stateData.tripTimes;
        newState.stateData.initialWaitTime = this.stateData.initialWaitTime;
        // TODO Check if those two lines are needed:
        newState.stateData.usingRentedBike = this.stateData.usingRentedBike;
        newState.stateData.carParked = this.stateData.carParked;
        return newState;
    }
    
    public void dumpPath() {
        System.out.printf("---- FOLLOWING CHAIN OF STATES ----\n");
        State s = this;
        while (s != null) {
            System.out.printf("%s via %s by %s\n", s, s.backEdge, s.getBackMode());
            s = s.backState;
        }
        System.out.printf("---- END CHAIN OF STATES ----\n");
    }
    
    public long getTimeInMillis() {
        return this.time;
    }
    
    // symmetric prefix check
    public boolean routeSequencePrefix(State that) {
        AgencyAndId[] rs0 = this.stateData.routeSequence;
        AgencyAndId[] rs1 = that.stateData.routeSequence;
        if (rs0 == rs1) { return true; }
        int n = rs0.length < rs1.length ? rs0.length : rs1.length;
        for (int i = 0; i < n; i++) {
            if (rs0[i] != rs1[i]) { return false; }
        }
        return true;
    }
    
    // symmetric subset check
    public boolean routeSequenceSubsetSymmetric(State that) {
        AgencyAndId[] rs0 = this.stateData.routeSequence;
        AgencyAndId[] rs1 = that.stateData.routeSequence;
        if (rs0 == rs1) { return true; }
        AgencyAndId[] shorter, longer;
        if (rs0.length < rs1.length) {
            shorter = rs0;
            longer = rs1;
        } else {
            shorter = rs1;
            longer = rs0;
        }
        /* bad complexity, but these are tiny arrays */
        for (AgencyAndId ais : shorter) {
            boolean match = false;
            for (AgencyAndId ail : longer) {
                if (ais == ail) {
                    match = true;
                    break;
                }
            }
            if (!match) { return false; }
        }
        return true;
    }
    
    // subset check: is this a subset of that?
    public boolean routeSequenceSubset(State that) {
        AgencyAndId[] rs0 = this.stateData.routeSequence;
        AgencyAndId[] rs1 = that.stateData.routeSequence;
        if (rs0 == rs1) { return true; }
        if (rs0.length > rs1.length) { return false; }
        /* bad complexity, but these are tiny arrays */
        for (AgencyAndId r0 : rs0) {
            boolean match = false;
            for (AgencyAndId r1 : rs1) {
                if (r0 == r1) {
                    match = true;
                    break;
                }
            }
            if (!match) { return false; }
        }
        return true;
    }
    
    public boolean routeSequenceSuperset(State that) {
        return that.routeSequenceSubset(this);
    }
    
    public double getWalkSinceLastTransit() {
        return this.walkDistance - this.stateData.lastTransitWalk;
    }
    
    public double getWalkAtLastTransit() {
        return this.stateData.lastTransitWalk;
    }
    
    public boolean multipleOptionsBefore() {
        boolean foundAlternatePaths = false;
        TraverseMode requestedMode = getNonTransitMode();
        for (Edge out : this.backState.vertex.getOutgoing()) {
            if (out == this.backEdge) {
                continue;
            }
            if (!(out instanceof StreetEdge)) {
                continue;
            }
            State outState = out.traverse(this.backState);
            if (outState == null) {
                continue;
            }
            if (!outState.getBackMode().equals(requestedMode)) {
                // walking a bike, so, not really an exit
                continue;
            }
            // this section handles the case of an option which is only an option if you walk your
            // bike. It is complicated because you will not need to walk your bike until one
            // edge after the current edge.
            
            // now, from here, try a continuing path.
            Vertex tov = outState.getVertex();
            boolean found = false;
            for (Edge out2 : tov.getOutgoing()) {
                State outState2 = out2.traverse(outState);
                if ((outState2 != null) && !outState2.getBackMode().equals(requestedMode)) {
                    // walking a bike, so, not really an exit
                    continue;
                }
                found = true;
                break;
            }
            if (!found) {
                continue;
            }
            
            // there were paths we didn't take.
            foundAlternatePaths = true;
            break;
        }
        return foundAlternatePaths;
    }

    public boolean allPathParsersAccept() {
        PathParser[] parsers = this.stateData.opt.rctx.pathParsers;
        for (int i = 0; i < parsers.length; i++) {
            if (!parsers[i].accepts(this.pathParserStates[i])) { return false; }
        }
        return true;
    }
    
    public String getPathParserStates() {
        StringBuilder sb = new StringBuilder();
        sb.append("( ");
        for (int i : this.pathParserStates) {
            sb.append(String.format("%02d ", i));
        }
        sb.append(")");
        return sb.toString();
    }
    
    /** @return the last TripPattern used in this path (which is set when leaving the vehicle). */
    public TripPattern getLastPattern() {
        return this.stateData.lastPattern;
    }
    
    public ServiceDay getServiceDay() {
        return this.stateData.serviceDay;
    }
    
    public Set<String> getBikeRentalNetworks() {
        return this.stateData.bikeRentalNetworks;
    }
    
    /**
     * Reverse the path implicit in the given state, re-traversing all edges in the opposite
     * direction so as to remove any unnecessary waiting in the resulting itinerary. This produces a
     * path that passes through all the same edges, but which may have a shorter overall duration
     * due to different weights on time-dependent (e.g. transit boarding) edges. If the optimize
     * parameter is false, the path will be reversed but will have the same duration. This is the
     * result of combining the functions from GraphPath optimize and reverse.
     *
     * @param optimize Should this path be optimized or just reversed?
     * @param forward Is this an on-the-fly reverse search in the midst of a forward search?
     * @returns a state at the other end (or this end, in the case of a forward search) of a
     *          reversed, optimized path
     */
    public State optimizeOrReverse(boolean optimize, boolean forward) {
        State orig = this;
        State unoptimized = orig;
        State ret = orig.reversedClone();
        long newInitialWaitTime = this.stateData.initialWaitTime;
        PathParser pathParsers[];
        
        // disable path parsing temporarily
        pathParsers = this.stateData.opt.rctx.pathParsers;
        this.stateData.opt.rctx.pathParsers = new PathParser[0];
        
        Edge edge = null;
        
        while (orig.getBackState() != null) {
            edge = orig.getBackEdge();

            if (optimize) {
                // first board/last alight: figure in wait time in on the fly optimization
                if ((edge instanceof TransitBoardAlight) && forward && (orig.getNumBoardings() == 1) && (
                // boarding in a forward main search
                        (((TransitBoardAlight) edge).boarding && !this.stateData.opt.arriveBy) ||
                        // alighting in a reverse main search
                        (!((TransitBoardAlight) edge).boarding && this.stateData.opt.arriveBy))) {
                    
                    ret = ((TransitBoardAlight) edge).traverse(ret, orig.getBackState().getTimeSeconds());
                    newInitialWaitTime = ret.stateData.initialWaitTime;
                } else {
                    ret = edge.traverse(ret);
                }
                
                if ((ret != null) && (ret.getBackMode() != null) && (orig.getBackMode() != null)
                        && (ret.getBackMode() != orig.getBackMode())) {
                    ret = ret.next; // Keep the mode the same as on the original graph path (in K+R)
                }
                
                if (ret == null) {
                    LOG.warn("Cannot reverse path at edge: " + edge + ", returning unoptimized "
                            + "path. If this edge is a PatternInterlineDwell, or if there is a "
                            + "time-dependent turn restriction here, or if there is no transit leg "
                            + "in a K+R result, this is not totally unexpected. Otherwise, you " + "might want to look into it.");
                    
                    // re-enable path parsing
                    this.stateData.opt.rctx.pathParsers = pathParsers;
                    
                    if (forward) {
                        return this;
                    } else {
                        return unoptimized.reverse();
                    }
                }
            } else {
                StateEditor editor = ret.edit(edge);
                // note the distinction between setFromState and setBackState
                editor.setFromState(orig);
                
                editor.incrementTimeInSeconds(orig.getAbsTimeDeltaSeconds());
                editor.incrementWeight(orig.getWeightDelta());
                editor.incrementWalkDistance(orig.getWalkDistanceDelta());
                editor.incrementPreTransitTime(orig.getPreTransitTimeDelta());

                // propagate the modes and alerts through to the reversed edge
                editor.setBackMode(orig.getBackMode());
                editor.addAlerts(orig.getBackAlerts());
                
                if (orig.isBikeRenting() != orig.getBackState().isBikeRenting()) {
                    editor.setBikeRenting(!orig.isBikeRenting());
                }
                if (orig.isCarParked() != orig.getBackState().isCarParked()) {
                    editor.setCarParked(!orig.isCarParked());
                }
                
                editor.setNumBoardings(getNumBoardings() - orig.getNumBoardings());
                
                ret = editor.makeState();
                
                // EdgeNarrative origNarrative = orig.getBackEdgeNarrative();
                // EdgeNarrative retNarrative = ret.getBackEdgeNarrative();
                // copyExistingNarrativeToNewNarrativeAsAppropriate(origNarrative, retNarrative);
            }

            orig = orig.getBackState();
        }
        
        // re-enable path parsing
        this.stateData.opt.rctx.pathParsers = pathParsers;
        
        if (forward) {
            State reversed = ret.reverse();
            if (getWeight() <= reversed.getWeight()) {
                LOG.warn("Optimization did not decrease weight: before " + this.getWeight() + " after " + reversed.getWeight());
            }
            if (getElapsedTimeSeconds() != reversed.getElapsedTimeSeconds()) {
                LOG.warn("Optimization changed time: before " + this.getElapsedTimeSeconds() + " after "
                        + reversed.getElapsedTimeSeconds());
            }
            if (getActiveTime() <= reversed.getActiveTime()) {
                // NOTE: this can happen and it isn't always bad (i.e. it doesn't always mean that
                // reverse-opt got called when it shouldn't have). Imagine three lines A, B and C
                // A trip takes line A at 7:00 and arrives at the first transit center at 7:30,
                // where line
                // B is boarded at 7:40 to another transit center with an arrival at 8:00. At 8:30,
                // line C
                // is boarded. Suppose line B runs every ten minutes and the other two run every
                // hour. The
                // optimizer will optimize the B->C connection, moving the trip on line B forward
                // ten minutes. However, it will not be able to move the trip on Line A forward
                // because
                // there is not another possible trip. The waiting time will get pushed towards the
                // the beginning, but not all the way.
                LOG.warn("Optimization did not decrease active time: before " + this.getActiveTime() + " after "
                        + reversed.getActiveTime() + ", boardings: " + this.getNumBoardings());
            }
            if (reversed.getWeight() < this.getBackState().getWeight()) {
                // This is possible; imagine a trip involving three lines, line A, line B and
                // line C. Lines A and C run hourly while Line B runs every ten minute starting
                // at 8:55. The user boards line A at 7:00 and gets off at the first transfer point
                // (point u) at 8:00. The user then boards the first run of line B at 8:55, an
                // optimal
                // transfer since there is no later trip on line A that could have been taken. The
                // user
                // deboards line B at point v at 10:00, and boards line C at 10:15. This is a
                // non-optimal transfer; the trip on line B can be moved forward 10 minutes. When
                // that happens, the first transfer becomes non-optimal (8:00 to 9:05) and the trip
                // on line A can be moved forward an hour, thus moving 55 minutes of waiting time
                // from a previous state to the beginning of the trip where it is significantly
                // cheaper.
                LOG.warn("Weight has been reduced enough to make it run backwards, now:" + reversed.getWeight() + " backState "
                        + getBackState().getWeight() + ", " + "number of boardings: " + getNumBoardings());
            }
            if (getTimeSeconds() != reversed.getTimeSeconds()) {
                LOG.warn("Times do not match");
            }
            if ((Math.abs(getWeight() - reversed.getWeight()) > 1) && (newInitialWaitTime == this.stateData.initialWaitTime)) {
                LOG.warn("Weight is changed (before: " + getWeight() + ", after: " + reversed.getWeight()
                        + "), initial wait times " + "constant at " + newInitialWaitTime);
            }
            if (newInitialWaitTime != reversed.stateData.initialWaitTime) {
                LOG.warn("Initial wait time not propagated: is " + reversed.stateData.initialWaitTime + ", should be "
                        + newInitialWaitTime);
            }
            
            // copy the path parser states so this path is not thrown out going forward
            // reversed.pathParserStates =
            // Arrays.copyOf(this.pathParserStates, this.pathParserStates.length, newLength);

            // copy things that didn't get copied
            reversed.initializeFieldsFrom(this);
            return reversed;
        } else {
            return ret;
        }
    }
    
    /**
     * Reverse-optimize a path after it is complete, by default
     */
    public State optimize() {
        return optimizeOrReverse(true, false);
    }
    
    /**
     * Reverse a path
     */
    public State reverse() {
        return optimizeOrReverse(false, false);
    }

    /**
     * After reverse-optimizing, many things are not set. Set them from the unoptimized state.
     * 
     * @param o The other state to initialize things from.
     */
    private void initializeFieldsFrom(State o) {
        StateData currentStateData = this.stateData;

        // easier to clone and copy back, plus more future proof
        this.stateData = o.stateData.clone();
        this.stateData.initialWaitTime = currentStateData.initialWaitTime;
        // this will get re-set on the next alight (or board in a reverse search)
        this.stateData.lastNextArrivalDelta = -1;
    }
    
    public boolean getReverseOptimizing() {
        return this.stateData.opt.reverseOptimizing;
    }
    
    public double getOptimizedElapsedTimeSeconds() {
        return getElapsedTimeSeconds() - this.stateData.initialWaitTime;
    }
}