package com.spatial4j.core.io;

import com.spatial4j.core.context.SpatialContext;
import com.spatial4j.core.exception.InvalidShapeException;
import com.spatial4j.core.shape.Circle;
import com.spatial4j.core.shape.Point;
import com.spatial4j.core.shape.Rectangle;
import com.spatial4j.core.shape.Shape;

import java.text.NumberFormat;
import java.util.Locale;
import java.util.StringTokenizer;

/**
 * Reads and writes {@link Shape}s to strings.
 */
public class ShapeReadWriter<CTX extends SpatialContext> {
  protected CTX ctx;

  @SuppressWarnings("unchecked")
  public ShapeReadWriter(SpatialContext ctx) {
    this.ctx = (CTX) ctx;
  }

  /**
   * Reads a shape from a given string (ie, X Y, XMin XMax... WKT)
   * <ul>
   *   <li>Point: X Y
   *   <br /> 1.23 4.56
   *   </li>
   *   <li>BOX: XMin YMin XMax YMax
   *   <br /> 1.23 4.56 7.87 4.56</li>
   *   <li><a href="http://en.wikipedia.org/wiki/Well-known_text">
   *     WKT (Well Known Text)</a>
   *   <br /> POLYGON( ... )
   *   <br /> <b>Note:</b>Polygons and WKT might not be supported by this
   *   spatial context; you'll have to use {@link com.spatial4j.core.context.jts.JtsSpatialContext}.
   *   </li>
   * </ul>
   * @param value A string representation of the shape; not null.
   * @return A Shape; not null.
   *
   * @see #writeShape
   */
  public Shape readShape(String value) throws InvalidShapeException {
    Shape s = readStandardShape(value);
    if(s == null) {
      throw new InvalidShapeException("Unable to read: "+value);
    }
    return s;
  }

  /**
   * Writes a shape to a String, in a format that can be read by {@link #readShape(String)}.
   * @param shape Not null.
   * @return Not null.
   */
  public String writeShape(Shape shape) {
    NumberFormat nf = NumberFormat.getInstance(Locale.US);
    nf.setGroupingUsed(false);
    nf.setMaximumFractionDigits(6);
    nf.setMinimumFractionDigits(6);
    //TODO use StringBuffer optional args to NumberFormat
    //TODO make decimal places configurable
    if (shape instanceof Point) {
      Point point = (Point) shape;
      return nf.format(point.getX()) + " " + nf.format(point.getY());
    }
    else if (shape instanceof Rectangle) {
      Rectangle rect = (Rectangle)shape;
      return
              nf.format(rect.getMinX()) + " " +
                      nf.format(rect.getMinY()) + " " +
                      nf.format(rect.getMaxX()) + " " +
                      nf.format(rect.getMaxY());
    }
    else if (shape instanceof Circle) {
      Circle c = (Circle) shape;
      return "Circle(" +
              nf.format(c.getCenter().getX()) + " " +
              nf.format(c.getCenter().getY()) + " " +
              "d=" + nf.format(c.getDistance()) +
              ")";
    }
    return shape.toString();
  }

  protected Shape readStandardShape(String str) {
    if (str == null || str.length() == 0) {
      throw new InvalidShapeException(str);
    }

    if (Character.isLetter(str.charAt(0))) {
      if (str.startsWith("Circle(") || str.startsWith("CIRCLE(")) {
        int idx = str.lastIndexOf(')');
        if (idx > 0) {
          String body = str.substring("Circle(".length(), idx);
          StringTokenizer st = new StringTokenizer(body, " ");
          String token = st.nextToken();
          Point pt;
          if (token.indexOf(',') != -1) {
            pt = readLatCommaLonPoint(token);
          } else {
            double x = Double.parseDouble(token);
            double y = Double.parseDouble(st.nextToken());
            pt = ctx.makePoint(x, y);
          }
          Double d = null;

          String arg = st.nextToken();
          idx = arg.indexOf('=');
          if (idx > 0) {
            String k = arg.substring(0, idx);
            if (k.equals("d") || k.equals("distance")) {
              d = Double.parseDouble(arg.substring(idx + 1));
            } else {
              throw new InvalidShapeException("unknown arg: " + k + " :: " + str);
            }
          } else {
            d = Double.parseDouble(arg);
          }
          if (st.hasMoreTokens()) {
            throw new InvalidShapeException("Extra arguments: " + st.nextToken() + " :: " + str);
          }
          if (d == null) {
            throw new InvalidShapeException("Missing Distance: " + str);
          }
          //NOTE: we are assuming the units of 'd' is the same as that of the spatial context.
          return ctx.makeCircle(pt, d);
        }
      }
      return null;
    }

    if (str.indexOf(',') != -1)
      return readLatCommaLonPoint(str);
    StringTokenizer st = new StringTokenizer(str, " ");
    double p0 = Double.parseDouble(st.nextToken());
    double p1 = Double.parseDouble(st.nextToken());
    if (st.hasMoreTokens()) {
      double p2 = Double.parseDouble(st.nextToken());
      double p3 = Double.parseDouble(st.nextToken());
      if (st.hasMoreTokens())
        throw new InvalidShapeException("Only 4 numbers supported (rect) but found more: " + str);
      return ctx.makeRect(p0, p2, p1, p3);
    }
    return ctx.makePoint(p0, p1);
  }

  //TODO reconsider... seems Geo only
  public Point readLatCommaLonPoint(String value) throws InvalidShapeException {
    double[] latLon = ParseUtils.parseLatitudeLongitude(value);
    return ctx.makePoint(latLon[1],latLon[0]);
  }
}

@@ -1738,267 +1738,165 @@
     
-NumberFormat nf = NumberFormat.getInstance(Locale.US);%0A    nf.setGroupingUsed(false);%0A    nf.setMaximumFractionDigits(6);%0A    nf.setMinimumFractionDigits(6);%0A    //TODO use StringBuffer optional args to NumberFormat%0A    //TODO make decimal places configurable
+return writeShape(shape,makeNumberFormat(6));%0A  %7D%0A%0A  /** Overloaded to provide a number format. */%0A  public String writeShape(Shape shape, NumberFormat nf) %7B
 %0A   
@@ -2664,16 +2664,404 @@
 );%0A  %7D%0A%0A
+  /**%0A   * A convenience method to create a suitable NumberFormat for writing numbers.%0A   */%0A  public static NumberFormat makeNumberFormat(int fractionDigits) %7B%0A    NumberFormat nf = NumberFormat.getInstance(Locale.ROOT);//not thread-safe%0A    nf.setGroupingUsed(false);%0A    nf.setMaximumFractionDigits(fractionDigits);%0A    nf.setMinimumFractionDigits(fractionDigits);%0A    return nf;%0A  %7D%0A%0A
   protec

Add NumberFormat overloaded argument so caller can re-use it and so it can specify some other # digits