import { EventDispatcher } from '../events/EventDispatcher';
import { TransformEvent } from '../events/TransformEvent';

import { Matrix } from '../geom/Matrix';
import { Matrix3D } from '../geom/Matrix3D';
import { Rectangle } from '../geom/Rectangle';
import { Vector3D } from '../geom/Vector3D';

import { ColorTransform } from './ColorTransform';

/**
 * The Transform class provides access to color adjustment properties and two-
 * or three-dimensional transformation objects that can be applied to a display
 * object. During the transformation, the color or the orientation and position
 * of a display object is adjusted(offset) from the current values or
 * coordinates to new values or coordinates. The Transform class also collects
 * data about color and two-dimensional matrix transformations that are applied
 * to a display object and all of its parent objects. You can access these
 * combined transformations through the `concatenatedColorTransform` and
 * `concatenatedMatrix` properties.
 *
 * To apply color transformations: create a ColorTransform object, set the color
 * adjustments using the object's methods and properties, and then assign the
 * `colorTransformation` property of the `transform` property of the display
 * object to the new ColorTransformation object.
 *
 * To apply two-dimensional transformations: create a Matrix object, set the
 * matrix's two-dimensional transformation, and then assign the
 * `transform.matrix` property of the display object to the new Matrix object.
 *
 * To apply three-dimensional transformations: start with a three-dimensional
 * display object. A three-dimensional display object has a `z` property value
 * other than zero. You do not need to create the Matrix3D object. For all
 * three-dimensional objects, a Matrix3D object is created automatically when
 * you assign a `z` value to a display object. You can access the display
 * object's Matrix3D object through the display object's `transform` property.
 * Using the methods of the Matrix3D class, you can add to or modify the
 * existing transformation settings. Also, you can create a custom Matrix3D
 * object, set the custom Matrix3D object's transformation elements, and then
 * assign the new Matrix3D object to the display object using the
 * `transform.matrix` property.
 *
 * To modify a perspective projection of the stage or root object: use the
 * `transform.matrix` property of the root display object to gain access to the
 * PerspectiveProjection object. Or, apply different perspective projection
 * properties to a display object by setting the perspective projection
 * properties of the display object's parent. The child display object inherits
 * the new properties. Specifically, create a PerspectiveProjection object and
 * set its properties, then assign the PerspectiveProjection object to the
 * `perspectiveProjection` property of the parent display object's `transform`
 * property. The specified projection transformation then applies to all the
 * display object's three-dimensional children.
 *
 * Since both PerspectiveProjection and Matrix3D objects perform perspective
 * transformations, do not assign both to a display object at the same time. Use
 * the PerspectiveProjection object for focal length and projection center
 * changes. For more control over the perspective transformation, create a
 * perspective projection Matrix3D object.
 */
export class Transform extends EventDispatcher {

	private static _tempVector: Vector3D = new Vector3D();

	private _backVector: Vector3D;
	private _colorTransform: ColorTransform;
	private _components: Array<Vector3D>;
	private _componentsDirty: boolean = true;
	private _downVector: Vector3D;
	private _forwardVector: Vector3D;
	private _invalidateColorTransform: TransformEvent;
	private _invalidateMatrix3D: TransformEvent;
	private _inverseMatrix3D: Matrix3D;
	private _inverseMatrix3DDirty: boolean = true;
	private _leftVector: Vector3D;
	private _matrix: Matrix;
	private _matrix3D: Matrix3D;
	private _matrix3DDirty: boolean;
	private _pixelBounds: Rectangle;
	private _rawData: Float32Array;
	private _rawDataComponents: Float32Array;
	private _rightVector: Vector3D;
	private _rotation: Vector3D;
	private _scale: Vector3D;
	private _skew: Vector3D;
	private _upVector: Vector3D;

	/**
	 *
	 */
	public get backVector(): Vector3D {
		if (!this._backVector)
			this._backVector = new Vector3D();

		this._matrix3D.copyColumnTo(2, this._backVector, true);

		return this._backVector;
	}

