import { Quaternion, Vector3D } from '@awayjs/core';

import { ShaderBase, _Render_RenderableBase, ElementsEvent, IElements, TriangleElements } from '@awayjs/renderer';

import { _Render_Shape, Shape } from '../renderables/Shape';
import { AnimationStateEvent } from '../events/AnimationStateEvent';
import { JointPose } from './data/JointPose';
import { Skeleton } from './data/Skeleton';
import { SkeletonJoint } from './data/SkeletonJoint';
import { SkeletonPose } from './data/SkeletonPose';
import { ISkeletonAnimationState } from './states/ISkeletonAnimationState';
import { IAnimationTransition } from './transitions/IAnimationTransition';

import { SkeletonAnimationSet } from './SkeletonAnimationSet';
import { AnimatorBase } from './AnimatorBase';

/**
 * Provides an interface for assigning skeleton-based animation data sets to sprite-based entity objects
 * and controlling the various available states of animation through an interative playhead that can be
 * automatically updated or manually triggered.
 */
export class SkeletonAnimator extends AnimatorBase {
	private _globalMatrices: Float32Array;
	private _globalPose: SkeletonPose = new SkeletonPose();
	private _globalPropertiesDirty: boolean;
	private _numJoints: number;
	private _morphedElements: Object = new Object();
	private _morphedElementsDirty: Object = new Object();
	private _condensedMatrices: Float32Array;

	private _skeletonAnimationSet: SkeletonAnimationSet;
	private _skeleton: Skeleton;
	private _forceCPU: boolean;
	private _useCondensedIndices: boolean;
	private _jointsPerVertex: number;
	private _activeSkeletonState: ISkeletonAnimationState;
	private _onTransitionCompleteDelegate: (event: AnimationStateEvent) => void;

	private _onIndicesUpdateDelegate: (event: ElementsEvent) => void;
	private _onVerticesUpdateDelegate: (event: ElementsEvent) => void;

	/**
	 * returns the calculated global matrices of the current skeleton pose.
	 *
	 * @see #globalPose
	 */
	public get globalMatrices(): Float32Array {
		if (this._globalPropertiesDirty)
			this.updateGlobalProperties();

		return this._globalMatrices;
	}

	/**
	 * returns the current skeleton pose output from the animator.
	 *
	 * @see away.animators.data.SkeletonPose
	 */
	public get globalPose(): SkeletonPose {
		if (this._globalPropertiesDirty)
			this.updateGlobalProperties();

		return this._globalPose;
	}

	/**
	 * Returns the skeleton object in use by the animator - this defines the number and heirarchy of joints used by the
	 * skinned geoemtry to which skeleon animator is applied.
	 */
	public get skeleton(): Skeleton {
		return this._skeleton;
	}

	/**
	 * Indicates whether the skeleton animator is disabled by default for GPU rendering, something that allows the animator to perform calculation on the GPU.
	 * Defaults to false.
	 */
	public get forceCPU(): boolean {
		return this._forceCPU;
	}

	/**
	 * Offers the option of enabling GPU accelerated animation on skeletons larger than 32 joints
	 * by condensing the number of joint index values required per sprite. Only applicable to
	 * skeleton animations that utilise more than one sprite object. Defaults to false.
	 */
	public get useCondensedIndices(): boolean {
		return this._useCondensedIndices;
	}

	public set useCondensedIndices(value: boolean) {
		this._useCondensedIndices = value;
	}

