package com.conveyal.r5.streets;

import com.conveyal.r5.trove.TDoubleAugmentedList;
import com.conveyal.r5.trove.TIntAugmentedList;
import com.google.common.math.Stats;
import gnu.trove.list.TDoubleList;
import gnu.trove.list.TIntList;
import gnu.trove.list.array.TDoubleArrayList;
import gnu.trove.list.array.TIntArrayList;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;

/**
 * This stores edge characteristics that affect the perceived difficulty or danger of traversing edges.
 * From these characteristics, we can produce costs per edge in units of subjective-seconds (effective time), or
 * in units of subjective-meters (effective distance) if they must be scaled by speeds.
 * Costs can be pre-calculated where they don't need to be scaled by speeds.
 *
 * Currently this requires special tags in the OSM input data, which must be generated by the data provider using
 * external scripts. This mechanism could eventually be extended, with R5 deriving these characteristics from
 * standard OSM tags that are used in a wider range of places.
 *
 * Input data has forward/backward tags, because OSM ways represent both directions of a road.
 * R5 edges are directed, so all costs are "forward".
 *
 * Instances of this class are for a single mode of travel; several instances can be used for different modes of travel.
 */
public class SingleModeTraversalTimes implements Serializable {

    private static final Logger LOG = LoggerFactory.getLogger(SingleModeTraversalTimes.class);

    // This instance augments this EdgeStore with additional information about the same edges.
    // TODO separate calculator from TraversalTimes storage so we don't need this reference,
    //  maybe initialize this data structure with a new "labeler".
    private final EdgeStore edgeStore;

    // The length of all array fields should be the same, and equal to this field.
    private int nEdges = 0;

    // The effective length of the edge in meters, accounting for slope, which will be scaled by speed.
    // This assumes a linear response to speed at increasing slopes, which is probably not accurate. Eventually
    // fixed time-like costs that should not be scaled by speed (e.g. due to danger) should be handled separately.
    // TODO slow down stairs, update above comment after final decision to store multipliers or perceived lengths
    TDoubleList perceivedLengthMultipliers = new TDoubleArrayList();

    // The expected wait times to turn left or right or go straight through the intersection after the edge.
    // These are stored as times because they are not scaled by user-specified speed so are effectively pre-divided.
    TIntList leftTurnSeconds = new TIntArrayList();
    TIntList rightTurnSeconds = new TIntArrayList();
    TIntList straightThroughSeconds = new TIntArrayList();

    public SingleModeTraversalTimes (EdgeStore edgeStore) {
        this.edgeStore = edgeStore;
    }

    public int size() {
        // All lists should be the same length by design.
        return nEdges;
    }

    public void summarize (String prefix) {
        LOG.info("{} length multipliers: {}", prefix, Stats.of(perceivedLengthMultipliers.toArray()));
        LOG.info("{} left turn times: {}", prefix, Stats.of(leftTurnSeconds.toArray()));
        LOG.info("{} right turn times: {}", prefix, Stats.of(rightTurnSeconds.toArray()));
        LOG.info("{} straight times: {}", prefix, Stats.of(straightThroughSeconds.toArray()));
    }

    /**
     * Interface for classes that calculate generalized costs for a single edge.
     * This could be a data class rather than an interface if we were not using the flyweight / column-store pattern.
     * TODO data class rather than interface, we're just instantiating zillions of "providers" instead of Records.
     */
    public interface Supplier {
        /** @return the factor by which the edge length should be scaled to account for slopes and other impedance when walking. */
        double perceivedLengthMultipler ();
        /** @return the expected amount of time it will take to make the given turn _after_ traversing this edge. */
        int turnTimeSeconds (TurnDirection turnDirection);
    }


    /**
     * Add data for a single edge, setting scaling factors to 1 and constant costs to 0.
     * This serves as a neutral starting point for adding generalized costs later in modifications.
     */
    public void addOneNeutralEdge () {
        perceivedLengthMultipliers.add(1);
        this.leftTurnSeconds.add(0);
        this.rightTurnSeconds.add(0);
        this.straightThroughSeconds.add(0);
        nEdges += 1;
    }

    public void setOneEdge (
            int edge,
            double perceivedLengthMultipler,
            int leftTurnSeconds,
            int rightTurnSeconds,
            int straightThroughSeconds
    ) {
        perceivedLengthMultipliers.set(edge, perceivedLengthMultipler);
        this.leftTurnSeconds.set(edge, leftTurnSeconds);
        this.rightTurnSeconds.set(edge, rightTurnSeconds);
        this.straightThroughSeconds.set(edge, straightThroughSeconds);
    }

    public void setOneEdge (int edge, Supplier supplier) {
        // TODO could pre-multiply by edgeLengthMm passed in from caller, which makes street modifications a little trickier
        setOneEdge(
            edge,
            supplier.perceivedLengthMultipler(),
            supplier.turnTimeSeconds(TurnDirection.LEFT),
            supplier.turnTimeSeconds(TurnDirection.RIGHT),
            supplier.turnTimeSeconds(TurnDirection.STRAIGHT)
        );
    }