	/**
	 * A ColorTransform object containing values that universally adjust the
	 * colors in the display object.
	 *
	 * @throws TypeError The colorTransform is null when being set
	 */
	public get colorTransform(): ColorTransform {
		return this._colorTransform;
	}

	public set colorTransform(val: ColorTransform) {
		if (val) {
			this._rawData.set(val._rawData, 16);
		} else {
			this._colorTransform.clear();
		}

		this.invalidateColorTransform();
	}

	/**
	 *
	 */
	public get inverseMatrix3D(): Matrix3D {
		if (!this._inverseMatrix3D)
			this._inverseMatrix3D = new Matrix3D();

		if (this._inverseMatrix3DDirty) {
			this._inverseMatrix3DDirty = false;
			this._inverseMatrix3D.copyFrom(this.matrix3D);
			this._inverseMatrix3D.invert();
		}

		return this._inverseMatrix3D;
	}

	/**
	 *
	 */
	public get downVector(): Vector3D {
		if (!this._downVector)
			this._downVector = new Vector3D();

		this._matrix3D.copyColumnTo(1, this._downVector, true);

		return this._downVector;
	}

	/**
	 *
	 */
	public get forwardVector(): Vector3D {
		if (!this._forwardVector)
			this._forwardVector = new Vector3D();

		this._matrix3D.copyColumnTo(2, this._forwardVector);

		return this._forwardVector;
	}

	/**
	 *
	 */
	public get leftVector(): Vector3D {
		if (!this._leftVector)
			this._leftVector = new Vector3D();

		this._matrix3D.copyColumnTo(0, this._backVector, true);

		return this._leftVector;
	}

	/**
	 * A Matrix object containing values that alter the scaling, rotation, and
	 * translation of the display object. If the `matrix` property is set to a
	 * value (not `null`), the `matrix3D` property is `null`. And if the
	 * `matrix3D` property is set to a value(not `null`), the `matrix` property
	 * is `null`.
	 *
	 * @throws TypeError The matrix is null when being set
	 */
	public get matrix(): Matrix {
		if (!this._matrix)
			this._matrix = new Matrix();
		if (this._matrix3DDirty)
			this.updateMatrix3D();

		this._matrix.a = this._matrix3D._rawData[0];
		this._matrix.b = this._matrix3D._rawData[1];
		this._matrix.c = this._matrix3D._rawData[4];
		this._matrix.d = this._matrix3D._rawData[5];
		this._matrix.tx = this._matrix3D._rawData[12];
		this._matrix.ty = this._matrix3D._rawData[13];

		return this._matrix;
	}

	public set matrix(value: Matrix) {
		if (!value)
			return;

		this._matrix3D.identity();

		this._matrix3D._rawData[0] = value.a;
		this._matrix3D._rawData[1] = value.b;
		this._matrix3D._rawData[4] = value.c;
		this._matrix3D._rawData[5] = value.d;
		this._matrix3D._rawData[12] = value.tx;
		this._matrix3D._rawData[13] = value.ty;

		this.invalidateComponents();

		this._matrix3DDirty = false;
		this._inverseMatrix3DDirty = true;
	}

	/**
	 * Provides access to the Matrix3D object of a three-dimensional display
	 * object. The Matrix3D object represents a transformation matrix that
	 * determines the display object's position and orientation. A Matrix3D
	 * object can also perform perspective projection.
	 *
	 * If the `matrix` property is set to a value(not `null`), the `matrix3D`
	 * property is `null`. And if the `matrix3D` property is set to a value(not
	 * `null`), the `matrix` property is `null`.
	 */
	public get matrix3D(): Matrix3D {
		if (this._matrix3DDirty)
			this.updateMatrix3D();

		return this._matrix3D;
	}

	public set matrix3D(val: Matrix3D) {
		if (!val) {
			this._matrix3D.identity();
		} else {
			this._rawData.set(val._rawData);
		}

		this.invalidateComponents();
		this._matrix3DDirty = false;
		this._inverseMatrix3DDirty = true;
	}

