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

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

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.MultiLineString;
import org.locationtech.jts.geom.MultiPoint;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;

/**
 * A framework for processes which transform an input {@link Geometry} into
 * an output {@link Geometry}, possibly changing its structure and type(s).
 * This class is a framework for implementing subclasses
 * which perform transformations on
 * various different Geometry subclasses.
 * It provides an easy way of applying specific transformations
 * to given geometry types, while allowing unhandled types to be simply copied.
 * Also, the framework ensures that if subcomponents change type
 * the parent geometries types change appropriately to maintain valid structure.
 * Subclasses will override whichever <code>transformX</code> methods
 * they need to to handle particular Geometry types.
 * <p>
 * A typically usage would be a transformation class that transforms <tt>Polygons</tt> into
 * <tt>Polygons</tt>, <tt>LineStrings</tt> or <tt>Points</tt>, depending on the geometry of the input
 * (For instance, a simplification operation).  
 * This class would likely need to override the {@link #transformMultiPolygon(MultiPolygon, Geometry)}
 * method to ensure that if input Polygons change type the result is a <tt>GeometryCollection</tt>,
 * not a <tt>MultiPolygon</tt>.
 * <p>
 * The default behaviour of this class is simply to recursively transform
 * each Geometry component into an identical object by deep copying down
 * to the level of, but not including, coordinates.
 * <p>
 * All <code>transformX</code> methods may return <code>null</code>,
 * to avoid creating empty or invalid geometry objects. This will be handled correctly
 * by the transformer.   <code>transform<i>XXX</i></code> methods should always return valid
 * geometry - if they cannot do this they should return <code>null</code>
 * (for instance, it may not be possible for a transformLineString implementation
 * to return at least two points - in this case, it should return <code>null</code>).
 * The {@link #transform(Geometry)} method itself will always
 * return a non-null Geometry object (but this may be empty).
 *
 * @version 1.7
 *
 * @see GeometryEditor
 */
public class GeometryTransformer
{

  /**
   * Possible extensions:
   * getParent() method to return immediate parent e.g. of LinearRings in Polygons
   */

  private Geometry inputGeom;

  protected GeometryFactory factory = null;

  // these could eventually be exposed to clients
  /**
   * <code>true</code> if empty geometries should not be included in the result
   */
  private boolean pruneEmptyGeometry = true;

  /**
   * <code>true</code> if a homogenous collection result
   * from a {@link GeometryCollection} should still
   * be a general GeometryCollection
   */
  private boolean preserveGeometryCollectionType = true;

  /**
   * <code>true</code> if the output from a collection argument should still be a collection
   */
  private boolean preserveCollections = false;

  /**
   * <code>true</code> if the type of the input should be preserved
   */
  private boolean preserveType = false;

  public GeometryTransformer() {
  }

  /**
   * Utility function to make input geometry available
   *
   * @return the input geometry
   */
  public Geometry getInputGeometry() { return inputGeom; }

  public final Geometry transform(Geometry inputGeom)
  {
    this.inputGeom = inputGeom;
    this.factory = inputGeom.getFactory();

    if (inputGeom instanceof Point)
      return transformPoint((Point) inputGeom, null);
    if (inputGeom instanceof MultiPoint)
      return transformMultiPoint((MultiPoint) inputGeom, null);
    if (inputGeom instanceof LinearRing)
      return transformLinearRing((LinearRing) inputGeom, null);
    if (inputGeom instanceof LineString)
      return transformLineString((LineString) inputGeom, null);
    if (inputGeom instanceof MultiLineString)
      return transformMultiLineString((MultiLineString) inputGeom, null);
    if (inputGeom instanceof Polygon)
      return transformPolygon((Polygon) inputGeom, null);
    if (inputGeom instanceof MultiPolygon)
      return transformMultiPolygon((MultiPolygon) inputGeom, null);
    if (inputGeom instanceof GeometryCollection)
      return transformGeometryCollection((GeometryCollection) inputGeom, null);

    throw new IllegalArgumentException("Unknown Geometry subtype: " + inputGeom.getClass().getName());
  }

  /**
   * Convenience method which provides standard way of
   * creating a {@link CoordinateSequence}
   *
   * @param coords the coordinate array to copy
   * @return a coordinate sequence for the array
   */
  protected final CoordinateSequence createCoordinateSequence(Coordinate[] coords)
  {
    return factory.getCoordinateSequenceFactory().create(coords);
  }

  /**
   * Convenience method which provides a standard way of copying {@link CoordinateSequence}s
   * @param seq the sequence to copy
   * @return a deep copy of the sequence
   */
  protected final CoordinateSequence copy(CoordinateSequence seq)
  {
    return seq.copy();
  }

  /**
   * Transforms a {@link CoordinateSequence}.
   * This method should always return a valid coordinate list for
   * the desired result type.  (E.g. a coordinate list for a LineString
   * must have 0 or at least 2 points).
   * If this is not possible, return an empty sequence -
   * this will be pruned out.
   *
   * @param coords the coordinates to transform
   * @param parent the parent geometry
   * @return the transformed coordinates
   */
  protected CoordinateSequence transformCoordinates(CoordinateSequence coords, Geometry parent)
  {
    return copy(coords);
  }

