/*
 * Copyright (c) 2011, 2013, 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 com.sun.javafx.geom.transform;

import com.sun.javafx.geom.BaseBounds;
import com.sun.javafx.geom.Path2D;
import com.sun.javafx.geom.Point2D;
import com.sun.javafx.geom.Rectangle;
import com.sun.javafx.geom.Shape;
import com.sun.javafx.geom.Vec3d;

/**
 *
 */
public class Translate2D extends BaseTransform {
    private double mxt;
    private double myt;

    public static BaseTransform getInstance(double mxt, double myt) {
        if (mxt == 0.0 && myt == 0.0) {
            return IDENTITY_TRANSFORM;
        } else {
            return new Translate2D(mxt, myt);
        }
    }

    public Translate2D(double tx, double ty) {
        this.mxt = tx;
        this.myt = ty;
    }

    public Translate2D(BaseTransform tx) {
        if (!tx.isTranslateOrIdentity()) {
            degreeError(Degree.TRANSLATE_2D);
        }
        this.mxt = tx.getMxt();
        this.myt = tx.getMyt();
    }

    @Override
    public Degree getDegree() {
        return Degree.TRANSLATE_2D;
    }

    @Override
    public double getDeterminant() {
        return 1.0;
    }

    @Override
    public double getMxt() {
        return mxt;
    }

    @Override
    public double getMyt() {
        return myt;
    }

    @Override
    public int getType() {
        return (mxt == 0.0 && myt == 0.0) ? TYPE_IDENTITY : TYPE_TRANSLATION;
    }

    @Override
    public boolean isIdentity() {
        return (mxt == 0.0 && myt == 0.0);
    }

    @Override
    public boolean isTranslateOrIdentity() {
        return true;
    }

    @Override
    public boolean is2D() {
        return true;
    }

    @Override
    public Point2D transform(Point2D src, Point2D dst) {
        if (dst == null) dst = makePoint(src, dst);
        dst.setLocation(
            (float) (src.x + mxt),
            (float) (src.y + myt));
        return dst;
    }

    @Override
    public Point2D inverseTransform(Point2D src, Point2D dst) {
        if (dst == null) dst = makePoint(src, dst);
        dst.setLocation(
            (float) (src.x - mxt),
            (float) (src.y - myt));
        return dst;
    }

    @Override
    public Vec3d transform(Vec3d src, Vec3d dst) {
        if (dst == null) {
            dst = new Vec3d();
        }
        dst.x = src.x + mxt;
        dst.y = src.y + myt;
        dst.z = src.z;
        return dst;
    }

    @Override
    public Vec3d deltaTransform(Vec3d src, Vec3d dst) {
        if (dst == null) {
            dst = new Vec3d();
        }
        dst.set(src);
        return dst;
    }

    @Override
    public Vec3d inverseTransform(Vec3d src, Vec3d dst) {
        if (dst == null) {
            dst = new Vec3d();
        }
        dst.x = src.x - mxt;
        dst.y = src.y - myt;
        dst.z = src.z;
        return dst;
    }

    @Override
    public Vec3d inverseDeltaTransform(Vec3d src, Vec3d dst) {
        if (dst == null) {
            dst = new Vec3d();
        }
        dst.set(src);
        return dst;
    }

    @Override
    public void transform(float[] srcPts, int srcOff,
                          float[] dstPts, int dstOff,
                          int numPts)
    {
        float tx = (float) this.mxt;
        float ty = (float) this.myt;
        if (dstPts == srcPts) {
            if (dstOff > srcOff && dstOff < srcOff + numPts * 2) {
                // If the arrays overlap partially with the destination higher
                // than the source and we transform the coordinates normally
                // we would overwrite some of the later source coordinates
                // with results of previous transformations.
                // To get around this we use arraycopy to copy the points
                // to their final destination with correct overwrite
                // handling and then transform them in place in the new
                // safer location.
                System.arraycopy(srcPts, srcOff, dstPts, dstOff, numPts * 2);
                // srcPts = dstPts;     // They are known to be equal.
                srcOff = dstOff;
            }
            if (dstOff == srcOff && tx == 0.0f && ty == 0.0f) {
                return;
            }
        }
        for (int i = 0; i < numPts; i++) {
            dstPts[dstOff++] = srcPts[srcOff++] + tx;
            dstPts[dstOff++] = srcPts[srcOff++] + ty;
        }
    }

