/*
 * 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.math;

import java.io.Serializable;
import java.util.function.DoubleUnaryOperator;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.MismatchedDimensionException;
import org.apache.sis.internal.util.DoubleDouble;
import org.apache.sis.internal.util.Numerics;
import org.apache.sis.internal.util.Strings;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.ArgumentChecks;

import static java.lang.Double.*;


/**
 * Equation of a line in a two dimensional space (<var>x</var>,<var>y</var>).
 * A line can be expressed by the <var>y</var> = <var>slope</var>⋅<var>x</var> + <var>y₀</var> equation
 * where <var>y₀</var> is the value of <var>y</var> at <var>x</var> = 0.
 *
 * <p>The equation parameters for a {@code Line} object can be set at construction time or using one
 * of the {@code setLine(…)} methods. The <var>y</var> value can be computed for a given <var>x</var>
 * value using the {@link #y(double)} method. Method {@link #x(double)} computes the converse and should
 * work even if the line is vertical.</p>
 *
 * <div class="note"><b>Comparison with Java2D geometries:</b>
 * At the difference of {@link java.awt.geom.Line2D} which is bounded by (<var>x₁</var>,<var>y₁</var>)
 * and (<var>x₂</var>,<var>y₂</var>) points, {@code Line} objects extend toward infinity.</div>
 *
 * @author  Martin Desruisseaux (MPO, IRD)
 * @version 1.0
 *
 * @see Plane
 * @see org.apache.sis.referencing.operation.builder.LinearTransformBuilder
 *
 * @since 0.5
 * @module
 */
public class Line implements DoubleUnaryOperator, Cloneable, Serializable {
    /**
     * Serial number for compatibility with different versions.
     */
    private static final long serialVersionUID = 2185952238314399110L;

    /**
     * Number of dimensions.
     */
    private static final int DIMENSION = 2;

    /**
     * The slope for this line.
     */
    private double slope;

    /**
     * The <var>y</var> value at <var>x</var> = 0.
     */
    private double y0;

    /**
     * Value of <var>x</var> at <var>y</var> = 0.
     * This value is used for vertical lines.
     */
    private double x0;

    /**
     * Constructs an uninitialized line. All methods will return {@link Double#NaN}.
     */
    public Line() {
        slope = y0 = x0 = NaN;
    }

    /**
     * Constructs a line with the specified slope and offset.
     * The linear equation will be <var>y</var> = <var>slope</var>⋅<var>x</var> + <var>y₀</var>.
     *
     * @param slope  the slope.
     * @param y0     the <var>y</var> value at <var>x</var> = 0.
     *
     * @see #setEquation(double, double)
     */
    public Line(final double slope, final double y0) {
        this.slope =  slope;
        this.y0    =  y0;
        this.x0    = -y0 / slope;
    }

    /**
     * Returns the slope.
     *
     * @return the slope.
     *
     * @see #x0()
     * @see #y0()
     */
    public final double slope() {
        return slope;
    }

    /**
     * Returns the <var>x</var> value for <var>y</var> = 0.
     * Coordinate (<var>x₀</var>, 0) is the intersection point with the <var>x</var> axis.
     *
     * @return the <var>x</var> value for <var>y</var> = 0.
     *
     * @see #y0()
     * @see #slope()
     */
    public final double x0() {
        return x0;
    }

    /**
     * Computes <var>x</var> = <var>f</var>⁻¹(<var>y</var>).
     * If the line is horizontal, then this method returns an infinite value.
     *
     * @param  y  the <var>y</var> value where to evaluate the inverse function.
     * @return the <var>x</var> value for the given <var>y</var> value.
     *
     * @see #y(double)
     */
    public final double x(final double y) {
        return x0 + y/slope;
    }

