/*******************************************************************************
 * Copyright (c) 2016 David Smiley
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License, Version 2.0 which
 * accompanies this distribution and is available at
 *    http://www.apache.org/licenses/LICENSE-2.0.txt
 ******************************************************************************/

package org.locationtech.spatial4j.shape;

import org.locationtech.spatial4j.context.SpatialContext;

import java.util.List;

/**
 * A factory for {@link Shape}s.
 * Stateless and thread-safe, except for any returned builders.
 */
public interface ShapeFactory {

  SpatialContext getSpatialContext();

  /** If true then {@link #normX(double)} will wrap longitudes outside of the standard
   * geodetic boundary into it. Example: 181 will become -179. */
  boolean isNormWrapLongitude();

  // TODO annoying that a ShapeReader must remember to call norm* methods.  Perhaps
  //  there should be another shapeFactory impl for shape reading?  :-/  Or not.

  /** Normalize the 'x' dimension. Might reduce precision or wrap it to be within the bounds. This
   * is called by {@link org.locationtech.spatial4j.io.ShapeReader}s before creating a shape. */
  double normX(double x);

  /** @see #normX(double)  */
  double normY(double y);

  /** (disclaimer: the Z dimension isn't fully supported)
   * @see #normX(double) */
  double normZ(double z);

  /**
   * Called to normalize a value that isn't X or Y or Z. X &amp; Y &amp; Z are normalized via
   * {@link org.locationtech.spatial4j.context.SpatialContext#normX(double)} &amp; normY &amp; normZ. This
   * is called by a {@link org.locationtech.spatial4j.io.ShapeReader} before creating a shape.
   */
  double normDist(double d);

  /** Ensure fits in the world bounds. It's called by any shape factory method that
   * gets an 'x' dimension. */
  void verifyX(double x);

  /** @see #verifyX(double)  */
  void verifyY(double y);

  /** (disclaimer: the Z dimension isn't fully supported)
   *  @see #verifyX(double)  */
  void verifyZ(double z);

  /** Construct a point. */
  Point pointXY(double x, double y);

  /** Construct a point of latitude, longitude coordinates */
  default Point pointLatLon(double latitude, double longitude) {
    return pointXY(longitude, latitude);
  }

  /** Construct a point of 3 dimensions.  The implementation might ignore unsupported
   * dimensions like 'z' or throw an error. */
  Point pointXYZ(double x, double y, double z);

  /** Construct a rectangle. */
  Rectangle rect(Point lowerLeft, Point upperRight);

  /**
   * Construct a rectangle. If just one longitude is on the dateline (+/- 180) and if
   * {@link SpatialContext#isGeo()}
   * then potentially adjust its sign to ensure the rectangle does not cross the
   * dateline (aka anti-meridian).
   */
  Rectangle rect(double minX, double maxX, double minY, double maxY);

  /** Construct a circle. The units of "distance" should be the same as x &amp; y. */
  Circle circle(double x, double y, double distance);

  /** Construct a circle. The units of "distance" should be the same as x &amp; y. */
  Circle circle(Point point, double distance);

  /** Constructs a line string with a possible buffer. It's an ordered sequence of connected vertexes,
   * with a buffer distance along the line in all directions. There
   * is no official shape/interface for it so we just return Shape. */
  @Deprecated // use a builder
  Shape lineString(List<Point> points, double buf);

  /** Construct a ShapeCollection, analogous to an OGC GeometryCollection. */
  @Deprecated // use a builder
  <S extends Shape> ShapeCollection<S> multiShape(List<S> coll);

  // BUILDERS:

  /** (Builder) Constructs a line string, with a possible buffer.
   * It's an ordered sequence of connected vertexes.
   * There is no official shape/interface for it yet so we just return Shape. */
  LineStringBuilder lineString();

  /** (Builder) Constructs a polygon.
   * There is no official shape/interface for it yet so we just return Shape. */
  PolygonBuilder polygon();

  /** (Builder) Constructs a Shape aggregate in which each component/member
   * is an instance of the specified class.
   */
  <T extends Shape> MultiShapeBuilder<T> multiShape(Class<T> shapeClass);

  /** (Builder) Constructs a MultiPoint. */
  MultiPointBuilder multiPoint();

  /** (Builder) Constructs a MultiLineString, or possibly the result of that buffered. */
  MultiLineStringBuilder multiLineString();

  /** (Builder) Constructs a MultiPolygon. */
  MultiPolygonBuilder multiPolygon();

  // misc:
  //Shape buffer(Shape shape);  ?

  // TODO need Polygon shape
  // TODO need LineString shape
  // TODO need BufferedLineString shape
  // TODO need ShapeCollection to be typed

  /** Builds a point and returns the generic specified type (usually whatever "this" is). */
  interface PointsBuilder<T> {
    /** @see ShapeFactory#pointXY(double, double) */
    T pointXY(double x, double y);
    /** @see ShapeFactory#pointXYZ(double, double, double) */
    T pointXYZ(double x, double y, double z);
    /** @see ShapeFactory#pointLatLon(double, double)  */
    default T pointLatLon(double latitude, double longitude) {
      return pointXY(longitude, latitude);
    }
  }

  /** @see #lineString() */
  interface LineStringBuilder extends PointsBuilder<LineStringBuilder> {
    // TODO add dimensionality hint method?

    LineStringBuilder buffer(double distance);

    Shape build();
  }

  /** @see #polygon() */
  interface PolygonBuilder extends PointsBuilder<PolygonBuilder> {
    // TODO add dimensionality hint method?

    /** Starts a new hole. You must add at least 4 points; furthermore the first and last must be the same.
     * And don't forget to call {@link HoleBuilder#endHole()}! */
    HoleBuilder hole();

    /** Builds the polygon and renders this builder instance invalid. */
    Shape build();// never a Rect

    Shape buildOrRect();

    interface HoleBuilder extends PointsBuilder<HoleBuilder> {
      /** Finishes the hole and returns the {@link PolygonBuilder}.*/
      PolygonBuilder endHole();
    }
  }

  // TODO add dimensionality hint method to the multi* builders?

  /** @see #multiShape(Class) */
  interface MultiShapeBuilder<T extends Shape> {
    // TODO add dimensionality hint method?

    MultiShapeBuilder<T> add(T shape);

    //ShapeCollection<T> build(); TODO wait till it's a typed interface
    Shape build();
  }

  /** @see #multiPoint() */
  interface MultiPointBuilder extends PointsBuilder<MultiPointBuilder> {

    Shape build();  // TODO MultiShape<Point>
  }

  /** @see #multiLineString() */
  interface MultiLineStringBuilder {

    LineStringBuilder lineString();

    MultiLineStringBuilder add(LineStringBuilder lineStringBuilder);

    Shape build(); // TODO MultiShape<LineString>
  }

  /** @see #multiPolygon() */
  interface MultiPolygonBuilder {

    PolygonBuilder polygon();

    MultiPolygonBuilder add(PolygonBuilder polygonBuilder);

    Shape build(); // TODO MultiShape<Polygon>
  }
}