    @Override
    public void transform(double[] srcPts, int srcOff,
                          double[] dstPts, int dstOff,
                          int numPts)
    {
        double tx = this.mxt;
        double ty = this.myt;
        if (dstPts == srcPts) {
            if (dstOff > srcOff && dstOff < srcOff + numPts * 2) {
                // If the arrays overlap partially with the destination higher
                // than the source and we transform the coordinates normally
                // we would overwrite some of the later source coordinates
                // with results of previous transformations.
                // To get around this we use arraycopy to copy the points
                // to their final destination with correct overwrite
                // handling and then transform them in place in the new
                // safer location.
                System.arraycopy(srcPts, srcOff, dstPts, dstOff, numPts * 2);
                // srcPts = dstPts;     // They are known to be equal.
                srcOff = dstOff;
            }
            if (dstOff == srcOff && tx == 0.0 && ty == 0.0) {
                return;
            }
        }
        for (int i = 0; i < numPts; i++) {
            dstPts[dstOff++] = srcPts[srcOff++] + tx;
            dstPts[dstOff++] = srcPts[srcOff++] + ty;
        }
    }

    @Override
    public void transform(float[] srcPts, int srcOff,
                          double[] dstPts, int dstOff,
                          int numPts)
    {
        double tx = this.mxt;
        double ty = this.myt;
        for (int i = 0; i < numPts; i++) {
            dstPts[dstOff++] = srcPts[srcOff++] + tx;
            dstPts[dstOff++] = srcPts[srcOff++] + ty;
        }
    }

    @Override
    public void transform(double[] srcPts, int srcOff,
                          float[] dstPts, int dstOff,
                          int numPts)
    {
        double tx = this.mxt;
        double ty = this.myt;
        for (int i = 0; i < numPts; i++) {
            dstPts[dstOff++] = (float) (srcPts[srcOff++] + tx);
            dstPts[dstOff++] = (float) (srcPts[srcOff++] + ty);
        }
    }

    @Override
    public void deltaTransform(float[] srcPts, int srcOff,
                               float[] dstPts, int dstOff,
                               int numPts)
    {
        if (srcPts != dstPts || srcOff != dstOff) {
            System.arraycopy(srcPts, srcOff, dstPts, dstOff, numPts * 2);
        }
    }

    @Override
    public void deltaTransform(double[] srcPts, int srcOff,
                               double[] dstPts, int dstOff,
                               int numPts)
    {
        if (srcPts != dstPts || srcOff != dstOff) {
            System.arraycopy(srcPts, srcOff, dstPts, dstOff, numPts * 2);
        }
    }

    @Override
    public void inverseTransform(float[] srcPts, int srcOff,
                                 float[] dstPts, int dstOff,
                                 int numPts)
    {
        float tx = (float) this.mxt;
        float ty = (float) this.myt;
        if (dstPts == srcPts) {
            if (dstOff > srcOff && dstOff < srcOff + numPts * 2) {
                // If the arrays overlap partially with the destination higher
                // than the source and we transform the coordinates normally
                // we would overwrite some of the later source coordinates
                // with results of previous transformations.
                // To get around this we use arraycopy to copy the points
                // to their final destination with correct overwrite
                // handling and then transform them in place in the new
                // safer location.
                System.arraycopy(srcPts, srcOff, dstPts, dstOff, numPts * 2);
                // srcPts = dstPts;     // They are known to be equal.
                srcOff = dstOff;
            }
            if (dstOff == srcOff && tx == 0.0f && ty == 0.0f) {
                return;
            }
        }
        for (int i = 0; i < numPts; i++) {
            dstPts[dstOff++] = srcPts[srcOff++] - tx;
            dstPts[dstOff++] = srcPts[srcOff++] - ty;
        }
    }