    /**
     * Returns the <var>y</var> value for <var>x</var> = 0.
     * Coordinate (0, <var>y₀</var>) is the intersection point with the <var>y</var> axis.
     *
     * @return the <var>y</var> value for <var>x</var> = 0.
     *
     * @see #x0()
     * @see #slope()
     */
    public final double y0() {
        return y0;
    }

    /**
     * Computes <var>y</var> = <var>f</var>(<var>x</var>).
     * If the line is vertical, then this method returns an infinite value.
     *
     * @param  x  the <var>x</var> value where to evaluate the function.
     * @return the <var>y</var> value for the given <var>x</var> value.
     *
     * @see #x(double)
     */
    public final double y(final double x) {
        return y0 + x*slope;
    }

    /**
     * Evaluates this equation for the given value. The default implementation delegates
     * to {@link #y(double) y(x)}, but subclasses may override with different formulas.
     * This method is provided for interoperability with libraries making use of {@link java.util.function}.
     *
     * @param  x  the value where to evaluate the function.
     * @return the function value for the given operand.
     *
     * @since 1.0
     */
    @Override
    public double applyAsDouble(double x) {
        return y(x);
    }

    /**
     * Translates the line. The slope stay unchanged.
     *
     * @param  dx  the horizontal translation.
     * @param  dy  the vertical translation.
     */
    public void translate(final double dx, final double dy) {
        if (slope == 0 || isInfinite(slope)) {
            x0 += dx;
            y0 += dy;
        } else {
            x0 += dx - dy/slope;
            y0 += dy - slope*dx;
        }
    }

    /**
     * Sets this line to the specified slope and offset.
     * The linear equation will be <var>y</var> = <var>slope</var>⋅<var>x</var> + <var>y₀</var>.
     *
     * @param  slope  the slope.
     * @param  y0     the <var>y</var> value at <var>x</var> = 0.
     *
     * @see #setFromPoints(double, double, double, double)
     * @see #fit(double[], double[])
     */
    public void setEquation(final double slope, final double y0) {
        this.slope =  slope;
        this.y0    =  y0;
        this.x0    = -y0 / slope;
    }

    /**
     * Sets this line from values of arbitrary {@code Number} type. This method is invoked by algorithms that
     * may produce other kind of numbers (for example with different precision) than the usual {@code double}
     * primitive type. The default implementation delegates to {@link #setEquation(double, double)}, but
     * subclasses can override this method if they want to process other kind of numbers in a special way.
     *
     * @param  slope  the slope.
     * @param  y0     the <var>y</var> value at <var>x</var> = 0.
     *
     * @since 0.8
     */
    public void setEquation(final Number slope, final Number y0) {
        setEquation(slope.doubleValue(), y0.doubleValue());
    }

    /**
     * Sets a line through the specified points.
     * The line will continue toward infinity after the points.
     *
     * @param  x1  coordinate <var>x</var> of the first point.
     * @param  y1  coordinate <var>y</var> of the first point.
     * @param  x2  coordinate <var>x</var> of the second point.
     * @param  y2  coordinate <var>y</var> of the second point.
     */
    public void setFromPoints(final double x1, final double y1, final double x2, final double y2) {
        this.slope = (y2 - y1) / (x2 - x1);
        this.x0    = x2 - y2/slope;
        this.y0    = y2 - slope*x2;
        if (isNaN(x0) && slope == 0) {
            // Occurs for horizontal lines right on the x axis.
            x0 = POSITIVE_INFINITY;
        }
        if (isNaN(y0) && isInfinite(slope)) {
            // Occurs for vertical lines right on the y axis.
            y0 = POSITIVE_INFINITY;
        }
    }

