/*
 * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
 * Copyright (c) 1998-2008, University of Maryland
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list of conditions
 * and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
 * and the following disclaimer in the documentation and/or other materials provided with the
 * distribution.
 *
 * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
 * contributors may be used to endorse or promote products derived from this software without specific
 * prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package edu.umd.cs.piccolox.util;

import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;

/**
 * Represents a sequence as points that's internally stored as a single array of
 * point components.
 */
public class XYArray implements MutablePoints, Cloneable {
    /** The coordinates of the points, specifically 2x the number of points. */
    private double[] points = null;

    /** the number of valid x, y pairs. */
    private int numPoints = 0;

    /**
     * Constructs an XYArray wrapping the given points.
     * 
     * @param points array of coordinates defining the points
     */
    public XYArray(final double[] points) {
        initPoints(points, points.length / 2);
    }

    /**
     * Constructs an XYArray of the given size.
     * 
     * @param n number of points XYArray should contain
     */
    public XYArray(final int n) {
        initPoints(null, n);
    }

    /**
     * Constructs an empty XYArray.
     */
    public XYArray() {
        this(0);
    }

    /**
     * Returns the number of points this XYArray represents.
     * 
     * @return number of points
     */
    public int getPointCount() {
        return numPoints;
    }

    /**
     * Converts negative indexes to positive ones by adding numPoints to it.
     * 
     * @param i index to be normalized
     * @return normalized index
     */
    private int normalize(final int i) {
        if (i >= numPoints) {
            throw new IllegalArgumentException("The point index " + i + " is not below " + numPoints);
        }

        if (i < 0) {
            return numPoints + i;
        }
        else {
            return i;
        }
    }

    /**
     * Returns the x component of the point at the given index.
     * 
     * @param i index of point
     * @return x component of point at given index
     */
    public double getX(final int i) {
        return points[normalize(i) * 2];
    }

    /**
     * Returns the y component of the point at the given index.
     * 
     * @param i index of point
     * @return y component of point at given index
     */
    public double getY(final int i) {
        return points[normalize(i) * 2 + 1];
    }

    /**
     * Returns modified point representing the wrapped point at the given index.
     * 
     * @param i index of desired point
     * @param dst point to be modified
     * @return dst
     */
    public Point2D getPoint(final int i, final Point2D dst) {
        final int pointIndex = normalize(i);
        dst.setLocation(points[pointIndex * 2], points[pointIndex * 2 + 1]);
        return dst;
    }

    /**
     * Sets the x component of the point at the given index.
     * 
     * @param i index of point to modify
     * @param x new x component
     */
    public void setX(final int i, final double x) {
        points[normalize(i) * 2] = x;
    }

    /**
     * Sets the y component of the point at the given index.
     * 
     * @param i index of point to modify
     * @param y new y component
     */
    public void setY(final int i, final double y) {
        points[normalize(i) * 2 + 1] = y;
    }

    /**
     * Sets the coordinates of the point at the given index.
     * 
     * @param i index of point to modify
     * @param x new x component
     * @param y new y component
     */
    public void setPoint(final int i, final double x, final double y) {
        final int pointIndex = normalize(i);
        points[pointIndex * 2] = x;
        points[pointIndex * 2 + 1] = y;
    }

    /**
     * Sets the coordinates of the point at the given index.
     * 
     * @param i index of point to modify
     * @param pt point from which coordinate is to be extracted
     */
    public void setPoint(final int i, final Point2D pt) {
        setPoint(i, pt.getX(), pt.getY());
    }

    /**
     * Applies the given transform to all points represented by this XYArray.
     * 
     * @param t transform to apply
     */
    public void transformPoints(final AffineTransform t) {
        t.transform(points, 0, points, 0, numPoints);
    }

    /**
     * Modifies dst to be the bounding box of the points represented by this
     * XYArray.
     * 
     * @param dst rectangle to be modified
     * @return the bounding rectangle
     */
    public Rectangle2D getBounds(final Rectangle2D dst) {
        int i = 0;
        if (dst.isEmpty() && getPointCount() > 0) {
            dst.setRect(getX(i), getY(i), 1.0d, 1.0d);
            i++;
        }
        while (i < getPointCount()) {
            dst.add(getX(i), getY(i));
            i++;
        }
        return dst;
    }