    /**
     * The generalized cost formulae provided to us are in terms of perceived distance.
     * We want our output to be in perceived time. When making this conversion, speed settings should affect certain
     * costs that are perceived relative to the length of the road; other costs like turn costs should be converted
     * to time using a standardized speed because they will not decrease for a faster walker / biker.
     * For the time being though, we return only distance units which are scaled by the speed in the
     *
     * Note that this method matches the implicit interface of TurnCostCalculator.
     * Perhaps GeneralizedCosts can be split out into a custom TurnCostCalculator and WeightCalculator.
     * TODO check toEdge vs. fromEdge parameter order, considering forward/reverse searches
     *
     * Note that TurnEffectiveTime and TraversalEffectiveDistance could be precomputed to reduce runtime computation.
     * This would also allow setting them directly from tags on the input data, or in scenario modifications.
     * Those two array fields would not clutter the EdgeStore significantly, and could be nulled when not present.
     * Also note that some of these are not effective/perceived times but real clock times - e.g. for left turns.
     * Therefore we may want to have an interface that computes turnTimes as well as turnCosts; and an interface that
     * computes edge traversalTimes as well as traversalCosts (which may be length scaling factors or constants).
     */


    public int turnTimeSeconds (int fromEdge, int toEdge) {
        // Only find nonzero turn cost if there is a previous edge (we are not on the first edge transition)
        // and if that previous edge appears within this generalized cost table.
        if (fromEdge < 0 || fromEdge >= this.size()) {
            return 0;
        }
        // This seems inefficient, we could fetch the angles without cursors (e.g. edgeStore.angleBetweenEdges()).
        EdgeStore.Edge e = edgeStore.getCursor();
        e.seek(toEdge);
        int inAngle = e.getInAngle();
        e.seek(fromEdge);
        int outAngle = e.getOutAngle();
        // The degrees conversions are inefficient, we should be using brads (2^8 per rotation) directly
        TurnDirection turnDirection = TurnDirection.betweenAnglesDegrees(inAngle, outAngle);

        if (turnDirection == TurnDirection.LEFT) {
            return leftTurnSeconds.get(toEdge);
        } else if (turnDirection == TurnDirection.RIGHT) {
            return rightTurnSeconds.get(toEdge);
        } else if (turnDirection == TurnDirection.STRAIGHT){
            return straightThroughSeconds.get(toEdge);
        } else {
            // Constant for u-turn time?
            return 60;
        }
    }

    public int traversalTimeSeconds (EdgeStore.Edge currentEdge, double speedMetersPerSecond) {
        // Should we round instead of tuncating to avoid zero travel times?
        return (int) FastMath.ceil(perceivedTraversalMeters(currentEdge) / speedMetersPerSecond);
    }

    private double perceivedTraversalMeters (EdgeStore.Edge edge) {
        double distance = edge.getLengthM();
        if (edge.edgeIndex < this.size()) {
            // This is performing a lot of extra multiplication. The perceived distances could be premultiplied, but
            // that would require somehow getting the edge lengths into the GeneralizedCosts factory methods.
            double multiplier = perceivedLengthMultipliers.get(edge.edgeIndex);
            return distance * multiplier;
        } else {
            // Some edges may not appear in this table, their length will be returned unscaled.
            // FIXME currently including split edges.
            return distance;
        }
    }

    // TODO promote from inner class to top level
    enum TurnDirection {
        STRAIGHT, RIGHT, LEFT, UTURN;
        public static TurnDirection forAngleDegrees (double angleDegrees) {
            if (angleDegrees < 27) {
                return STRAIGHT;
            } else if (angleDegrees < 180) {
                return RIGHT;
            } else if (angleDegrees < 333) {
                return LEFT;
            } else {
                return STRAIGHT;
            }
        }
        public static TurnDirection betweenAnglesDegrees (int inAngle, int outAngle) {
            if (inAngle < outAngle) {
                inAngle += 360;
            }
            int turnAngle = inAngle - outAngle;
            return TurnDirection.forAngleDegrees(turnAngle);
        }
    }

    public SingleModeTraversalTimes extendOnlyCopy (EdgeStore edgeStore) {
        SingleModeTraversalTimes copy = new SingleModeTraversalTimes(edgeStore);
        // If we eventually store lengths instead of factors we can use TIntAugmentedList.
        copy.perceivedLengthMultipliers = new TDoubleAugmentedList(perceivedLengthMultipliers);
        copy.rightTurnSeconds = new TIntAugmentedList(rightTurnSeconds);
        copy.leftTurnSeconds = new TIntAugmentedList(leftTurnSeconds);
        copy.straightThroughSeconds = new TIntAugmentedList(straightThroughSeconds);
        copy.nEdges = nEdges;
        return copy;
    }

    /**
     * Copy traversal time multiplier and turn times from an old edge to a new one.
     * The multiplier will be overwritten with the supplied one if the parameter is non-null;
     * For use only on scenario StreetLayers.
     */
    public void copyTimes (int oldEdge, int newEdge, Double perceivedLengthMultiplier) {
        if (edgeStore.isExtendOnlyCopy()) {
            if (perceivedLengthMultiplier == null) {
                perceivedLengthMultiplier = perceivedLengthMultipliers.get(oldEdge);
            }
            perceivedLengthMultipliers.set(newEdge, perceivedLengthMultiplier);
            leftTurnSeconds.set(newEdge, leftTurnSeconds.get(oldEdge));
            rightTurnSeconds.set(newEdge, rightTurnSeconds.get(oldEdge));
            straightThroughSeconds.set(newEdge, straightThroughSeconds.get(oldEdge));
        } else {
            throw new UnsupportedOperationException("Cannot extend non-scenario layer.");
        }
    }

}
