/*
 * 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.referencing.operation.transform;

import java.util.Arrays;
import org.opengis.geometry.DirectPosition;
import org.opengis.referencing.operation.Matrix;
import org.apache.sis.referencing.util.DirectPositionView;
import org.apache.sis.referencing.util.ExtendedPrecisionMatrix;
import org.apache.sis.referencing.util.Formulas;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
import org.apache.sis.referencing.internal.Arithmetic;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.internal.Numerics;
import org.apache.sis.math.Fraction;


/**
 * A usually affine, or otherwise a projective transform for the generic cases.
 * This implementation is used for cases other than identity, translation only,
 * scale only, 1D transform, 2D transform or axis swapping.
 *
 * <p>A projective transform is capable of mapping an arbitrary quadrilateral into another arbitrary quadrilateral,
 * while preserving the straightness of lines. In the special case where the transform is affine, the parallelism of
 * lines in the source is preserved in the output.</p>
 *
 * @author  Martin Desruisseaux (IRD, Geomatys)
 *
 * @see java.awt.geom.AffineTransform
 */
class ProjectiveTransform extends AbstractLinearTransform implements ExtendedPrecisionMatrix {
    /**
     * Serial number for inter-operability with different versions.
     */
    private static final long serialVersionUID = -4813507361303377148L;

    /**
     * The number of rows.
     */
    private final int numRow;

    /**
     * The number of columns.
     */
    private final int numCol;

    /**
     * Number of columns for coefficients other than scales and shears in the {@link #elt} array.
     * There is one column for translation terms and one column for denominators.
     */
    private static final int NON_SCALE_COLUMNS = 2;

    /**
     * Elements of the matrix augmented with one column containing common denominators.
     * Column indices vary fastest.
     *
     * <h4>Denominator column</h4>
     * An additional column is appended after the translation column.
     * That column contains a denominator inferred from fractional values found on the row.
     * All elements in the matrix row shall be multiplied by that denominator.
     * The intend is to increase the chances that matrix elements are integer values.
     * If no fractional value is found, the default denominator value is 1.
     */
    private final double[] elt;

    /**
     * Same numbers than {@link #elt} excluding the denominators and with potentially extended precision.
     * Zero values <em>shall</em> be represented by null elements.
     */
    private final Number[] numbers;

    /**
     * Constructs a transform from the specified matrix.
     * The matrix is usually square and affine, but this is not enforced.
     * Non-affine transforms (e.g. projective transforms) are accepted but may not be invertible.
     *
     * @param matrix  the matrix containing the coefficients of this projective transform.
     */
    protected ProjectiveTransform(final Matrix matrix) {
        numRow = matrix.getNumRow();
        numCol = matrix.getNumCol();
        /*
         * Get the matrix elements as `Number` instances if possible.
         * Those instances allow better precision than `double` values.
         * Those numbers are available only through SIS-specific API.
         */
        final boolean hasNumbers;
        if (matrix instanceof ExtendedPrecisionMatrix) {
            // Use `writable = true` because we need a copy protected from changes.
            numbers = ((ExtendedPrecisionMatrix) matrix).getElementAsNumbers(true);
            hasNumbers = true;
        } else if (matrix instanceof MatrixSIS) {
            final MatrixSIS m = (MatrixSIS) matrix;
            numbers = new Number[numRow * numCol];
            for (int i=0; i<numbers.length; i++) {
                final Number e = m.getNumber(i / numCol, i % numCol);
                if (!ExtendedPrecisionMatrix.isZero(e)) {
                    numbers[i] = e;
                }
            }
            hasNumbers = true;
        } else {
            numbers = new Number[numRow * numCol];
            hasNumbers = false;
        }
        /*
         * Get the matrix elements as `double` values through the standard matrix API.
         * We do that as a way to preserve negative zero, which is lost in `numbers`.
         * The `numbers` array is either completed or compared for consistency.
         */
        final int dstDim    = numRow - 1;               // Last row is [0 0 … 1] in an affine transform.
        final int rowStride = numCol + 1;               // The `elt` array has an extra column for denominators.
        elt = new double[numRow * rowStride - 1];       // We don't need the denominator of the [0 0 … 1] row.
        for (int k=0,i=0; i < numRow; i++) {
            for (int j=0; j < numCol; j++) {
                final double e = matrix.getElement(i, j);
                elt[k++] = e;                           // May be negative zero.
                if (hasNumbers) {
                    assert epsilonEqual(e, numbers[i*numCol + j]);
                } else if (e != 0) {
                    final int v = (int) e;              // Check if we can store as integer.
                    numbers[i*numCol + j] = (v == e) ? Integer.valueOf(v) : Double.valueOf(e);
                }
            }
            if (i != dstDim) {
                elt[k++] = 1;
            } else {
                assert k == elt.length;
            }
        }
        /*
         * At this point, this `ProjectiveTransform` is initialized and valid.
         * Optionally update the elements values for reducing rounding errors
         * when a denominator can be identified. This is where the denominator
         * column in the `elt` array may get values different than 1.
         */
        if (hasNumbers) {
            for (int row=0; row < dstDim; row++) {
                final int lower = numCol * row;
                final int upper = numCol + lower;
                final Integer denominator;
                try {
                    Fraction sum = null;
                    for (int i=lower; i<upper; i++) {
                        final Number element = numbers[i];
                        if (element instanceof Fraction) {
                            final Fraction f = (Fraction) element;
                            sum = (sum != null) ? sum.add(f) : f;
                        }
                    }
                    if (sum == null) {
                        continue;
                    }
                    denominator = sum.denominator;
                } catch (ArithmeticException e) {
                    continue;
                }
                int k = row * rowStride;
                for (int i=lower; i<upper; i++) {
                    final Number element = Arithmetic.multiply(numbers[i], denominator);
                    if (element != null) {
                        elt[k] = element.doubleValue();
                    }
                    k++;
                }
                elt[k] = denominator.doubleValue();
            }
        }
    }

