/*******************************************************************************
 * Copyright (c) 2015 Voyager Search and MITRE
 * 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.context;

import org.locationtech.spatial4j.distance.CartesianDistCalc;
import org.locationtech.spatial4j.distance.DistanceCalculator;
import org.locationtech.spatial4j.distance.GeodesicSphereDistCalc;
import org.locationtech.spatial4j.exception.InvalidShapeException;
import org.locationtech.spatial4j.io.BinaryCodec;
import org.locationtech.spatial4j.io.LegacyShapeWriter;
import org.locationtech.spatial4j.io.SupportedFormats;
import org.locationtech.spatial4j.io.WKTReader;
import org.locationtech.spatial4j.shape.*;
import org.locationtech.spatial4j.shape.impl.RectangleImpl;

import java.text.ParseException;
import java.util.List;

/**
 * This is a facade to most of Spatial4j, holding things like {@link DistanceCalculator},
 * {@link ShapeFactory},
 * {@link org.locationtech.spatial4j.io.ShapeIO}.
 * <p>
 * If you want a typical geodetic context, just reference {@link #GEO}.  Otherwise,
 * You should either create and configure a {@link SpatialContextFactory} and then call
 * {@link SpatialContextFactory#newSpatialContext()}, OR, call
 * {@link org.locationtech.spatial4j.context.SpatialContextFactory#makeSpatialContext(java.util.Map, ClassLoader)}
 * to do this via configuration data.
 * <p>
 * Thread-safe &amp; immutable.
 */
public class SpatialContext {

  /** A popular default SpatialContext implementation for geospatial. */
  public static final SpatialContext GEO = new SpatialContext(new SpatialContextFactory());

  //These are non-null
  private final boolean geo;
  private final ShapeFactory shapeFactory;
  private final DistanceCalculator calculator;
  private final Rectangle worldBounds;
  private final BinaryCodec binaryCodec;
  private final SupportedFormats formats;

  /**
   * Consider using {@link org.locationtech.spatial4j.context.SpatialContextFactory} instead.
   *
   * @param geo Establishes geo vs cartesian / Euclidean.
   * @param calculator Optional; defaults to haversine or cartesian depending on {@code geo}.
   * @param worldBounds Optional; defaults to GEO_WORLDBOUNDS or MAX_WORLDBOUNDS depending on units.
   */
  @Deprecated
  public SpatialContext(boolean geo, DistanceCalculator calculator, Rectangle worldBounds) {
    this(initFromLegacyConstructor(geo, calculator, worldBounds));
  }

  private static SpatialContextFactory initFromLegacyConstructor(boolean geo,
                                                                 DistanceCalculator calculator,
                                                                 Rectangle worldBounds) {
    SpatialContextFactory factory = new SpatialContextFactory();
    factory.geo = geo;
    factory.distCalc = calculator;
    factory.worldBounds = worldBounds;
    return factory;
  }

  @Deprecated
  public SpatialContext(boolean geo) {
    this(initFromLegacyConstructor(geo, null, null));
  }

  /**
   * Called by {@link org.locationtech.spatial4j.context.SpatialContextFactory#newSpatialContext()}.
   */
  public SpatialContext(SpatialContextFactory factory) {
    this.geo = factory.geo;

    this.shapeFactory = factory.makeShapeFactory(this);

    if (factory.distCalc == null) {
      this.calculator = isGeo()
              ? new GeodesicSphereDistCalc.Haversine()
              : new CartesianDistCalc();
    } else {
      this.calculator = factory.distCalc;
    }

    //TODO remove worldBounds from Spatial4j: see Issue #55
    Rectangle bounds = factory.worldBounds;
    if (bounds == null) {
      this.worldBounds = isGeo()
              ? new RectangleImpl(-180, 180, -90, 90, this)
              : new RectangleImpl(-Double.MAX_VALUE, Double.MAX_VALUE,
              -Double.MAX_VALUE, Double.MAX_VALUE, this);
    } else {
      if (isGeo() && !bounds.equals(new RectangleImpl(-180, 180, -90, 90, this)))
        throw new IllegalArgumentException("for geo (lat/lon), bounds must be " + GEO.getWorldBounds());
      if (bounds.getMinX() > bounds.getMaxX())
        throw new IllegalArgumentException("worldBounds minX should be <= maxX: "+ bounds);
      if (bounds.getMinY() > bounds.getMaxY())
        throw new IllegalArgumentException("worldBounds minY should be <= maxY: "+ bounds);
      //hopefully worldBounds' rect implementation is compatible
      this.worldBounds = new RectangleImpl(bounds, this);
    }

    this.binaryCodec = factory.makeBinaryCodec(this);
    
    factory.checkDefaultFormats();
    this.formats = factory.makeFormats(this);
  }

  /** A factory for {@link Shape}s. */
  public ShapeFactory getShapeFactory() {
    return shapeFactory;
  }

