/*
 * 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.apache.sis.geometry.wrapper.esri;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.nio.ByteBuffer;
import java.io.ObjectStreamException;
import com.esri.core.geometry.Geometry;
import com.esri.core.geometry.Line;
import com.esri.core.geometry.Point;
import com.esri.core.geometry.Polygon;
import com.esri.core.geometry.Polyline;
import com.esri.core.geometry.MultiPath;
import com.esri.core.geometry.MultiPoint;
import com.esri.core.geometry.OperatorCentroid2D;
import com.esri.core.geometry.OperatorImportFromWkb;
import com.esri.core.geometry.OperatorImportFromWkt;
import com.esri.core.geometry.WkbImportFlags;
import com.esri.core.geometry.WktImportFlags;
import org.apache.sis.setup.GeometryLibrary;
import org.apache.sis.geometry.wrapper.Geometries;
import org.apache.sis.geometry.wrapper.GeometryType;
import org.apache.sis.geometry.wrapper.GeometryWrapper;
import org.apache.sis.util.internal.CollectionsExt;
import org.apache.sis.math.Vector;


/**
 * The factory of geometry objects backed by ESRI.
 *
 * @author  Johann Sorel (Geomatys)
 * @author  Martin Desruisseaux (Geomatys)
 * @author  Alexis Manin (Geomatys)
 */
public final class Factory extends Geometries<Geometry> {
    /**
     * For cross-version compatibility.
     */
    private static final long serialVersionUID = 7832006589071845318L;

    /**
     * The singleton instance of this factory.
     */
    public static final Factory INSTANCE = new Factory();

