import * as matrix from './matrix';
import { assignProps } from './util';
import * as vector from './vector';

const mIdentity = matrix.identity;

const EPSILON = 5e-5;

function isNotAroundZero(val: number) {
    return val > EPSILON || val < -EPSILON;
}

const scaleTmp: vector.VectorArray = [];
const tmpTransform: matrix.MatrixArray = [];
const originTransform = matrix.create();
const abs = Math.abs;

class Transformable {

    parent: Transformable

    x: number
    y: number

    scaleX: number
    scaleY: number

    skewX: number
    skewY: number

    // Suppose positive y is towards screen-bottom and positive x is towards screen-right;
    // positive rotation means rotating anticlockwise. (opposite to CSS transform rotate)
    rotation: number

    /**
     * Will translated the element to the anchor position before applying other transforms.
     */
    anchorX: number
    anchorY: number
    /**
     * Origin of scale, rotation, skew
     */
    originX: number
    originY: number

    /**
     * Scale ratio
     */
    globalScaleRatio: number

    transform: matrix.MatrixArray
    invTransform: matrix.MatrixArray

    /**
     * Get computed local transform
     */
    getLocalTransform(m?: matrix.MatrixArray) {
        return transformableGetLocalTransform(this, m);
    }

    /**
     * Set position from array
     */
    setPosition(arr: number[]) {
        this.x = arr[0];
        this.y = arr[1];
    }
    /**
     * Set scale from array
     */
    setScale(arr: number[]) {
        this.scaleX = arr[0];
        this.scaleY = arr[1];
    }

    /**
     * Set skew from array
     */
    setSkew(arr: number[]) {
        this.skewX = arr[0];
        this.skewY = arr[1];
    }

    /**
     * Set origin from array
     */
    setOrigin(arr: number[]) {
        this.originX = arr[0];
        this.originY = arr[1];
    }

    /**
     * If needs to compute transform
     */
    needLocalTransform(): boolean {
        return isNotAroundZero(this.rotation)
            || isNotAroundZero(this.x)
            || isNotAroundZero(this.y)
            || isNotAroundZero(this.scaleX - 1)
            || isNotAroundZero(this.scaleY - 1)
            || isNotAroundZero(this.skewX)
            || isNotAroundZero(this.skewY);
    }

    /**
     * Update global transform
     */
    updateTransform() {
        const parentTransform = this.parent && this.parent.transform;
        const needLocalTransform = this.needLocalTransform();

        let m = this.transform;
        if (!(needLocalTransform || parentTransform)) {
            if (m) {
                mIdentity(m);
                // reset invTransform
                this.invTransform = null;
            }
            return;
        }

        m = m || matrix.create();

        if (needLocalTransform) {
            this.getLocalTransform(m);
        }
        else {
            mIdentity(m);
        }

        // 应用父节点变换
        if (parentTransform) {
            if (needLocalTransform) {
                matrix.mul(m, parentTransform, m);
            }
            else {
                matrix.copy(m, parentTransform);
            }
        }
        // 保存这个变换矩阵
        this.transform = m;

        this._resolveGlobalScaleRatio(m);

        this.invTransform = this.invTransform || matrix.create();
        matrix.invert(this.invTransform, m);
    }

    private _resolveGlobalScaleRatio(m: matrix.MatrixArray) {
        const globalScaleRatio = this.globalScaleRatio;
        if (globalScaleRatio != null && globalScaleRatio !== 1) {
            this.getGlobalScale(scaleTmp);
            const relX = scaleTmp[0] < 0 ? -1 : 1;
            const relY = scaleTmp[1] < 0 ? -1 : 1;
            const sx = ((scaleTmp[0] - relX) * globalScaleRatio + relX) / scaleTmp[0] || 0;
            const sy = ((scaleTmp[1] - relY) * globalScaleRatio + relY) / scaleTmp[1] || 0;

            m[0] *= sx;
            m[1] *= sx;
            m[2] *= sy;
            m[3] *= sy;
        }
    }

    /**
     * Get computed global transform
     * NOTE: this method will force update transform on all ancestors.
     * Please be aware of the potential performance cost.
     */
    getComputedTransform() {
        let transformNode: Transformable = this;
        const ancestors: Transformable[] = [];
        while (transformNode) {
            ancestors.push(transformNode);
            transformNode = transformNode.parent;
        }

        // Update from topdown.
        while (transformNode = ancestors.pop()) {
            transformNode.updateTransform();
        }

        return this.transform;
    }

