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

import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RectangularShape;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform;
import org.apache.sis.referencing.operation.transform.LinearTransform;
import org.apache.sis.referencing.util.j2d.AffineTransform2D;
import org.apache.sis.referencing.util.j2d.IntervalRectangle;
import org.apache.sis.referencing.internal.Resources;
import org.apache.sis.util.Static;
import org.apache.sis.util.ArgumentChecks;

import static java.lang.Math.*;
import static java.awt.geom.AffineTransform.*;


/**
 * Bridge between {@link Matrix} and Java2D {@link AffineTransform} instances.
 * Those {@code AffineTransform} instances can be viewed as 3×3 matrices.
 * Contains also utility methods operating on {@link AffineTransform} instances.
 *
 * @author  Martin Desruisseaux (IRD, Geomatys)
 * @version 1.1
 * @since   0.4
 */
public final class AffineTransforms2D extends Static {
    /**
     * Do not allows instantiation of this class.
     */
    private AffineTransforms2D() {
    }

    /**
     * Returns the given transform as a Java2D affine transform.
     *
     * @param  transform  the transform to convert, or {@code null}.
     * @return the transform argument if it can be safely casted (including {@code null} argument) or converted.
     * @throws IllegalArgumentException if the given transform cannot be casted or converted.
     *
     * @see #toMathTransform(AffineTransform)
     */
    public static AffineTransform castOrCopy(final MathTransform transform) throws IllegalArgumentException {
        if (transform == null || transform instanceof AffineTransform) {
            return (AffineTransform) transform;
        }
        if (transform instanceof LinearTransform) {
            return castOrCopy(((LinearTransform) transform).getMatrix());
        }
        throw new IllegalArgumentException(Resources.format(Resources.Keys.NotAnAffineTransform));
    }

    /**
     * Returns the given matrix as a Java2D affine transform.
     * If the given matrix is already an instance of {@link AffineTransform}, then it is returned directly.
     * Otherwise the values are copied in a new {@code AffineTransform} instance.
     *
     * @param  matrix  the matrix to return as an affine transform, or {@code null}.
     * @return the matrix argument if it can be safely casted (including {@code null} argument),
     *         or a copy of the given matrix otherwise.
     * @throws IllegalArgumentException if the given matrix size is not 3×3 or if the matrix is not affine.
     *
     * @see Matrices#isAffine(Matrix)
     */
    public static AffineTransform castOrCopy(final Matrix matrix) throws IllegalArgumentException {
        if (matrix == null || matrix instanceof AffineTransform) {
            return (AffineTransform) matrix;
        }
        MatrixSIS.ensureSizeMatch(3, 3, matrix);
        if (!Matrices.isAffine(matrix)) {
            throw new IllegalArgumentException(Resources.format(Resources.Keys.NotAnAffineTransform));
        }
        return new AffineTransform(matrix.getElement(0,0), matrix.getElement(1,0),
                                   matrix.getElement(0,1), matrix.getElement(1,1),
                                   matrix.getElement(0,2), matrix.getElement(1,2));
    }

    /**
     * Creates a 3×3 matrix from the given affine transform.
     *
     * @param  transform  the affine transform to copy as a matrix.
     * @return a matrix containing the same terms than the given affine transform.
     */
    public static Matrix3 toMatrix(final AffineTransform transform) {
        return new Matrix3(transform.getScaleX(), transform.getShearX(), transform.getTranslateX(),
                           transform.getShearY(), transform.getScaleY(), transform.getTranslateY(),
                           0,                     0,                     1);
    }

    /**
     * Creates a math transform from the given affine transform.
     * This method is the converse of {@link #castOrCopy(MathTransform)}.
     *
     * @param  transform  the affine transform to cast or copy as a {@link MathTransform}, or {@code null}.
     * @return a {@link MathTransform} doing the same operation than the given {@link AffineTransform},
     *         or {@code null} if the given transform was null.
     *
     * @see #castOrCopy(MathTransform)
     *
     * @since 1.1
     */
    public static LinearTransform toMathTransform(final AffineTransform transform) {
        if (transform == null || transform instanceof LinearTransform) {
            return (LinearTransform) transform;
        } else {
            return new AffineTransform2D(transform);
        }
    }