	/**
	 * Creates a new <code>SkeletonAnimator</code> object.
	 *
	 * @param skeletonAnimationSet The animation data set containing the skeleton animations used by the animator.
	 * @param skeleton The skeleton object used for calculating the resulting global matrices for transforming skinned sprite data.
	 * @param forceCPU Optional value that only allows the animator to perform calculation on the CPU. Defaults to false.
	 */
	constructor(animationSet: SkeletonAnimationSet, skeleton: Skeleton, forceCPU: boolean = false) {
		super(animationSet);

		this._skeletonAnimationSet = animationSet;
		this._skeleton = skeleton;
		this._forceCPU = forceCPU;
		this._jointsPerVertex = animationSet.jointsPerVertex;

		this._numJoints = this._skeleton.numJoints;
		this._globalMatrices = new Float32Array(this._numJoints * 12);

		let j: number = 0;
		for (let i: number = 0; i < this._numJoints; ++i) {
			this._globalMatrices[j++] = 1;
			this._globalMatrices[j++] = 0;
			this._globalMatrices[j++] = 0;
			this._globalMatrices[j++] = 0;
			this._globalMatrices[j++] = 0;
			this._globalMatrices[j++] = 1;
			this._globalMatrices[j++] = 0;
			this._globalMatrices[j++] = 0;
			this._globalMatrices[j++] = 0;
			this._globalMatrices[j++] = 0;
			this._globalMatrices[j++] = 1;
			this._globalMatrices[j++] = 0;
		}

		this._onTransitionCompleteDelegate = (event: AnimationStateEvent) => this.onTransitionComplete(event);
		this._onIndicesUpdateDelegate = (event: ElementsEvent) => this.onIndicesUpdate(event);
		this._onVerticesUpdateDelegate = (event: ElementsEvent) => this.onVerticesUpdate(event);
	}

	/**
	 * @inheritDoc
	 */
	public clone(): AnimatorBase {
		return new SkeletonAnimator(this._skeletonAnimationSet, this._skeleton, this._forceCPU);
	}

	/**
	 * Plays an animation state registered with the given name in the animation data set.
	 *
	 * @param name The data set name of the animation state to be played.
	 * @param transition An optional transition object that determines how the animator will transition from the currently active animation state.
	 * @param offset An option offset time (in milliseconds) that resets the state's internal clock to the absolute time of the animator plus the offset value. Required for non-looping animation states.
	 */
	public play(name: string, transition: IAnimationTransition = null, offset: number = NaN): void {
		if (this._pActiveAnimationName == name)
			return;

		this._pActiveAnimationName = name;

		if (!this._pAnimationSet.hasAnimation(name))
			throw new Error('Animation root node ' + name + ' not found!');

		if (transition && this._pActiveNode) {
			//setup the transition
			this._pActiveNode = transition.getAnimationNode(this, this._pActiveNode, this._pAnimationSet.getAnimation(name), this._pAbsoluteTime);
			this._pActiveNode.addEventListener(AnimationStateEvent.TRANSITION_COMPLETE, this._onTransitionCompleteDelegate);
		} else
			this._pActiveNode = this._pAnimationSet.getAnimation(name);

		this._pActiveState = this.getAnimationState(this._pActiveNode);

		if (this.updatePosition) {
			//update straight away to reset position deltas
			this._pActiveState.update(this._pAbsoluteTime);
			this._pActiveState.positionDelta;
		}

		this._activeSkeletonState = <ISkeletonAnimationState> this._pActiveState;

		this.start();

		//apply a time offset if specified
		if (!isNaN(offset))
			this.reset(name, offset);
	}

	/**
	 * @inheritDoc
	 */
	public setRenderState(shader: ShaderBase, renderable: _Render_Shape): void {
		// do on request of globalProperties
		if (this._globalPropertiesDirty)
			this.updateGlobalProperties();

		const elements: TriangleElements = <TriangleElements> (<Shape> renderable.renderable).elements;

		elements.useCondensedIndices = this._useCondensedIndices;

		if (this._useCondensedIndices) {
			// using a condensed data set
			this.updateCondensedMatrices(elements.condensedIndexLookUp);
			shader.setVertexConstFromArray(this._skeletonAnimationSet.matricesIndex, this._condensedMatrices);
		} else {
			if (this._pAnimationSet.usesCPU) {
				if (this._morphedElementsDirty[elements.id])
					this.morphElements(renderable, elements);

				return;
			}
			shader.setVertexConstFromArray(this._skeletonAnimationSet.matricesIndex, this._globalMatrices);
		}
	}

	/**
	 * @inheritDoc
	 */
	public testGPUCompatibility(shader: ShaderBase): void {
		if (!this._useCondensedIndices && (this._forceCPU || this._jointsPerVertex > 4 || shader.numUsedVertexConstants + this._numJoints * 3 > 128))
			this._pAnimationSet.cancelGPUCompatibility();
	}

