/*
 * 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.Arrays;
import java.util.Iterator;
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.GeometryCollection;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.GeometryFilter;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.index.SpatialIndex;
import org.locationtech.jts.index.chain.MonotoneChain;
import org.locationtech.jts.index.chain.MonotoneChainBuilder;
import org.locationtech.jts.index.hprtree.HPRtree;
import org.locationtech.jts.index.kdtree.KdNode;
import org.locationtech.jts.index.kdtree.KdTree;
import org.locationtech.jts.index.quadtree.Quadtree;
import org.locationtech.jts.index.strtree.AbstractNode;
import org.locationtech.jts.index.strtree.Boundable;
import org.locationtech.jts.index.strtree.GeometryItemDistance;
import org.locationtech.jts.index.strtree.STRtree;
//import org.locationtech.jts.triangulatepoly.VertexSequencePackedRtree;
import org.locationtech.jts.math.MathUtil;


public class SpatialIndexFunctions
{
  public static Geometry kdTreeQuery(Geometry pts, Geometry queryEnv, double tolerance)
  {
    KdTree index = buildKdTree(pts, tolerance);
    // if no query env provided query everything inserted 
    if (queryEnv == null) queryEnv = pts;
    List result = index.query(queryEnv.getEnvelopeInternal());
    Coordinate[] resultCoords = KdTree.toCoordinates(result);
    return pts.getFactory().createMultiPoint(resultCoords);
  }

  private static KdTree indexKDcache = null;
  private static Geometry indexKDGeom = null;
  
  public static Geometry kdTreeQueryCached(Geometry pts, Geometry queryEnv, double tolerance)
  {
    if (indexKDGeom != pts || indexKDcache == null) {
      indexKDcache = buildKdTree(pts, tolerance);
      indexKDGeom = pts;
    }
    // if no query env provided query everything inserted 
    if (queryEnv == null) queryEnv = pts;
    List result = indexKDcache.query(queryEnv.getEnvelopeInternal());
    Coordinate[] resultCoords = KdTree.toCoordinates(result);
    return pts.getFactory().createMultiPoint(resultCoords);
  }

  public static Geometry kdTreeQueryRepeated(Geometry pts, Geometry queryEnv, double tolerance)
  {
    KdTree index = buildKdTree(pts, tolerance);
    // if no query env provided query everything inserted 
    if (queryEnv == null) queryEnv = pts;
    List result = index.query(queryEnv.getEnvelopeInternal());
    Coordinate[] resultCoords = KdTree.toCoordinates(result, true);
    return pts.getFactory().createMultiPoint(resultCoords);
  }

  public static Geometry kdTreeGraphSeed(Geometry geom) {
    return kdTreeGraph(geom, buildKdTreeSeed(geom, 0));
  }

  public static Geometry kdTreeGraph(Geometry geom) {
    return kdTreeGraph(geom, buildKdTree(geom, 0));
  }
  
  private static Geometry kdTreeGraph(Geometry geom, KdTree index) {
    KdNode root = index.getRoot();
    List<Geometry> edges = new ArrayList<Geometry>();
    
    double x = geom.getEnvelopeInternal().centre().getX();
    double xInc = geom.getEnvelopeInternal().getWidth() / 2;
    addGraphEdges(root, true, 0, x, xInc, edges, geom.getFactory());
    return geom.getFactory().buildGeometry(edges);
  }
  
  private static void addGraphEdges(KdNode node, 
      boolean isXLevel, int depth, double x, double xInc,
      List<Geometry> edges, GeometryFactory factory) {
    double xInc2 = xInc / 2;
    KdNode left = node.getLeft();
    if (left != null) {
      double xLeft = x - xInc2;
      Geometry edgeLeft = factory.createLineString(new Coordinate[] {
          new Coordinate(x, -depth), new Coordinate(xLeft, -depth-1)
      });
      edges.add(edgeLeft);
      addGraphEdges(left, ! isXLevel, depth+1, xLeft, xInc2, edges, factory);
    }
    KdNode right = node.getRight();
    if (right != null) {
      double xRight = x + xInc2;
      Geometry edgeRight = factory.createLineString(new Coordinate[] {
          new Coordinate(x, -depth), new Coordinate(xRight, -depth-1)
      });
      edges.add(edgeRight);
      addGraphEdges(right, ! isXLevel, depth+1, xRight, xInc2, edges, factory);
    }
  }

  public static Geometry kdTreeSplits(Geometry geom) {
    return kdTreeSplits(geom, buildKdTree(geom, 0));
  }
  
  public static Geometry kdTreeSplitsSeed(Geometry geom) {
    return kdTreeSplits(geom, buildKdTreeSeed(geom, 0));
  }

  private static Geometry kdTreeSplits(Geometry geom, KdTree index) {
    Envelope extent = geom.getEnvelopeInternal();
    KdNode root = index.getRoot();
    List<Geometry> splits = new ArrayList<Geometry>();
    
    addSplits(root, true, extent, splits, geom.getFactory());
    return geom.getFactory().buildGeometry(splits);
  }
  
  private static void addSplits(KdNode node, boolean isXLevel, Envelope extent, List<Geometry> splits,
      GeometryFactory factory) {
    double splitVal = node.splitValue(isXLevel);
    Geometry splitLine = createSplitLine(extent, splitVal, isXLevel, factory);
    splits.add(splitLine);
    
    KdNode left = node.getLeft();
    if (left != null) {
      addSplits(left, ! isXLevel, splitExtent(extent, splitVal, isXLevel, true), splits, factory);
    }
    KdNode right = node.getRight();
    if (right != null) {
      addSplits(right, ! isXLevel, splitExtent(extent, splitVal, isXLevel, false), splits, factory);
    }
  }

  private static Envelope splitExtent(Envelope extent, double splitVal, boolean isXLevel, boolean isLeft) {
    double xMin = extent.getMinX();
    double yMin = extent.getMinY();
    double xMax = extent.getMaxX();
    double yMax = extent.getMaxY();
    if (isXLevel) {
      if (isLeft) {
        xMax = splitVal;
      }
      else {
        xMin = splitVal;
      }
    }
    else {
      if (isLeft) {
        yMax = splitVal;
      }
      else {
        yMin = splitVal;
      }
    }
    return new Envelope(xMin, xMax, yMin, yMax);
  }

  private static Geometry createSplitLine(Envelope extent, double splitVal, 
      boolean isXLevel, GeometryFactory factory) {
    
    double x1 = isXLevel ? splitVal : extent.getMinX();
    double y1 = isXLevel ? extent.getMinY() : splitVal;
    double x2 = isXLevel ? splitVal : extent.getMaxX();
    double y2 = isXLevel ? extent.getMaxY() : splitVal;
    
    Coordinate[] pts = { new Coordinate(x1, y1), new Coordinate(x2, y2) };
    return factory.createLineString(pts);
  }

  private static KdTree buildKdTree(Geometry geom, double tolerance) {
    final KdTree index = new KdTree(tolerance);
    Coordinate[] pt = geom.getCoordinates();
    for (int i = 0; i < pt.length; i++) {
      index.insert(pt[i]);
    }
    return index;
  }
  
  private static KdTree buildKdTreeSeed(Geometry geom, double tolerance) {
    final KdTree tree = new KdTree(tolerance);
    Coordinate[] pt = geom.getCoordinates();
    
    //-- seed the tree with some randomly selected points
    int numSeed = pt.length / 100;
    double rand = 0;
    for (int i = 0; i < numSeed; i++) {
      rand = MathUtil.quasirandom(rand);
      int index = (int) (pt.length * rand);
      tree.insert(pt[index]);
    }
    //-- insert all the points
    for (int i = 0; i < pt.length; i++) {
      tree.insert(pt[i]);
    }
    return tree;
  }
  
  public static Geometry strTreeBounds(Geometry geoms)
  {
    STRtree index = new STRtree();
    loadIndex(geoms, index);
    List bounds = new ArrayList();
    addBounds(index.getRoot(), bounds, geoms.getFactory());
    return geoms.getFactory().buildGeometry(bounds);
  }

  private static void addBounds(Boundable bnd, List bounds,
      GeometryFactory factory) {
    // don't include bounds of leaf nodes
    if (! (bnd instanceof AbstractNode)) return;
    
    Envelope env = (Envelope) bnd.getBounds();
    bounds.add(factory.toGeometry(env));
    if (bnd instanceof AbstractNode) {
      AbstractNode node = (AbstractNode) bnd;
      List children = node.getChildBoundables();
      for (Iterator i = children.iterator(); i.hasNext(); ) {
        Boundable child = (Boundable) i.next();
        addBounds(child, bounds, factory);
      }
    }
  }

  public static Geometry hprTreeQuery(Geometry geoms, Geometry queryEnv)
  {
    HPRtree index = new HPRtree();
    loadIndex(geoms, index);
    // if no query env provided query everything inserted 
    if (queryEnv == null) queryEnv = geoms;
    List result = index.query(queryEnv.getEnvelopeInternal());
    return geoms.getFactory().buildGeometry(result);
  }

  private static HPRtree indexHPRcache = null;
  private static Geometry indexHPRGeom = null;
  
  public static Geometry hprTreeQueryCached(Geometry geoms, Geometry queryEnv)
  {
    if (indexHPRGeom != geoms || indexHPRcache == null) {
      indexHPRcache = new HPRtree();
      loadIndex(geoms, indexHPRcache);
      indexHPRGeom = geoms;
    }
    // if no query env provided query everything inserted 
    if (queryEnv == null) queryEnv = geoms;
    List result = indexHPRcache.query(queryEnv.getEnvelopeInternal());
    return geoms.getFactory().buildGeometry(result);
  }

  private static void loadIndex(Geometry geom, SpatialIndex index) {
    geom.apply(new GeometryFilter() {

      public void filter(Geometry geom) {
        // only insert atomic geometries
        if (geom instanceof GeometryCollection) return;
        index.insert(geom.getEnvelopeInternal(), geom);
      }
      
    });
  }

  public static Geometry hprTreeBounds(Geometry geoms)
  {
    HPRtree index = new HPRtree();
    loadIndex(geoms, index);
    index.build();
    Envelope[] bounds = index.getBounds();
    Geometry[] polys = new Geometry[bounds.length];
    int i = 0;
    for (Envelope env : bounds) {
      polys[i++] = geoms.getFactory().toGeometry(env);
    }
    return geoms.getFactory().createGeometryCollection(polys);
  }
  
  private static STRtree indexSTRcache = null;
  private static Geometry indexSTRGeom = null;
  
  public static Geometry strTreeQueryCached(Geometry geoms, Geometry queryEnv)
  {
    if (indexSTRGeom != geoms || indexSTRcache == null) {
      indexSTRcache = new STRtree();
      loadIndex(geoms, indexSTRcache);
      indexSTRGeom = geoms;
    }
    // if no query env provided query everything inserted 
    if (queryEnv == null) queryEnv = geoms;
    List result = indexSTRcache.query(queryEnv.getEnvelopeInternal());
    return geoms.getFactory().buildGeometry(result);
  }
  
  public static Geometry strTreeQuery(Geometry geoms, Geometry queryEnv)
  {
    STRtree index = new STRtree();
    loadIndex(geoms, index);
    // if no query env provided query everything inserted 
    if (queryEnv == null) queryEnv = geoms;
    List result = index.query(queryEnv.getEnvelopeInternal());
    return geoms.getFactory().buildGeometry(result);
  }
  
  public static Geometry strTreeNN(Geometry geoms, Geometry geom)
  {
    STRtree index = new STRtree();
    loadIndex(geoms, index);
    Object result = index.nearestNeighbour(geom.getEnvelopeInternal(), geom, new GeometryItemDistance());
    return (Geometry) result;
  }

  public static Geometry strTreeNNInSet(Geometry geoms)
  {
    STRtree index = new STRtree();
    loadIndex(geoms, index);
    Object[] result = index.nearestNeighbour(new GeometryItemDistance());
    Geometry[] resultGeoms = new Geometry[] { (Geometry) result[0], (Geometry) result[1] };
    return geoms.getFactory().createGeometryCollection(resultGeoms);
  }

  public static Geometry strTreeNNk(Geometry geoms, Geometry geom, int k)
  {
    STRtree index = new STRtree();
    loadIndex(geoms, index);
    Object[] knnObjects = index.nearestNeighbour(geom.getEnvelopeInternal(), geom, new GeometryItemDistance(), k);
    List knnGeoms = new ArrayList(Arrays.asList(knnObjects));
    Geometry geometryCollection = geoms.getFactory().buildGeometry(knnGeoms);
    return geometryCollection;
  }
  
  public static Geometry quadTreeQuery(Geometry geoms, Geometry queryEnv)
  {
    Quadtree index = buildQuadtree(geoms);
    // if no query env provided query everything inserted 
    if (queryEnv == null) queryEnv = geoms;
    List result = index.query(queryEnv.getEnvelopeInternal());
    return geoms.getFactory().buildGeometry(result);
  }

  private static Quadtree buildQuadtree(Geometry geom) {
    final Quadtree index = new Quadtree();
    geom.apply(new GeometryFilter() {

      public void filter(Geometry geom) {
        // only insert atomic geometries
        if (geom instanceof GeometryCollection) return;
        index.insert(geom.getEnvelopeInternal(), geom);
      }
      
    });
    return index;
  }
  
  public static Geometry monotoneChains(Geometry geom) {
    Coordinate[] pts = geom.getCoordinates();
    List<MonotoneChain> chains = MonotoneChainBuilder.getChains(pts);
    List<LineString> lines = new ArrayList<LineString>();
    for (MonotoneChain mc : chains) {
      Coordinate[] mcPts = mc.getCoordinates();
      LineString line = geom.getFactory().createLineString(mcPts);
      lines.add(line);
    }
    return geom.getFactory().buildGeometry(lines);
  }
  
  /*
  public static Geometry sprTreeBounds(Geometry geom)
  {
    Coordinate[] pts = geom.getCoordinates();
    VertexSequencePackedRtree index = new VertexSequencePackedRtree(pts);
    Envelope[] bounds = index.getBounds();
    Geometry[] polys = new Geometry[bounds.length];
    int i = 0;
    for (Envelope env : bounds) {
      polys[i++] = geom.getFactory().toGeometry(env);
    }
    return geom.getFactory().createGeometryCollection(polys);
  }
  */

}