    /**
     * Returns whether the given number are equal, with a tolerance of 1 ULP.
     * A null {@code Number} is interpreted as zero.
     */
    private static boolean epsilonEqual(final double e, final Number v) {
        return Numerics.epsilonEqual(e, (v != null) ? v.doubleValue() : 0, Math.ulp(e));
    }

    /**
     * If a more efficient implementation of this math transform can be used, returns it.
     * Otherwise returns {@code this} unchanged.
     */
    final LinearTransform optimize() {
        if (numCol < numRow) {
            return this;
        }
        final int n = (numRow - 1) * numCol;
        for (int i = 0; i < numCol;) {
            if (!isIdentity(numbers[n + i], ++i == numCol)) {
                return this;            // Transform is not affine (ignoring if square or not).
            }
        }
        /*
         * Note: we could check for CopyTransform case here, but this check is rather done in
         * MathTransforms.linear(Matrix) in order to avoid ProjectiveTransform instantiation.
         *
         * Check which elements are non-zero. For ScaleTransform, they must be on the diagonal.
         * For TranslationTransform, they must be in the last column. Note that the transform
         * should not be identity (except for testing purpose) since the identity case should
         * have been handled by MathTransforms.linear(Matrix) before to reach this point.
         */
        boolean isScale       = true;                       // ScaleTransform accepts non-square matrix.
        boolean isTranslation = (numRow == numCol);         // TranslationTransform is restricted to square matrix.
        final int lastColumn  = numCol - 1;
        for (int i=0; i<n; i++) {
            final Number element = numbers[i];
            final int row  = (i / numCol);
            final int col  = (i % numCol);
            if (col != row)        isScale       &= (element == null);
            if (col != lastColumn) isTranslation &= isIdentity(element, col == row);
            if (!(isScale | isTranslation)) {
                return this;
            }
        }
        if (isTranslation) {
            return new TranslationTransform(numRow, numbers);
        } else {
            return new ScaleTransform(numRow, numCol, numbers);
        }
    }

    /**
     * Returns {@code true} if the given element has the expected value of an identity matrix.
     *
     * @param  element   the element to test.
     * @param  diagonal  whether the element is on the diagonal.
     * @return whether the given element is an element of an identity matrix.
     *
     * @see #isIdentity()
     */
    private static boolean isIdentity(final Number element, final boolean diagonal) {
        return diagonal ? (element != null && element.doubleValue() == 1) : (element == null);
    }

    /**
     * Gets the dimension of input points.
     */
    @Override
    public final int getSourceDimensions() {
        return numCol - 1;
    }

    /**
     * Gets the dimension of output points.
     */
    @Override
    public final int getTargetDimensions() {
        return numRow - 1;
    }

    /**
     * Gets the number of rows in the matrix.
     */
    @Override
    public final int getNumRow() {
        return numRow;
    }

    /**
     * Gets the number of columns in the matrix.
     */
    @Override
    public final int getNumCol() {
        return numCol;
    }

    /**
     * Returns a copy of all matrix elements in a flat, row-major array. Zero values <em>shall</em> be null.
     * Callers can write in the returned array if and only if the {@code writable} argument is {@code true}.
     */
    @Override
    public final Number[] getElementAsNumbers(final boolean writable) {
        return writable ? numbers.clone() : numbers;
    }