	/**
	 * Applies the calculated time delta to the active animation state node or state transition object.
	 */
	public _pUpdateDeltaTime(dt: number): void {
		super._pUpdateDeltaTime(dt);

		//invalidate pose matrices
		this._globalPropertiesDirty = true;

		//trigger geometry invalidation if using CPU animation
		if (this._pAnimationSet.usesCPU)
			this.invalidateElements();
	}

	private updateCondensedMatrices(condensedIndexLookUp: Array<number>): void {
		let j: number = 0, k: number = 0;
		const len: number = condensedIndexLookUp.length;
		let srcIndex: number;

		this._condensedMatrices = new Float32Array(len * 12);

		for (let i: number = 0; i < len; i++) {
			srcIndex = condensedIndexLookUp[i] * 12; //12 required for the three 4-component vectors that store the matrix
			k = 12;
			// copy into condensed
			while (k--)
				this._condensedMatrices[j++] = this._globalMatrices[srcIndex++];
		}
	}

	private updateGlobalProperties(): void {
		this._globalPropertiesDirty = false;

		//get global pose
		this.localToGlobalPose(this._activeSkeletonState.getSkeletonPose(this._skeleton), this._globalPose, this._skeleton);

		// convert pose to matrix
		let mtxOffset: number = 0;
		const globalPoses: Array<JointPose> = this._globalPose.jointPoses;
		let raw: Float32Array;
		let ox: number, oy: number, oz: number, ow: number;
		let xy2: number, xz2: number, xw2: number;
		let yz2: number, yw2: number, zw2: number;
		let n11: number, n12: number, n13: number;
		let n21: number, n22: number, n23: number;
		let n31: number, n32: number, n33: number;
		let m11: number, m12: number, m13: number, m14: number;
		let m21: number, m22: number, m23: number, m24: number;
		let m31: number, m32: number, m33: number, m34: number;
		const joints: Array<SkeletonJoint> = this._skeleton.joints;
		let pose: JointPose;
		let quat: Quaternion;
		let vec: Vector3D;
		let t: number;

		for (let i: number = 0; i < this._numJoints; ++i) {
			pose = globalPoses[i];
			quat = pose.orientation;
			vec = pose.translation;
			ox = quat.x;
			oy = quat.y;
			oz = quat.z;
			ow = quat.w;

			xy2 = (t = 2.0 * ox) * oy;
			xz2 = t * oz;
			xw2 = t * ow;
			yz2 = (t = 2.0 * oy) * oz;
			yw2 = t * ow;
			zw2 = 2.0 * oz * ow;

			yz2 = 2.0 * oy * oz;
			yw2 = 2.0 * oy * ow;
			zw2 = 2.0 * oz * ow;
			ox *= ox;
			oy *= oy;
			oz *= oz;
			ow *= ow;

			n11 = (t = ox - oy) - oz + ow;
			n12 = xy2 - zw2;
			n13 = xz2 + yw2;
			n21 = xy2 + zw2;
			n22 = -t - oz + ow;
			n23 = yz2 - xw2;
			n31 = xz2 - yw2;
			n32 = yz2 + xw2;
			n33 = -ox - oy + oz + ow;

			// prepend inverse bind pose
			raw = joints[i].inverseBindPose;
			m11 = raw[0];
			m12 = raw[4];
			m13 = raw[8];
			m14 = raw[12];
			m21 = raw[1];
			m22 = raw[5];
			m23 = raw[9];
			m24 = raw[13];
			m31 = raw[2];
			m32 = raw[6];
			m33 = raw[10];
			m34 = raw[14];

			this._globalMatrices[mtxOffset] = n11 * m11 + n12 * m21 + n13 * m31;
			this._globalMatrices[mtxOffset + 1] = n11 * m12 + n12 * m22 + n13 * m32;
			this._globalMatrices[mtxOffset + 2] = n11 * m13 + n12 * m23 + n13 * m33;
			this._globalMatrices[mtxOffset + 3] = n11 * m14 + n12 * m24 + n13 * m34 + vec.x;
			this._globalMatrices[mtxOffset + 4] = n21 * m11 + n22 * m21 + n23 * m31;
			this._globalMatrices[mtxOffset + 5] = n21 * m12 + n22 * m22 + n23 * m32;
			this._globalMatrices[mtxOffset + 6] = n21 * m13 + n22 * m23 + n23 * m33;
			this._globalMatrices[mtxOffset + 7] = n21 * m14 + n22 * m24 + n23 * m34 + vec.y;
			this._globalMatrices[mtxOffset + 8] = n31 * m11 + n32 * m21 + n33 * m31;
			this._globalMatrices[mtxOffset + 9] = n31 * m12 + n32 * m22 + n33 * m32;
			this._globalMatrices[mtxOffset + 10] = n31 * m13 + n32 * m23 + n33 * m33;
			this._globalMatrices[mtxOffset + 11] = n31 * m14 + n32 * m24 + n33 * m34 + vec.z;

			mtxOffset = mtxOffset + 12;
		}
	}

