/*
 * Copyright (c) 2011, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package javafx.scene.transform;

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.DoublePropertyBase;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.geometry.Point3D;

import com.sun.javafx.geom.transform.Affine3D;
import com.sun.javafx.geom.transform.BaseTransform;
import javafx.geometry.Point2D;


/**
 * This class represents an {@code Affine} object that rotates coordinates
 * around an anchor point. This operation is equivalent to translating the
 * coordinates so that the anchor point is at the origin (S1), then rotating them
 * about the new origin (S2), and finally translating so that the
 * intermediate origin is restored to the coordinates of the original
 * anchor point (S3).
 * <p>
 * The matrix representing the rotation transformation around an axis {@code (x,y,z)}
 * by an angle {@code t} is as follows:
 * <pre>
 *              [   cos(t)   -sin(t)   0   x-x*cos(t)+y*sin(t)   ]
 *              [   sin(t)    cos(t)   0   y-x*sin(t)-y*cos(t)   ]
 *              [     0         0      1           z             ]
 * </pre>
 * <p>
 * For example, to rotate a text 30 degrees around the Z-axis at
 * anchor point of (50,30):
 * <pre>{@code
 * Text text = new Text("This is a test");
 * text.setX(10);
 * text.setY(50);
 * text.setFont(new Font(20));
 *
 * text.getTransforms().add(new Rotate(30, 50, 30));
 * }</pre>
 *
 * @since JavaFX 2.0
 */

public class Rotate extends Transform {

    /**
     * Specifies the X-axis as the axis of rotation.
     */
    public static final Point3D X_AXIS = new Point3D(1,0,0);

    /**
     * Specifies the Y-axis as the axis of rotation.
     */
    public static final Point3D Y_AXIS = new Point3D(0,1,0);

    /**
     * Specifies the Z-axis as the axis of rotation.
     */
    public static final Point3D Z_AXIS = new Point3D(0,0,1);

    /**
     * Avoids lot of repeated computation.
     * @see #MatrixCache
     */
    private MatrixCache cache;

    /**
     * Avoids lot of repeated computation.
     * @see #MatrixCache
     */
    private MatrixCache inverseCache;

    /**
     * Creates a default Rotate transform (identity).
     */
    public Rotate() {
    }

    /**
     * Creates a two-dimensional Rotate transform.
     * The pivot point is set to (0,0)
     * @param angle the angle of rotation measured in degrees
     */
    public Rotate(double angle) {
        setAngle(angle);
    }

    /**
     * Creates a three-dimensional Rotate transform.
     * The pivot point is set to (0,0,0)
     * @param angle the angle of rotation measured in degrees
     * @param axis the axis of rotation
     */
    public Rotate(double angle, Point3D axis) {
        setAngle(angle);
        setAxis(axis);
    }

    /**
     * Creates a two-dimensional Rotate transform with pivot.
     * @param angle the angle of rotation measured in degrees
     * @param pivotX the X coordinate of the rotation pivot point
     * @param pivotY the Y coordinate of the rotation pivot point
     */
    public Rotate(double angle, double pivotX, double pivotY) {
        setAngle(angle);
        setPivotX(pivotX);
        setPivotY(pivotY);
    }

    /**
     * Creates a simple Rotate transform with three-dimensional pivot.
     * @param angle the angle of rotation measured in degrees
     * @param pivotX the X coordinate of the rotation pivot point
     * @param pivotY the Y coordinate of the rotation pivot point
     * @param pivotZ the Z coordinate of the rotation pivot point
     */
    public Rotate(double angle, double pivotX, double pivotY, double pivotZ) {
        this(angle, pivotX, pivotY);
        setPivotZ(pivotZ);
    }

    /**
     * Creates a three-dimensional Rotate transform with pivot.
     * @param angle the angle of rotation measured in degrees
     * @param pivotX the X coordinate of the rotation pivot point
     * @param pivotY the Y coordinate of the rotation pivot point
     * @param pivotZ the Z coordinate of the rotation pivot point
     * @param axis the axis of rotation
     */
    public Rotate(double angle, double pivotX, double pivotY, double pivotZ, Point3D axis) {
        this(angle, pivotX, pivotY);
        setPivotZ(pivotZ);
        setAxis(axis);
    }

