/**
 * This class is an helper for Edges and Vertexes to store various data about elevation profiles.
 */
public class ElevationProfileSegment implements Serializable {

  private static final long serialVersionUID = MavenVersion.VERSION.getUID();

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

  private PackedCoordinateSequence elevationProfile;

  private double length;

  private double slopeSpeedEffectiveLength;

  private double bicycleSafetyEffectiveLength;

  private double slopeWorkCost;

  private double maxSlope;

  protected boolean slopeOverride;

  private boolean flattened;

  public ElevationProfileSegment(double length) {
    this.length = length;
    slopeSpeedEffectiveLength = length;
    bicycleSafetyEffectiveLength = length;
    slopeWorkCost = length;
  }

  public double getMaxSlope() {
    return maxSlope;
  }

  public void setSlopeOverride(boolean slopeOverride) {
    this.slopeOverride = slopeOverride;
  }

  public boolean getSlopeOverride() {
    return slopeOverride;
  }

  public void setLength(double length) {
    this.length = length;
  }

  public double getLength() {
    return length;
  }

  public void setSlopeSpeedEffectiveLength(double slopeSpeedEffectiveLength) {
    this.slopeSpeedEffectiveLength = slopeSpeedEffectiveLength;
  }

  public double getSlopeSpeedEffectiveLength() {
    return slopeSpeedEffectiveLength;
  }

  public void setBicycleSafetyEffectiveLength(double bicycleSafetyEffectiveLength) {
    this.bicycleSafetyEffectiveLength = bicycleSafetyEffectiveLength;
  }

  public double getBicycleSafetyEffectiveLength() {
    return bicycleSafetyEffectiveLength;
  }

  public void setSlopeWorkCost(double slopeWorkCost) {
    this.slopeWorkCost = slopeWorkCost;
  }

  public double getSlopeWorkCost() {
    return slopeWorkCost;
  }

  public PackedCoordinateSequence getElevationProfile() {
    return elevationProfile;
  }

  public PackedCoordinateSequence getElevationProfile(double start, double end) {
    return ElevationUtils.getPartialElevationProfile(elevationProfile, start, end);
  }

  public void setElevationProfile(PackedCoordinateSequence elevationProfile) {
    this.elevationProfile = elevationProfile;
  }

  public boolean setElevationProfile(
      PackedCoordinateSequence elev, boolean computed, boolean slopeLimit) {
    if (elev == null || elev.size() < 2) {
      return false;
    }

    if (slopeOverride && !computed) {
      return false;
    }

    elevationProfile = elev;

    // compute the various costs of the elevation changes
    double lengthMultiplier = ElevationUtils.getLengthMultiplierFromElevation(elev);
    if (Double.isNaN(lengthMultiplier)) {
      LOG.error("lengthMultiplier from elevation profile is NaN, setting to 1");
      lengthMultiplier = 1;
    }

    length *= lengthMultiplier;
    bicycleSafetyEffectiveLength *= lengthMultiplier;

    SlopeCosts costs = ElevationUtils.getSlopeCosts(elev, slopeLimit);
    slopeSpeedEffectiveLength = costs.slopeSpeedEffectiveLength;
    maxSlope = costs.maxSlope;
    slopeWorkCost = costs.slopeWorkCost;
    bicycleSafetyEffectiveLength += costs.slopeSafetyCost;
    flattened = costs.flattened;

    return costs.flattened;
  }

  public String toString() {
    String out = "";
    if (elevationProfile == null || elevationProfile.size() == 0) {
      return "(empty elevation profile)";
    }
    for (int i = 0; i < elevationProfile.size(); ++i) {
      Coordinate coord = elevationProfile.getCoordinate(i);
      out += "(" + coord.x + "," + coord.y + "), ";
    }
    return out.substring(0, out.length() - 2);
  }

  public boolean isFlattened() {
    return flattened;
  }
}
public class MultiShortestPathTree extends AbstractShortestPathTree {

  private static final long serialVersionUID = MavenVersion.VERSION.getUID();

  private Map<Vertex, List<State>> stateSets;

  public MultiShortestPathTree(RoutingRequest options) {
    super(options);
    stateSets = new IdentityHashMap<Vertex, List<State>>();
  }

