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

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

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.shape.random.RandomPointsBuilder;
import org.locationtech.jts.shape.random.RandomPointsInGridBuilder;
import org.locationtech.jtstest.geomfunction.Metadata;


public class CreateRandomShapeFunctions {

  public static Geometry randomPointsInGrid(Geometry g, int nPts) {
  	RandomPointsInGridBuilder shapeBuilder = new RandomPointsInGridBuilder(FunctionsUtil.getFactoryOrDefault(g));
  	shapeBuilder.setExtent(FunctionsUtil.getEnvelopeOrDefault(g));
  	shapeBuilder.setNumPoints(nPts);
    return shapeBuilder.getGeometry();
  }

  public static Geometry randomPointsInGridCircles(Geometry g, int nPts) {
  	RandomPointsInGridBuilder shapeBuilder = new RandomPointsInGridBuilder(FunctionsUtil.getFactoryOrDefault(g));
  	shapeBuilder.setExtent(FunctionsUtil.getEnvelopeOrDefault(g));
  	shapeBuilder.setNumPoints(nPts);
  	shapeBuilder.setConstrainedToCircle(true);
    return shapeBuilder.getGeometry();
  }

  public static Geometry randomPointsInGridWithGutter(Geometry g, int nPts,
      @Metadata(title="Gutter fraction")
      double gutterFraction) {
  	RandomPointsInGridBuilder shapeBuilder = new RandomPointsInGridBuilder(FunctionsUtil.getFactoryOrDefault(g));
  	shapeBuilder.setExtent(FunctionsUtil.getEnvelopeOrDefault(g));
  	shapeBuilder.setNumPoints(nPts);
  	shapeBuilder.setGutterFraction(gutterFraction);
    return shapeBuilder.getGeometry();
  }

  public static Geometry randomPoints(Geometry g, int nPts) {
  	RandomPointsBuilder shapeBuilder = new RandomPointsBuilder(FunctionsUtil.getFactoryOrDefault(g));
  	shapeBuilder.setExtent(FunctionsUtil.getEnvelopeOrDefault(g));
  	shapeBuilder.setNumPoints(nPts);
    return shapeBuilder.getGeometry();
  }

  public static Geometry randomPointsInPolygon(Geometry g, int nPts) {
  	RandomPointsBuilder shapeBuilder = new RandomPointsBuilder(FunctionsUtil.getFactoryOrDefault(g));
  	shapeBuilder.setExtent(g);
  	shapeBuilder.setNumPoints(nPts);
    return shapeBuilder.getGeometry();
  }

  public static Geometry randomPointsInTriangle(Geometry g, int nPts) {
    GeometryFactory geomFact = FunctionsUtil.getFactoryOrDefault(g);
    Coordinate[] gpts = g.getCoordinates();
    Coordinate tri0 = gpts[0];
    Coordinate tri1 = gpts[1];
    Coordinate tri2 = gpts[2];
    
    List pts = new ArrayList();

    for (int i = 0; i < nPts; i++) {
      pts.add(geomFact.createPoint(randomPointInTriangle(tri0, tri1, tri2)));
    }
    return geomFact.buildGeometry(pts);
  }

  private static Coordinate randomPointInTriangle(Coordinate p0, Coordinate p1, Coordinate p2)
  {
    double s = Math.random();
    double t = Math.random();
    if (s + t > 1) {
      s = 1.0 - s;
      t = 1.0 - t;
    }
    double a = 1 - (s + t);
    double b = s;
    double c = t;
    
    double rpx = a * p0.x + b * p1.x + c * p2.x; 
    double rpy = a * p0.y + b * p1.y + c * p2.y; 
    
    return new Coordinate(rpx, rpy);
  }

  public static Geometry randomRadialPoints(Geometry g, int nPts) {
    Envelope env = FunctionsUtil.getEnvelopeOrDefault(g);
    GeometryFactory geomFact = FunctionsUtil.getFactoryOrDefault(g);
    double xLen = env.getWidth();
    double yLen = env.getHeight();
    double rMax = Math.min(xLen, yLen) / 2.0;
    
    double centreX = env.getMinX() + xLen/2;
    double centreY = env.getMinY() + yLen/2;
    
    List pts = new ArrayList();

    for (int i = 0; i < nPts; i++) {
      double rand = Math.random();
      // use rand^2 to accentuate radial distribution
      double r = rMax * rand * rand;
      // produces even distribution
      //double r = rMax * Math.sqrt(rand);
      double ang = 2 * Math.PI * Math.random();
      double x = centreX + r * Math.cos(ang);
      double y = centreY + r * Math.sin(ang);
      pts.add(geomFact.createPoint(new Coordinate(x, y)));
    }
    return geomFact.buildGeometry(pts);
  }
  