	/**
	 * A Rectangle object that defines the bounding rectangle of the display
	 * object on the stage.
	 */
	public get pixelBounds(): Rectangle {
		return this._pixelBounds;
	}

	/**
	 * Defines the position of the 3d object, relative to the local coordinates
	 * of the parent `ObjectContainer3D`.
	 */
	public get position(): Vector3D {
		return this._matrix3D.position;
	}

	/**
	 *
	 */
	public get rightVector(): Vector3D {
		if (!this._rightVector)
			this._rightVector = new Vector3D();

		this._matrix3D.copyColumnTo(0, this._rightVector);

		return this._rightVector;
	}

	/**
	 * Defines the rotation of the 3d object, relative to the local coordinates
	 * of the parent `ObjectContainer3D` .
	 */
	public get rotation(): Vector3D {
		if (this._componentsDirty)
			this._updateComponents();

		return this._rotation;
	}

	/**
	 * Defines the scale of the 3d object, relative to the local coordinates of
	 * the parent `ObjectContainer3D` .
	 */
	public get scale(): Vector3D {
		if (this._componentsDirty)
			this._updateComponents();

		return this._scale;
	}

	/**
	 * Defines the scale of the 3d object, relative to the local coordinates of
	 * the parent `ObjectContainer3D` .
	 */
	public get skew(): Vector3D {
		if (this._componentsDirty)
			this._updateComponents();

		return this._skew;
	}

	/**
	 *
	 */
	public get upVector(): Vector3D {
		if (!this._upVector)
			this._upVector = new Vector3D();

		this._matrix3D.copyColumnTo(1, this._upVector);

		return this._upVector;
	}

	constructor(rawData: Float32Array = null) {
		super();

		this._rawData = rawData || new Float32Array(24);

		//create the view for matrix3D
		this._matrix3D = new Matrix3D(new Float32Array(this._rawData.buffer, 0, 16));

		//create the view for colorTransform
		this._colorTransform = new ColorTransform(new Float32Array(this._rawData.buffer, 64, 8));

		if (rawData == null) {
			this._matrix3D.identity();
			this._colorTransform.clear();
		}
	}

	/**
	 *
	 * @param value
	 */
	public append(value: Matrix3D) {
		console.log(`append(${value}) is not implemented yet in core/Transform`);
	}

	/**
	 *
	 */
	public clearColorTransform(): void {
		if (!this._colorTransform)
			return;

		this._colorTransform.clear();
		this.invalidateColorTransform();
	}

	/**
	 *
	 */
	public clearMatrix3D(): void {
		this._matrix3D.identity();
		this.invalidateComponents();
	}

	/**
	 *
	 */
	public clone(): Transform {
		const transform: Transform = new Transform();

		this.copyRawDataTo(transform);

		return transform;
	}

	/**
	 *
	 * @param transform
	 */
	public copyRawDataTo(transform: Transform) {
		if (this._matrix3DDirty)
			this.updateMatrix3D();

		transform._rawData.set(this._rawData);

		this.invalidateComponents();

		this.invalidateColorTransform();
	}

	/**
	 *
	 */
	public dispose(): void {

	}

	public invalidateColorTransform(): void {
		if (!this._invalidateColorTransform) {
			this._invalidateColorTransform = new TransformEvent(TransformEvent.INVALIDATE_COLOR_TRANSFORM, this);
		}

		this.dispatchEvent(this._invalidateColorTransform);
	}

	public invalidateComponents(): void {
		this.invalidatePosition();

		this._componentsDirty = true;
	}

	/**
	 * Invalidates the 3D transformation matrix, causing it to be updated upon the next request
	 *
	 * @private
	 */
	public invalidateMatrix3D(): void {
		this._inverseMatrix3DDirty = true;
		if (this._matrix3DDirty)
			return;

		this._matrix3DDirty = true;

		if (!this._invalidateMatrix3D)
			this._invalidateMatrix3D = new TransformEvent(TransformEvent.INVALIDATE_MATRIX3D, this);

		this.dispatchEvent(this._invalidateMatrix3D);
	}

