/*
 * Copyright 2013 Hannes Janetzek
 * Copyright 2016 Andrey Novikov
 * Copyright 2017-2019 Gustl22
 *
 * This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
 *
 * This program is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.oscim.core;

import org.oscim.utils.ArrayUtils;
import org.oscim.utils.geom.GeometryUtils;

import java.util.Arrays;

/* TODO
 * - check indexPos < Short.Max
 * - should make internals private, maybe
 */

/**
 * The GeometryBuffer class holds temporary geometry data for processing.
 * Only One geometry type can be set at a time. Use 'clear()' to reset the
 * internal state.
 * <p/>
 * 'points[]' holds interleaved x,y coordinates
 * <p/>
 * 'index[]' is used to store number of points within a geometry and encode
 * multi-linestrings and (multi-)polygons.
 */
public class GeometryBuffer {

    private static final int GROW_INDICES = 64;
    private static final int GROW_POINTS = 512;

    /**
     * The Enum GeometryType.
     */
    public enum GeometryType {
        NONE(0),
        POINT(1),
        LINE(2),
        POLY(3),
        TRIS(4);

        private GeometryType(int type) {
            nativeInt = type;
        }

        public final int nativeInt;
    }

    /**
     * The points.
     * <p>
     * POLY/LINE: store point in order of polygon with
     * points[2 * n + 0] = x; points[2 * n + 1] = y; n is a N.
     * <p>
     * MESH: store points anywhere with
     * points[3 * n + 0] = x; points[3 * n + 1] = y; points[3 * n + 2] = z; n ∈ ℕ0.
     */
    public float[] points;

    /**
     * The indexes.
     * <p>
     * POLY/LINE: store 2 * number of points of each polygon / line. Point is (x, y).
     * <p>
     * MESH: store point indices of triangle (p1, p2, p3) with
     * index[3 * n + 0] = p1; index[3 * n + 1] = p2; index[3 * n + 2] = p3; n ∈ ℕ0.
     * Point p is (x, y, z).
     */
    public int[] index;

    /**
     * The current index position.
     */
    public int indexCurrentPos;

    /**
     * The next position to insert a point in points array (equal to array size).
     */
    public int pointNextPos;

    /**
     * The current geometry type.
     */
    public GeometryType type;

    private PointF mTmpPoint = new PointF();
    private int pointLimit;

    public GeometryBuffer() {
        this(32, 4);
    }

    /**
     * Instantiates a new geometry buffer.
     *
     * @param numPoints  the num of expected 2D points
     * @param numIndices the num of expected indices
     */
    public GeometryBuffer(int numPoints, int numIndices) {
        this(new float[numPoints * 2], new int[numIndices]);
    }

    /**
     * Instantiates a new geometry buffer.
     *
     * @param points the points
     * @param index  the index
     */
    public GeometryBuffer(float[] points, int[] index) {
        if (points == null)
            points = new float[GROW_POINTS];
        if (index == null)
            index = new int[GROW_INDICES];

        this.points = points;
        this.index = index;
        this.type = GeometryType.NONE;
        this.indexCurrentPos = 0;
        this.pointNextPos = 0;
        this.pointLimit = points.length - 2;
    }

    /**
     * @param buffer the buffer to copy
     */
    public GeometryBuffer(GeometryBuffer buffer) {
        int indexSize = 0;
        while (indexSize < buffer.index.length && buffer.index[indexSize] != -1) {
            indexSize++;
        }
        this.points = Arrays.copyOf(buffer.points, buffer.pointNextPos);
        this.index = Arrays.copyOf(buffer.index, indexSize);

        this.pointNextPos = buffer.pointNextPos;
        this.indexCurrentPos = buffer.indexCurrentPos;
        this.type = buffer.type;
    }

    /**
     * @param out PointF to set coordinates to.
     * @param i   the 2D point position.
     * @return when out is null a temporary PointF is
     * returned which belongs to GeometryBuffer.
     */
    public void getPoint(int i, PointF out) {
        out.x = points[(i << 1)];
        out.y = points[(i << 1) + 1];
    }

    /**
     * @param i the 2D point position.
     * @return the x-coordinate of point.
     */
    public float getPointX(int i) {
        return points[(i << 1)];
    }

    /**
     * @param i the 2D point position.
     * @return the y-coordinate of point.
     */
    public float getPointY(int i) {
        return points[(i << 1) + 1];
    }

    /**
     * @param i the 2D point position.
     * @return the PointF that belongs to GeometryBuffer.
     */
    public PointF getPoint(int i) {
        PointF out = mTmpPoint;
        out.x = points[(i << 1)];
        out.y = points[(i << 1) + 1];
        return out;
    }

    /**
     * Get the number of 2D points.
     *
     * @return the number of 2D points.
     */
    public int getNumPoints() {
        return pointNextPos >> 1;
    }

    /**
     * Get the used size of points array.
     * Equal to the next position to insert point in points array.
     *
     * @return the size of point array.
     */
    public int getPointsSize() {
        return pointNextPos;
    }