  @Metadata(description="Create Halton points using bases 2 and 3")
  public static Geometry haltonPoints(Geometry g, int nPts)
  {
    return haltonPointsWithBases(g, nPts, 2, 3);
  }
  
  @Metadata(description="Create Halton points using bases 5 and 7")
  public static Geometry haltonPoints57(Geometry g, int nPts)
  {
    return haltonPointsWithBases(g, nPts, 5, 7);
  }
  
  @Metadata(description="Create Halton points using provided bases")
  public static Geometry haltonPointsWithBases(Geometry g, int nPts, 
      @Metadata(title="Base 1")
      int basei, 
      @Metadata(title="Base 2")
      int basej)
  {
    Envelope env = FunctionsUtil.getEnvelopeOrDefault(g);
    Coordinate[] pts = new Coordinate[nPts];
    double baseX = env.getMinX();
    double baseY = env.getMinY();
    
    int i = 0;
    while (i < nPts) {
      double x = baseX + env.getWidth() * haltonOrdinate(i + 1, basei);
      double y = baseY + env.getHeight() * haltonOrdinate(i + 1, basej);
      Coordinate p = new Coordinate(x, y);
      if (! env.contains(p))
        continue;
      pts[i++] = p;
    }
    return FunctionsUtil.getFactoryOrDefault(g).createMultiPoint(pts);
  }
  
  private static double haltonOrdinate(int index, int base)
  {
    double result = 0;
    double f = 1.0 / base;
    int i = index;
    while (i > 0) {
        result = result + f * (i % base);
        i = (int) Math.floor(i / (double) base);
        f = f / base;
    }
    return result;
  }
  
  static final double PHI2 = 1.32471795724474602596;
  
  /**
   * Creates a set of quasi-random 2D points using the Roberts recurrences.
   * <a href='http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences'>Roberts recurrences</a> 
   * are based on the generalized Golden Ratio (for the 2D case, Phi2).
   * They have excellent low-discrepancy characteristics.
   * This mean they are non-periodic and have less clustering
   * than random points or Halton points.
   * 
   * @param geom
   * @param nPts
   * @return
   */
  @Metadata(description="Create Roberts quasi-random points")
  public static Geometry robertsPoints(Geometry geom, int nPts)
  {
    Envelope env = FunctionsUtil.getEnvelopeOrDefault(geom);
    Coordinate[] pts = new Coordinate[nPts];
    double baseX = env.getMinX();
    double baseY = env.getMinY();
    
    final double A1 = 1.0 / PHI2;
    final double A2 = 1.0/(PHI2 * PHI2);
    double r1 = 0.5;
    double r2 = 0.5;
    int i = 0;
    while (i < nPts) {
      r1 = quasirandom(r1, A1);
      r2 = quasirandom(r2, A2);
      double x = baseX + env.getWidth() * r1;
      double y = baseY + env.getHeight() * r2;
      Coordinate p = new Coordinate(x, y);
      if (! env.contains(p))
        continue;
      pts[i++] = p;
    }
    return FunctionsUtil.getFactoryOrDefault(geom).createMultiPoint(pts);
  }

  private static double quasirandom(double curr, double alpha) {
    double next = curr + alpha;
    if (next < 1) return next;
    return next - Math.floor(next);
  }
  
  public static Geometry randomSegments(Geometry g, int nPts) {
    Envelope env = FunctionsUtil.getEnvelopeOrDefault(g);
    GeometryFactory geomFact = FunctionsUtil.getFactoryOrDefault(g);
    double xLen = env.getWidth();
    double yLen = env.getHeight();

    List lines = new ArrayList();

    for (int i = 0; i < nPts; i++) {
      double x0 = env.getMinX() + xLen * Math.random();
      double y0 = env.getMinY() + yLen * Math.random();
      double x1 = env.getMinX() + xLen * Math.random();
      double y1 = env.getMinY() + yLen * Math.random();
      lines.add(geomFact.createLineString(new Coordinate[] {
          new Coordinate(x0, y0), new Coordinate(x1, y1) }));
    }
    return geomFact.buildGeometry(lines);
  }