	/**
	 *
	 */
	public invalidatePosition(): void {
		this._inverseMatrix3DDirty = true;

		if (!this._invalidateMatrix3D)
			this._invalidateMatrix3D = new TransformEvent(TransformEvent.INVALIDATE_MATRIX3D, this);

		this.dispatchEvent(this._invalidateMatrix3D);
	}

	/**
     * Rotates the 3d object around to face a point defined relative to the
     * local coordinates of the parent `ObjectContainer3D` .
     *
     * @param target The vector defining the point to be looked at.
     * @param upAxis An optional vector used to define the desired up
     *               orientation of the 3d object after rotation has occurred.
     */
	public lookAt(position: Vector3D, upAxis: Vector3D = null): void {
		if (upAxis == null)
			upAxis = Vector3D.Y_AXIS;

		const vec: Vector3D = Matrix3D.getPointAtMatrix(
			new Vector3D(), position.subtract(this._matrix3D.position), upAxis, Matrix3D.CALCULATION_MATRIX)
			.decompose()[1];

		this.rotateTo(vec.x, vec.y, vec.z);
	}

	/**
	 * Moves the 3d object backwards along it's local z axis
	 *
	 * @param    distance    The length of the movement
	 */
	public moveBackward(distance: number): void {
		this.translateLocal(Vector3D.Z_AXIS, -distance);
	}

	/**
	 * Moves the 3d object backwards along it's local y axis
	 *
	 * @param    distance    The length of the movement
	 */
	public moveDown(distance: number): void {
		this.translateLocal(Vector3D.Y_AXIS, -distance);
	}

	/**
	 * Moves the 3d object forwards along it's local z axis
	 *
	 * @param    distance    The length of the movement
	 */
	public moveForward(distance: number): void {
		this.translateLocal(Vector3D.Z_AXIS, distance);
	}

	/**
	 * Moves the 3d object backwards along it's local x axis
	 *
	 * @param    distance    The length of the movement
	 */

	public moveLeft(distance: number): void {
		this.translateLocal(Vector3D.X_AXIS, -distance);
	}

	/**
	 * Moves the 3d object forwards along it's local x axis
	 *
	 * @param    distance    The length of the movement
	 */
	public moveRight(distance: number): void {
		this.translateLocal(Vector3D.X_AXIS, distance);
	}

	/**
	 * Moves the 3d object directly to a point in space
	 *
	 * @param    dx        The amount of movement along the local x axis.
	 * @param    dy        The amount of movement along the local y axis.
	 * @param    dz        The amount of movement along the local z axis.
	 */

	public moveTo(dx: number, dy: number, dz: number): void {
		this._matrix3D._rawData[12] = dx;
		this._matrix3D._rawData[13] = dy;
		this._matrix3D._rawData[14] = dz;

		this.invalidatePosition();
	}

	/**
	 * Moves the 3d object forwards along it's local y axis
	 *
	 * @param    distance    The length of the movement
	 */
	public moveUp(distance: number): void {
		this.translateLocal(Vector3D.Y_AXIS, distance);
	}

	/**
	 * Rotates the 3d object around it's local x-axis
	 *
	 * @param    angle        The amount of rotation in degrees
	 */
	public pitch(angle: number): void {
		this.rotate(Vector3D.X_AXIS, angle);
	}

	/**
	 * Rotates the 3d object around it's local z-axis
	 *
	 * @param    angle        The amount of rotation in degrees
	 */
	public roll(angle: number): void {
		this.rotate(Vector3D.Z_AXIS, angle);
	}

	/**
	 * Rotates the 3d object around an axis by a defined angle
	 *
	 * @param    axis        The vector defining the axis of rotation
	 * @param    angle        The amount of rotation in degrees
	 */
	public rotate(axis: Vector3D, angle: number): void {
		this.matrix3D.prependRotation(angle, axis);

		this.invalidateComponents();
	}