    setLocalTransform(m: vector.VectorArray) {
        if (!m) {
            // TODO return or set identity?
            return;
        }
        let sx = m[0] * m[0] + m[1] * m[1];
        let sy = m[2] * m[2] + m[3] * m[3];

        const rotation = Math.atan2(m[1], m[0]);

        const shearX = Math.PI / 2 + rotation - Math.atan2(m[3], m[2]);
        sy = Math.sqrt(sy) * Math.cos(shearX);
        sx = Math.sqrt(sx);

        this.skewX = shearX;
        this.skewY = 0;
        this.rotation = -rotation;

        this.x = +m[4];
        this.y = +m[5];
        this.scaleX = sx;
        this.scaleY = sy;

        this.originX = 0;
        this.originY = 0;
    }
    /**
     * 分解`transform`矩阵到`position`, `rotation`, `scale`
     */
    decomposeTransform() {
        if (!this.transform) {
            return;
        }
        const parent = this.parent;
        let m = this.transform;
        if (parent && parent.transform) {
            // Get local transform and decompose them to position, scale, rotation
            parent.invTransform = parent.invTransform || matrix.create();
            matrix.mul(tmpTransform, parent.invTransform, m);
            m = tmpTransform;
        }
        const ox = this.originX;
        const oy = this.originY;
        if (ox || oy) {
            originTransform[4] = ox;
            originTransform[5] = oy;
            matrix.mul(tmpTransform, m, originTransform);
            tmpTransform[4] -= ox;
            tmpTransform[5] -= oy;
            m = tmpTransform;
        }

        this.setLocalTransform(m);
    }

    /**
     * Get global scale
     */
    getGlobalScale(out?: vector.VectorArray): vector.VectorArray {
        const m = this.transform;
        out = out || [];
        if (!m) {
            out[0] = 1;
            out[1] = 1;
            return out;
        }
        out[0] = Math.sqrt(m[0] * m[0] + m[1] * m[1]);
        out[1] = Math.sqrt(m[2] * m[2] + m[3] * m[3]);
        if (m[0] < 0) {
            out[0] = -out[0];
        }
        if (m[3] < 0) {
            out[1] = -out[1];
        }
        return out;
    }
    /**
     * 变换坐标位置到 shape 的局部坐标空间
     */
    transformCoordToLocal(x: number, y: number): number[] {
        const v2 = [x, y];
        const invTransform = this.invTransform;
        if (invTransform) {
            vector.applyTransform(v2, v2, invTransform);
        }
        return v2;
    }

    /**
     * 变换局部坐标位置到全局坐标空间
     */
    transformCoordToGlobal(x: number, y: number): number[] {
        const v2 = [x, y];
        const transform = this.transform;
        if (transform) {
            vector.applyTransform(v2, v2, transform);
        }
        return v2;
    }


    getLineScale() {
        const m = this.transform;
        // Get the line scale.
        // Determinant of `m` means how much the area is enlarged by the
        // transformation. So its square root can be used as a scale factor
        // for width.
        return m && abs(m[0] - 1) > 1e-10 && abs(m[3] - 1) > 1e-10
            ? Math.sqrt(abs(m[0] * m[3] - m[2] * m[1]))
            : 1;
    }

    copyTransform(source: Transformable) {
        copyTransform(this, source);
    }


    static getLocalTransform(target: Transformable, m?: matrix.MatrixArray): matrix.MatrixArray {
        m = m || [];

        const ox = target.originX || 0;
        const oy = target.originY || 0;
        const sx = target.scaleX;
        const sy = target.scaleY;
        const ax = target.anchorX;
        const ay = target.anchorY;
        const rotation = target.rotation || 0;
        const x = target.x;
        const y = target.y;
        const skewX = target.skewX ? Math.tan(target.skewX) : 0;
        // TODO: zrender use different hand in coordinate system and y axis is inversed.
        const skewY = target.skewY ? Math.tan(-target.skewY) : 0;

        // The order of transform (-anchor * -origin * scale * skew * rotate * origin * translate).
        // We merge (-origin * scale * skew) into one. Also did identity in these operations.
        // origin
        if (ox || oy || ax || ay) {
            const dx = ox + ax;
            const dy = oy + ay;
            m[4] = -dx * sx - skewX * dy * sy;
            m[5] = -dy * sy - skewY * dx * sx;
        }
        else {
            m[4] = m[5] = 0;
        }
        // scale
        m[0] = sx;
        m[3] = sy;
        // skew
        m[1] = skewY * sx;
        m[2] = skewX * sy;

        // Apply rotation
        rotation && matrix.rotate(m, m, rotation);

        // Translate back from origin and apply translation
        m[4] += ox + x;
        m[5] += oy + y;

        // NOTICE: All of `m[0~5]` must be set, since `m` may be reused.
        return m;
    }

    private static initDefaultProps = (function () {
        const proto = Transformable.prototype;
        proto.scaleX =
        proto.scaleY =
        proto.globalScaleRatio = 1;
        proto.x =
        proto.y =
        proto.originX =
        proto.originY =
        proto.skewX =
        proto.skewY =
        proto.rotation =
        proto.anchorX =
        proto.anchorY = 0;
    })()
};

export const transformableGetLocalTransform = Transformable.getLocalTransform;

export function transformableCreate(): Transformable {
    return new Transformable();
}

export const TRANSFORMABLE_PROPS = [
    'x', 'y', 'originX', 'originY', 'anchorX', 'anchorY', 'rotation', 'scaleX', 'scaleY', 'skewX', 'skewY'
] as const;

export type TransformProp = (typeof TRANSFORMABLE_PROPS)[number]

export function copyTransform<TOut extends Partial<Pick<Transformable, TransformProp>>>(
    target: TOut,
    source: Pick<Transformable, TransformProp>
): TOut {
    return assignProps(target, source, TRANSFORMABLE_PROPS);
}

export default Transformable;