/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package org.locationtech.spatial4j.io;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Iterator;

import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.context.SpatialContextFactory;
import org.locationtech.spatial4j.shape.Circle;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Rectangle;
import org.locationtech.spatial4j.shape.Shape;
import org.locationtech.spatial4j.shape.ShapeCollection;
import org.locationtech.spatial4j.shape.impl.BufferedLine;
import org.locationtech.spatial4j.shape.impl.BufferedLineString;


/**
 * Wrap the 'Encoded Polyline Algorithm Format' defined in: 
 * <a href="https://developers.google.com/maps/documentation/utilities/polylinealgorithm">Google Maps API</a>
 * with flags to encode various Shapes:  Point, Line, Polygon, etc
 * 
 * For more details, see <a href="https://github.com/locationtech/spatial4j/blob/master/FORMATS.md#polyshape">FORMATS.md</a>
 * 
 * This format works well for geographic shapes (-180...+180 / -90...+90), but is not appropriate for other coordinate systems
 * 
 * @see <a href="https://developers.google.com/maps/documentation/utilities/polylinealgorithm">Google Maps API</a>
 * @see PolyshapeReader
 */
public class PolyshapeWriter implements ShapeWriter {

  public PolyshapeWriter(SpatialContext ctx, SpatialContextFactory factory) {

  }

  @Override
  public String getFormatName() {
    return ShapeIO.POLY;
  }


  @Override
  public void write(Writer output, Shape shape) throws IOException {
    if (shape == null) {
      throw new NullPointerException("Shape can not be null");
    }
    write(new Encoder(output), shape);
  }

  public void write(Encoder enc, Shape shape) throws IOException {
    if (shape instanceof Point) {
      Point v = (Point) shape;
      enc.write(KEY_POINT);
      enc.write(v.getX(), v.getY());
      return;
    }
    if (shape instanceof Rectangle) {
      Rectangle v = (Rectangle) shape;
      enc.write(KEY_BOX);
      enc.write(v.getMinX(), v.getMinY());
      enc.write(v.getMaxX(), v.getMaxY());
      return;
    }
    if (shape instanceof BufferedLine) {
      BufferedLine v = (BufferedLine) shape;
      enc.write(KEY_LINE);
      if(v.getBuf()>0) {
        enc.writeArg(v.getBuf());
      }
      enc.write(v.getA().getX(), v.getA().getY());
      enc.write(v.getB().getX(), v.getB().getY());
      return;
    }
    if (shape instanceof BufferedLineString) {
      BufferedLineString v = (BufferedLineString) shape;
      enc.write(KEY_LINE);
      if(v.getBuf()>0) {
        enc.writeArg(v.getBuf());
      }
      BufferedLine last = null;
      Iterator<BufferedLine> iter = v.getSegments().iterator();
      while (iter.hasNext()) {
        BufferedLine seg = iter.next();
        enc.write(seg.getA().getX(), seg.getA().getY());
        last = seg;
      }
      if (last != null) {
        enc.write(last.getB().getX(), last.getB().getY());
      }
      return;
    }
    if (shape instanceof Circle) {
      // See: https://github.com/geojson/geojson-spec/wiki/Proposal---Circles-and-Ellipses-Geoms
      Circle v = (Circle) shape;
      Point center = v.getCenter();
      double radius = v.getRadius();
      enc.write(KEY_CIRCLE);
      enc.writeArg(radius);
      enc.write(center.getX(), center.getY());
      return;
    }
    if (shape instanceof ShapeCollection) {
      @SuppressWarnings("unchecked") ShapeCollection<Shape> v = (ShapeCollection<Shape>) shape;
      Iterator<Shape> iter = v.iterator();
      while(iter.hasNext()) {
        write(enc, iter.next());
        if(iter.hasNext()) {
          enc.seperator();
        }
      }
      return;
    }
    enc.writer.write("{unkwnwon " + LegacyShapeWriter.writeShape(shape) +"}");
  }

  @Override
  public String toString(Shape shape) {
    try {
      StringWriter buffer = new StringWriter();
      write(buffer, shape);
      return buffer.toString();
    } catch (IOException ex) {
      throw new RuntimeException(ex);
    }
  }
  
  public static final char KEY_POINT      = '0';
  public static final char KEY_LINE       = '1';
  public static final char KEY_POLYGON    = '2';
  public static final char KEY_MULTIPOINT = '3';
  public static final char KEY_CIRCLE     = '4';
  public static final char KEY_BOX        = '5';
  
  public static final char KEY_ARG_START  = '(';
  public static final char KEY_ARG_END    =  ')';
  public static final char KEY_SEPERATOR  = ' ';
    

  /**
   * Encodes a sequence of LatLngs into an encoded path string.
   * 
   * from Apache 2.0 licensed:
   * https://github.com/googlemaps/android-maps-utils/blob/master/library/src/com/google/maps/android/PolyUtil.java
   */
  public static class Encoder {
    long lastLat = 0;
    long lastLng = 0;
    
    final Writer writer;
    
    public Encoder(Writer writer) {
      this.writer = writer;
    }
    
    public void seperator() throws IOException {
      writer.write(KEY_SEPERATOR);
      lastLat = lastLng = 0;
    }

    public void startRing() throws IOException {
      writer.write(KEY_ARG_START);
      lastLat = lastLng = 0;
    }
    
    public void write(char event) throws IOException {
      writer.write(event);
      lastLat = lastLng = 0;
    }

    public void writeArg(double value) throws IOException {
      writer.write(KEY_ARG_START);
      encode(Math.round(value * 1e5));
      writer.write(KEY_ARG_END);
    }
    
    public void write(double latitude, double longitude) throws IOException {

      long lat = Math.round(latitude * 1e5);
      long lng = Math.round(longitude * 1e5);

      long dLat = lat - lastLat;
      long dLng = lng - lastLng;

      encode(dLat);
      encode(dLng);

      lastLat = lat;
      lastLng = lng;
    }
    
    private void encode(long v) throws IOException {
      v = v < 0 ? ~(v << 1) : v << 1;
      while (v >= 0x20) {
        writer.write(Character.toChars((int) ((0x20 | (v & 0x1f)) + 63)));
        v >>= 5;
      }
      writer.write(Character.toChars((int) (v + 63)));
    }
  }
}