    /**
     * Transforms the given shape.
     * This method is similar to {@link AffineTransform#createTransformedShape(Shape)} except that:
     *
     * <ul>
     *   <li>It tries to preserve the shape kind when possible. For example if the given shape
     *       is an instance of {@link RectangularShape} and the given transform does not involve
     *       rotation, then the returned shape may be some instance of the same class.</li>
     *   <li>It tries to recycle the given object if {@code overwrite} is {@code true}.</li>
     * </ul>
     *
     * @param  transform       the affine transform to use.
     * @param  shape           the shape to transform, or {@code null}.
     * @param  allowOverwrite  if {@code true}, this method is allowed to overwrite {@code shape} with the
     *                         transform result. If {@code false}, then {@code shape} is never modified.
     * @return the transform of the given shape, or {@code null} if the given shape was null.
     *         May or may not be the same instance than the given shape.
     *
     * @see AffineTransform#createTransformedShape(Shape)
     */
    public static Shape transform(final AffineTransform transform, Shape shape, boolean allowOverwrite) {
        ArgumentChecks.ensureNonNull("transform", transform);
        if (shape == null) {
            return null;
        }
        final int type = transform.getType();
        if (type == TYPE_IDENTITY) {
            return shape;
        }
        /*
         * If there is only scale, flip, quadrant rotation or translation,
         * then we can optimize the transformation of rectangular shapes.
         */
        if ((type & (TYPE_GENERAL_ROTATION | TYPE_GENERAL_TRANSFORM)) == 0) {
            // For a Rectangle input, the output should be a rectangle as well.
            if (shape instanceof Rectangle2D) {
                final Rectangle2D rect = (Rectangle2D) shape;
                return transform(transform, rect, allowOverwrite ? rect : null);
            }
            /*
             * For other rectangular shapes, we restrict to cases without
             * rotation or flip because we don't know if the shape is symmetric.
             */
            if ((type & (TYPE_FLIP | TYPE_MASK_ROTATION)) == 0) {
                if (shape instanceof RectangularShape) {
                    RectangularShape rect = (RectangularShape) shape;
                    if (!allowOverwrite) {
                        rect = (RectangularShape) rect.clone();
                    }
                    final Rectangle2D frame = rect.getFrame();
                    rect.setFrame(transform(transform, frame, frame));
                    return rect;
                }
            }
        }
        if (shape instanceof Path2D) {
            final Path2D path = (Path2D) shape;
            if (allowOverwrite) {
                path.transform(transform);
            } else {
                shape = path.createTransformedShape(transform);
            }
        } else if (shape instanceof Area) {
            final Area area = (Area) shape;
            if (allowOverwrite) {
                area.transform(transform);
            } else {
                shape = area.createTransformedArea(transform);
            }
        } else {
            shape = new Path2D.Double(shape, transform);
        }
        return shape;
    }

    /**
     * Calculates a rectangle which entirely contains the direct transform of {@code bounds}.
     * This operation is equivalent to the following code, except that it can reuse the
     * given {@code dest} rectangle and is potentially more efficient:
     *
     * {@snippet lang="java" :
     *     return transform.createTransformedShape(bounds).getBounds2D();
     *     }
     *
     * Note that if the given rectangle is an image bounds, then the given transform shall map the
     * <strong>upper-left corner</strong> of pixels (as in Java2D usage), not the center of pixels
     * (OGC usage).
     *
     * @param  transform  the affine transform to use.
     * @param  bounds     the rectangle to transform, or {@code null}.
     *                    this rectangle will not be modified except if {@code dest} references the same object.
     * @param  dest       rectangle in which to place the result. If {@code null}, a new rectangle will be created.
     * @return the direct transform of the {@code bounds} rectangle, or {@code null} if {@code bounds} was null.
     *
     * @see org.apache.sis.geometry.Shapes2D#transform(MathTransform2D, Rectangle2D, Rectangle2D)
     */
    public static Rectangle2D transform(final AffineTransform transform,
            final Rectangle2D bounds, final Rectangle2D dest)
    {
        ArgumentChecks.ensureNonNull("transform", transform);
        if (bounds == null) {
            return null;
        }
        double xmin = Double.POSITIVE_INFINITY;
        double ymin = Double.POSITIVE_INFINITY;
        double xmax = Double.NEGATIVE_INFINITY;
        double ymax = Double.NEGATIVE_INFINITY;
        final Point2D.Double point = new Point2D.Double();
        for (int i=0; i<4; i++) {
            point.x = (i & 1) == 0 ? bounds.getMinX() : bounds.getMaxX();
            point.y = (i & 2) == 0 ? bounds.getMinY() : bounds.getMaxY();
            transform.transform(point, point);
            if (point.x < xmin) xmin = point.x;
            if (point.x > xmax) xmax = point.x;
            if (point.y < ymin) ymin = point.y;
            if (point.y > ymax) ymax = point.y;
        }
        if (dest != null) {
            dest.setRect(xmin, ymin, xmax-xmin, ymax-ymin);
            return dest;
        }
        return new IntervalRectangle(xmin, ymin, xmax, ymax);
    }

