/*
 * 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.internal.feature.j2d;

import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.lang.reflect.Array;
import java.awt.Shape;
import java.awt.geom.PathIterator;
import java.awt.geom.IllegalPathStateException;
import org.apache.sis.internal.referencing.j2d.ShapeUtilities;
import org.apache.sis.util.StringBuilders;


/**
 * Provides some information about a {@link Shape} object.
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 1.0
 * @since   1.0
 * @module
 */
public final class ShapeProperties {
    /**
     * The geometry.
     */
    private final Shape geometry;

    /**
     * {@code true} if last call to {@link #coordinates(double)} found only closed shapes.
     * In such case, the shapes are presumed polygons. Note that we do not verify if some
     * polygons are actually holes inside other polygons.
     */
    private boolean isPolygon;

    /**
     * Creates a new inspector for the given geometry.
     *
     * @param  geometry  the shape for which to inspect properties.
     */
    public ShapeProperties(final Shape geometry) {
        this.geometry = geometry;
    }

    /**
     * Appends the two first coordinates of {@code source} at the end of {@code target}, expanding the target array if necessary.
     *
     * @param  source  array of coordinates to add. Only the two first values are used.
     * @param  target  where to add the two coordinates.
     * @param  index   index in {@code target} where to add the two coordinates.
     * @return {@code target}, possible as a new array if it was necessary to expand it.
     */
    private static double[] addPoint(final double[] source, double[] target, final int index) {
        if (index >= target.length) {
            target = Arrays.copyOf(target, index*2);
        }
        System.arraycopy(source, 0, target, index, 2);
        return target;
    }

    /**
     * Same as {@link #addPoint(double[], double[], int)} but for single-precision numbers.
     */
    private static float[] addPoint(final float[] source, float[] target, final int index) {
        if (index >= target.length) {
            target = Arrays.copyOf(target, index*2);
        }
        System.arraycopy(source, 0, target, index, 2);
        return target;
    }

    /**
     * Returns coordinates of the given geometry as a list of (<var>x</var>,<var>y</var>) tuples in {@code float[]}
     * or {@code double[]} arrays. This method guarantees that all arrays have at least 2 points (4 coordinates).
     * It should be invoked only for small or medium shapes. For large shapes, the path iterator should be used
     * directly without copy to arrays.
     *
     * @param  flatness   maximal distance between the approximated segments and any point on the curve.
     * @return coordinate tuples. They are presumed polygons if {@link #isPolygon} is {@code true}.
     */
    private List<?> coordinates(final double flatness) {
        final PathIterator it = geometry.getPathIterator(null, flatness);
        isPolygon = true;
        if (ShapeUtilities.isFloat(geometry)) {
            return coordinatesAsFloats(it);
        } else {
            return coordinatesAsDoubles(it);
        }
    }

    /**
     * {@link #coordinates(double)} implementation for the double-precision case.
     * The {@link #isPolygon} field needs to be set before to invoke this method.
     */
    private List<double[]> coordinatesAsDoubles(final PathIterator it) {
        final List<double[]> polylines = new ArrayList<>();
        double[] polyline = new double[10];
        final double[] coords = new double[6];
        /*
         * Double-precision variant of this method. Source code below is identical to the single-precision variant,
         * but the methods invoked are different because of method overloading. Trying to have a common code is too
         * complex (too many code are different despite looking the same).
         */
        int i = 0;
        while (!it.isDone()) {
            switch (it.currentSegment(coords)) {
                case PathIterator.SEG_MOVETO: {
                    if (i > 2) {
                        isPolygon = false;          // MOVETO without CLOSE: this is a linestring instead than a polygon.
                        polylines.add(Arrays.copyOf(polyline, i));
                    }
                    System.arraycopy(coords, 0, polyline, 0, 2);
                    i = 2;
                    break;
                }
                case PathIterator.SEG_LINETO: {
                    polyline = addPoint(coords, polyline, i);
                    i += 2;
                    break;
                }
                case PathIterator.SEG_CLOSE: {
                    if (i > 2) {
                        if (polyline[0] != polyline[i-2] || polyline[1] != polyline[i-1]) {
                            polyline = addPoint(polyline, polyline, i);
                            i += 2;
                        }
                        polylines.add(Arrays.copyOf(polyline, i));
                    }
                    i = 0;
                    break;
                }
                default: throw new IllegalPathStateException();
            }
            it.next();
        }
        if (i > 2) {
            isPolygon = false;          // LINETO without CLOSE: this is a linestring instead than a polygon.
            polylines.add(Arrays.copyOf(polyline, i));
        }
        return polylines;
    }

