/*
 * 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.Reader;
import java.io.StringReader;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;

import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.context.SpatialContextFactory;
import org.locationtech.spatial4j.exception.InvalidShapeException;
import org.locationtech.spatial4j.shape.Shape;
import org.locationtech.spatial4j.shape.ShapeFactory;


/**
 * @see PolyshapeWriter
 */
public class PolyshapeReader implements ShapeReader {
  final SpatialContext ctx;
  final ShapeFactory shpFactory;

  public PolyshapeReader(SpatialContext ctx, SpatialContextFactory factory) {
    this.ctx = ctx;
    this.shpFactory = ctx.getShapeFactory();
  }

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

  @Override
  public Shape read(Object value) throws IOException, ParseException, InvalidShapeException {
    return read(new StringReader(value.toString().trim()));
  }

  @Override
  public Shape readIfSupported(Object value) throws InvalidShapeException {
    String v = value.toString().trim();
    char first = v.charAt(0);
    if(first >= '0' && first <= '9') {
      try {
        return read(new StringReader(v));
      } catch (ParseException | IOException e) {
      }
    }
    return null;
  }

  // --------------------------------------------------------------
  // Read GeoJSON
  // --------------------------------------------------------------

  @Override
  public final Shape read(Reader r) throws ParseException, IOException
  {
    XReader reader = new XReader(r, shpFactory);
    Double arg = null;
    
    Shape lastShape = null;
    List<Shape> shapes = null;
    while(!reader.isDone()) {
      char event = reader.readKey();
      if(event<'0' || event > '9') {
        if(event == PolyshapeWriter.KEY_SEPERATOR) {
          continue; // read the next key
        }
        throw new ParseException("expecting a shape key.  not '"+event+"'", -1);
      }

      if(lastShape!=null) {
        if(shapes==null) {
          shapes = new ArrayList<>();
        }
        shapes.add(lastShape);
      }
      arg = null;
      
      if(reader.peek()==PolyshapeWriter.KEY_ARG_START) {
        reader.readKey(); // skip the key
        arg = reader.readDouble();
        if(reader.readKey()!=PolyshapeWriter.KEY_ARG_END) {
          throw new ParseException("expecting an argument end", -1);
        }
      }
      if(reader.isEvent()) {
        throw new ParseException("Invalid input. Event should be followed by data", -1);
      }
      
      switch(event) {
        case PolyshapeWriter.KEY_POINT: {
          lastShape = shpFactory.pointXY(shpFactory.normX(reader.readLat()), shpFactory.normY(reader.readLng()));
          break;
        }
        case PolyshapeWriter.KEY_LINE: {
          ShapeFactory.LineStringBuilder lineBuilder = shpFactory.lineString();
          reader.readPoints(lineBuilder);
          
          if(arg!=null) {
            lineBuilder.buffer(shpFactory.normDist(arg));
          }
          lastShape = lineBuilder.build();
          break;
        }
        case PolyshapeWriter.KEY_BOX: {
          double lat1 = shpFactory.normX(reader.readLat());
          double lon1 = shpFactory.normY(reader.readLng());
          lastShape = shpFactory.rect(lat1, shpFactory.normX(reader.readLat()), 
                  lon1, shpFactory.normY(reader.readLng()));
          break;
        }
        case PolyshapeWriter.KEY_MULTIPOINT : {
          lastShape = reader.readPoints(shpFactory.multiPoint()).build();
          break;
        }
        case PolyshapeWriter.KEY_CIRCLE : {
          if(arg==null) {
            throw new IllegalArgumentException("the input should have a radius argument");
          }
          lastShape = shpFactory.circle(shpFactory.normX(reader.readLat()), shpFactory.normY(reader.readLng()), 
                shpFactory.normDist(arg.doubleValue()));
          break;
        }
        case PolyshapeWriter.KEY_POLYGON: {
          lastShape = readPolygon(reader);
          break;
        }
        default: {
          throw new ParseException("unhandled key: "+event, -1);
        }
      }
    }
    
    if(shapes!=null) {
      if(lastShape!=null) {
        shapes.add(lastShape);
      }

      ShapeFactory.MultiShapeBuilder<Shape> multiBuilder = shpFactory.multiShape(Shape.class); 
      for (Shape shp : shapes) {
        multiBuilder.add(shp);
      }

      return multiBuilder.build();
    }
    return lastShape;
  }
  
  protected Shape readPolygon(XReader reader) throws IOException {
    ShapeFactory.PolygonBuilder polyBuilder = shpFactory.polygon();
    
    reader.readPoints(polyBuilder);

    if(!reader.isDone() && reader.peek()==PolyshapeWriter.KEY_ARG_START) {
      while(reader.isEvent() && reader.peek()==PolyshapeWriter.KEY_ARG_START) {
        reader.readKey(); // eat the event;
        reader.readPoints(polyBuilder.hole()).endHole();
      }
    }

    return polyBuilder.build();
  }

  /**
   * 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 XReader {
    int lat = 0;
    int lng = 0;
    
    int head = -1;
    final Reader input;
    final ShapeFactory shpFactory;

    public XReader(final Reader input, ShapeFactory shpFactory) throws IOException {
      this.input = input;
      this.shpFactory = shpFactory;
      head = input.read();
    }
    
    public <T extends ShapeFactory.PointsBuilder> T readPoints(T builder) throws IOException {
      while(isData()) {
        builder.pointXY(shpFactory.normX(readLat()), shpFactory.normY(readLng()));
      }
      return builder;
    }

    public double readLat() throws IOException {
      lat += readInt();
      return lat * 1e-5;
    }

    public double readLng() throws IOException {
      lng += readInt();
      return lng * 1e-5;
    }
    
    public double readDouble() throws IOException {
      return readInt() * 1e-5;
    }
    
    public int peek() {
      return head;
    }

    public char readKey() throws IOException {
      lat = lng = 0; // reset the offset
      char key = (char)head;
      head = input.read();
      return key;
    }

    public boolean isData() {
      return head >= '?';
    }

    public boolean isDone() {
      return head < 0;
    }
    
    public boolean isEvent() {
      return head > 0 && head < '?';
    }
    
    int readInt() throws IOException
    {
      int b;
      int result = 1;
      int shift = 0;
      do {
        b = head - 63 - 1;
        result += b << shift;
        shift += 5;
        
        head = input.read();
      } while (b >= 0x1f);
      return (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
    }
  }
}