    /**
     * Constructs an array of point coordinates for n points and copies the old
     * values if provided.
     * 
     * @param points array to populate with point values, or null to generate a
     *            new array
     * @param n number of points
     * @param old old values to repopulate the array with, or null if not
     *            desired
     * @return initialized points
     */
    public static double[] initPoints(final double[] points, final int n, final double[] old) {
        final double[] result;
        if (points == null || n * 2 > points.length) {
            result = new double[n * 2];
        }
        else {
            result = points;
        }
        if (old != null && result != old) {
            System.arraycopy(old, 0, result, 0, Math.min(old.length, n * 2));
        }

        return result;
    }

    /**
     * Constructs an array of point coordinates for n points.
     * 
     * @param srcPoints array to populate with point values, or null to generate
     *            a new array
     * @param n number of points
     */
    private void initPoints(final double[] srcPoints, final int n) {
        this.points = initPoints(srcPoints, n, this.points);
        if (srcPoints == null) {
            numPoints = 0;
        }
        else {
            numPoints = srcPoints.length / 2;
        }
    }

    /**
     * Adds a subsequence of the points provided at the given position.
     * 
     * @param index position at which the points should be inserted
     * @param newPoints points from which to extract the subsequence of points
     * @param start the start index within newPoints to start extracting points
     * @param end the end index within newPoints to finish extracting points
     */
    public void addPoints(final int index, final Points newPoints, final int start, final int end) {
        final int sanitizedEnd;
        if (end < 0) {
            sanitizedEnd = newPoints.getPointCount() + end + 1;
        }
        else {
            sanitizedEnd = end;
        }
        final int n = numPoints + sanitizedEnd - start;
        points = initPoints(points, n, points);
        final int pos1 = index * 2;
        final int pos2 = (index + sanitizedEnd - start) * 2;
        final int len = (numPoints - index) * 2;

        System.arraycopy(points, pos1, points, pos2, len);

        numPoints = n;
        if (newPoints != null) {
            for (int count = 0, currentPos = start; currentPos < sanitizedEnd; count++, currentPos++) {
                setPoint(index + count, newPoints.getX(currentPos), newPoints.getY(currentPos));
            }
        }
    }

    /**
     * Inserts all the provided points at the given position.
     * 
     * @param pos index at which to insert the points
     * @param pts points to be inserted
     */
    public void addPoints(final int pos, final Points pts) {
        addPoints(pos, pts, 0, pts.getPointCount());
    }

    /**
     * Adds the provided points to the end of the points.
     * 
     * @param pts points to be added
     */
    public void appendPoints(final Points pts) {
        addPoints(numPoints, pts);
    }

    /**
     * Creates an XYArray representing the given points.
     * 
     * @param pts points to copy
     * @return XYArray representing the points provided
     */
    public static XYArray copyPoints(final Points pts) {
        final XYArray newList = new XYArray(pts.getPointCount());
        newList.appendPoints(pts);
        return newList;
    }

    /**
     * Adds a point to the index provided.
     * 
     * @param pos index at which to add the point
     * @param x x coordinate of new point
     * @param y y coordinate of new point
     */
    public void addPoint(final int pos, final double x, final double y) {
        addPoints(pos, null, 0, 1);
        setPoint(pos, x, y);
    }

    /**
     * Inserts the given point at the given index.
     * 
     * @param pos index at which to add the point
     * @param pt point to be inserted *
     */
    public void addPoint(final int pos, final Point2D pt) {
        addPoint(pos, pt.getX(), pt.getY());
    }

    /**
     * Remove a subsequence of points from this XYArray starting as pos.
     * 
     * @param pos the position to start removing points
     * @param num the number of points to remove
     */
    public void removePoints(final int pos, final int num) {
        int sanitizedNum = Math.min(num, numPoints - pos);
        if (sanitizedNum > 0) {
            System.arraycopy(points, (pos + sanitizedNum) * 2, points, pos * 2, (numPoints - (pos + sanitizedNum)) * 2);
            numPoints -= sanitizedNum;
        }
    }

    /**
     * Remove all points from this XYArray.
     */
    public void removeAllPoints() {
        removePoints(0, numPoints);
    }

    /**
     * Returns a clone of this XYArray ensuring a deep copy of coordinates is
     * made.
     * 
     * @return cloned XYArray
     */
    public Object clone() {
        XYArray ps = null;

        try {
            ps = (XYArray) super.clone();
            ps.points = initPoints(ps.points, numPoints, points);
            ps.numPoints = numPoints;
        }
        catch (final CloneNotSupportedException e) {
            // wow, this is terrible.
        }

        return ps;
    }
}