    /**
     * Given a set of data points <var>x</var>[0 … <var>n</var>-1], <var>y</var>[0 … <var>n</var>-1],
     * fits them to a straight line <var>y</var> = <var>slope</var>⋅<var>x</var> + <var>y₀</var> in a
     * least-squares senses.
     * This method assumes that the <var>x</var> values are precise and all uncertainty is in <var>y</var>.
     *
     * <p>The default implementation delegates to {@link #fit(Vector, Vector)}.</p>
     *
     * @param  x  vector of <var>x</var> values (independent variable).
     * @param  y  vector of <var>y</var> values (dependent variable).
     * @return estimation of the correlation coefficient. The closer this coefficient is to +1 or -1, the better the fit.
     *
     * @throws IllegalArgumentException if <var>x</var> and <var>y</var> do not have the same length.
     */
    public double fit(final double[] x, final double[] y) {
        ArgumentChecks.ensureNonNull("x", x);
        ArgumentChecks.ensureNonNull("y", y);
        return fit(new ArrayVector.Doubles(x), new ArrayVector.Doubles(y));
    }

    /**
     * Given a set of data points <var>x</var>[0 … <var>n</var>-1], <var>y</var>[0 … <var>n</var>-1],
     * fits them to a straight line <var>y</var> = <var>slope</var>⋅<var>x</var> + <var>y₀</var> in a
     * least-squares senses.
     * This method assumes that the <var>x</var> values are precise and all uncertainty is in <var>y</var>.
     *
     * <p>The default implementation delegates to {@link #fit(Iterable)}.</p>
     *
     * @param  x  vector of <var>x</var> values (independent variable).
     * @param  y  vector of <var>y</var> values (dependent variable).
     * @return estimation of the correlation coefficient. The closer this coefficient is to +1 or -1, the better the fit.
     *
     * @throws IllegalArgumentException if <var>x</var> and <var>y</var> do not have the same length.
     *
     * @since 0.8
     */
    public double fit(final Vector x, final Vector y) {
        ArgumentChecks.ensureNonNull("x", x);
        ArgumentChecks.ensureNonNull("y", y);
        return fit(new CompoundDirectPositions(x, y));
    }