    /**
     * Reset buffer.
     */
    public GeometryBuffer clear() {
        index[0] = 0;
        indexCurrentPos = 0;
        pointNextPos = 0;
        type = GeometryType.NONE;
        return this;
    }

    /**
     * Adds a point with the coordinate x, y.
     *
     * @param x the x ordinate
     * @param y the y ordinate
     */
    public GeometryBuffer addPoint(float x, float y) {
        if (pointNextPos > pointLimit)
            ensurePointSize((pointNextPos >> 1) + 1, true);

        points[pointNextPos++] = x;
        points[pointNextPos++] = y;

        index[indexCurrentPos] += 2;
        return this;
    }

    public boolean isPoly() {
        return type == GeometryType.POLY;
    }

    public boolean isLine() {
        return type == GeometryType.LINE;
    }

    public boolean isPoint() {
        return type == GeometryType.POINT;
    }

    public boolean isTris() {
        return type == GeometryType.TRIS;
    }

    /**
     * Sets the point x,y at position pos.
     *
     * @param pos the 2D point position
     * @param x   the x coordinate (abscissa)
     * @param y   the y coordinate (ordinate)
     */
    public void setPoint(int pos, float x, float y) {
        points[(pos << 1) + 0] = x;
        points[(pos << 1) + 1] = y;
    }

    /**
     * Set geometry type for points.
     */
    public void startPoints() {
        setOrCheckMode(GeometryType.POINT);
    }

    /**
     * Start a new line. Sets geometry type for lines.
     */
    public GeometryBuffer startLine() {
        setOrCheckMode(GeometryType.LINE);

        /* ignore */
        if (index[indexCurrentPos] > 0) {

            /* start next */
            if ((index[0] >= 0) && (++indexCurrentPos >= index.length))
                ensureIndexSize(indexCurrentPos, true);

            /* initialize with zero points */
            index[indexCurrentPos] = 0;
        }

        /* set new end marker */
        if (index.length > indexCurrentPos + 1)
            index[indexCurrentPos + 1] = -1;
        return this;
    }

    /**
     * Start a new polygon. Sets geometry type for polygons.
     */
    public GeometryBuffer startPolygon() {
        boolean start = (type == GeometryType.NONE);
        setOrCheckMode(GeometryType.POLY);

        if ((indexCurrentPos + 3) > index.length)
            ensureIndexSize(indexCurrentPos + 2, true);

        if (!start && index[indexCurrentPos] != 0) {
            /* end polygon */
            index[++indexCurrentPos] = 0;

            /* next polygon start */
            indexCurrentPos++;
        }

        /* initialize with zero points */
        index[indexCurrentPos] = 0;

        /* set new end marker */
        if (index.length > indexCurrentPos + 1)
            index[indexCurrentPos + 1] = -1;

        return this;
    }

    /**
     * Starts a new polygon hole (inner ring).
     */
    public void startHole() {
        checkMode(GeometryType.POLY);

        if ((indexCurrentPos + 2) > index.length)
            ensureIndexSize(indexCurrentPos + 1, true);

        /* initialize with zero points */
        index[++indexCurrentPos] = 0;

        /* set new end marker */
        if (index.length > indexCurrentPos + 1)
            index[indexCurrentPos + 1] = -1;
    }

    /**
     * Translate.
     *
     * @param dx the x translation.
     * @param dy the y translation.
     * @return a reference to this object.
     */
    public GeometryBuffer translate(float dx, float dy) {
        for (int i = 0; i < pointNextPos; i += 2) {
            points[i] += dx;
            points[i + 1] += dy;
        }
        return this;
    }

    /**
     * Scale.
     *
     * @param scaleX the x scale.
     * @param scaleY the y scale.
     * @return a reference to this object.
     */
    public GeometryBuffer scale(float scaleX, float scaleY) {
        for (int i = 0; i < pointNextPos; i += 2) {
            points[i] *= scaleX;
            points[i + 1] *= scaleY;
        }
        return this;
    }

    /**
     * Ensure that 'points' array can hold the number of points.
     *
     * @param size the number of points to hold
     * @param copy the current data when array is reallocated
     * @return the float[] array holding current coordinates
     */
    public float[] ensurePointSize(int size, boolean copy) {
        if (size * 2 < points.length)
            return points;

        size = size * 2 + GROW_POINTS;

        float[] newPoints = new float[size];
        if (copy)
            System.arraycopy(points, 0, newPoints, 0, points.length);

        points = newPoints;
        pointLimit = size - 2;

        return points;
    }

    /**
     * Ensure index size.
     *
     * @param size the size
     * @param copy the copy
     * @return the short[] array holding current index
     */
    public int[] ensureIndexSize(int size, boolean copy) {
        if (size < index.length)
            return index;

        int[] newIndex = new int[size + GROW_INDICES];
        if (copy)
            System.arraycopy(index, 0, newIndex, 0, index.length);

        index = newIndex;

        return index;
    }