    /**
     * Calculates a rectangle which entirely contains the inverse transform of {@code bounds}.
     * This operation is equivalent to the following code, except that it can reuse the
     * given {@code dest} rectangle and is potentially more efficient:
     *
     * {@snippet lang="java" :
     *     return createInverse().createTransformedShape(bounds).getBounds2D();
     *     }
     *
     * @param  transform  the affine transform to use.
     * @param  bounds     the rectangle to transform, or {@code null}.
     *                    this rectangle will not be modified except if {@code dest} references the same object.
     * @param  dest       rectangle in which to place the result. If {@code null}, a new rectangle will be created.
     * @return the inverse transform of the {@code bounds} rectangle, or {@code null} if {@code bounds} was null.
     * @throws NoninvertibleTransformException if the affine transform cannot be inverted.
     */
    public static Rectangle2D inverseTransform(final AffineTransform transform,
            final Rectangle2D bounds, final Rectangle2D dest) throws NoninvertibleTransformException
    {
        ArgumentChecks.ensureNonNull("transform", transform);
        if (bounds == null) {
            return null;
        }
        double xmin = Double.POSITIVE_INFINITY;
        double ymin = Double.POSITIVE_INFINITY;
        double xmax = Double.NEGATIVE_INFINITY;
        double ymax = Double.NEGATIVE_INFINITY;
        final Point2D.Double point = new Point2D.Double();
        for (int i=0; i<4; i++) {
            point.x = (i & 1) == 0 ? bounds.getMinX() : bounds.getMaxX();
            point.y = (i & 2) == 0 ? bounds.getMinY() : bounds.getMaxY();
            transform.inverseTransform(point, point);
            if (point.x < xmin) xmin = point.x;
            if (point.x > xmax) xmax = point.x;
            if (point.y < ymin) ymin = point.y;
            if (point.y > ymax) ymax = point.y;
        }
        if (dest != null) {
            dest.setRect(xmin, ymin, xmax-xmin, ymax-ymin);
            return dest;
        }
        return new IntervalRectangle(xmin, ymin, xmax, ymax);
    }

    /**
     * Calculates the inverse transform of a point without applying the translation components.
     * In other words, calculates the inverse transform of a displacement vector.
     *
     * @param  transform  the affine transform to use.
     * @param  vector     the vector to transform stored as a point.
     *                    this point will not be modified except if {@code dest} references the same object.
     * @param  dest       point in which to place the result. If {@code null}, a new point will be created.
     * @return the inverse transform of the {@code vector}, or {@code null} if {@code source} was null.
     * @throws NoninvertibleTransformException if the affine transform cannot be inverted.
     */
    public static Point2D inverseDeltaTransform(final AffineTransform transform,
            final Point2D vector, final Point2D dest) throws NoninvertibleTransformException
    {
        ArgumentChecks.ensureNonNull("transform", transform);
        if (vector == null) {
            return null;
        }
        final double m00 = transform.getScaleX();
        final double m11 = transform.getScaleY();
        final double m01 = transform.getShearX();
        final double m10 = transform.getShearY();
        final double det = m00*m11 - m01*m10;
        if (!(abs(det) > Double.MIN_VALUE)) {
            throw new NoninvertibleTransformException(null);
        }
        final double x0 = vector.getX();
        final double y0 = vector.getY();
        final double x = (x0*m11 - y0*m01) / det;
        final double y = (y0*m00 - x0*m10) / det;
        if (dest != null) {
            dest.setLocation(x, y);
            return dest;
        }
        return new Point2D.Double(x, y);
    }

    /**
     * Returns an estimation about whether the specified transform swaps <var>x</var> and <var>y</var> axes.
     * This method assumes that the specified affine transform is built from arbitrary translations, scales or
     * rotations, but no shear. It returns {@code +1} if the (<var>x</var>, <var>y</var>) axis order seems to be
     * preserved, {@code -1} if the transform seems to swap axis to the (<var>y</var>, <var>x</var>) axis order,
     * or {@code 0} if this method cannot make a decision.
     *
     * @param  transform  the affine transform to inspect.
     * @return {@code true} if the given transform seems to swap axis order.
     */
    public static int getSwapXY(final AffineTransform transform) {
        ArgumentChecks.ensureNonNull("transform", transform);
        final int flip = getFlip(transform);
        if (flip != 0) {
            final double scaleX = getScaleX0(transform);
            final double scaleY = getScaleY0(transform) * flip;
            final double y = abs(transform.getShearY()/scaleY - transform.getShearX()/scaleX);
            final double x = abs(transform.getScaleY()/scaleY + transform.getScaleX()/scaleX);
            if (x > y) return +1;
            if (x < y) return -1;
            // At this point, we may have (x == y) or some NaN value.
        }
        return 0;
    }