    /**
     * {@link #coordinates(double)} implementation for the single-precision case.
     * The {@link #isPolygon} field needs to be set before to invoke this method.
     */
    private List<float[]> coordinatesAsFloats(final PathIterator it) {
        final List<float[]> polylines = new ArrayList<>();
        float[] polyline = new float[10];
        final float[] coords = new float[6];
        /*
         * Single-precision variant of this method. Source code below is identical to the double-precision variant,
         * but the methods invoked are different because of method overloading. Trying to have a common code is too
         * complex (too many code are different despite looking the same).
         */
        int i = 0;
        while (!it.isDone()) {
            switch (it.currentSegment(coords)) {
                case PathIterator.SEG_MOVETO: {
                    if (i > 2) {
                        isPolygon = false;          // MOVETO without CLOSE: this is a linestring instead than a polygon.
                        polylines.add(Arrays.copyOf(polyline, i));
                    }
                    System.arraycopy(coords, 0, polyline, 0, 2);
                    i = 2;
                    break;
                }
                case PathIterator.SEG_LINETO: {
                    polyline = addPoint(coords, polyline, i);
                    i += 2;
                    break;
                }
                case PathIterator.SEG_CLOSE: {
                    if (i > 2) {
                        if (polyline[0] != polyline[i-2] || polyline[1] != polyline[i-1]) {
                            polyline = addPoint(polyline, polyline, i);
                            i += 2;
                        }
                        polylines.add(Arrays.copyOf(polyline, i));
                    }
                    i = 0;
                    break;
                }
                default: throw new IllegalPathStateException();
            }
            it.next();
        }
        if (i > 2) {
            isPolygon = false;          // LINETO without CLOSE: this is a linestring instead than a polygon.
            polylines.add(Arrays.copyOf(polyline, i));
        }
        return polylines;
    }

    /**
     * Returns a WKT representation of the geometry.
     * Current implementation assumes that all closed shapes are polygons and that polygons have no hole
     * (i.e. if a polygon is followed by more data, this method assumes that the additional data is a disjoint polygon).
     *
     * @param  flatness   maximal distance between the approximated segments and any point on the curve.
     * @return Well Known Text representation of the geometry.
     *
     * @see <a href="https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry">Well-known text on Wikipedia</a>
     */
    public String toWKT(final double flatness) {
        final List<?> polylines = coordinates(flatness);
        final boolean isMulti;
        switch (polylines.size()) {
            case 0:  return "POLYGON EMPTY";
            case 1:  isMulti = false; break;
            default: isMulti = true;  break;
        }
        final StringBuilder buffer = new StringBuilder(80);
        if (isMulti) {
            buffer.append("MULTI");
        }
        buffer.append(isPolygon ? "POLYGON" : "LINESTRING").append(' ');
        if (isMulti) buffer.append('(');
        for (int j=0; j<polylines.size(); j++) {
            final Object polyline = polylines.get(j);
            if (j != 0) buffer.append(", ");
            buffer.append('(');
            if (isPolygon) buffer.append('(');
            final int length = Array.getLength(polyline);
            for (int i=0; i<length; i++) {
                if (i != 0) {
                    if ((i & 1) == 0) buffer.append(',');
                    buffer.append(' ');
                }
                if (polyline instanceof double[]) {
                    buffer.append(((double[]) polyline)[i]);
                } else {
                    buffer.append(((float[]) polyline)[i]);
                }
                StringBuilders.trimFractionalPart(buffer);
            }
            if (isPolygon) buffer.append(')');
            buffer.append(')');
        }
        if (isMulti) buffer.append(')');
        return buffer.toString();
    }
}