    private void setOrCheckMode(GeometryType m) {
        if (type == m)
            return;

        if (type != GeometryType.NONE)
            throw new IllegalArgumentException("not cleared " + m + "<>" + type);

        type = m;
    }

    private void checkMode(GeometryType m) {
        if (type != m)
            throw new IllegalArgumentException("not cleared " + m + "<>" + type);
    }

    /**
     * Add a {@link Point}.
     *
     * @param p the point.
     */
    public void addPoint(Point p) {
        addPoint((float) p.x, (float) p.y);
    }

    /**
     * Add a {@link PointF}.
     *
     * @param p the point.
     */
    public void addPoint(PointF p) {
        addPoint(p.x, p.y);
    }

    /**
     * Remove points with distance less than minSqDist
     * <p/>
     * TODO could avoid superfluous copying
     *
     * @param minSqDist
     * @param keepLines keep endpoint when line would
     *                  otherwise collapse into a single point
     */
    public void simplify(float minSqDist, boolean keepLines) {
        int outPos = 0;
        int inPos = 0;
        for (int idx = 0; idx < index.length; idx++) {
            if (index[idx] < 0)
                break;
            if (index[idx] == 0)
                continue;

            int first = inPos;
            float px = points[inPos++];
            float py = points[inPos++];

            /* add first point */
            points[outPos++] = px;
            points[outPos++] = py;
            int cnt = 2;

            for (int pt = 2, end = index[idx]; pt < end; pt += 2) {
                float cx = points[inPos++];
                float cy = points[inPos++];
                float dx = cx - px;
                float dy = cy - py;

                if ((dx * dx + dy * dy) < minSqDist) {
                    if (!keepLines || (pt < end - 2))
                        continue;
                }
                px = cx;
                py = cy;
                points[outPos++] = cx;
                points[outPos++] = cy;
                cnt += 2;
            }

            if ((type == GeometryType.POLY) &&
                    (points[first] == px) &&
                    (points[first + 1] == py)) {
                /* remove identical start/end point */
                cnt -= 2;
                outPos -= 2;
            }
            index[idx] = cnt;
        }
    }

    /**
     * Calculates geometry area, only polygon outer ring is taken into account.
     *
     * @return unsigned polygon area, 0 for other geometries
     * @see GeometryUtils#area(float[], int)
     */
    public float area() {
        float area = isClockwise();
        return area < 0 ? -area : area;
    }

    /**
     * @see GeometryUtils#isClockwise(float[], int).
     */
    public float isClockwise() {
        if (isPoint() || isLine() || getNumPoints() < 3)
            return 0f;

        // use only outer ring
        return GeometryUtils.isClockwise(points, index[0]);
    }

    /**
     * Remove the last point.
     */
    public void removeLastPoint() {
        if (!isTris()) {
            pointNextPos -= 2;
            index[indexCurrentPos] -= 2;
        }
    }

    /**
     * Reverse the order of points for lines and polygons.
     */
    public void reverse() {
        if (isLine() || isPoly()) {
            int count = 0;
            for (int num : index) {
                if (num < 0)
                    break;
                ArrayUtils.reverse(points, count, count + num, 2);
                count += num;
            }
        }
    }

    public String toString() {
        StringBuffer sb = new StringBuffer();
        int o = 0;
        for (int i = 0; i < index.length; i++) {
            if (index[i] < 0)
                break;

            if (!isTris()) {
                if (index[i] == 0)
                    continue;
                sb.append("POLY (")
                        .append(i)
                        .append(") { ");

                for (int j = 0; j < index[i]; j += 2) {
                    sb.append('[')
                            .append(points[o + j])
                            .append(", ")
                            .append(points[o + j + 1])
                            .append(']');

                    if (j % 4 == 0)
                        sb.append('\n');
                }
                sb.append(" } \tnumPoints:")
                        .append(index[i])
                        .append('\n');
                o += index[i];
            } else {
                if (i % 3 == 0)
                    sb.append("TRIS { ");
                sb.append('\t')
                        .append(index[i])
                        .append('[')
                        .append(points[3 * index[i]])
                        .append(", ")
                        .append(points[3 * index[i] + 1])
                        .append(", ")
                        .append(points[3 * index[i] + 2])
                        .append(']');
                if (i % 3 == 2)
                    sb.append(" }\n");
            }
        }
        return sb.toString();
    }

    public static GeometryBuffer makeCircle(float x, float y,
                                            float radius, int segments) {
        GeometryBuffer g = new GeometryBuffer(segments, 1);
        makeCircle(g, x, y, radius, segments);
        return g;
    }

    public static GeometryBuffer makeCircle(GeometryBuffer g,
                                            float x, float y, float radius, int segments) {
        g.clear();
        g.startPolygon();
        for (int i = 0; i < segments; i++) {
            double rad = Math.toRadians(i * (360f / segments));

            g.addPoint((float) (x + Math.cos(rad) * radius),
                    (float) (y + Math.sin(rad) * radius));
        }
        return g;
    }
}