	public getRenderableElements(renderable: _Render_RenderableBase, sourceElements: TriangleElements): IElements {
		this._morphedElementsDirty[sourceElements.id] = true;

		//early out for GPU animations
		if (!this._pAnimationSet.usesCPU)
			return sourceElements;

		let targetElements: TriangleElements;

		if (!(targetElements = this._morphedElements[sourceElements.id])) {
			//not yet stored
			sourceElements.normals;
			sourceElements.tangents;
			targetElements = <TriangleElements> (this._morphedElements[sourceElements.id] = sourceElements.clone());
			//turn off auto calculations on the morphed geometry
			targetElements.autoDeriveNormals = false;
			targetElements.autoDeriveTangents = false;
			//add event listeners for any changes in UV values on the source geometry
			sourceElements.addEventListener(ElementsEvent.INVALIDATE_INDICES, this._onIndicesUpdateDelegate);
			sourceElements.addEventListener(ElementsEvent.INVALIDATE_VERTICES, this._onVerticesUpdateDelegate);
		}

		return targetElements;
	}

	/**
	 * If the animation can't be performed on GPU, transform vertices manually
	 * @param subGeom The subgeometry containing the weights and joint index data per vertex.
	 * @param pass The material pass for which we need to transform the vertices
	 */
	public morphElements(renderable: _Render_RenderableBase, sourceElements: TriangleElements): void {
		this._morphedElementsDirty[sourceElements.id] = false;

		const numVertices: number = sourceElements.numVertices;
		const sourcePositions: ArrayBufferView = sourceElements.positions.get(numVertices);
		const sourceNormals: Float32Array = sourceElements.normals.get(numVertices);
		const sourceTangents: Float32Array = sourceElements.tangents.get(numVertices);

		const posDim: number = sourceElements.positions.dimensions;
		const posStride: number = sourceElements.positions.stride;
		const normalStride: number = sourceElements.normals.stride;
		const tangentStride: number = sourceElements.tangents.stride;

		const jointIndices: Float32Array = <Float32Array> sourceElements.jointIndices.get(numVertices);
		const jointWeights: Float32Array = <Float32Array> sourceElements.jointWeights.get(numVertices);
		const jointStride: number = sourceElements.jointIndices.stride;

		const targetElements: TriangleElements = this._morphedElements[sourceElements.id];

		const targetPositions: ArrayBufferView = targetElements.positions.get(numVertices);
		const targetNormals: Float32Array = targetElements.normals.get(numVertices);
		const targetTangents: Float32Array = targetElements.tangents.get(numVertices);
		targetElements.positions.attributesBuffer.invalidate();
		targetElements.normals.attributesBuffer.invalidate();
		targetElements.tangents.attributesBuffer.invalidate();

		let index: number = 0;
		let i0: number = 0;
		let i1: number = 0;
		let i2: number = 0;
		let i3: number = 0;
		let k: number;
		let vx: number, vy: number, vz: number;
		let nx: number, ny: number, nz: number;
		let tx: number, ty: number, tz: number;
		let weight: number;
		let vertX: number, vertY: number, vertZ: number;
		let normX: number, normY: number, normZ: number;
		let tangX: number, tangY: number, tangZ: number;
		let m11: number, m12: number, m13: number, m14: number;
		let m21: number, m22: number, m23: number, m24: number;
		let m31: number, m32: number, m33: number, m34: number;

		while (index < numVertices) {
			i0 = index * posStride;
			vertX = sourcePositions[i0];
			vertY = sourcePositions[i0 + 1];
			vertZ = (posDim == 3) ? sourcePositions[i0 + 2] : 0;
			i1 = index * normalStride;
			normX = sourceNormals[i1];
			normY = sourceNormals[i1 + 1];
			normZ = sourceNormals[i1 + 2];
			i2 = index * tangentStride;
			tangX = sourceTangents[i2];
			tangY = sourceTangents[i2 + 1];
			tangZ = sourceTangents[i2 + 2];
			vx = 0;
			vy = 0;
			vz = 0;
			nx = 0;
			ny = 0;
			nz = 0;
			tx = 0;
			ty = 0;
			tz = 0;
			k = 0;
			i3 = index * jointStride;
			while (k < this._jointsPerVertex) {
				weight = jointWeights[i3 + k];
				if (weight > 0) {
					// implicit /3*12 (/3 because indices are multiplied by 3 for gpu matrix access, *12 because it's the matrix size)
					const mtxOffset: number = jointIndices[i3 + k] << 2;
					m11 = this._globalMatrices[mtxOffset];
					m12 = this._globalMatrices[mtxOffset + 1];
					m13 = this._globalMatrices[mtxOffset + 2];
					m14 = this._globalMatrices[mtxOffset + 3];
					m21 = this._globalMatrices[mtxOffset + 4];
					m22 = this._globalMatrices[mtxOffset + 5];
					m23 = this._globalMatrices[mtxOffset + 6];
					m24 = this._globalMatrices[mtxOffset + 7];
					m31 = this._globalMatrices[mtxOffset + 8];
					m32 = this._globalMatrices[mtxOffset + 9];
					m33 = this._globalMatrices[mtxOffset + 10];
					m34 = this._globalMatrices[mtxOffset + 11];
					vx += weight * (m11 * vertX + m12 * vertY + m13 * vertZ + m14);
					vy += weight * (m21 * vertX + m22 * vertY + m23 * vertZ + m24);
					vz += weight * (m31 * vertX + m32 * vertY + m33 * vertZ + m34);
					nx += weight * (m11 * normX + m12 * normY + m13 * normZ);
					ny += weight * (m21 * normX + m22 * normY + m23 * normZ);
					nz += weight * (m31 * normX + m32 * normY + m33 * normZ);
					tx += weight * (m11 * tangX + m12 * tangY + m13 * tangZ);
					ty += weight * (m21 * tangX + m22 * tangY + m23 * tangZ);
					tz += weight * (m31 * tangX + m32 * tangY + m33 * tangZ);
					k++;
				} else {
					//if zero weight encountered, skip to the next vertex
					k = this._jointsPerVertex;
				}
			}

			targetPositions[i0] = vx;
			targetPositions[i0 + 1] = vy;
			if (posDim == 3) targetPositions[i0 + 2] = vz;
			targetNormals[i1] = nx;
			targetNormals[i1 + 1] = ny;
			targetNormals[i1 + 2] = nz;
			targetTangents[i2] = tx;
			targetTangents[i2 + 1] = ty;
			targetTangents[i2 + 2] = tz;

			index++;
		}
	}

