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

import java.util.ArrayList;
import java.util.List;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineSegment;
import org.locationtech.jts.geom.LineString;

/**
 * Creates a buffer polygon with variable width along a line.
 * <p>
 * Only single lines are supported as input, since buffer widths 
 * generally need to be specified specifically for each line.
 * 
 * @author Martin Davis
 *
 */
public class VariableWidthBuffer {

  /**
   * Creates a buffer polygon along a line with the width interpolated
   * between a start width and an end width.
   *  
   * @param line the line to buffer
   * @param startWidth the buffer width at the start of the line
   * @param endWidth the buffer width at the end of the line
   * @return the variable-width buffer polygon
   */
  public static Geometry buffer(Geometry line, double startWidth,
      double endWidth) {
    double[] width = VariableWidthBuffer.interpolate((LineString) line,
        startWidth, endWidth);
    VariableWidthBuffer vb = new VariableWidthBuffer(line, width);
    return vb.getResult();
  }
  
  public static Geometry bufferAternating(Geometry line, double width1,
      double width2) {
    int nPts = line.getNumPoints();
    double[] width = new double[nPts];
    for (int i = 0; i < width.length; i++) {
      width[i] = (i % 2) == 0 ? width1 : width2;
    }
    VariableWidthBuffer vb = new VariableWidthBuffer(line, width);
    return vb.getResult();
  }

  /**
   * Computes a list of values for the points along a line by
   * interpolating between values for the start and end point.
   * The interpolation is
   * based on the distance of each point along the line
   * relative to the total line length.
   * 
   * @param line the line to interpolate along
   * @param start the start value 
   * @param end the end value
   * @return the array of interpolated values
   */
  public static double[] interpolate(LineString line, double start,
      double end) {
    start = Math.abs(start);
    end = Math.abs(end);
    double[] widths = new double[line.getNumPoints()];
    widths[0] = start;
    widths[widths.length - 1] = end;

    double totalLen = line.getLength();
    Coordinate[] pts = line.getCoordinates();
    double currLen = 0;
    for (int i = 1; i < widths.length; i++) {
      double segLen = pts[i].distance(pts[i - 1]);
      currLen += segLen;
      double lenFrac = currLen / totalLen;
      double delta = lenFrac * (end - start);
      widths[i] = start + delta;
    }
    return widths;
  }

  private static double[] abs(double[] v) {
    double[] a = new double[v.length];
    for (int i = 0; i < v.length; i++) {
      a[i] = Math.abs(v[i]);
    }
    return a;
  }
  
  private LineString line;
  private double[] width;
  private GeometryFactory geomFactory;

  /**
   * Creates a generator for a variable-width line buffer.
   * 
   * @param line
   * @param width
   */
  public VariableWidthBuffer(Geometry line, double[] width) {
    this.line = (LineString) line;
    this.width = abs(width);
    geomFactory = line.getFactory();
  }
  
  /**
   * Gets the computed variable-width line buffer.
   * 
   * @return a polygon
   */
  public Geometry getResult() {
    List parts = new ArrayList();

    Coordinate[] pts = line.getCoordinates();
    for (int i = 0; i < line.getNumPoints(); i++) {
      double dist = width[i] / 2;
      // skip zero-width fillets
      if (dist > 0) {
        Geometry ptBuf = line.getPointN(i).buffer(dist);
        parts.add(ptBuf);
      }

      if (i == 0) continue;
      
      double width0 = width[i - 1];
      double width1 = width[i];
      if (width0 > 0 || width1 > 0) {
        Coordinate[] curvePts = generateSegmentCurve(pts[i - 1], pts[i],
            width0, width1);
        Geometry segBuf = geomFactory.createPolygon(curvePts);
        parts.add(segBuf);
      }
    }

    GeometryCollection partsGeom = geomFactory
        .createGeometryCollection(GeometryFactory.toGeometryArray(parts));
    Geometry buffer = partsGeom.union();
    return buffer;
  }

  private Coordinate[] generateSegmentCurve(Coordinate p0, Coordinate p1,
      double width0, double width1) {
    LineSegment seg = new LineSegment(p0, p1);

    double dist0 = width0 / 2;
    double dist1 = width1 / 2;
    Coordinate s0 = seg.pointAlongOffset(0, dist0);
    Coordinate s1 = seg.pointAlongOffset(1, dist1);
    Coordinate s2 = seg.pointAlongOffset(1, -dist1);
    Coordinate s3 = seg.pointAlongOffset(0, -dist0);

    Coordinate[] pts = new Coordinate[] { s0, s1, s2, s3, s0 };

    return pts;
  }

}