    /**
     * Invoked at deserialization time for obtaining the unique instance of this {@code Factory} class.
     *
     * @return {@link #INSTANCE}.
     * @throws ObjectStreamException if the object state is invalid.
     */
    protected Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }

    /**
     * Creates the singleton instance.
     */
    private Factory() {
        super(GeometryLibrary.ESRI, Geometry.class, Point.class, Polyline.class, Polygon.class);
    }

    /**
     * Returns the geometry object to return to the user in public API.
     *
     * @param  wrapper  the wrapper for which to get the geometry, or {@code null}.
     * @return the ESRI geometry instance, or {@code null} if the given wrapper was null.
     * @throws ClassCastException if the given wrapper is not an instance of the class expected by this factory.
     */
    @Override
    public Object getGeometry(final GeometryWrapper wrapper) {
        if (wrapper instanceof Wrapper) {
            // Intentionally stronger cast than needed.
            return ((Wrapper) wrapper).implementation();
        } else {
            return super.getGeometry(wrapper);
        }
    }

    /**
     * Returns a wrapper for the given {@code <G>} or {@code GeometryWrapper} geometry.
     *
     * @param  geometry  the geometry instance to wrap (can be {@code null}).
     * @return a wrapper for the given geometry implementation, or {@code null}.
     * @throws ClassCastException if the given geometry is not an instance of valid type.
     */
    @Override
    public GeometryWrapper castOrWrap(final Object geometry) {
        if (geometry == null || geometry instanceof Wrapper) {
            return (Wrapper) geometry;
        } else {
            return new Wrapper((Geometry) geometry);
        }
    }

    /**
     * Creates a wrapper for the given geometry instance.
     *
     * @param  geometry  the geometry to wrap.
     * @return wrapper for the given geometry.
     */
    @Override
    protected GeometryWrapper createWrapper(final Geometry geometry) {
        return new Wrapper(geometry);
    }

    /**
     * Creates a two-dimensional point from the given coordinates.
     */
    @Override
    public Object createPoint(final double x, final double y) {
        return new Point(x, y);
    }

    /**
     * Creates a three-dimensional point from the given coordinates.
     */
    @Override
    public Object createPoint(final double x, final double y, final double z) {
        return new Point(x, y, z);
    }

    /**
     * Creates a polyline from the given coordinate values.
     * Each {@link Double#NaN} coordinate value starts a new path.
     *
     * @param  polygon      whether to return the path as a polygon instead of polyline.
     * @param  dimension    the number of dimensions ({@value #BIDIMENSIONAL} or {@value #TRIDIMENSIONAL}).
     * @param  coordinates  sequence of (x,y) or (x,y,z) tuples.
     * @throws UnsupportedOperationException if this operation is not implemented for the given number of dimensions.
     */
    @Override
    public Geometry createPolyline(final boolean polygon, final int dimension, final Vector... coordinates) {
        if (dimension != BIDIMENSIONAL) {
            throw new UnsupportedOperationException(unsupported(dimension));
        }
        boolean lineTo = false;
        final Polyline path = new Polyline();
        for (final Vector v : coordinates) {
            if (v != null) {
                final int size = v.size();
                for (int i=0; i<size;) {
                    final double x = v.doubleValue(i++);
                    final double y = v.doubleValue(i++);
                    if (Double.isNaN(x) || Double.isNaN(y)) {
                        lineTo = false;
                    } else if (lineTo) {
                        path.lineTo(x, y);
                    } else {
                        path.startPath(x, y);
                        lineTo = true;
                    }
                }
            }
        }
        if (polygon) {
            final Polygon p = new Polygon();
            p.add(path, false);
            return p;
        }
        return path;
    }

    /**
     * Creates a multi-polygon from an array of geometries.
     * Callers must ensure that the given objects are ESRI geometries.
     *
     * @param  geometries  the polygons or linear rings to put in a multi-polygons.
     * @throws ClassCastException if an element in the array is not an ESRI geometry.
     */
    @Override
    public GeometryWrapper createMultiPolygon(final Object[] geometries) {
        final Polygon polygon = new Polygon();
        for (final Object geometry : geometries) {
            polygon.add((MultiPath) implementation(geometry), false);
        }
        return new Wrapper(polygon);
    }

    /**
     * Creates a geometry from components.
     * The expected {@code components} type depend on the target geometry type:
     * <ul>
     *   <li>If {@code type} is a multi-geometry, then the components shall be an array of {@link Point},
     *       {@link Geometry}, {@link Polyline} or {@link Polygon} elements, depending on the desired target type.</li>
     *   <li>Otherwise the components shall be an array or collection of {@link Point} instances.</li>
     * </ul>
     *
     * @param  type        type of geometry to create.
     * @param  components  the components. Valid classes depend on the type of geometry to create.
     * @return geometry built from the given components.
     * @throws ClassCastException if the given object is not an array or a collection of supported geometry components.
     */
    @Override
    public GeometryWrapper createFromComponents(final GeometryType type, final Object components) {
        /*
         * No exhaustive `if (x instanceof y)` checks in this method.
         * `ClassCastException` shall be handled by the caller.
         */
        final Collection<?> data = (components instanceof Collection<?>)
                ? (Collection<?>) components : Arrays.asList((Object[]) components);
        /*
         * ESRI API does not distinguish between single geometry and geometry collection, except MultiPoint.
         * So if the number of components is 1, there is no reason to create a new geometry object.
         */
        Geometry geometry = (Geometry) CollectionsExt.singletonOrNull(data);
        if (geometry == null) {
            boolean isPolygon = false;
            switch (type) {
                case MULTILINESTRING:
                case LINESTRING: break;
                case MULTIPOLYGON:
                case POLYGON: isPolygon=true; break;
                case GEOMETRYCOLLECTION: {
                    for (final Object component : data) {
                        isPolygon = (((Geometry) component).getType() == Geometry.Type.Polygon);
                        if (!isPolygon) break;
                    }
                    break;
                }
                case GEOMETRY:      // Default to multi-points for now.
                case POINT:
                case MULTIPOINT: {
                    final MultiPoint points = new MultiPoint();
                    for (final Object p : data) {
                        points.add((Point) p);
                    }
                    geometry = points;
                    if (type == GeometryType.POINT) {
                        geometry = new Point(OperatorCentroid2D.local().execute(geometry, null));
                    }
                    break;
                }
                default: throw new AssertionError(type);
            }
            if (geometry == null) {
                final MultiPath path = isPolygon ? new Polygon() : new Polyline();
                if (type.isCollection()) {
                    for (final Object component : data) {
                        path.add((MultiPath) component, false);
                    }
                } else {
                    final Iterator<?> it = data.iterator();
                    if (it.hasNext()) {
                        final Line segment = new Line();
                        segment.setEnd((Point) it.next());
                        while (it.hasNext()) {
                            segment.setStartXY(segment.getEndX(), segment.getEndY());
                            segment.setEnd((Point) it.next());
                            path.addSegment(segment, false);
                        }
                    }
                }
                geometry = path;
            }
        }
        return new Wrapper(geometry);
    }

    /**
     * Parses the given Well Known Text (WKT).
     *
     * @param  wkt  the Well Known Text to parse.
     * @return the geometry object for the given WKT.
     */
    @Override
    public GeometryWrapper parseWKT(final String wkt) {
        return new Wrapper(OperatorImportFromWkt.local().execute(WktImportFlags.wktImportDefaults, Geometry.Type.Unknown, wkt, null));
    }

    /**
     * Reads the given Well Known Binary (WKB).
     *
     * @param  data  the sequence of bytes to parse.
     * @return the geometry object for the given WKB.
     */
    @Override
    public GeometryWrapper parseWKB(final ByteBuffer data) {
        return new Wrapper(OperatorImportFromWkb.local().execute(WkbImportFlags.wkbImportDefaults, Geometry.Type.Unknown, data, null));
    }
}