    /**
     * Defines the angle of rotation measured in degrees.
     */
    private DoubleProperty angle;


    public final void setAngle(double value) {
        angleProperty().set(value);
    }

    public final double getAngle() {
        return angle == null ? 0.0 : angle.get();
    }

    public final DoubleProperty angleProperty() {
        if (angle == null) {
            angle = new DoublePropertyBase() {

                @Override
                public void invalidated() {
                    transformChanged();
                }

                @Override
                public Object getBean() {
                    return Rotate.this;
                }

                @Override
                public String getName() {
                    return "angle";
                }
            };
        }
        return angle;
    }

    /**
     * Defines the X coordinate of the rotation pivot point.
     *
     * @defaultValue 0.0
     */
    private DoubleProperty pivotX;


    public final void setPivotX(double value) {
        pivotXProperty().set(value);
    }

    public final double getPivotX() {
        return pivotX == null ? 0.0 : pivotX.get();
    }

    public final DoubleProperty pivotXProperty() {
        if (pivotX == null) {
            pivotX = new DoublePropertyBase() {

                @Override
                public void invalidated() {
                    transformChanged();
                }

                @Override
                public Object getBean() {
                    return Rotate.this;
                }

                @Override
                public String getName() {
                    return "pivotX";
                }
            };
        }
        return pivotX;
    }

    /**
     * Defines the Y coordinate of the rotation pivot point.
     *
     * @defaultValue 0.0
     */
    private DoubleProperty pivotY;


    public final void setPivotY(double value) {
        pivotYProperty().set(value);
    }

    public final double getPivotY() {
        return pivotY == null ? 0.0 : pivotY.get();
    }

    public final DoubleProperty pivotYProperty() {
        if (pivotY == null) {
            pivotY = new DoublePropertyBase() {

                @Override
                public void invalidated() {
                    transformChanged();
                }

                @Override
                public Object getBean() {
                    return Rotate.this;
                }

                @Override
                public String getName() {
                    return "pivotY";
                }
            };
        }
        return pivotY;
    }

    /**
     * Defines the Z coordinate of the rotation pivot point.
     *
     * @defaultValue 0.0
     */
    private DoubleProperty pivotZ;


    public final void setPivotZ(double value) {
        pivotZProperty().set(value);
    }

    public final double getPivotZ() {
        return pivotZ == null ? 0.0 : pivotZ.get();
    }

    public final DoubleProperty pivotZProperty() {
        if (pivotZ == null) {
            pivotZ = new DoublePropertyBase() {

                @Override
                public void invalidated() {
                    transformChanged();
                }

                @Override
                public Object getBean() {
                    return Rotate.this;
                }

                @Override
                public String getName() {
                    return "pivotZ";
                }
            };
        }
        return pivotZ;
    }

    /**
     * Defines the axis of rotation at the pivot point.
     */
    private ObjectProperty<Point3D> axis;


    public final void setAxis(Point3D value) {
        axisProperty().set(value);
    }

    public final Point3D getAxis() {
        return axis == null ? Z_AXIS : axis.get();
    }

    public final ObjectProperty<Point3D> axisProperty() {
        if (axis == null) {
            axis = new ObjectPropertyBase<Point3D>(Z_AXIS) {

                @Override
                public void invalidated() {
                    transformChanged();
                }

                @Override
                public Object getBean() {
                    return Rotate.this;
                }

                @Override
                public String getName() {
                    return "axis";
                }
            };
        }
        return axis;
    }

    /* *************************************************************************
     *                                                                         *
     *                         Element getters                                 *
     *                                                                         *
     **************************************************************************/

    @Override
    public double getMxx() {
        updateCache();
        return cache.mxx;
    }

    @Override
    public double getMxy() {
        updateCache();
        return cache.mxy;
    }