  protected Geometry transformPoint(Point geom, Geometry parent) {
    return factory.createPoint(
        transformCoordinates(geom.getCoordinateSequence(), geom));
  }

  protected Geometry transformMultiPoint(MultiPoint geom, Geometry parent) {
    List transGeomList = new ArrayList();
    for (int i = 0; i < geom.getNumGeometries(); i++) {
      Geometry transformGeom = transformPoint((Point) geom.getGeometryN(i), geom);
      if (transformGeom == null) continue;
      if (transformGeom.isEmpty()) continue;
      transGeomList.add(transformGeom);
    }
    if (transGeomList.isEmpty()) {
      return factory.createMultiPoint();
    }
    return factory.buildGeometry(transGeomList);
  }

  /**
   * Transforms a LinearRing.
   * The transformation of a LinearRing may result in a coordinate sequence
   * which does not form a structurally valid ring (i.e. a degenerate ring of 3 or fewer points).
   * In this case a LineString is returned. 
   * Subclasses may wish to override this method and check for this situation
   * (e.g. a subclass may choose to eliminate degenerate linear rings)
   * 
   * @param geom the ring to simplify
   * @param parent the parent geometry
   * @return a LinearRing if the transformation resulted in a structurally valid ring
   * @return a LineString if the transformation caused the LinearRing to collapse to 3 or fewer points
   */
  protected Geometry transformLinearRing(LinearRing geom, Geometry parent) {
    CoordinateSequence seq = transformCoordinates(geom.getCoordinateSequence(), geom);
    if (seq == null) 
      return factory.createLinearRing((CoordinateSequence) null);
    int seqSize = seq.size();
    // ensure a valid LinearRing
    if (seqSize > 0 && seqSize < 4 && ! preserveType)
      return factory.createLineString(seq);
    return factory.createLinearRing(seq);
  }

  /**
   * Transforms a {@link LineString} geometry.
   *
   * @param geom
   * @param parent
   * @return
   */
  protected Geometry transformLineString(LineString geom, Geometry parent) {
    // should check for 1-point sequences and downgrade them to points
    return factory.createLineString(
        transformCoordinates(geom.getCoordinateSequence(), geom));
  }

  protected Geometry transformMultiLineString(MultiLineString geom, Geometry parent) {
    List transGeomList = new ArrayList();
    for (int i = 0; i < geom.getNumGeometries(); i++) {  
      Geometry transformGeom = transformLineString((LineString) geom.getGeometryN(i), geom);
      if (transformGeom == null) continue;
      if (transformGeom.isEmpty()) continue;
      transGeomList.add(transformGeom);
    }
    if (transGeomList.isEmpty()) {
      return factory.createMultiLineString();
    }
    return factory.buildGeometry(transGeomList);
  }

  protected Geometry transformPolygon(Polygon geom, Geometry parent) {
    boolean isAllValidLinearRings = true;
    Geometry shell = transformLinearRing(geom.getExteriorRing(), geom);

    // handle empty inputs, or inputs which are made empty
    boolean shellIsNullOrEmpty = shell == null || shell.isEmpty();
    if (geom.isEmpty() && shellIsNullOrEmpty ) {
      return factory.createPolygon();
    }
    
    if (shellIsNullOrEmpty || ! (shell instanceof LinearRing))
      isAllValidLinearRings = false;

    ArrayList holes = new ArrayList();
    for (int i = 0; i < geom.getNumInteriorRing(); i++) {
      Geometry hole = transformLinearRing(geom.getInteriorRingN(i), geom);
      if (hole == null || hole.isEmpty()) {
        continue;
      }
      if (! (hole instanceof LinearRing))
        isAllValidLinearRings = false;

      holes.add(hole);
    }

    if (isAllValidLinearRings)
      return factory.createPolygon((LinearRing) shell,
                                   (LinearRing[]) holes.toArray(new LinearRing[] {  }));
    else {
      List components = new ArrayList();
      if (shell != null) components.add(shell);
      components.addAll(holes);
      return factory.buildGeometry(components);
    }
  }

  protected Geometry transformMultiPolygon(MultiPolygon geom, Geometry parent) {
    List transGeomList = new ArrayList();
    for (int i = 0; i < geom.getNumGeometries(); i++) {
      Geometry transformGeom = transformPolygon((Polygon) geom.getGeometryN(i), geom);
      if (transformGeom == null) continue;
      if (transformGeom.isEmpty()) continue;
      transGeomList.add(transformGeom);
    }
    if (transGeomList.isEmpty()) {
      return factory.createMultiPolygon();
    }
    return factory.buildGeometry(transGeomList);
  }

  protected Geometry transformGeometryCollection(GeometryCollection geom, Geometry parent) {
    List transGeomList = new ArrayList();
    for (int i = 0; i < geom.getNumGeometries(); i++) {
      Geometry transformGeom = transform(geom.getGeometryN(i));
      if (transformGeom == null) continue;
      if (pruneEmptyGeometry && transformGeom.isEmpty()) continue;
      transGeomList.add(transformGeom);
    }
    if (preserveGeometryCollectionType)
      return factory.createGeometryCollection(GeometryFactory.toGeometryArray(transGeomList));
    return factory.buildGeometry(transGeomList);
  }

}