    /**
     * Given a sequence of points, fits them to a straight line
     * <var>y</var> = <var>slope</var>⋅<var>x</var> + <var>y₀</var> in a least-squares senses.
     * Points shall be two dimensional with coordinate values in the (<var>x</var>,<var>y</var>) order.
     * This method assumes that the <var>x</var> values are precise and all uncertainty is in <var>y</var>.
     * {@link Double#NaN} coordinate values are ignored.
     *
     * @param  points  the two-dimensional points.
     * @return estimation of the correlation coefficient. The closer this coefficient is to +1 or -1, the better the fit.
     * @throws MismatchedDimensionException if a point is not two-dimensional.
     */
    public double fit(final Iterable<? extends DirectPosition> points) {
        ArgumentChecks.ensureNonNull("points", points);
        int i = 0, n = 0;
        final DoubleDouble mean_x = new DoubleDouble();
        final DoubleDouble mean_y = new DoubleDouble();
        for (final DirectPosition p : points) {
            final int dimension = p.getDimension();
            if (dimension != DIMENSION) {
                throw new MismatchedDimensionException(Errors.format(Errors.Keys.MismatchedDimension_3,
                            Strings.toIndexed("points", i), DIMENSION, dimension));
            }
            i++;
            final double x,y;
            if (!isNaN(y = p.getOrdinate(1)) &&     // Test first the dimension which is most likely to contain NaN.
                !isNaN(x = p.getOrdinate(0)))
            {
                mean_x.addKahan(x);
                mean_y.addKahan(y);
                n++;
            }
        }
        mean_x.divide(n);
        mean_y.divide(n);
        /*
         * We have to solve two equations with two unknowns:
         *
         *   1)    mean(y)   = m⋅mean(x) + y₀
         *   2)    mean(x⋅y) = m⋅mean(x²) + y₀⋅mean(x)
         *
         * Those formulas lead to a quadratic equation. However, the formulas become very simples
         * if we set 'mean(x) = 0'. We can achieve this result by computing instead of (2):
         *
         *   2b)   mean(Δx⋅y) = m⋅mean(Δx²)
         *
         * where dx = x-mean(x). In this case mean(Δx) == 0.
         */
        final DoubleDouble a       = new DoubleDouble();
        final DoubleDouble b       = new DoubleDouble();
        final DoubleDouble mean_x2 = new DoubleDouble();
        final DoubleDouble mean_y2 = new DoubleDouble();
        final DoubleDouble mean_xy = new DoubleDouble();
        for (final DirectPosition p : points) {
            final double y;
            if (!isNaN(y       = p.getOrdinate(1)) &&       // Test first the dimension which is most likely to contain NaN.
                !isNaN(a.value = p.getOrdinate(0)))
            {
                a.error = 0;
                a.subtract(mean_x);     // Δx = x - mean_x
                b.setFrom(a);
                b.multiply(a);          // (Δx)² = Δx * Δx
                mean_x2.add(b);         // mean_x² += (Δx)²

                a.multiply(y);          // xy = Δx * y
                mean_xy.add(a);         // mean_xy += xy

                a.setToProduct(y, y);   // y² = y * y
                mean_y2.add(a);         // mean_y² += y²
            }
        }
        mean_x2.divide(n);
        mean_y2.divide(n);
        mean_xy.divide(n);
        /*
         * Assuming that 'mean(x) == 0', then the correlation
         * coefficient can be approximate by:
         *
         * R = mean(xy) / sqrt( mean(x²) * (mean(y²) - mean(y)²) )
         */
        a.setFrom(mean_xy);
        a.divide (mean_x2);     // slope = mean_xy / mean_x²
        b.setFrom(mean_x);
        b.multiply(a);
        b.negate();
        b.add(mean_y);          // y₀ = mean_y - mean_x * slope
        setEquation(a, b);
        /*
         * Compute the correlation coefficient:
         * mean_xy / sqrt(mean_x2 * (mean_y2 - mean_y * mean_y))
         */
        a.setFrom(mean_y);
        a.multiply(mean_y);
        a.negate();
        a.add(mean_y2);
        a.multiply(mean_x2);
        a.sqrt();
        a.inverseDivide(mean_xy);
        return a.doubleValue();
    }

    /**
     * Returns a clone of this line.
     *
     * @return a clone of this line.
     */
    @Override
    public Line clone() {
        try {
            return (Line) super.clone();
        } catch (CloneNotSupportedException exception) {
            throw new AssertionError(exception);
        }
    }

    /**
     * Compares this line with the specified object for equality.
     *
     * @param  object  the object to compare with this line for equality.
     * @return {@code true} if both objects are equal.
     */
    @Override
    public boolean equals(final Object object) {
        if (object != null && getClass() == object.getClass()) {
            final Line that = (Line) object;
            return Numerics.equals(this.slope, that.slope) &&
                   Numerics.equals(this.y0,    that.y0   ) &&
                   Numerics.equals(this.x0,    that.x0   );
        } else {
            return false;
        }
    }

    /**
     * Returns a hash code value for this line.
     */
    @Override
    public int hashCode() {
        return Long.hashCode(serialVersionUID ^ (doubleToLongBits(slope) + 31*doubleToLongBits(y0)));
    }

    /**
     * Returns a string representation of this line. This method returns the linear equation
     * in the form <var>y</var> = <var>slope</var>⋅<var>x</var> + <var>y₀</var>.
     *
     * @return a string representation of this line.
     */
    @Override
    public String toString() {
        final StringBuilder buffer = new StringBuilder(50);
        if (isInfinite(slope)) {
            buffer.append("x = ").append(x0);
        } else {
            buffer.append("y = ");
            String separator = "";
            if (slope != 0) {
                buffer.append(slope).append("⋅x");
                separator = " + ";
            }
            if (y0 != 0) {
                buffer.append(separator).append(y0);
            }
        }
        return buffer.toString();
    }
}