    @Override
    public double getMxz() {
        updateCache();
        return cache.mxz;
    }

    @Override
    public double getTx() {
        updateCache();
        return cache.tx;
    }

    @Override
    public double getMyx() {
        updateCache();
        return cache.myx;
    }

    @Override
    public double getMyy() {
        updateCache();
        return cache.myy;
    }

    @Override
    public double getMyz() {
        updateCache();
        return cache.myz;
    }

    @Override
    public double getTy() {
        updateCache();
        return cache.ty;
    }

    @Override
    public double getMzx() {
        updateCache();
        return cache.mzx;
    }

    @Override
    public double getMzy() {
        updateCache();
        return cache.mzy;
    }

    @Override
    public double getMzz() {
        updateCache();
        return cache.mzz;
    }

    @Override
    public double getTz() {
        updateCache();
        return cache.tz;
    }

    /* *************************************************************************
     *                                                                         *
     *                           State getters                                 *
     *                                                                         *
     **************************************************************************/

    @Override
    boolean computeIs2D() {
        final Point3D a = getAxis();
        return (a.getX() == 0.0 && a.getY() == 0.0) || getAngle() == 0;
    }

    @Override
    boolean computeIsIdentity() {
        if (getAngle() == 0.0) {
            return true;
        }

        final Point3D a = getAxis();
        return a.getX() == 0 && a.getY() == 0 && a.getZ() == 0.0;
    }

    /* *************************************************************************
     *                                                                         *
     *                           Array getters                                 *
     *                                                                         *
     **************************************************************************/

    @Override
    void fill2DArray(double[] array) {
        updateCache();
        array[0] = cache.mxx;
        array[1] = cache.mxy;
        array[2] = cache.tx;
        array[3] = cache.myx;
        array[4] = cache.myy;
        array[5] = cache.ty;
    }

    @Override
    void fill3DArray(double[] array) {
        updateCache();
        array[0] = cache.mxx;
        array[1] = cache.mxy;
        array[2] = cache.mxz;
        array[3] = cache.tx;
        array[4] = cache.myx;
        array[5] = cache.myy;
        array[6] = cache.myz;
        array[7] = cache.ty;
        array[8] = cache.mzx;
        array[9] = cache.mzy;
        array[10] = cache.mzz;
        array[11] = cache.tz;
        return;
    }

    /* *************************************************************************
     *                                                                         *
     *                         Transform creators                              *
     *                                                                         *
     **************************************************************************/

    @Override
    public Transform createConcatenation(Transform transform) {
        if (transform instanceof Rotate) {
            Rotate r = (Rotate) transform;
            final double px = getPivotX();
            final double py = getPivotY();
            final double pz = getPivotZ();

            if ((r.getAxis() == getAxis() ||
                        r.getAxis().normalize().equals(getAxis().normalize())) &&
                    px == r.getPivotX() &&
                    py == r.getPivotY() &&
                    pz == r.getPivotZ()) {
                return new Rotate(getAngle() + r.getAngle(), px, py, pz, getAxis());
            }
        }

        if (transform instanceof Affine) {
            Affine a = (Affine) transform.clone();
            a.prepend(this);
            return a;
        }

        return super.createConcatenation(transform);
    }

    @Override
    public Transform createInverse() throws NonInvertibleTransformException {
        return new Rotate(-getAngle(), getPivotX(), getPivotY(), getPivotZ(),
                getAxis());
    }

    @Override
    public Rotate clone() {
        return new Rotate(getAngle(), getPivotX(), getPivotY(), getPivotZ(),
                getAxis());
    }

    /* *************************************************************************
     *                                                                         *
     *                     Transform, Inverse Transform                        *
     *                                                                         *
     **************************************************************************/

    @Override
    public Point2D transform(double x, double y) {
        ensureCanTransform2DPoint();

        updateCache();

        return new Point2D(
            cache.mxx * x + cache.mxy * y + cache.tx,
            cache.myx * x + cache.myy * y + cache.ty);
    }

