package com.srbenoit.math.grapher;

/**
 * A generic graphable that accepts a list of points then produces a piecewise linear graph that
 * interpolates the points linearly.
 */
public class PointListGraph implements Graphable {

    /** the list of X coordinates */
    private final transient double[] xCoords;

    /** the Y coordinates corresponding to the X coordinates */
    private final transient double[] yCoords;

    /** the lowest X value */
    private final transient double[] xRange;

    /** the highest X value */
    private final transient double[] yRange;

    /**
     * Constructs a new <code>PointListGraph</code>.
     *
     * @param  xValues  the list of X coordinates
     * @param  yValues  the list of Y coordinates
     */
    public PointListGraph(final double[] xValues, final double[] yValues) {

        boolean sorted;
        double tempX;
        double tempY;

        if ((xValues == null) || (yValues == null)) {
            throw new IllegalArgumentException("null array");
        }

        if (xValues.length == 0) {
            throw new IllegalArgumentException("zero length array");
        }

        if (xValues.length != yValues.length) {
            throw new IllegalArgumentException("Value length mismatch");
        }

        this.xCoords = xValues.clone();
        this.yCoords = yValues.clone();

        // Put the lists in order by X coordinate (silly bubble sort)
        sorted = false;

        while (!sorted) {
            sorted = true;

            for (int i = 1; i < this.xCoords.length; i++) {

                if (this.xCoords[i - 1] > this.xCoords[i]) {
                    tempX = this.xCoords[i];
                    tempY = this.yCoords[i];
                    this.xCoords[i] = this.xCoords[i - 1];
                    this.yCoords[i] = this.yCoords[i - 1];
                    this.xCoords[i - 1] = tempX;
                    this.yCoords[i - 1] = tempY;
                    sorted = false;
                }
            }
        }

        this.xRange = new double[2];
        this.yRange = new double[2];

        // Compute domain and range limits
        this.xRange[0] = this.xCoords[0];
        this.xRange[1] = this.xCoords[0];
        this.yRange[0] = this.yCoords[0];
        this.yRange[1] = this.yCoords[0];

        for (int i = 1; i < this.xCoords.length; i++) {

            if (!Double.isInfinite(this.xCoords[i])) {

                if (this.xCoords[i] < this.xRange[0]) {
                    this.xRange[0] = this.xCoords[i];
                }

                if (this.xCoords[i] > this.xRange[1]) {
                    this.xRange[1] = this.xCoords[i];
                }
            }

            if (!Double.isInfinite(this.yCoords[i])) {

                if (this.yCoords[i] < this.yRange[0]) {
                    this.yRange[0] = this.yCoords[i];
                }

                if (this.yCoords[i] > this.yRange[1]) {
                    this.yRange[1] = this.yCoords[i];
                }
            }
        }

        // Pad the domain and range to get some margin on a plot
        tempX = (this.xRange[1] - this.xRange[0]) * 0.1;
        tempY = (this.yRange[1] - this.yRange[0]) * 0.1;
        this.xRange[0] -= tempX;
        this.xRange[1] += tempX;
        this.yRange[0] -= tempY;
        this.yRange[1] += tempY;
    }

    /**
     * Gets the number of dimensions of the graph (2 for a function of a single value).
     *
     * @return  the number of dimensions of the graph
     */
    public int graphDimensions() {

        return 2;
    }

    /**
     * Gets a default domain over which the graph will show the main features of the function. This
     * domain should be computed based on known attributes of the function.
     *
     * @return  a two-double array containing the left and right endpoints of the default domain
     */
    public double[] defaultDomain() {

        return this.xRange.clone();
    }

    /**
     * Gets a default range over which the graph will show the main features of the function. This
     * range should be computed based on known attributes of the function.
     *
     * @return  a two-double array containing the lower and upper limits of the default range
     */
    public double[] defaultRange() {

        return this.yRange.clone();
    }

    /**
     * Computes the graph value at a coordinate or coordinates. The number of coordinates needed is
     * one less than the dimension.
     *
     * @param   coordinates  the list of coordinates
     * @return  the graph value at that location
     */
    public double valueAt(final double... coordinates) {

        double value;
        double frac;

        if (coordinates.length != 1) {
            throw new IllegalArgumentException("Invalid number of coordinates");
        }

        value = Double.NaN;

        for (int i = 1; i < this.xCoords.length; i++) {

            if (this.xCoords[i] >= coordinates[0]) {

                if (this.xCoords[i - 1] <= coordinates[0]) {
                    frac = (coordinates[0] - this.xCoords[i - 1])
                        / (this.xCoords[i] - this.xCoords[i - 1]);
                    value = this.yCoords[i - 1] + (frac * (this.yCoords[i] - this.yCoords[i - 1]));
                }

                break;
            }
        }

        return value;
    }
}