    /**
     * Retrieves the value at the specified row and column of the matrix.
     * If the value is zero, then this method <em>shall</em> return {@code null}.
     */
    @Override
    public final Number getElementOrNull(final int row, final int column) {
        ArgumentChecks.ensureBetween("row",    0, numRow - 1, row);
        ArgumentChecks.ensureBetween("column", 0, numCol - 1, column);
        return numbers[row * numCol + column];
    }

    /**
     * Returns the matrix element at the given index.
     */
    @Override
    public final double getElement(final int row, final int column) {
        ArgumentChecks.ensureBetween("row",    0, numRow - 1, row);
        ArgumentChecks.ensureBetween("column", 0, numCol - 1, column);
        final Number element = numbers[row * numCol + column];
        if (element != null) {
            return element.doubleValue();
        }
        /*
         * Fallback on the `elt` array only for 0 values for avoiding the need to divide by the denominator.
         * Do not return a hard-coded 0 value in order to preserve the sign of negative zero.
         */
        final int rowStride = numCol + 1;
        return elt[row * rowStride + column];
    }

    /**
     * Tests whether this transform does not move any points.
     *
     * <h4>Note</h4>
     * This method should always returns {@code false}, because
     * {@code MathTransforms.linear(…)} should have created specialized implementations for identity cases.
     * Nevertheless we perform the full check as a safety, in case someone instantiated this class directly
     * instead of using a factory method.
     */
    @Override
    public final boolean isIdentity() {
        if (numRow != numCol) {
            return false;
        }
        for (int i=0; i < numbers.length; i++) {
            if (!isIdentity(numbers[i], (i / numCol) == (i % numCol))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Converts a single coordinate tuple in a list of coordinate values,
     * and optionally computes the derivative at that location.
     *
     * @return {@inheritDoc}
     */
    @Override
    public final Matrix transform(final double[] srcPts, final int srcOff,
                                  final double[] dstPts, final int dstOff,
                                  final boolean derivate)
    {
        if (!derivate) {
            transform(srcPts, srcOff, dstPts, dstOff, 1);
            return null;
        }
        // A non-null position is required in case this transform is non-affine.
        Matrix derivative = derivative(new DirectPositionView.Double(srcPts, srcOff, getSourceDimensions()));
        transform(srcPts, srcOff, dstPts, dstOff, 1);
        return derivative;
    }

    /**
     * Transforms an array of floating point coordinates by this matrix. Point coordinates must have a dimension
     * equal to <code>{@link Matrix#getNumCol}-1</code>. For example, for square matrix of size 4×4, coordinate
     * points are three-dimensional and stored in the arrays starting at the specified offset ({@code srcOff}) in
     * the order
     * <code>[x₀, y₀, z₀,
     *        x₁, y₁, z₁...,
     *        x<sub>n</sub>, y<sub>n</sub>, z<sub>n</sub>]</code>.
     *
     * @param srcPts  the array containing the source point coordinates.
     * @param srcOff  the offset to the first point to be transformed in the source array.
     * @param dstPts  the array into which the transformed point coordinates are returned.
     * @param dstOff  the offset to the location of the first transformed point that is stored in the
     *                destination array. The source and destination array sections can overlap.
     * @param numPts  the number of points to be transformed.
     */
    @Override
    public final void transform(double[] srcPts, int srcOff, final double[] dstPts, int dstOff, int numPts) {
        final int srcDim, dstDim;
        int srcInc = srcDim = numCol - 1;               // The last coordinate will be assumed equal to 1.
        int dstInc = dstDim = numRow - 1;
        if (srcPts == dstPts) {
            switch (IterationStrategy.suggest(srcOff, srcDim, dstOff, dstDim, numPts)) {
                case ASCENDING: {
                    break;
                }
                case DESCENDING: {
                    srcOff += (numPts - 1) * srcDim;
                    dstOff += (numPts - 1) * dstDim;
                    srcInc = -srcInc;
                    dstInc = -dstInc;
                    break;
                }
                default: {
                    srcPts = Arrays.copyOfRange(srcPts, srcOff, srcOff + numPts*srcDim);
                    srcOff = 0;
                    break;
                }
            }
        }
        final double[] buffer = new double[numRow];
        while (--numPts >= 0) {
            int mix = 0;
            for (int j=0; j<numRow; j++) {
                double sum = elt[mix + srcDim];         // Initialize to translation term.
                for (int i=0; i<srcDim; i++) {
                    final double e = elt[mix++];
                    if (e != 0) {
                        /*
                         * The purpose of the test for non-zero value is not performance (it is actually more likely
                         * to slow down the calculation), but to get a valid sum even if some source coordinates are NaN.
                         * This occurs when the ProjectiveTransform is used for excluding some dimensions, for example
                         * getting 2D points from 3D points. In such case, the fact that the excluded dimensions had
                         * NaN values should not force the retained dimensions to get NaN values.
                         */
                        if (Formulas.USE_FMA) {
                            sum = Math.fma(srcPts[srcOff + i], e, sum);
                        } else {
                            sum += srcPts[srcOff + i] * e;
                        }
                    }
                }
                buffer[j] = sum;
                mix += NON_SCALE_COLUMNS;       // Skip the translation column and the denominator column.
            }
            int k = numCol;
            final int rowStride = numCol + 1;
            final double w = buffer[dstDim];
            for (int j=0; j<dstDim; j++) {
                // `w` is equal to 1 if the transform is affine.
                dstPts[dstOff + j] = buffer[j] / (w * elt[k]);
                k += rowStride;
            }
            srcOff += srcInc;
            dstOff += dstInc;
        }
    }

    /**
     * Transforms an array of floating point coordinates by this matrix. Point coordinates must have a dimension
     * equal to <code>{@link Matrix#getNumCol()} - 1</code>. For example, for square matrix of size 4×4, coordinate
     * points are three-dimensional and stored in the arrays starting at the specified offset ({@code srcOff})
     * in the order
     * <code>[x₀, y₀, z₀,
     *        x₁, y₁, z₁...,
     *        x<sub>n</sub>, y<sub>n</sub>, z<sub>n</sub>]</code>.
     *
     * @param srcPts  the array containing the source point coordinates.
     * @param srcOff  the offset to the first point to be transformed in the source array.
     * @param dstPts  the array into which the transformed point coordinates are returned.
     * @param dstOff  the offset to the location of the first transformed point that is stored in the
     *                destination array. The source and destination array sections can overlap.
     * @param numPts  the number of points to be transformed.
     */
    @Override
    public final void transform(float[] srcPts, int srcOff, final float[] dstPts, int dstOff, int numPts) {
        final int srcDim, dstDim;
        int srcInc = srcDim = numCol - 1;
        int dstInc = dstDim = numRow - 1;
        if (srcPts == dstPts) {
            switch (IterationStrategy.suggest(srcOff, srcDim, dstOff, dstDim, numPts)) {
                case ASCENDING: {
                    break;
                }
                case DESCENDING: {
                    srcOff += (numPts - 1) * srcDim;
                    dstOff += (numPts - 1) * dstDim;
                    srcInc = -srcInc;
                    dstInc = -dstInc;
                    break;
                }
                default: {
                    srcPts = Arrays.copyOfRange(srcPts, srcOff, srcOff + numPts*srcDim);
                    srcOff = 0;
                    break;
                }
            }
        }
        final double[] buffer = new double[numRow];
        while (--numPts >= 0) {
            int mix = 0;
            for (int j=0; j<numRow; j++) {
                double sum = elt[mix + srcDim];
                for (int i=0; i<srcDim; i++) {
                    final double e = elt[mix++];
                    if (e != 0) {                                   // See comment in transform(double[], ...)
                        if (Formulas.USE_FMA) {
                            sum = Math.fma(srcPts[srcOff + i], e, sum);
                        } else {
                            sum += srcPts[srcOff + i] * e;
                        }
                    }
                }
                buffer[j] = sum;
                mix += NON_SCALE_COLUMNS;
            }
            int k = numCol;
            final int rowStride = numCol + 1;
            final double w = buffer[dstDim];
            for (int j=0; j<dstDim; j++) {
                dstPts[dstOff + j] = (float) (buffer[j] / (w * elt[k]));
                k += rowStride;
            }
            srcOff += srcInc;
            dstOff += dstInc;
        }
    }

    /**
     * Transforms an array of floating point coordinates by this matrix.
     *
     * @param srcPts  the array containing the source point coordinates.
     * @param srcOff  the offset to the first point to be transformed in the source array.
     * @param dstPts  the array into which the transformed point coordinates are returned.
     * @param dstOff  the offset to the location of the first transformed point that is stored in the destination array.
     * @param numPts  the number of points to be transformed.
     */
    @Override
    public final void transform(final double[] srcPts, int srcOff, final float[] dstPts, int dstOff, int numPts) {
        final int srcDim = numCol - 1;
        final int dstDim = numRow - 1;
        final double[] buffer = new double[numRow];
        while (--numPts >= 0) {
            int mix = 0;
            for (int j=0; j<numRow; j++) {
                double sum = elt[mix + srcDim];
                for (int i=0; i<srcDim; i++) {
                    final double e = elt[mix++];
                    if (e != 0) {                                   // See comment in transform(double[], ...)
                        if (Formulas.USE_FMA) {
                            sum = Math.fma(srcPts[srcOff + i], e, sum);
                        } else {
                            sum += srcPts[srcOff + i] * e;
                        }
                    }
                }
                buffer[j] = sum;
                mix += NON_SCALE_COLUMNS;
            }
            int k = numCol;
            final int rowStride = numCol + 1;
            final double w = buffer[dstDim];
            for (int j=0; j<dstDim; j++) {
                dstPts[dstOff++] = (float) (buffer[j] / (w * elt[k]));
                k += rowStride;
            }
            srcOff += srcDim;
        }
    }

    /**
     * Transforms an array of floating point coordinates by this matrix.
     *
     * @param srcPts  the array containing the source point coordinates.
     * @param srcOff  the offset to the first point to be transformed in the source array.
     * @param dstPts  the array into which the transformed point coordinates are returned.
     * @param dstOff  the offset to the location of the first transformed point that is stored in the destination array.
     * @param numPts  the number of points to be transformed.
     */
    @Override
    public final void transform(final float[] srcPts, int srcOff, final double[] dstPts, int dstOff, int numPts) {
        final int srcDim = numCol - 1;
        final int dstDim = numRow - 1;
        final double[] buffer = new double[numRow];
        while (--numPts >= 0) {
            int mix = 0;
            for (int j=0; j<numRow; j++) {
                double sum = elt[mix + srcDim];
                for (int i=0; i<srcDim; i++) {
                    final double e = elt[mix++];
                    if (e != 0) {                                   // See comment in transform(double[], ...)
                        if (Formulas.USE_FMA) {
                            sum = Math.fma(srcPts[srcOff + i], e, sum);
                        } else {
                            sum += srcPts[srcOff + i] * e;
                        }
                    }
                }
                buffer[j] = sum;
                mix += NON_SCALE_COLUMNS;
            }
            int k = numCol;
            final int rowStride = numCol + 1;
            final double w = buffer[dstDim];
            for (int j=0; j<dstDim; j++) {
                dstPts[dstOff++] = buffer[j] / (w * elt[k]);
                k += rowStride;
            }
            srcOff += srcDim;
        }
    }

    /**
     * Gets the derivative of this transform at a point. For an affine transform,
     * the derivative is the same everywhere and the given point is ignored.
     * For a perspective transform, the given point is used.
     *
     * @param  point  the coordinate tuple where to evaluate the derivative.
     */
    @Override
    public final Matrix derivative(final DirectPosition point) {
        final int srcDim    = numCol - 1;
        final int dstDim    = numRow - 1;
        final int rowStride = numCol + 1;
        /*
         * In the `transform(…)` method, all coordinate values are divided by a `w` coefficient
         * which depends on the position. We need to reproduce that division here. Note that `w`
         * coefficient is different than 1 only if the transform is non-affine.
         */
        int mix = dstDim * rowStride;
        double w = elt[mix + srcDim];                   // `w` is equal to 1 if the transform is affine.
        for (int i=0; i<srcDim; i++) {
            final double e = elt[mix++];
            if (e != 0) {                               // For avoiding NullPointerException if affine.
                w += point.getOrdinate(i) * e;
            }
        }
        /*
         * In the usual affine case (w=1), the derivative is a copy of this matrix
         * with last row and last column omitted.
         */
        mix = 0;
        int k = numCol;
        final MatrixSIS matrix = Matrices.createZero(dstDim, srcDim);
        for (int j=0; j<dstDim; j++) {
            final double r = w * elt[k];
            for (int i=0; i<srcDim; i++) {
                matrix.setElement(j, i, elt[mix++] / r);
            }
            mix += NON_SCALE_COLUMNS;       // Skip the translation column and the denominator column.
            k += rowStride;
        }
        return matrix;
    }

    /**
     * {@inheritDoc}
     *
     * @return {@inheritDoc}
     */
    @Override
    protected int computeHashCode() {
        return Arrays.hashCode(elt) + 31 * super.computeHashCode();
    }

    /**
     * Compares this math transform with an object which is known to be an instance of the same class.
     */
    @Override
    protected boolean equalsSameClass(final Object object) {
        final ProjectiveTransform that = (ProjectiveTransform) object;
        return numRow == that.numRow &&
               numCol == that.numCol &&
               Arrays.equals(elt, that.elt) &&
               Arrays.equals(numbers, that.numbers);
    }
}