    @Override
    public Point3D transform(double x, double y, double z) {
        updateCache();

        return new Point3D(
            cache.mxx * x + cache.mxy * y + cache.mxz * z + cache.tx,
            cache.myx * x + cache.myy * y + cache.myz * z + cache.ty,
            cache.mzx * x + cache.mzy * y + cache.mzz * z + cache.tz);
    }

    @Override
    void transform2DPointsImpl(double[] srcPts, int srcOff,
            double[] dstPts, int dstOff, int numPts) {
        updateCache();

        while (--numPts >= 0) {
            final double x = srcPts[srcOff++];
            final double y = srcPts[srcOff++];

            dstPts[dstOff++] = cache.mxx * x + cache.mxy * y + cache.tx;
            dstPts[dstOff++] = cache.myx * x + cache.myy * y + cache.ty;
        }
    }

    @Override
    void transform3DPointsImpl(double[] srcPts, int srcOff,
            double[] dstPts, int dstOff, int numPts) {

        updateCache();

        while (--numPts >= 0) {
            final double x = srcPts[srcOff++];
            final double y = srcPts[srcOff++];
            final double z = srcPts[srcOff++];

            dstPts[dstOff++] = cache.mxx * x + cache.mxy * y + cache.mxz * z + cache.tx;
            dstPts[dstOff++] = cache.myx * x + cache.myy * y + cache.myz * z + cache.ty;
            dstPts[dstOff++] = cache.mzx * x + cache.mzy * y + cache.mzz * z + cache.tz;
        }
    }

    @Override
    public Point2D deltaTransform(double x, double y) {
        ensureCanTransform2DPoint();

        updateCache();

        return new Point2D(
            cache.mxx * x + cache.mxy * y,
            cache.myx * x + cache.myy * y);
    }

    @Override
    public Point3D deltaTransform(double x, double y, double z) {
        updateCache();

        return new Point3D(
            cache.mxx * x + cache.mxy * y + cache.mxz * z,
            cache.myx * x + cache.myy * y + cache.myz * z,
            cache.mzx * x + cache.mzy * y + cache.mzz * z);
    }

    @Override
    public Point2D inverseTransform(double x, double y) {
        ensureCanTransform2DPoint();

        updateInverseCache();

        return new Point2D(
            inverseCache.mxx * x + inverseCache.mxy * y + inverseCache.tx,
            inverseCache.myx * x + inverseCache.myy * y + inverseCache.ty);
    }

    @Override
    public Point3D inverseTransform(double x, double y, double z) {
        updateInverseCache();

        return new Point3D(
            inverseCache.mxx * x + inverseCache.mxy * y + inverseCache.mxz * z
                + inverseCache.tx,
            inverseCache.myx * x + inverseCache.myy * y + inverseCache.myz * z
                + inverseCache.ty,
            inverseCache.mzx * x + inverseCache.mzy * y + inverseCache.mzz * z
                + inverseCache.tz);
    }

    @Override
    void inverseTransform2DPointsImpl(double[] srcPts, int srcOff,
            double[] dstPts, int dstOff, int numPts) {
        updateInverseCache();

        while (--numPts >= 0) {
            final double x = srcPts[srcOff++];
            final double y = srcPts[srcOff++];

            dstPts[dstOff++] = inverseCache.mxx * x + inverseCache.mxy * y
                    + inverseCache.tx;
            dstPts[dstOff++] = inverseCache.myx * x + inverseCache.myy * y
                    + inverseCache.ty;
        }
    }

    @Override
    void inverseTransform3DPointsImpl(double[] srcPts, int srcOff,
            double[] dstPts, int dstOff, int numPts) {

        updateInverseCache();

        while (--numPts >= 0) {
            final double x = srcPts[srcOff++];
            final double y = srcPts[srcOff++];
            final double z = srcPts[srcOff++];

            dstPts[dstOff++] = inverseCache.mxx * x + inverseCache.mxy * y
                    + inverseCache.mxz * z + inverseCache.tx;
            dstPts[dstOff++] = inverseCache.myx * x + inverseCache.myy * y
                    + inverseCache.myz * z + inverseCache.ty;
            dstPts[dstOff++] = inverseCache.mzx * x + inverseCache.mzy * y
                    + inverseCache.mzz * z + inverseCache.tz;
        }
    }