    @Override
    public void inverseDeltaTransform(float[] srcPts, int srcOff,
                                      float[] dstPts, int dstOff,
                                      int numPts)
    {
        if (srcPts != dstPts || srcOff != dstOff) {
            System.arraycopy(srcPts, srcOff, dstPts, dstOff, numPts * 2);
        }
    }

    @Override
    public void inverseTransform(double[] srcPts, int srcOff,
                                 double[] dstPts, int dstOff,
                                 int numPts)
    {
        double tx = this.mxt;
        double ty = this.myt;
        if (dstPts == srcPts) {
            if (dstOff > srcOff && dstOff < srcOff + numPts * 2) {
                // If the arrays overlap partially with the destination higher
                // than the source and we transform the coordinates normally
                // we would overwrite some of the later source coordinates
                // with results of previous transformations.
                // To get around this we use arraycopy to copy the points
                // to their final destination with correct overwrite
                // handling and then transform them in place in the new
                // safer location.
                System.arraycopy(srcPts, srcOff, dstPts, dstOff, numPts * 2);
                // srcPts = dstPts;     // They are known to be equal.
                srcOff = dstOff;
            }
            if (dstOff == srcOff && tx == 0f && ty == 0f) {
                return;
            }
        }
        for (int i = 0; i < numPts; i++) {
            dstPts[dstOff++] = srcPts[srcOff++] - tx;
            dstPts[dstOff++] = srcPts[srcOff++] - ty;
        }
    }

    @Override
    public BaseBounds transform(BaseBounds bounds, BaseBounds result) {
        float minX = (float) (bounds.getMinX() + mxt);
        float minY = (float) (bounds.getMinY() + myt);
        float minZ = bounds.getMinZ();
        float maxX = (float) (bounds.getMaxX() + mxt);
        float maxY = (float) (bounds.getMaxY() + myt);
        float maxZ = bounds.getMaxZ();
        return result.deriveWithNewBounds(minX, minY, minZ, maxX, maxY, maxZ);
    }

    @Override
    public void transform(Rectangle rect, Rectangle result) {
        transform(rect, result, mxt, myt);
    }

    @Override
    public BaseBounds inverseTransform(BaseBounds bounds, BaseBounds result) {
        float minX = (float) (bounds.getMinX() - mxt);
        float minY = (float) (bounds.getMinY() - myt);
        float minZ = bounds.getMinZ();
        float maxX = (float) (bounds.getMaxX() - mxt);
        float maxY = (float) (bounds.getMaxY() - myt);
        float maxZ = bounds.getMaxZ();
        return result.deriveWithNewBounds(minX, minY, minZ, maxX, maxY, maxZ);
    }

    @Override
    public void inverseTransform(Rectangle rect, Rectangle result) {
        transform(rect, result, -mxt, -myt);
    }

    static void transform(Rectangle rect, Rectangle result,
                          double mxt, double myt)
    {
        int imxt = (int) mxt;
        int imyt = (int) myt;
        if (imxt == mxt && imyt == myt) {
            result.setBounds(rect);
            result.translate(imxt, imyt);
        } else {
            double x1 = rect.x + mxt;
            double y1 = rect.y + myt;
            double x2 = Math.ceil(x1 + rect.width);
            double y2 = Math.ceil(y1 + rect.height);
            x1 = Math.floor(x1);
            y1 = Math.floor(y1);
            result.setBounds((int) x1, (int) y1, (int) (x2 - x1), (int) (y2 - y1));
        }
    }

    @Override
    public Shape createTransformedShape(Shape s) {
        return new Path2D(s, this);
    }

    @Override
    public void setToIdentity() {
        this.mxt = this.myt = 0.0;
    }

    @Override
    public void setTransform(BaseTransform xform) {
        if (!xform.isTranslateOrIdentity()) {
            degreeError(Degree.TRANSLATE_2D);
        }
        this.mxt = xform.getMxt();
        this.myt = xform.getMyt();
    }