  public SupportedFormats getFormats() {
    return formats;
  }

  public DistanceCalculator getDistCalc() {
    return calculator;
  }

  /** Convenience that uses {@link #getDistCalc()} */
  public double calcDistance(Point p, double x2, double y2) {
    return getDistCalc().distance(p, x2, y2);
  }

  /** Convenience that uses {@link #getDistCalc()} */
  public double calcDistance(Point p, Point p2) {
    return getDistCalc().distance(p, p2);
  }

  /**
   * The extent of x &amp; y coordinates should fit within the return'ed rectangle.
   * Do *NOT* invoke reset() on this return type.
   */
  public Rectangle getWorldBounds() {
    return worldBounds;
  }

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

  /** Is the mathematical world model based on a sphere, or is it a flat plane? The word
   * "geodetic" or "geodesic" is sometimes used to refer to the former, and the latter is sometimes
   * referred to as "Euclidean" or "cartesian". */
  public boolean isGeo() {
    return geo;
  }

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

  /** Normalize the 'y' dimension. Might reduce precision or wrap it to be within the bounds. This
   * is called by {@link org.locationtech.spatial4j.io.WKTReader} before creating a shape. */
  @Deprecated
  public double normY(double y) { return shapeFactory.normY(y); }

  /** Ensure fits in {@link #getWorldBounds()}. It's called by any shape factory method that
   * gets an 'x' dimension. */
  @Deprecated
  public void verifyX(double x) {
    shapeFactory.verifyX(x);
  }

  /** Ensure fits in {@link #getWorldBounds()}. It's called by any shape factory method that
   * gets a 'y' dimension. */
  @Deprecated
  public void verifyY(double y) {
    shapeFactory.verifyY(y);
  }

  /** Construct a point. */
  @Deprecated
  public Point makePoint(double x, double y) {
    return shapeFactory.pointXY(x, y);
  }

  /** Construct a rectangle. */
  @Deprecated
  public Rectangle makeRectangle(Point lowerLeft, Point upperRight) {
    return shapeFactory.rect(lowerLeft, upperRight);
  }

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

  /** Construct a circle. The units of "distance" should be the same as x &amp; y. */
  @Deprecated
  public Circle makeCircle(double x, double y, double distance) {
    return shapeFactory.circle(x, y, distance);
  }

  /** Construct a circle. The units of "distance" should be the same as x &amp; y. */
  @Deprecated
  public Circle makeCircle(Point point, double distance) {
    return shapeFactory.circle(point, distance);
  }

  /** Constructs a line string. It's an ordered sequence of connected vertexes. There
   * is no official shape/interface for it yet so we just return Shape. */
  @Deprecated
  public Shape makeLineString(List<Point> points) {
    return shapeFactory.lineString(points, 0);
  }

  /** Constructs a buffered line string. 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
  public Shape makeBufferedLineString(List<Point> points, double buf) {
    return shapeFactory.lineString(points, buf);
  }

  /** Construct a ShapeCollection, analogous to an OGC GeometryCollection. */
  @Deprecated
  public <S extends Shape> ShapeCollection<S> makeCollection(List<S> coll) {
    return shapeFactory.multiShape(coll);
  }

  /** The {@link org.locationtech.spatial4j.io.WKTReader} used by {@link #readShapeFromWkt(String)}. */
  @Deprecated
  public WKTReader getWktShapeParser() {
    return (WKTReader)formats.getWktReader();
  }

  /** Reads a shape from the string formatted in WKT.
   * @see org.locationtech.spatial4j.io.WKTReader
   * @param wkt non-null WKT.
   * @return non-null
   * @throws ParseException if it failed to parse.
   */
  @Deprecated
  public Shape readShapeFromWkt(String wkt) throws ParseException, InvalidShapeException {
    return getWktShapeParser().parse(wkt);
  }

  public BinaryCodec getBinaryCodec() { return binaryCodec; }

  /**
   * Try to read a shape from any supported formats
   * 
   * @return shape or null if unable to parse any shape
   */
  @Deprecated
  public Shape readShape(String value) throws InvalidShapeException {
    return formats.read(value);
  }

  /** Writes the shape to a String using the old/deprecated
   * {@link org.locationtech.spatial4j.io.LegacyShapeWriter}. The JTS based subclass will write it
   * to WKT if the legacy format doesn't support that shape.
   * <b>Spatial4j in the near future won't support writing shapes to strings.</b>
   * @param shape non-null
   * @return non-null
   */
  @Deprecated
  public String toString(Shape shape) {
    return LegacyShapeWriter.writeShape(shape);
  }
  
  @Override
  public String toString() {
    if (this.equals(GEO)) {
      return GEO.getClass().getSimpleName()+".GEO";
    } else {
      return getClass().getSimpleName()+"{" +
          "geo=" + geo +
          ", calculator=" + calculator +
          ", worldBounds=" + worldBounds +
          '}';
    }
  }
}
