/* Copyright 2013 The jeo project. All rights reserved.
 *
 * Licensed 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.oscim.utils.geom;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.MultiLineString;
import org.locationtech.jts.geom.MultiPoint;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.PrecisionModel;

import java.lang.reflect.Array;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.Iterator;

/**
 * Builder for geometry objects.
 * <p>
 * Example usage:
 * <p/>
 * <pre>
 * <code>
 * GeometryBuilder gb = new GeometryBuilder();
 *
 * // create array two 2d points and turn into a line string
 * gb.points(1,2,3,4,5,6).toLineString();
 *
 * // build a polygon with holes
 * gb.points(0,0,10,0,10,10,0,10,0,0).ring()
 *   .points(4,4,6,4,6,6,4,6,4,4).ring()
 *   .toPolygon();
 *
 * </code>
 * </pre>
 * <p/>
 * </p>
 *
 * @author Justin Deoliveira, OpenGeo
 */
public class GeomBuilder {

    GeometryFactory factory;
    Deque<Coordinate> cstack = new ArrayDeque<Coordinate>();
    Deque<Geometry> gstack = new ArrayDeque<Geometry>();

    /**
     * Constructs a builder with the default geometry factory.
     */
    public GeomBuilder() {
        this(new GeometryFactory());
    }

    /**
     * Constructs a builder with an explicit geometry factory.
     */
    public GeomBuilder(GeometryFactory factory) {
        this.factory = factory;
    }

    /**
     * Constructs a builder with a specific srid for geometry objects.
     */
    public GeomBuilder(int srid) {
        this.factory = new GeometryFactory(new PrecisionModel(), srid);
    }

    /**
     * Adds a 2d point to the coordinate stack.
     */
    public GeomBuilder point(double x, double y) {
        cstack.push(new Coordinate(x, y));
        return this;
    }

    /**
     * Adds a 3d point to the coordinate stack.
     */
    public GeomBuilder pointz(double x, double y, double z) {
        cstack.push(new Coordinate(x, y, z));
        return this;
    }

    /**
     * Adds an array of 2d points to the coordinate stack.
     */
    public GeomBuilder points(double... ord) {
        if (ord.length % 2 != 0) {
            throw new IllegalArgumentException("Must specify even number of ordinates");
        }

        for (int i = 0; i < ord.length; i += 2) {
            point(ord[i], ord[i + 1]);
        }

        return this;
    }

    /**
     * Adds an array of 3d points to the coordinate stack.
     */
    public GeomBuilder pointsz(double... ord) {
        if (ord.length % 3 != 0) {
            throw new IllegalArgumentException("Must specify ordinates as triples");
        }

        for (int i = 0; i < ord.length; i += 3) {
            pointz(ord[i], ord[i + 1], ord[i + 2]);
        }

        return this;
    }

    /**
     * Creates a Point from the last point on the coordinate stack, and places
     * the result
     * on the geometry stack.
     */
    public GeomBuilder point() {
        gstack.push(factory.createPoint(cpop()));
        return this;
    }

    /**
     * Creates a LineString from all points on the coordinate stack, and places
     * the result
     * on the geometry stack.
     */
    public GeomBuilder lineString() {
        gstack.push(factory.createLineString(cpopAll()));
        return this;
    }

    /**
     * Creates a LinearRing from all points on the coordinate stack, and places
     * the result
     * on the geometry stack.
     * <p>
     * If the first and last coordinate on the point stack are not equal an
     * additional point will be added.
     * </p>
     */
    public GeomBuilder ring() {
        Coordinate[] coords = cpopAll();
        if (coords.length > 1 && !coords[0].equals(coords[coords.length - 1])) {
            Coordinate[] tmp = new Coordinate[coords.length + 1];
            System.arraycopy(coords, 0, tmp, 0, coords.length);
            tmp[tmp.length - 1] = new Coordinate(tmp[0]);
            coords = tmp;
        }

        gstack.push(factory.createLinearRing(coords));
        return this;
    }

    /**
     * Creates a Polygon from all LinearRings on the geometry stack and places
     * the result back
     * on the geometry stack.
     */
    public GeomBuilder polygon() {
        if (gstack.isEmpty() || !(gstack.peek() instanceof LinearRing)) {
            ring();
        }

        LinearRing[] rings = gpopAll(LinearRing.class);
        LinearRing outer = rings[0];
        LinearRing[] inner = null;
        if (rings.length > 1) {
            inner = Arrays.copyOfRange(rings, 1, rings.length);
        }

        gstack.push(factory.createPolygon(outer, inner));
        return this;
    }

    /**
     * Creates a MultiPoint from all coordinates on the coordinate stack,
     * plaching the result
     * back on the geometry stack.
     * <p>
     * If the coordinate stack is empty this method will consume all Point
     * geometries on the geometry stack.
     * </p>
     */
    public GeomBuilder multiPoint() {
        if (!cstack.isEmpty()) {
            gstack.push(factory.createMultiPoint(cpopAll()));
        } else {
            gstack.push(factory.createMultiPoint(gpopAll(Point.class)));
        }
        return this;
    }

    /**
     * Creates a MultiLineString from all LineStrings on the geometry stack and
     * places the result
     * back on the geometry stack.
     */
    public GeomBuilder multiLineString() {
        gstack.push(factory.createMultiLineString(gpopAll(LineString.class)));
        return this;
    }