  public Set<Vertex> getVertices() {
    return stateSets.keySet();
  }

  /** ** {@link ShortestPathTree} Interface ** */
  @Override
  public boolean add(State newState) {
    Vertex vertex = newState.getVertex();
    List<State> states = stateSets.get(vertex);

    // if the vertex has no states, add one and return
    if (states == null) {
      states = new ArrayList<State>();
      stateSets.put(vertex, states);
      states.add(newState);
      return true;
    }

    // if the vertex has any states that dominate the new state, don't add the state
    // if the new state dominates any old states, remove them
    Iterator<State> it = states.iterator();
    while (it.hasNext()) {
      State oldState = it.next();
      // order is important, because in the case of a tie
      // we want to reject the new state
      if (dominates(oldState, newState)) return false;
      if (dominates(newState, oldState)) it.remove();
    }

    // any states remaining are codominent with the new state
    states.add(newState);
    return true;
  }

  public static boolean dominates(State thisState, State other) {
    if (other.weight == 0) {
      return false;
    }
    // Multi-state (bike rental, P+R) - no domination for different states
    if (thisState.isBikeRenting() != other.isBikeRenting()) return false;
    if (thisState.isCarParked() != other.isCarParked()) return false;

    if (thisState.backEdge != other.getBackEdge()
        && ((thisState.backEdge instanceof StreetEdge)
            && (!((StreetEdge) thisState.backEdge).getTurnRestrictions().isEmpty()))) return false;

    if (thisState.routeSequenceSubset(other)) {
      // TODO subset is not really the right idea
      return thisState.weight <= other.weight
          && thisState.getElapsedTimeSeconds() <= other.getElapsedTimeSeconds();
      // && this.getNumBoardings() <= other.getNumBoardings();
    }

    // If returning more than one result from GenericAStar, the search can be very slow
    // unless you replace the following code with:
    // return false;
    boolean walkDistanceBetter = thisState.walkDistance <= other.getWalkDistance() * 1.05;
    double weightRatio = thisState.weight / other.weight;
    boolean weightBetter = (weightRatio < 1.02 && thisState.weight - other.weight < 30);
    boolean timeBetter = thisState.getElapsedTimeSeconds() - other.getElapsedTimeSeconds() <= 30;

    return walkDistanceBetter && weightBetter && timeBetter;
    //    	return this.weight < other.weight;
  }

  @Override
  public State getState(Vertex dest) {
    Collection<State> states = stateSets.get(dest);
    if (states == null) return null;
    State ret = null;
    for (State s : states) {
      if ((ret == null || s.betterThan(ret)) && s.isFinal() && s.allPathParsersAccept()) {
        ret = s;
      }
    }
    return ret;
  }

  @Override
  public List<State> getStates(Vertex dest) {
    return stateSets.get(dest);
  }

  @Override
  public int getVertexCount() {
    return stateSets.keySet().size();
  }

  /**
   * Check that a state coming out of the queue is still in the Pareto-optimal set for this vertex,
   * which indicates that it has not been ruled out as a state on an optimal path. Many shortest
   * path algorithms will decrease the key of an entry in the priority queue when it is updated, or
   * remove it when it is dominated.
   *
   * <p>When the Fibonacci heap was replaced with a binary heap, the decrease-key operation was
   * removed for the same reason: both improve theoretical run time complexity, at the cost of high
   * constant factors and more complex code.
   *
   * <p>So there can be dominated (useless) states in the queue. When they come out we want to
   * ignore them rather than spend time branching out from them.
   */
  @Override
  public boolean visit(State state) {
    boolean ret = false;
    for (State s : stateSets.get(state.getVertex())) {
      if (s == state) {
        ret = true;
        break;
      }
    }
    return ret;
  }

  public String toString() {
    return "MultiSPT(" + this.stateSets.size() + " vertices)";
  }

  @Override
  public Collection<State> getAllStates() {
    ArrayList<State> allStates = new ArrayList<State>();
    for (List<State> stateSet : stateSets.values()) {
      allStates.addAll(stateSet);
    }
    return allStates;
  }
}