/*
 * 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.simplify;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateList;
import org.locationtech.jts.geom.Triangle;

/**
 * Simplifies a linestring (sequence of points) using the 
 * Visvalingam-Whyatt algorithm.
 * The Visvalingam-Whyatt algorithm simplifies geometry 
 * by removing vertices while trying to minimize the area changed.
 * 
 * @version 1.7
 */
class VWLineSimplifier
{
  public static Coordinate[] simplify(Coordinate[] pts, double distanceTolerance)
  {
    VWLineSimplifier simp = new VWLineSimplifier(pts, distanceTolerance);
    return simp.simplify();
  }

  private Coordinate[] pts;
  private double tolerance;

  public VWLineSimplifier(Coordinate[] pts, double distanceTolerance)
  {
    this.pts = pts;
    this.tolerance = distanceTolerance * distanceTolerance;
  }

  public Coordinate[] simplify()
  {
    VWLineSimplifier.VWVertex vwLine = VWVertex.buildLine(pts);
    double minArea = tolerance;
    do {
      minArea = simplifyVertex(vwLine);
    } while (minArea < tolerance);
    Coordinate[] simp = vwLine.getCoordinates();
    // ensure computed value is a valid line
    if (simp.length < 2) {
      return new Coordinate[] { simp[0], new Coordinate(simp[0]) };
    }
    return simp;
  }

  private double simplifyVertex(VWLineSimplifier.VWVertex vwLine)
  {
    /**
     * Scan vertices in line and remove the one with smallest effective area.
     */
    // TODO: use an appropriate data structure to optimize finding the smallest area vertex
    VWLineSimplifier.VWVertex curr = vwLine;
    double minArea = curr.getArea();
    VWLineSimplifier.VWVertex minVertex = null;
    while (curr != null) {
      double area = curr.getArea();
      if (area < minArea) {
        minArea = area;
        minVertex = curr;
      }
      curr = curr.next;
    }
    if (minVertex != null && minArea < tolerance) {
      minVertex.remove();
    }
    if (! vwLine.isLive()) return -1;
    return minArea;
  }


  static class VWVertex
  {
    public static VWLineSimplifier.VWVertex buildLine(Coordinate[] pts)
    {
      VWLineSimplifier.VWVertex first = null;
      VWLineSimplifier.VWVertex prev = null;
      for (int i = 0; i < pts.length; i++) {
        VWLineSimplifier.VWVertex v = new VWVertex(pts[i]);
        if (first == null)
          first = v;
        v.setPrev(prev);
        if (prev != null) {
          prev.setNext(v);
          prev.updateArea();
        }
        prev = v;
      }
      return first;
    }
    
    public static double MAX_AREA = Double.MAX_VALUE;
    
    private Coordinate pt;
    private VWLineSimplifier.VWVertex prev;
    private VWLineSimplifier.VWVertex next;
    private double area = MAX_AREA;
    private boolean isLive = true;

    public VWVertex(Coordinate pt)
    {
      this.pt = pt;
    }

    public void setPrev(VWLineSimplifier.VWVertex prev)
    {
      this.prev = prev;
    }

    public void setNext(VWLineSimplifier.VWVertex next)
    {
      this.next = next;
    }

    public void updateArea()
    {
      if (prev == null || next == null) {
        area = MAX_AREA;
        return;
      }
      area = Math.abs(Triangle.area(prev.pt, pt, next.pt));
    }

    public double getArea()
    {
      return area;
    }
    public boolean isLive()
    {
      return isLive;
    }
    public VWLineSimplifier.VWVertex remove()
    {
      VWLineSimplifier.VWVertex tmpPrev = prev;
      VWLineSimplifier.VWVertex tmpNext = next;
      VWLineSimplifier.VWVertex result = null;
      if (prev != null) {
        prev.setNext(tmpNext);
        prev.updateArea();
        result = prev;
      }
      if (next != null) {
        next.setPrev(tmpPrev);
        next.updateArea();
        if (result == null)
          result = next;
      }
      isLive = false;
      return result;
    }
    public Coordinate[] getCoordinates()
    {
      CoordinateList coords = new CoordinateList();
      VWLineSimplifier.VWVertex curr = this;
      do {
        coords.add(curr.pt, false);
        curr = curr.next;
      } while (curr != null);
      return coords.toCoordinateArray();
    }
  }
}