    /**
     * Returns an estimation of the rotation angle in radians. This method assumes that the specified affine
     * transform is built from arbitrary translations, scales or rotations, but no shear. If a flip has been
     * applied, then this method assumes that the flipped axis is the <var>y</var> one in <cite>source CRS</cite>
     * space. For a <cite>grid to world CRS</cite> transform, this is the row number in grid coordinates.
     *
     * @param  transform  the affine transform to inspect.
     * @return an estimation of the rotation angle in radians,
     *         or {@link Double#NaN NaN} if the angle cannot be estimated.
     */
    public static double getRotation(final AffineTransform transform) {
        ArgumentChecks.ensureNonNull("transform", transform);
        final int flip = getFlip(transform);
        if (flip != 0) {
            final double scaleX = getScaleX0(transform);
            final double scaleY = getScaleY0(transform) * flip;
            return atan2(transform.getShearY()/scaleY - transform.getShearX()/scaleX,
                         transform.getScaleY()/scaleY + transform.getScaleX()/scaleX);
        }
        return Double.NaN;
    }

    /**
     * Returns {@code -1} if one axis has been flipped, {@code +1} if no axis has been flipped, or 0 if unknown.
     * A flipped axis in an axis with direction reversed (typically the <var>y</var> axis). This method assumes
     * that the specified affine transform is built from arbitrary translations, scales or rotations, but no shear.
     * Note that it is not possible to determine which of the <var>x</var> or <var>y</var> axis has been flipped.
     *
     * <p>This method can be used in order to set the sign of a scale according the flipping state.
     * The example below choose to apply the sign on the <var>y</var> scale, but this is an arbitrary
     * (while common) choice:</p>
     *
     * {@snippet lang="java" :
     *     double scaleX0 = getScaleX0(transform);
     *     double scaleY0 = getScaleY0(transform);
     *     int    flip    = getFlip(transform);
     *     if (flip != 0) {
     *         scaleY0 *= flip;
     *         // ... continue the process here.
     *     }
     *     }
     *
     * This method is similar to the following code, except that this method distinguishes
     * between "unflipped" and "unknown" states.
     *
     * {@snippet lang="java" :
     *     boolean flipped = (tr.getType() & TYPE_FLIP) != 0;
     *     }
     *
     * @param  transform  the affine transform to inspect.
     * @return -1 if an axis has been flipped, +1 if no flipping, or 0 if unknown.
     */
    public static int getFlip(final AffineTransform transform) {
        ArgumentChecks.ensureNonNull("transform", transform);
        final double scaleX = Math.signum(transform.getScaleX());
        final double scaleY = Math.signum(transform.getScaleY());
        final double shearX = Math.signum(transform.getShearX());
        final double shearY = Math.signum(transform.getShearY());
        if (scaleX ==  scaleY && shearX == -shearY) return +1;
        if (scaleX == -scaleY && shearX ==  shearY) return -1;
        return 0;
    }

    /**
     * Returns the magnitude of scale factor <var>x</var> by canceling the
     * effect of eventual flip and rotation. This factor is calculated by:
     *
     * <p><img src="doc-files/scaleX0.png" alt="Scale factor on x axis"></p>
     *
     * @param  transform  the affine transform to inspect.
     * @return the magnitude of scale factor <var>x</var>.
     */
    public static double getScaleX0(final AffineTransform transform) {
        ArgumentChecks.ensureNonNull("transform", transform);
        final double scale = transform.getScaleX();
        final double shear = transform.getShearX();
        if (shear == 0) return abs(scale);                  // Optimization for a very common case.
        if (scale == 0) return abs(shear);                  // Not as common as above, but still common enough.
        return hypot(scale, shear);
    }

    /**
     * Returns the magnitude of scale factor <var>y</var> by canceling the
     * effect of eventual flip and rotation. This factor is calculated by:
     *
     * <p><img src="doc-files/scaleY0.png" alt="Scale factor on y axis"></p>
     *
     * @param  transform  the affine transform to inspect.
     * @return the magnitude of scale factor <var>y</var>.
     */
    public static double getScaleY0(final AffineTransform transform) {
        ArgumentChecks.ensureNonNull("transform", transform);
        final double scale = transform.getScaleY();
        final double shear = transform.getShearY();
        if (shear == 0) return abs(scale);                  // Optimization for a very common case.
        if (scale == 0) return abs(shear);                  // Not as common as above, but still common enough.
        return hypot(scale, shear);
    }

    /**
     * Returns a global scale factor for the specified affine transform. This scale factor combines
     * {@link #getScaleX0 getScaleX0(tr)} and {@link #getScaleY0 getScaleY0(tr)}. The way to compute
     * such a "global" scale is somewhat arbitrary and may change in any future version.
     *
     * @param  tr  the affine transform to inspect.
     * @return a "global" scale factor.
     */
    public static double getScale(final AffineTransform tr) {
        return 0.5 * (AffineTransforms2D.getScaleX0(tr) + AffineTransforms2D.getScaleY0(tr));
    }
}
