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

/* $Id: TransformStackElement.java 1732018 2016-02-24 04:51:06Z gadams $ */

package org.apache.xmlgraphics.java2d;

import java.awt.geom.AffineTransform;

// CSOFF: EmptyBlock
// CSOFF: NoWhitespaceAfter
// CSOFF: OperatorWrap
// CSOFF: WhitespaceAround

/**
 * Contains a description of an elementary transform stack element,
 * such as a rotate or translate. A transform stack element has a
 * type and a value, which is an array of double values.<br>
 *
 * @version $Id: TransformStackElement.java 1732018 2016-02-24 04:51:06Z gadams $
 *
 * Originally authored by Vincent Hardy and Paul Evenblij.
 */
public abstract class TransformStackElement implements Cloneable {

    /**
     * Transform type
     */
    private TransformType type;

    /**
     * Value
     */
    private double[] transformParameters;

    /**
     * @param type transform type
     * @param transformParameters parameters for transform
     */
    protected TransformStackElement(TransformType type,
                                    double[] transformParameters) {
        this.type = type;
        this.transformParameters = transformParameters;
    }

    /**
     * @return an object which is a deep copy of this one
     */
    public Object clone() {
        TransformStackElement newElement = null;

        // start with a shallow copy to get our implementations right
        try {
            newElement = (TransformStackElement) super.clone();
        } catch (java.lang.CloneNotSupportedException ex) {
            throw new AssertionError();
        }

        // now deep copy the parameter array
        double[] transformParameters = new double[this.transformParameters.length];
        System.arraycopy(this.transformParameters, 0, transformParameters, 0, transformParameters.length);
        newElement.transformParameters = transformParameters;
        return newElement;
    }

    /*
     * Factory methods
     */

    public static TransformStackElement createTranslateElement(double tx,
                                                               double ty) {
        return new TransformStackElement(TransformType.TRANSLATE,
                                         new double[]{ tx, ty }) {
                boolean isIdentity(double[] parameters) {
                    return parameters[0] == 0 && parameters[1] == 0;
                }
            };
    }

    public static TransformStackElement createRotateElement(double theta) {
        return new TransformStackElement(TransformType.ROTATE,
                                         new double[]{ theta }) {
                boolean isIdentity(double[] parameters) {
                    return Math.cos(parameters[0]) == 1;
                }
            };
    }

    public static TransformStackElement createScaleElement(double scaleX,
                                                           double scaleY) {
        return new TransformStackElement(TransformType.SCALE,
                                         new double[]{ scaleX, scaleY }) {
                boolean isIdentity(double[] parameters) {
                    return parameters[0] == 1 && parameters[1] == 1;
                }
            };
    }

    public static TransformStackElement createShearElement(double shearX,
                                                           double shearY) {
        return new TransformStackElement(TransformType.SHEAR,
                                         new double[]{ shearX, shearY }) {
                boolean isIdentity(double[] parameters) {
                    return parameters[0] == 0 && parameters[1] == 0;
                }
            };
    }

    public static TransformStackElement createGeneralTransformElement(
        AffineTransform txf) {
        double[] matrix = new double[6];
        txf.getMatrix(matrix);
        return new TransformStackElement(TransformType.GENERAL, matrix) {
                boolean isIdentity(double[] m) {
                    return (m[0] == 1 && m[2] == 0 && m[4] == 0
                            && m[1] == 0 && m[3] == 1 && m[5] == 0);
                }
            };
    }

    /**
     * Implementation should determine if the parameter list represents
     * an identity transform, for the instance transform type.
     */
    abstract boolean isIdentity(double[] parameters);

    /**
     * @return true iff this transform is the identity transform
     */
    public boolean isIdentity() {
        return isIdentity(transformParameters);
    }

    /**
     * @return array of values containing this transform element's parameters
     */
    public double[] getTransformParameters() {
        return transformParameters;
    }

    /**
     * @return this transform type
     */
    public TransformType getType() {
        return type;
    }

    /*
     * Concatenation utility. Requests this transform stack element
     * to concatenate with the input stack element. Only elements
     * of the same types are concatenated. For example, if this
     * element represents a translation, it will concatenate with
     * another translation, but not with any other kind of
     * stack element.
     * @param stackElement element to be concatenated with this one.
     * @return true if the input stackElement was concatenated with
     *         this one. False otherwise.
     */
    public boolean concatenate(TransformStackElement stackElement) {
        boolean canConcatenate = false;

        if (type.toInt() == stackElement.type.toInt()) {
            canConcatenate = true;
            switch(type.toInt()) {
            case TransformType.TRANSFORM_TRANSLATE:
                transformParameters[0] += stackElement.transformParameters[0];
                transformParameters[1] += stackElement.transformParameters[1];
                break;
            case TransformType.TRANSFORM_ROTATE:
                transformParameters[0] += stackElement.transformParameters[0];
                break;
            case TransformType.TRANSFORM_SCALE:
                transformParameters[0] *= stackElement.transformParameters[0];
                transformParameters[1] *= stackElement.transformParameters[1];
                break;
            case TransformType.TRANSFORM_GENERAL:
                transformParameters
                    = matrixMultiply(transformParameters,
                                     stackElement.transformParameters);
                break;
            default:
                canConcatenate = false;
            }
        }

        return canConcatenate;
    }

    /**
     *  Multiplies two 2x3 matrices of double precision values
     */
    private double[] matrixMultiply(double[] matrix1, double[] matrix2) {
        double[] product = new double[6];
        AffineTransform transform1 = new AffineTransform(matrix1);
        transform1.concatenate(new AffineTransform(matrix2));
        transform1.getMatrix(product);
        return product;
    }

}