  public static Geometry randomSegmentsInGrid(Geometry g, int nPts) {
    Envelope env = FunctionsUtil.getEnvelopeOrDefault(g);
    GeometryFactory geomFact = FunctionsUtil.getFactoryOrDefault(g);

    int nCell = (int) Math.sqrt(nPts) + 1;

    double xLen = env.getWidth() / nCell;
    double yLen = env.getHeight() / nCell;

    List lines = new ArrayList();

    for (int i = 0; i < nCell; i++) {
      for (int j = 0; j < nCell; j++) {
        double x0 = env.getMinX() + i * xLen + xLen * Math.random();
        double y0 = env.getMinY() + j * yLen + yLen * Math.random();
        double x1 = env.getMinX() + i * xLen + xLen * Math.random();
        double y1 = env.getMinY() + j * yLen + yLen * Math.random();
        lines.add(geomFact.createLineString(new Coordinate[] {
            new Coordinate(x0, y0), new Coordinate(x1, y1) }));
      }
    }
    return geomFact.buildGeometry(lines);
  }

  public static Geometry randomSegmentsRectilinear(Geometry g, int nPts) {
    Envelope env = FunctionsUtil.getEnvelopeOrDefault(g);
    GeometryFactory geomFact = FunctionsUtil.getFactoryOrDefault(g);
    double xLen = env.getWidth();
    double yLen = env.getHeight();

    List lines = new ArrayList();

    for (int i = 0; i < nPts; i++) {
      double x0 = env.getMinX() + xLen * Math.random();
      double x1 = env.getMinY() + yLen * Math.random();
      double v = env.getMinX() + xLen * Math.random();
      double y0 = v;
      double y1 = v;
      boolean isXFixed = Math.random() < 0.5;
      if (isXFixed) {
        y0 = x0;
        y1 = x1;
        x0 = v;
        x1 = v;
      }
      lines.add(geomFact.createLineString(new Coordinate[] {
          new Coordinate(x0, y0), new Coordinate(x1, y1) }));
    }
    return geomFact.buildGeometry(lines);
  }

  public static Geometry randomLineString(Geometry g, int nPts) {
    Envelope env = FunctionsUtil.getEnvelopeOrDefault(g);
    GeometryFactory geomFact = FunctionsUtil.getFactoryOrDefault(g);
    double width = env.getWidth();
    double hgt = env.getHeight();

    Coordinate[] pts = new Coordinate[nPts];

    for (int i = 0; i < nPts; i++) {
      double xLen = width * Math.random();
      double yLen = hgt * Math.random();
      pts[i] = randomPtInRectangleAround(env.centre(), xLen, yLen);
    }
    return geomFact.createLineString(pts);
  }

  public static Geometry randomRectilinearWalk(Geometry g, int nPts) {
    Envelope env = FunctionsUtil.getEnvelopeOrDefault(g);
    GeometryFactory geomFact = FunctionsUtil.getFactoryOrDefault(g);
    double xLen = env.getWidth();
    double yLen = env.getHeight();

    Coordinate[] pts = new Coordinate[nPts];

    boolean xory = true;
    for (int i = 0; i < nPts; i++) {
      Coordinate pt = null;
      if (i == 0) {
       pt = randomPtInRectangleAround(env.centre(), xLen, yLen);
      }
      else {
        double dist = xLen * (Math.random() - 0.5);
        double x = pts[i-1].x;
        double y = pts[i-1].y;
        if (xory) {
          x += dist;
        }
        else {
          y += dist;
        }
        // switch orientation
        xory = ! xory;
        pt = new Coordinate(x, y);
      }
      pts[i] = pt;
    }
    return geomFact.createLineString(pts);
  }

  private static int randomQuadrant(int exclude)
  {
    while (true) { 
      int quad = (int) (Math.random() * 4);
      if (quad > 3) quad = 3;
      if (quad != exclude) return quad;
    }
  }
  
  private static Coordinate randomPtInRectangleAround(Coordinate centre, double width, double height)
  {
    double x0 = centre.x + width * (Math.random() - 0.5);
    double y0 = centre.y + height * (Math.random() - 0.5);
    return new Coordinate(x0, y0);    
  }

}