    @Override
    public Point2D inverseDeltaTransform(double x, double y) {
        ensureCanTransform2DPoint();

        updateInverseCache();

        return new Point2D(
            inverseCache.mxx * x + inverseCache.mxy * y,
            inverseCache.myx * x + inverseCache.myy * y);
    }

    @Override
    public Point3D inverseDeltaTransform(double x, double y, double z) {
        updateInverseCache();

        return new Point3D(
            inverseCache.mxx * x + inverseCache.mxy * y + inverseCache.mxz * z,
            inverseCache.myx * x + inverseCache.myy * y + inverseCache.myz * z,
            inverseCache.mzx * x + inverseCache.mzy * y + inverseCache.mzz * z);
    }

    /* *************************************************************************
     *                                                                         *
     *                               Other API                                 *
     *                                                                         *
     **************************************************************************/

    /**
     * Returns a string representation of this {@code Rotate} object.
     * @return a string representation of this {@code Rotate} object.
     */
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("Rotate [");

        sb.append("angle=").append(getAngle());
        sb.append(", pivotX=").append(getPivotX());
        sb.append(", pivotY=").append(getPivotY());
        sb.append(", pivotZ=").append(getPivotZ());
        sb.append(", axis=").append(getAxis());

        return sb.append("]").toString();
    }

    /* *************************************************************************
     *                                                                         *
     *                    Internal implementation stuff                        *
     *                                                                         *
     **************************************************************************/

    @Override
    void apply(final Affine3D trans) {
        double localPivotX = getPivotX();
        double localPivotY = getPivotY();
        double localPivotZ = getPivotZ();
        double localAngle = getAngle();

        if (localPivotX != 0 || localPivotY != 0 || localPivotZ != 0) {
            trans.translate(localPivotX, localPivotY, localPivotZ);
            trans.rotate(Math.toRadians(localAngle),
                         getAxis().getX(),getAxis().getY(), getAxis().getZ());
            trans.translate(-localPivotX, -localPivotY, -localPivotZ);
        } else {
            trans.rotate(Math.toRadians(localAngle),
                         getAxis().getX(), getAxis().getY(), getAxis().getZ());
        }
    }

    @Override
    BaseTransform derive(BaseTransform trans) {
        if (isIdentity()) {
            return trans;
        }

        double localPivotX = getPivotX();
        double localPivotY = getPivotY();
        double localPivotZ = getPivotZ();
        double localAngle = getAngle();

        if (localPivotX != 0 || localPivotY != 0 || localPivotZ != 0) {
            trans = trans.deriveWithTranslation(localPivotX, localPivotY, localPivotZ);
            trans = trans.deriveWithRotation(Math.toRadians(localAngle),
                         getAxis().getX(),getAxis().getY(), getAxis().getZ());
            return trans.deriveWithTranslation(-localPivotX, -localPivotY, -localPivotZ);
        } else {
            return trans.deriveWithRotation(Math.toRadians(localAngle),
                         getAxis().getX(), getAxis().getY(), getAxis().getZ());
        }
    }

    @Override
    void validate() {
        getAxis();
        getAngle();
        getPivotX();
        getPivotY();
        getPivotZ();
    }

    @Override
    protected void transformChanged() {
        if (cache != null) {
            cache.invalidate();
        }
        super.transformChanged();
    }

    @Override
    void appendTo(Affine a) {
        a.appendRotation(getAngle(), getPivotX(), getPivotY(), getPivotZ(),
                getAxis());
    }

    @Override
    void prependTo(Affine a) {
        a.prependRotation(getAngle(), getPivotX(), getPivotY(), getPivotZ(),
                getAxis());
    }

    /**
     * Updates the matrix cache
     */
    private void updateCache() {
        if (cache == null) {
            cache = new MatrixCache();
        }

        if (!cache.valid) {
            cache.update(getAngle(), getAxis(),
                    getPivotX(), getPivotY(), getPivotZ());
        }
    }

    /**
     * Updates the inverse matrix cache
     */
    private void updateInverseCache() {
        if (inverseCache == null) {
            inverseCache = new MatrixCache();
        }

        if (!inverseCache.valid) {
            inverseCache.update(-getAngle(), getAxis(),
                    getPivotX(), getPivotY(), getPivotZ());
        }
    }

    /**
     * Matrix cache. Computing single transformation matrix elements for
     * a general rotation is quite expensive. Also each of those partial
     * computations need some common operations to be made (compute sin
     * and cos, normalize axis). Therefore with the direct element computations
     * if all the getters for the elements are called to get the matrix,
     * the result is slow.
     *
     * If a matrix element is asked for, we can reasonably anticipate that
     * some other elements will be asked for as well. So when any element
     * needs to be computed, we compute the entire matrix, cache it,
     * and use the stored values until the transform changes.
     */
    private static class MatrixCache {
        boolean valid = false;
        boolean is3D = false;

        double mxx, mxy, mxz, tx,
               myx, myy, myz, ty,
               mzx, mzy, mzz, tz;

        public MatrixCache() {
            // to have the 3D part right when using 2D-only
            mzz = 1.0;
        }

        public void update(double angle, Point3D axis,
                double px, double py, double pz) {

            final double rads = Math.toRadians(angle);
            final double sin = Math.sin(rads);
            final double cos = Math.cos(rads);

            if (axis == Z_AXIS ||
                    (axis.getX() == 0.0 &&
                     axis.getY() == 0.0 &&
                     axis.getZ() > 0.0)) {
                // 2D case
                mxx = cos;
                mxy = -sin;
                tx = px * (1 - cos) + py * sin;
                myx = sin;
                myy = cos;
                ty = py * (1 - cos) - px * sin;

                if (is3D) {
                    // Was 3D, needs to set the 3D values
                    mxz = 0.0;
                    myz = 0.0;
                    mzx = 0.0;
                    mzy = 0.0;
                    mzz = 1.0;
                    tz = 0.0;
                    is3D = false;
                }
                valid = true;
                return;
            }
            // 3D case
            is3D = true;

            double axisX, axisY, axisZ;

            if (axis == X_AXIS || axis == Y_AXIS || axis == Z_AXIS) {
                axisX = axis.getX();
                axisY = axis.getY();
                axisZ = axis.getZ();
            } else {
                // normalize
                final double mag = Math.sqrt(axis.getX() * axis.getX() +
                        axis.getY() * axis.getY() + axis.getZ() * axis.getZ());

                if (mag == 0.0) {
                    mxx = 1; mxy = 0; mxz = 0; tx = 0;
                    myx = 0; myy = 1; myz = 0; ty = 0;
                    mzx = 0; mzy = 0; mzz = 1; tz = 0;
                    valid = true;
                    return;
                } else {
                    axisX = axis.getX() / mag;
                    axisY = axis.getY() / mag;
                    axisZ = axis.getZ() / mag;
                }
            }

            mxx = cos + axisX * axisX * (1 - cos);
            mxy = axisX * axisY * (1 - cos) - axisZ * sin;
            mxz = axisX * axisZ * (1 - cos) + axisY * sin;
            tx = px * (1 - mxx) - py * mxy - pz * mxz;

            myx = axisY * axisX * (1 - cos) + axisZ * sin;
            myy = cos + axisY * axisY * (1 - cos);
            myz = axisY * axisZ * (1 - cos) - axisX * sin;
            ty = py * (1 - myy) - px * myx - pz * myz;

            mzx = axisZ * axisX * (1 - cos) - axisY * sin;
            mzy = axisZ * axisY * (1 - cos) + axisX * sin;
            mzz = cos + axisZ * axisZ * (1 - cos);
            tz = pz * (1 - mzz) - px * mzx - py * mzy;

            valid = true;
        }

        public void invalidate() {
            valid = false;
        }
    }
}