	/**
	 * Rotates the 3d object directly to a euler angle
	 *
	 * @param ax The angle in degrees of the rotation around the x axis.
	 * @param ay The angle in degrees of the rotation around the y axis.
	 * @param az The angle in degrees of the rotation around the z axis.
	 */
	public rotateTo(ax: number, ay: number, az: number): void {
		if (this._componentsDirty)
			this._updateComponents();

		this._rotation.x = ax;
		this._rotation.y = ay;
		this._rotation.z = az;

		this.invalidateMatrix3D();
	}

	/**
	 *
	 * @param sx
	 * @param sy
	 * @param sz
	 */
	public scaleTo(sx: number, sy: number, sz: number): void {
		if (this._componentsDirty)
			this._updateComponents();

		this._scale.x = sx;
		this._scale.y = sy;
		this._scale.z = sz;

		this.invalidateMatrix3D();
	}

	/**
	 *
	 * @param sx
	 * @param sy
	 * @param sz
	 */
	public skewTo(sx: number, sy: number, sz: number): void {
		if (this._componentsDirty)
			this._updateComponents();

		this._skew.x = sx;
		this._skew.y = sy;
		this._skew.z = sz;

		this.invalidateMatrix3D();
	}

	/**
	 * Moves the 3d object along a vector by a defined length.
	 *
	 * @param axis     The vector defining the axis of movement.
	 * @param distance The length of the movement.
	 */
	public translate(axis: Vector3D, distance: number): void {
		const x: number = axis.x, y: number = axis.y, z: number = axis.z;
		const len: number = distance / Math.sqrt(x * x + y * y + z * z);

		this.matrix3D.appendTranslation(x * len, y * len, z * len);

		this.invalidatePosition();
	}

	/**
	 * Moves the 3d object along a vector by a defined length.
	 *
	 * @param axis     The vector defining the axis of movement.
	 * @param distance The length of the movement.
	 */
	public translateLocal(axis: Vector3D, distance: number): void {
		const x: number = axis.x, y: number = axis.y, z: number = axis.z;
		const len: number = distance / Math.sqrt(x * x + y * y + z * z);

		this.matrix3D.prependTranslation(x * len, y * len, z * len);

		this.invalidatePosition();
	}

	/**
	 *
	 */
	public updateMatrix3D(): void {
		Transform._tempVector.copyFrom(this._matrix3D.position);

		this._matrix3D.recompose(this._components);

		this._matrix3DDirty = false;
	}

	/**
	 * Rotates the 3d object around it's local y-axis
	 *
	 * @param angle The amount of rotation in degrees
	 */
	public yaw(angle: number): void {
		this.rotate(Vector3D.Y_AXIS, angle);
	}

	private _updateComponents(): void {
		if (!this._components) {
			// Cached vector of transformation components used when
			// recomposing the transform matrix in updateTransform()
			this._components = new Array<Vector3D>(4);

			this._rawDataComponents = new Float32Array(12);
			this._rawDataComponents[3] = 1;
			this._rawDataComponents[7] = 1;
			this._rawDataComponents[11] = 1;

			this._rotation = new Vector3D(new Float32Array(this._rawDataComponents.buffer, 0, 4));
			this._skew = new Vector3D(new Float32Array(this._rawDataComponents.buffer, 16, 4));
			this._scale = new Vector3D(new Float32Array(this._rawDataComponents.buffer, 32, 4));

			this._components[0] = Transform._tempVector;
			this._components[1] = this._rotation;
			this._components[2] = this._skew;
			this._components[3] = this._scale;
		}

		const elements: Array<Vector3D> = this._matrix3D.decompose();
		let vec: Vector3D;

		vec = elements[1];

		this._rotation.x = vec.x;
		this._rotation.y = vec.y;
		this._rotation.z = vec.z;

		vec = elements[2];

		this._skew.x = vec.x;
		this._skew.y = vec.y;
		this._skew.z = vec.z;

		vec = elements[3];

		this._scale.x = vec.x;
		this._scale.y = vec.y;
		this._scale.z = vec.z;

		this._componentsDirty = false;
	}
}