	/**
	 * Converts a local hierarchical skeleton pose to a global pose
	 * @param targetPose The SkeletonPose object that will contain the global pose.
	 * @param skeleton The skeleton containing the joints, and as such, the hierarchical data to transform to global poses.
	 */
	private localToGlobalPose(sourcePose: SkeletonPose, targetPose: SkeletonPose, skeleton: Skeleton): void {
		const globalPoses: Array<JointPose> = targetPose.jointPoses;
		let globalJointPose: JointPose;
		const joints: Array<SkeletonJoint> = skeleton.joints;
		const len: number = sourcePose.numJointPoses;
		const jointPoses: Array<JointPose> = sourcePose.jointPoses;
		let parentIndex: number;
		let joint: SkeletonJoint;
		let parentPose: JointPose;
		let pose: JointPose;
		let or: Quaternion;
		let tr: Vector3D;
		let t: Vector3D;
		let q: Quaternion;

		let x1: number, y1: number, z1: number, w1: number;
		let x2: number, y2: number, z2: number, w2: number;
		let x3: number, y3: number, z3: number;

		// :s
		if (globalPoses.length != len)
			globalPoses.length = len;

		for (let i: number = 0; i < len; ++i) {
			globalJointPose = globalPoses[i];

			if (globalJointPose == null)
				globalJointPose = globalPoses[i] = new JointPose();

			joint = joints[i];
			parentIndex = joint.parentIndex;
			pose = jointPoses[i];

			q = globalJointPose.orientation;
			t = globalJointPose.translation;

			if (parentIndex < 0) {
				tr = pose.translation;
				or = pose.orientation;
				q.x = or.x;
				q.y = or.y;
				q.z = or.z;
				q.w = or.w;
				t.x = tr.x;
				t.y = tr.y;
				t.z = tr.z;
			} else {
				// append parent pose
				parentPose = globalPoses[parentIndex];

				// rotate point
				or = parentPose.orientation;
				tr = pose.translation;
				x2 = or.x;
				y2 = or.y;
				z2 = or.z;
				w2 = or.w;
				x3 = tr.x;
				y3 = tr.y;
				z3 = tr.z;

				w1 = -x2 * x3 - y2 * y3 - z2 * z3;
				x1 = w2 * x3 + y2 * z3 - z2 * y3;
				y1 = w2 * y3 - x2 * z3 + z2 * x3;
				z1 = w2 * z3 + x2 * y3 - y2 * x3;

				// append parent translation
				tr = parentPose.translation;
				t.x = -w1 * x2 + x1 * w2 - y1 * z2 + z1 * y2 + tr.x;
				t.y = -w1 * y2 + x1 * z2 + y1 * w2 - z1 * x2 + tr.y;
				t.z = -w1 * z2 - x1 * y2 + y1 * x2 + z1 * w2 + tr.z;

				// append parent orientation
				x1 = or.x;
				y1 = or.y;
				z1 = or.z;
				w1 = or.w;
				or = pose.orientation;
				x2 = or.x;
				y2 = or.y;
				z2 = or.z;
				w2 = or.w;

				q.w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2;
				q.x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2;
				q.y = w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2;
				q.z = w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2;
			}
		}
	}