    @Override
    public void invert() {
        this.mxt = -this.mxt;
        this.myt = -this.myt;
    }

    @Override
    public void restoreTransform(double mxx, double myx,
                                 double mxy, double myy,
                                 double mxt, double myt)
    {
        if (mxx != 1.0 || myx != 0.0 ||
            mxy != 0.0 || myy != 1.0)
        {
            degreeError(Degree.TRANSLATE_2D);
        }
        this.mxt = mxt;
        this.myt = myt;
    }

    @Override
    public void restoreTransform(double mxx, double mxy, double mxz, double mxt,
                                 double myx, double myy, double myz, double myt,
                                 double mzx, double mzy, double mzz, double mzt)
    {
        if (mxx != 1.0 || mxy != 0.0 || mxz != 0.0 ||
            myx != 0.0 || myy != 1.0 || myz != 0.0 ||
            mzx != 0.0 || mzy != 0.0 || mzz != 1.0 || mzt != 0.0)
        {
            degreeError(Degree.TRANSLATE_2D);
        }
        this.mxt = mxt;
        this.myt = myt;
    }

    @Override
    public BaseTransform deriveWithTranslation(double mxt, double myt) {
        this.mxt += mxt;
        this.myt += myt;
        return this;
    }

    @Override
    public BaseTransform deriveWithTranslation(double mxt, double myt, double mzt) {
        if (mzt == 0.0) {
            this.mxt += mxt;
            this.myt += myt;
            return this;
        }
        Affine3D a = new Affine3D();
        a.translate(this.mxt + mxt, this.myt + myt, mzt);
        return a;
    }

    @Override
    public BaseTransform deriveWithScale(double mxx, double myy, double mzz) {
        if (mzz == 1.0) {
            if (mxx == 1.0 && myy == 1.0) {
                return this;
            }
            Affine2D a = new Affine2D();
            a.translate(this.mxt, this.myt);
            a.scale(mxx, myy);
            return a;
        }
        Affine3D a = new Affine3D();
        a.translate(this.mxt, this.myt);
        a.scale(mxx, myy, mzz);
        return a;

    }

    @Override
    public BaseTransform deriveWithRotation(double theta,
            double axisX, double axisY, double axisZ) {
        if (theta == 0.0) {
            return this;
        }
        if (almostZero(axisX) && almostZero(axisY)) {
            if (axisZ == 0.0) {
                return this;
            }
            Affine2D a = new Affine2D();
            a.translate(this.mxt, this.myt);
            if (axisZ > 0) {
                a.rotate(theta);
            } else if (axisZ < 0) {
                a.rotate(-theta);
            }
            return a;
        }
        Affine3D a = new Affine3D();
        a.translate(this.mxt, this.myt);
        a.rotate(theta, axisX, axisY, axisZ);
        return a;
    }

    @Override
    public BaseTransform deriveWithPreTranslation(double mxt, double myt) {
        this.mxt += mxt;
        this.myt += myt;
        return this;
    }

    @Override
    public BaseTransform deriveWithConcatenation(double mxx, double myx,
                                                 double mxy, double myy,
                                                 double mxt, double myt)
    {
        if (mxx == 1.0 && myx == 0.0 && mxy == 0.0 && myy == 1.0) {
            this.mxt += mxt;
            this.myt += myt;
            return this;
        } else {
            return new Affine2D(mxx, myx,
                                mxy, myy,
                                this.mxt + mxt, this.myt + myt);
        }
    }

    @Override
    public BaseTransform deriveWithConcatenation(
            double mxx,   double mxy,   double mxz,   double mxt,
            double myx,   double myy,   double myz,   double myt,
            double mzx,   double mzy,   double mzz,   double mzt) {
        if (                                   mxz == 0.0
                                            && myz == 0.0
                && mzx == 0.0 && mzy == 0.0 && mzz == 1.0 && mzt == 0.0) {
            return deriveWithConcatenation(mxx, myx,
                                           mxy, myy,
                                           mxt, myt);
        }

        return new Affine3D(mxx, mxy, mxz, mxt + this.mxt,
                            myx, myy, myz, myt + this.myt,
                            mzx, mzy, mzz, mzt);
    }