    /**
     * Creates a MultiPolygon from all Polygons on the geometry stack and places
     * the result
     * back on the geometry stack.
     */
    public GeomBuilder multiPolygon() {
        gstack.push(factory.createMultiPolygon(gpopAll(Polygon.class)));
        return this;
    }

    /**
     * Creates a GeometryCollection from all Geometries on the geometry stack
     * and places the result
     * back on the geometry stack.
     */
    public GeomBuilder collection() {
        gstack.push(factory.createGeometryCollection(gpopAll(Geometry.class)));
        return this;
    }

    /**
     * Buffers the geometry at the top of the geometry stack, and places the
     * result back on the
     * geometry stack.
     */
    public GeomBuilder buffer(double amt) {
        gstack.push(gpop(Geometry.class).buffer(amt));
        return this;
    }

    /**
     * Consumes the top of the geometry stack.
     */
    public Geometry get() {
        return gpop(Geometry.class);
    }

    /**
     * Builds and returns a Point.
     * <p>
     * This method is equivalent to:
     * <p/>
     * <pre>
     *   (Point) point().get();
     * </pre>
     * <p/>
     * </p>
     */
    public Point toPoint() {
        return point().gpop(Point.class);
    }

    /**
     * Builds and returns a LineString.
     * <p>
     * This method is equivalent to:
     * <p/>
     * <pre>
     *   (LineString) lineString().get();
     * </pre>
     * <p/>
     * </p>
     */
    public LineString toLineString() {
        return lineString().gpop(LineString.class);
    }

    /**
     * Builds and returns a LineString.
     * <p>
     * This method is equivalent to:
     * <p/>
     * <pre>
     *   (LinearRing) ring().get();
     * </pre>
     * <p/>
     * </p>
     */
    public LinearRing toLinearRing() {
        return ring().gpop(LinearRing.class);
    }

    /**
     * Builds and returns a Polygon.
     * <p>
     * This method is equivalent to:
     * <p/>
     * <pre>
     *   (Polygon) polygon().get();
     * </pre>
     * <p/>
     * </p>
     */
    public Polygon toPolygon() {
        return polygon().gpop(Polygon.class);
    }

    /**
     * Builds and returns a MultiPoint.
     * <p>
     * This method is equivalent to:
     * <p/>
     * <pre>
     *   (MultiPoint) multiPoint().get();
     * </pre>
     * <p/>
     * </p>
     */
    public MultiPoint toMultiPoint() {
        return multiPoint().gpop(MultiPoint.class);
    }

    /**
     * Builds and returns a MultiLineString.
     * <p>
     * This method is equivalent to:
     * <p/>
     * <pre>
     *   (MultiLineString) multiLineString().get();
     * </pre>
     * <p/>
     * </p>
     */
    public MultiLineString toMultiLineString() {
        return multiLineString().gpop(MultiLineString.class);
    }

    /**
     * Builds and returns a MultiPolygon.
     * <p>
     * This method is equivalent to:
     * <p/>
     * <pre>
     *   (MultiPolygon) multiPolygon().get();
     * </pre>
     * <p/>
     * </p>
     */
    public MultiPolygon toMultiPolygon() {
        return multiPolygon().gpop(MultiPolygon.class);
    }

    /**
     * Builds and returns a GEometryCollection.
     * <p>
     * This method is equivalent to:
     * <p/>
     * <pre>
     *   (GeometryCollection) collection().get();
     * </pre>
     * <p/>
     * </p>
     */
    public GeometryCollection toCollection() {
        return collection().gpop(GeometryCollection.class);
    }

    Coordinate cpop() {
        return cpop(1)[0];
    }

    Coordinate[] cpop(int n) {
        if (cstack.size() < n) {
            throw new IllegalStateException(String.format("Expected %d values on coordinate stack, "
                            + "but found %d",
                    n,
                    cstack.size()));
        }

        Coordinate[] c = new Coordinate[n];
        for (int i = 0; i < n; i++) {
            c[n - i - 1] = cstack.pop();
        }
        return c;
    }

    Coordinate[] cpopAll() {
        if (cstack.isEmpty()) {
            throw new IllegalStateException("Coordinate stack is empty");
        }

        return cpop(cstack.size());
    }

    <T extends Geometry> T gpop(Class<T> clazz) {
        return gpop(1, clazz)[0];
    }

    <T extends Geometry> T[] gpop(int n, Class<T> clazz) {
        if (gstack.size() < n) {
            throw new IllegalStateException(String.format("Expected %d values on geometry stack, "
                    + "but found %d", n, gstack.size()));
        }

        @SuppressWarnings("unchecked")
        T[] l = (T[]) Array.newInstance(clazz, n);
        for (int i = 0; i < n; i++) {
            Object g = gstack.pop();
            if (!clazz.isInstance(g)) {
                throw new IllegalStateException(String.format("Expected %s on geometry stack, but "
                        + "found %s", clazz.getSimpleName(), g.getClass().getSimpleName()));
            }

            l[n - i - 1] = clazz.cast(g);
        }
        return l;
    }

    <T extends Geometry> T[] gpopAll(Class<T> clazz) {
        if (gstack.isEmpty()) {
            throw new IllegalArgumentException("Geometry stack is empty");
        }

        int n = 0;
        Iterator<Geometry> it = gstack.iterator();
        while (it.hasNext() && clazz.isInstance(it.next())) {
            n++;
        }

        if (n == 0) {
            throw new IllegalArgumentException(
                    String.format("Expected %s on geometry stack",
                            clazz.getSimpleName()));
        }

        return gpop(n, clazz);
    }
}