	private onTransitionComplete(event: AnimationStateEvent): void {
		if (event.type == AnimationStateEvent.TRANSITION_COMPLETE) {
			event.animationNode.removeEventListener(AnimationStateEvent.TRANSITION_COMPLETE, this._onTransitionCompleteDelegate);
			//if this is the current active state transition, revert control to the active node
			if (this._pActiveState == event.animationState) {
				this._pActiveNode = this._pAnimationSet.getAnimation(this._pActiveAnimationName);
				this._pActiveState = this.getAnimationState(this._pActiveNode);
				this._activeSkeletonState = <ISkeletonAnimationState> this._pActiveState;
			}
		}
	}

	private onIndicesUpdate(event: ElementsEvent): void {
		const elements: TriangleElements = <TriangleElements> event.target;

		(<TriangleElements> this._morphedElements[elements.id]).setIndices(elements.indices);
	}

	private onVerticesUpdate(event: ElementsEvent): void {
		const elements: TriangleElements = <TriangleElements> event.target;

		//only update uvs
		if (event.attributesView != elements.uvs && event.attributesView != elements.getCustomAtributes('secondaryUVs'))
			return;

		const morphElements: TriangleElements = <TriangleElements> this._morphedElements[elements.id];
		const morphUVs: Float32Array = <Float32Array> morphElements.uvs.get(elements.numVertices);

		morphElements.invalidateVertices(morphElements.uvs);

		const uvStride: number = morphElements.uvs.stride;
		const uvs: Float32Array = <Float32Array> event.attributesView.get(elements.numVertices);

		const len: number = elements.numVertices * uvStride;
		for (let i: number = 0; i < len; i += uvStride) {
			morphUVs[i] = uvs[i];
			morphUVs[i + 1] = uvs[i + 1];
		}
	}
}