    @Override
    public BaseTransform deriveWithConcatenation(BaseTransform tx) {
        if (tx.isTranslateOrIdentity()) {
            this.mxt += tx.getMxt();
            this.myt += tx.getMyt();
            return this;
        } else if (tx.is2D()) {
            return getInstance(tx.getMxx(), tx.getMyx(),
                               tx.getMxy(), tx.getMyy(),
                               this.mxt + tx.getMxt(), this.myt + tx.getMyt());
        } else {
            Affine3D t3d = new Affine3D(tx);
            t3d.preTranslate(this.mxt, this.myt, 0.0);
            return t3d;
        }
    }

    @Override
    public BaseTransform deriveWithPreConcatenation(BaseTransform tx) {
        if (tx.isTranslateOrIdentity()) {
            this.mxt += tx.getMxt();
            this.myt += tx.getMyt();
            return this;
        } else if (tx.is2D()) {
            Affine2D t2d = new Affine2D(tx);
            t2d.translate(this.mxt, this.myt);
            return t2d;
        } else {
            Affine3D t3d = new Affine3D(tx);
            t3d.translate(this.mxt, this.myt, 0.0);
            return t3d;
        }
    }

    @Override
    public BaseTransform deriveWithNewTransform(BaseTransform tx) {
        if (tx.isTranslateOrIdentity()) {
            this.mxt = tx.getMxt();
            this.myt = tx.getMyt();
            return this;
        } else {
            return getInstance(tx);
        }
    }

    @Override
    public BaseTransform createInverse() {
        if (isIdentity()) {
            return IDENTITY_TRANSFORM;
        } else {
            return new Translate2D(-this.mxt, -this.myt);
        }
    }

    // Round values to sane precision for printing
    // Note that Math.sin(Math.PI) has an error of about 10^-16
    private static double _matround(double matval) {
        return Math.rint(matval * 1E15) / 1E15;
    }

    @Override
    public String toString() {
        return ("Translate2D["
                + _matround(mxt) + ", "
                + _matround(myt) + "]");
    }

    @Override
    public BaseTransform copy() {
        return new Translate2D(this.mxt, this.myt);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof BaseTransform) {
            BaseTransform tx = (BaseTransform) obj;
            return (tx.isTranslateOrIdentity() &&
                    tx.getMxt() == this.mxt &&
                    tx.getMyt() == this.myt);
        }
        return false;
    }

    private static final long BASE_HASH;
    static {
        long bits = 0;
        bits = bits * 31 + Double.doubleToLongBits(IDENTITY_TRANSFORM.getMzz());
        bits = bits * 31 + Double.doubleToLongBits(IDENTITY_TRANSFORM.getMzy());
        bits = bits * 31 + Double.doubleToLongBits(IDENTITY_TRANSFORM.getMzx());
        bits = bits * 31 + Double.doubleToLongBits(IDENTITY_TRANSFORM.getMyz());
        bits = bits * 31 + Double.doubleToLongBits(IDENTITY_TRANSFORM.getMxz());
        bits = bits * 31 + Double.doubleToLongBits(IDENTITY_TRANSFORM.getMyy());
        bits = bits * 31 + Double.doubleToLongBits(IDENTITY_TRANSFORM.getMyx());
        bits = bits * 31 + Double.doubleToLongBits(IDENTITY_TRANSFORM.getMxy());
        bits = bits * 31 + Double.doubleToLongBits(IDENTITY_TRANSFORM.getMxx());
        bits = bits * 31 + Double.doubleToLongBits(IDENTITY_TRANSFORM.getMzt());
        BASE_HASH = bits;
    }

    @Override
    public int hashCode() {
        if (isIdentity()) return 0;
        long bits = BASE_HASH;
        bits = bits * 31 + Double.doubleToLongBits(getMyt());
        bits = bits * 31 + Double.doubleToLongBits(getMxt());
        return (((int) bits) ^ ((int) (bits >> 32)));
    }
}
