/*
 * Copyright (c) 2016 Vivid Solutions.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
 * and the Eclipse Distribution License is available at
 *
 * http://www.eclipse.org/org/documents/edl-v10.php.
 */

package org.locationtech.jts.linearref;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LineSegment;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.MultiLineString;

/**
 * Represents a location along a {@link LineString} or {@link MultiLineString}.
 * The referenced geometry is not maintained within
 * this location, but must be provided for operations which require it.
 * Various methods are provided to manipulate the location value
 * and query the geometry it references.
 */
public class LinearLocation
    implements Comparable
{
   /**
    * Gets a location which refers to the end of a linear {@link Geometry}.
    * @param linear the linear geometry
    * @return a new <tt>LinearLocation</tt>
    */
  public static LinearLocation getEndLocation(Geometry linear)
  {
    // assert: linear is LineString or MultiLineString
    LinearLocation loc = new LinearLocation();
    loc.setToEnd(linear);
    return loc;
  }

  /**
   * Computes the {@link Coordinate} of a point a given fraction
   * along the line segment <tt>(p0, p1)</tt>.
   * If the fraction is greater than 1.0 the last
   * point of the segment is returned.
   * If the fraction is less than or equal to 0.0 the first point
   * of the segment is returned.
   * The Z ordinate is interpolated from the Z-ordinates of the given points,
   * if they are specified.
   *
   * @param p0 the first point of the line segment
   * @param p1 the last point of the line segment
   * @param frac the length to the desired point
   * @return the <tt>Coordinate</tt> of the desired point
   */
  public static Coordinate pointAlongSegmentByFraction(Coordinate p0, Coordinate p1, double frac)
  {
    if (frac <= 0.0) return p0;
    if (frac >= 1.0) return p1;

    double x = (p1.x - p0.x) * frac + p0.x;
    double y = (p1.y - p0.y) * frac + p0.y;
    // interpolate Z value. If either input Z is NaN, result z will be NaN as well.
    double z = (p1.getZ() - p0.getZ()) * frac + p0.getZ();
    return new Coordinate(x, y, z);
  }

  private int componentIndex = 0;
  private int segmentIndex = 0;
  private double segmentFraction = 0.0;

  /**
   * Creates a location referring to the start of a linear geometry
   */
  public LinearLocation()
  {
  }

  public LinearLocation(int segmentIndex, double segmentFraction) {
    this(0, segmentIndex, segmentFraction);
  }

  public LinearLocation(int componentIndex, int segmentIndex, double segmentFraction)
  {
    this.componentIndex = componentIndex;
    this.segmentIndex = segmentIndex;
    this.segmentFraction = segmentFraction;
    normalize();
  }

  private LinearLocation(int componentIndex, int segmentIndex, double segmentFraction, boolean doNormalize)
  {
    this.componentIndex = componentIndex;
    this.segmentIndex = segmentIndex;
    this.segmentFraction = segmentFraction;
    if (doNormalize) 
      normalize();
  }

  /**
   * Creates a new location equal to a given one.
   * 
   * @param loc a LinearLocation
   */
  public LinearLocation(LinearLocation loc)
  {
    this.componentIndex = loc.componentIndex;
    this.segmentIndex = loc.segmentIndex;
    this.segmentFraction = loc.segmentFraction;
  }

  /**
   * Ensures the individual values are locally valid.
   * Does <b>not</b> ensure that the indexes are valid for
   * a particular linear geometry.
   *
   * @see clamp
   */
  private void normalize()
  {
    if (segmentFraction < 0.0) {
      segmentFraction = 0.0;
    }
    if (segmentFraction > 1.0) {
      segmentFraction = 1.0;
    }

    if (componentIndex < 0) {
      componentIndex = 0;
      segmentIndex = 0;
      segmentFraction = 0.0;
    }
    if (segmentIndex < 0) {
      segmentIndex = 0;
      segmentFraction = 0.0;
    }
    if (segmentFraction == 1.0) {
      segmentFraction = 0.0;
      segmentIndex += 1;
    }
  }


  /**
   * Ensures the indexes are valid for a given linear {@link Geometry}.
   *
   * @param linear a linear geometry
   */
  public void clamp(Geometry linear)
  {
    if (componentIndex >= linear.getNumGeometries()) {
      setToEnd(linear);
      return;
    }
    if (segmentIndex >= linear.getNumPoints()) {
      LineString line = (LineString) linear.getGeometryN(componentIndex);
      segmentIndex = numSegments(line);
      segmentFraction = 1.0;
    }
  }
  /**
   * Snaps the value of this location to
   * the nearest vertex on the given linear {@link Geometry},
   * if the vertex is closer than <tt>minDistance</tt>.
   *
   * @param linearGeom a linear geometry
   * @param minDistance the minimum allowable distance to a vertex
   */
  public void snapToVertex(Geometry linearGeom, double minDistance)
  {
    if (segmentFraction <= 0.0 || segmentFraction >= 1.0)
      return;
    double segLen = getSegmentLength(linearGeom);
    double lenToStart = segmentFraction * segLen;
    double lenToEnd = segLen - lenToStart;
    if (lenToStart <= lenToEnd && lenToStart < minDistance) {
      segmentFraction = 0.0;
    }
    else if (lenToEnd <= lenToStart && lenToEnd < minDistance) {
      segmentFraction = 1.0;
    }
  }

  /**
   * Gets the length of the segment in the given
   * Geometry containing this location.
   *
   * @param linearGeom a linear geometry
   * @return the length of the segment
   */
  public double getSegmentLength(Geometry linearGeom)
  {
    LineString lineComp = (LineString) linearGeom.getGeometryN(componentIndex);

    // ensure segment index is valid
    int segIndex = segmentIndex;
    if (segmentIndex >= numSegments(lineComp))
      segIndex = lineComp.getNumPoints() - 2;

    Coordinate p0 = lineComp.getCoordinateN(segIndex);
    Coordinate p1 = lineComp.getCoordinateN(segIndex + 1);
    return p0.distance(p1);
  }

  /**
   * Sets the value of this location to
   * refer to the end of a linear geometry.
   *
   * @param linear the linear geometry to use to set the end
   */
  public void setToEnd(Geometry linear)
  {
    componentIndex = linear.getNumGeometries() - 1;
    LineString lastLine = (LineString) linear.getGeometryN(componentIndex);
    segmentIndex = numSegments(lastLine);
    segmentFraction = 0.0;
  }

  /**
   * Gets the component index for this location.
   *
   * @return the component index
   */
  public int getComponentIndex() { return componentIndex; }

  /**
   * Gets the segment index for this location
   *
   * @return the segment index
   */
  public int getSegmentIndex() { return segmentIndex; }

  /**
   * Gets the segment fraction for this location
   *
   * @return the segment fraction
   */
  public double getSegmentFraction() { return segmentFraction; }

  /**
   * Tests whether this location refers to a vertex
   *
   * @return true if the location is a vertex
   */
  public boolean isVertex()
  {
    return segmentFraction <= 0.0 || segmentFraction >= 1.0;
  }

  /**
   * Gets the {@link Coordinate} along the
   * given linear {@link Geometry} which is
   * referenced by this location.
   *
   * @param linearGeom the linear geometry referenced by this location
   * @return the <tt>Coordinate</tt> at the location
   */
  public Coordinate getCoordinate(Geometry linearGeom)
  {
    LineString lineComp = (LineString) linearGeom.getGeometryN(componentIndex);
    Coordinate p0 = lineComp.getCoordinateN(segmentIndex);
    if (segmentIndex >= numSegments(lineComp))
      return p0;
    Coordinate p1 = lineComp.getCoordinateN(segmentIndex + 1);
    return pointAlongSegmentByFraction(p0, p1, segmentFraction);
  }

  /**
   * Gets a {@link LineSegment} representing the segment of the 
   * given linear {@link Geometry} which contains this location.
   *
   * @param linearGeom a linear geometry
   * @return the <tt>LineSegment</tt> containing the location
   */
  public LineSegment getSegment(Geometry linearGeom)
  {
    LineString lineComp = (LineString) linearGeom.getGeometryN(componentIndex);
    Coordinate p0 = lineComp.getCoordinateN(segmentIndex);
    // check for endpoint - return last segment of the line if so
    if (segmentIndex >= numSegments(lineComp)) {
    	Coordinate prev = lineComp.getCoordinateN(lineComp.getNumPoints() - 2);
      return new LineSegment(prev, p0);
    }
    Coordinate p1 = lineComp.getCoordinateN(segmentIndex + 1);
    return new LineSegment(p0, p1);
  }

  /**
   * Tests whether this location refers to a valid
   * location on the given linear {@link Geometry}.
   *
   * @param linearGeom a linear geometry
   * @return true if this location is valid
   */
  public boolean isValid(Geometry linearGeom)
  {
    if (componentIndex < 0 || componentIndex >= linearGeom.getNumGeometries())
      return false;

    LineString lineComp = (LineString) linearGeom.getGeometryN(componentIndex);
    if (segmentIndex < 0 || segmentIndex > lineComp.getNumPoints())
      return false;
    if (segmentIndex == lineComp.getNumPoints() && segmentFraction != 0.0)
      return false;

    if (segmentFraction < 0.0 || segmentFraction > 1.0)
      return false;
    return true;
  }

  /**
   *  Compares this object with the specified object for order.
   *
   *@param  o  the <code>LineStringLocation</code> with which this <code>Coordinate</code>
   *      is being compared
   *@return    a negative integer, zero, or a positive integer as this <code>LineStringLocation</code>
   *      is less than, equal to, or greater than the specified <code>LineStringLocation</code>
   */
  public int compareTo(Object o) {
    LinearLocation other = (LinearLocation) o;
    // compare component indices
    if (componentIndex < other.componentIndex) return -1;
    if (componentIndex > other.componentIndex) return 1;
    // compare segments
    if (segmentIndex < other.segmentIndex) return -1;
    if (segmentIndex > other.segmentIndex) return 1;
    // same segment, so compare segment fraction
    if (segmentFraction < other.segmentFraction) return -1;
    if (segmentFraction > other.segmentFraction) return 1;
    // same location
    return 0;
  }

  /**
   *  Compares this object with the specified index values for order.
   *
   * @param componentIndex1 a component index
   * @param segmentIndex1 a segment index
   * @param segmentFraction1 a segment fraction
   * @return    a negative integer, zero, or a positive integer as this <code>LineStringLocation</code>
   *      is less than, equal to, or greater than the specified locationValues
   */
  public int compareLocationValues(int componentIndex1, int segmentIndex1, double segmentFraction1) {
    // compare component indices
    if (componentIndex < componentIndex1) return -1;
    if (componentIndex > componentIndex1) return 1;
    // compare segments
    if (segmentIndex < segmentIndex1) return -1;
    if (segmentIndex > segmentIndex1) return 1;
    // same segment, so compare segment fraction
    if (segmentFraction < segmentFraction1) return -1;
    if (segmentFraction > segmentFraction1) return 1;
    // same location
    return 0;
  }

  /**
   *  Compares two sets of location values for order.
   *
   * @param componentIndex0 a component index
   * @param segmentIndex0 a segment index
   * @param segmentFraction0 a segment fraction
   * @param componentIndex1 another component index
   * @param segmentIndex1 another segment index
   * @param segmentFraction1 another segment fraction
   *@return    a negative integer, zero, or a positive integer
   *      as the first set of location values
   *      is less than, equal to, or greater than the second set of locationValues
   */
  public static int compareLocationValues(
      int componentIndex0, int segmentIndex0, double segmentFraction0,
      int componentIndex1, int segmentIndex1, double segmentFraction1)
  {
    // compare component indices
    if (componentIndex0 < componentIndex1) return -1;
    if (componentIndex0 > componentIndex1) return 1;
    // compare segments
    if (segmentIndex0 < segmentIndex1) return -1;
    if (segmentIndex0 > segmentIndex1) return 1;
    // same segment, so compare segment fraction
    if (segmentFraction0 < segmentFraction1) return -1;
    if (segmentFraction0 > segmentFraction1) return 1;
    // same location
    return 0;
  }

  /**
   * Tests whether two locations
   * are on the same segment in the parent {@link Geometry}.
   * 
   * @param loc a location on the same geometry
   * @return true if the locations are on the same segment of the parent geometry
   */
  public boolean isOnSameSegment(LinearLocation loc)
  {
  	if (componentIndex != loc.componentIndex) return false;
  	if (segmentIndex == loc.segmentIndex) return true;
  	if (loc.segmentIndex - segmentIndex == 1 
  			&& loc.segmentFraction == 0.0) 
  		return true;
  	if (segmentIndex - loc.segmentIndex == 1 
  			&& segmentFraction == 0.0) 
  		return true;
  	return false;
  }

  /**
   * Tests whether this location is an endpoint of
   * the linear component it refers to.
   * 
   * @param linearGeom the linear geometry referenced by this location
   * @return true if the location is a component endpoint
   */
  public boolean isEndpoint(Geometry linearGeom)
  {
    LineString lineComp = (LineString) linearGeom.getGeometryN(componentIndex);
    // check for endpoint
    int nseg = numSegments(lineComp);
    return segmentIndex >= nseg
        || (segmentIndex == nseg - 1 && segmentFraction >= 1.0);
  }

  /**
   * Converts a linear location to the lowest equivalent location index.
   * The lowest index has the lowest possible component and segment indices.
   * <p>
   * Specifically:
   * <ul>
   * <li>if the location point is an endpoint, a location value is returned as (nseg-1, 1.0)
   * <li>if the location point is ambiguous (i.e. an endpoint and a startpoint), the lowest endpoint location is returned
   * </ul>
   * If the location index is already the lowest possible value, the original location is returned.
   * 
   * @param linearGeom the linear geometry referenced by this location
   * @return the lowest equivalent location
   */
  public LinearLocation toLowest(Geometry linearGeom)
  {
    // TODO: compute lowest component index
    LineString lineComp = (LineString) linearGeom.getGeometryN(componentIndex);
    int nseg = numSegments(lineComp);
    // if not an endpoint can be returned directly
    if (segmentIndex < nseg) return this;
    return new LinearLocation(componentIndex, nseg - 1, 1.0, false);
  }
  
  /**
   * Copies this location
   *
   * @return a copy of this location
   * @deprecated
   */
  public Object clone()
  {
    return copy();
  }
  
  /**
   * Copies this location
   *
   * @return a copy of this location
   */
  public LinearLocation copy() {
	return new LinearLocation(componentIndex, segmentIndex, segmentFraction);
  }
  
  public String toString()
  {
    return "LinearLoc[" 
    + componentIndex + ", "
    + segmentIndex + ", "
    + segmentFraction + "]";
  }
  
  /**
   * Gets the count of the number of line segments
   * in a {@link LineString}.  This is one less than the 
   * number of coordinates.
   * 
   * @param line a LineString
   * @return the number of segments
   */
  private static int numSegments(LineString line) {
    int npts = line.getNumPoints();
    if (npts <= 1) return 0;
    return npts - 1;
  }
}
