/* 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.locationtech.spatial4j.util;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.prep.PreparedGeometry;
import org.locationtech.jts.geom.prep.PreparedGeometryFactory;

import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.LineString;
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;

/**
 * Geometry module utility module.
 *
 * <p>
 * Class taken from http://github.com/jeo/jeo, Nov 2017 by Justin Deoliveira.  
 * </p>
 * 
 * @author Justin Deoliveira, OpenGeo
 */
public class Geom {

    /**
     * static default factory
     */
    public final static GeometryFactory factory = new GeometryFactory();

    /**
     * Geometry type enumeration.
     */
    public enum Type {
        POINT(Point.class),
        LINESTRING(LineString.class),
        POLYGON(Polygon.class),
        MULTIPOINT(MultiPoint.class),
        MULTILINESTRING(MultiLineString.class),
        MULTIPOLYGON(MultiPolygon.class),
        GEOMETRY(Geometry.class),
        GEOMETRYCOLLECTION(GeometryCollection.class);
        
        private final Class<? extends Geometry> type;
        private final String name;
        private final String simpleName;
        
        Type(Class<? extends Geometry> type) {
            this.type = type;
            this.name = type.getSimpleName();
            this.simpleName = (name.startsWith("Multi") ? name.substring(5) : name);
        }
        
        /**
         * Return the {@code Geometry} class associated with this type.
         *
         * @return the {@code Geometry} class
         */
        public Class<? extends Geometry> getType() {
            return type;
        }
        
        /**
         * Equivalent to {@linkplain #getName()}.
         *
         * @return the name of this type
         */
        @Override
        public String toString() {
            return name;
        }
        
        /**
         * Return a name for this type that is suitable for text descriptions.
         *
         * @return the name
         */
        public String getName() {
            return name;
        }
        
        /**
         * Get the 'simple name'. Returns the same value as {@linkplain #getName()}
         * except for MULTIPOINT, MULTILINESTRING and MULTIPOLYGON, for which it returns
         * the name without the 'Multi' prefix.
         *
         * @return the simple name
         */
        public String getSimpleName() {
            return simpleName;
        }
        
        /**
         * Get the {@code Geometries} for the given object.
         *
         * @param geom a JTS Geometry object
         *
         * @return the {@code Geometries} for the argument's class, or {@code null}
         *         if the argument is {@code null}
         */
        public static Type from(Geometry geom) {
            if (geom != null) {
                return from(geom.getClass());
            }
        
            return null;
        }
        
        /**
         * Get the {@code Geometries} for the given {@code Geometry} class.
         *
         * @param geomClass the class
         *
         * @return the constant for this class
         */
        public static Type from(Class<?> geomClass) {
            for (Type gt : Type.values()) {
                if (gt.type == geomClass) {
                    return gt;
                }
            }
            
            //no direct match look for a subclass
            Type match = null;
        
            for (Type gt : Type.values()) {
                if (gt == GEOMETRY || gt == GEOMETRYCOLLECTION) {
                    continue;
                }
                
                if (gt.type.isAssignableFrom(geomClass)) {
                    if (match == null) {
                        match = gt;
                    } else {
                        // more than one match
                        return null;
                    }
                }
            }
            
            if (match == null) {
                //no matches from concrete classes, try abstract classes
                if (GeometryCollection.class.isAssignableFrom(geomClass)) {
                    return GEOMETRYCOLLECTION;
                }
                if (Geometry.class.isAssignableFrom(geomClass)) {
                    return GEOMETRY;
                }
            }
            
            return match;
        }
        
        /**
         * Get the {@code Geometries} for the specified name.
         * 
         * @param name The name of the geometry, eg: "POINT"
         * 
         * @return The constant for the name.
         */
        public static Type from(String name) {
            for (Type gt : Type.values()) {
                if (gt.getName().equalsIgnoreCase(name)) {
                    return gt;
                }
            }
            return null;
        }
    }

    /**
     * Creates a new geometry builder.
     */
    public static GeomBuilder build() {
        return new GeomBuilder();
    }

    /**
     * Convenience method to build a Point geometry.
     */
    public static Point point(double x, double y) {
        return build().point(x, y).toPoint();
    }

    /**
     * Convenience method to build a LineString geometry.
     * 
     * @param ord Even number of ordinates forming coordinates for the line string.
     */
    public static LineString lineString(double... ord) {
        return build().points(ord).toLineString();
    }

    /**
     * Convenience method to build a Polygon geometry.
     * 
     * @param ord Even number of ordinates forming coordinates for the outer ring of the polygon.
     */
    public static Polygon polygon(double... ord) {
        return build().points(ord).toPolygon();
    }

    /**
     * Returns an iterable over the points of a multipoint.
     */
    public static Iterable<Point> iterate(MultiPoint mp) {
        return new GeometryIterable<>(mp);
    }

    /**
     * Returns an iterable over the lines of a multilinestring.
     */
    public static Iterable<LineString> iterate(MultiLineString ml) {
        return new GeometryIterable<>(ml);
    }

    /**
     * Returns an iterable over the polygons of a multipolygon.
     */
    public static Iterable<Polygon> iterate(MultiPolygon mp) {
        return new GeometryIterable<>(mp);
    }
    
    /**
     * Returns an iterable over the geometries of a geometry collection.. 
     */
    public static Iterable<Geometry> iterate(GeometryCollection gc) {
        return new GeometryIterable<>(gc);
    }

    /**
     * Returns an iterable over the interior rings of a polygon.
     */
    public static Iterable<LineString> holes(final Polygon p) {
        return new Iterable<LineString>() {
            int i = 0;

            @Override
            public Iterator<LineString> iterator() {
                return new Iterator<LineString>() {
                    @Override
                    public boolean hasNext() {
                        return i < p.getNumInteriorRing();
                    }

                    @Override
                    public LineString next() {
                        return p.getInteriorRingN(i++);
                    }

                    @Override
                    public void remove() {
                        throw new UnsupportedOperationException();
                    }
                };
            }
        };
    }

    /**
     * Returns the first point in a multi point, or <code>null</code> if the multi point is empty.
     */
    public static Point first(MultiPoint mp) {
        return mp.getNumGeometries() > 0 ? (Point) mp.getGeometryN(0) : null;
    }

    /**
     * Returns the first line in a multi line, or <code>null</code> if the multi line is empty.
     */
    public static LineString first(MultiLineString ml) {
        return ml.getNumGeometries() > 0 ? (LineString) ml.getGeometryN(0) : null;
    }

    /**
     * Returns the first polygon in a multi polygon, or <code>null</code> if the multi polygon is empty.
     */
    public static Polygon first(MultiPolygon mp) {
        return mp.getNumGeometries() > 0 ? (Polygon) mp.getGeometryN(0) : null;
    }

    /**
     * Returns the geometries of a geometry collection as an array.
     */
    @SuppressWarnings("unchecked")
    public static <T extends Geometry> T[] array(GeometryCollection gc, T[] array) {
        for (int i =0 ; i < gc.getNumGeometries(); i++) {
            array[i] = (T) gc.getGeometryN(i);
        }
        return array;
    }

    /**
     * Retypes (ie. narrows) a geometry collection if possible.
     * <p>
     * If the geometry contains a single object it is narrowed to that object. If the geometry collection is
     * homogeneous it is narrowed to the appropriate sub collection type. Otherwise the collection is returned
     * as is.
     * </p>
     *
     * @see {@link GeometryFactory#buildGeometry(Collection)}
     */
    public static Geometry narrow(GeometryCollection gc) {

        if (gc.getNumGeometries() == 0) {
            return gc;
        }

        List<Geometry> objects = new ArrayList<>(gc.getNumGeometries());
        for (Geometry g : iterate(gc)) {
            objects.add(g);
        }

        return gc.getFactory().buildGeometry(objects);
    }

    /**
     * Recursively flattens a geometry collection into it's constituent geometry objects.
     */
    public static List<Geometry> flatten(GeometryCollection gc) {
        return flatten(Collections.singletonList(gc));
    }

    /**
     * Recursively flattens a list of geometries into it's constituent geometry objects.
     */
    public static List<Geometry> flatten(List<Geometry> gl) {
        List<Geometry> flat = new ArrayList<>();
        LinkedList<Geometry> q = new LinkedList<>(gl);

        while (!q.isEmpty()) {
            Geometry g = q.removeFirst();
            if (g instanceof GeometryCollection) {
                for (Geometry h : iterate((GeometryCollection)g)) {
                    q.addLast(h);
                }
            }
            else {
                flat.add(g);
            }
        }

        return flat;
    }

    /**
     * Recursively unwraps a geometry collection containing single object.
     */
    public static Geometry singlify(Geometry geom) {
        while (geom instanceof GeometryCollection && geom.getNumGeometries() == 1) {
            geom = geom.getGeometryN(0);
        }
        return geom;
    }

    /*
     * Private iterable class.
     */
    private static class GeometryIterable<T extends Geometry> implements Iterable<T> {

        GeometryCollection gc;

        GeometryIterable(GeometryCollection gc) {
            this.gc = gc;
        }

        @Override
        public Iterator<T> iterator() {
            return new GeometryIterator<>(gc);
        }
    }

    /*
     * Private iterator class.
     */
    private static class GeometryIterator<T extends Geometry> implements Iterator<T> {

        int i = 0; 
        GeometryCollection gc;

        GeometryIterator(GeometryCollection gc) {
            this.gc = gc;
        }

        @Override
        public boolean hasNext() {
            return i < gc.getNumGeometries();
        }

        @SuppressWarnings("unchecked")
        @Override
        public T next() {
            return (T) gc.getGeometryN(i++);
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

    /**
     * Creates a prepared geometry.
     * <p>
     * Prepared geometries make operations like intersection must faster.
     * </p>
     */
    public static PreparedGeometry prepare(Geometry g) {
        return PreparedGeometryFactory.prepare(g);
    }

    /**
     * Converts a geometry object into the associated geometry collection. For
     * example Polygon to MultiPolygon.
     * <p>
     *  If the input is already a collection it is returned as is.
     * </p>
     */
    public static GeometryCollection multi(Geometry g) {
        switch(Geom.Type.from(g)) {
            case POINT:
                return factory.createMultiPoint(new Point[]{(Point)g});
            case LINESTRING:
                return factory.createMultiLineString(new LineString[]{(LineString)g});
            case POLYGON:
                return factory.createMultiPolygon(new Polygon[]{(Polygon)g});
            default:
                return (GeometryCollection) g;
        }
    }
}
