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

import { JointPose } from '../data/JointPose';
import { Skeleton } from '../data/Skeleton';
import { SkeletonPose } from '../data/SkeletonPose';
import { SkeletonNaryLERPNode } from '../nodes/SkeletonNaryLERPNode';

import { AnimationStateBase } from './AnimationStateBase';
import { ISkeletonAnimationState } from './ISkeletonAnimationState';

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

/**
 *
 */
export class SkeletonNaryLERPState extends AnimationStateBase implements ISkeletonAnimationState {
	private _skeletonAnimationNode: SkeletonNaryLERPNode;
	private _skeletonPose: SkeletonPose = new SkeletonPose();
	private _skeletonPoseDirty: boolean = true;
	private _blendWeights: Array<number> = new Array<number>();
	private _inputs: Array<ISkeletonAnimationState> = new Array<ISkeletonAnimationState>();

	constructor(animator: AnimatorBase, skeletonAnimationNode: SkeletonNaryLERPNode) {
		super(animator, skeletonAnimationNode);

		this._skeletonAnimationNode = skeletonAnimationNode;

		let i: number = this._skeletonAnimationNode.numInputs;

		while (i--)
			this._inputs[i] = <ISkeletonAnimationState> animator.getAnimationState(this._skeletonAnimationNode._iInputs[i]);
	}

	/**
	 * @inheritDoc
	 */
	public phase(value: number): void {
		this._skeletonPoseDirty = true;

		this._pPositionDeltaDirty = true;

		for (let j: number = 0; j < this._skeletonAnimationNode.numInputs; ++j) {
			if (this._blendWeights[j])
				this._inputs[j].update(value);
		}
	}

	/**
	 * @inheritDoc
	 */
	public _pUdateTime(time: number): void {
		for (let j: number = 0; j < this._skeletonAnimationNode.numInputs; ++j) {
			if (this._blendWeights[j])
				this._inputs[j].update(time);
		}

		super._pUpdateTime(time);
	}

	/**
	 * Returns the current skeleton pose of the animation in the clip based on the internal playhead position.
	 */
	public getSkeletonPose(skeleton: Skeleton): SkeletonPose {
		if (this._skeletonPoseDirty)
			this.updateSkeletonPose(skeleton);

		return this._skeletonPose;
	}

	/**
	 * Returns the blend weight of the skeleton aniamtion node that resides at the given input index.
	 *
	 * @param index The input index for which the skeleton animation node blend weight is requested.
	 */
	public getBlendWeightAt(index: number): number {
		return this._blendWeights[index];
	}

	/**
	 * Sets the blend weight of the skeleton aniamtion node that resides at the given input index.
	 *
	 * @param index The input index on which the skeleton animation node blend weight is to be set.
	 * @param blendWeight The blend weight value to use for the given skeleton animation node index.
	 */
	public setBlendWeightAt(index: number, blendWeight: number): void {
		this._blendWeights[index] = blendWeight;

		this._pPositionDeltaDirty = true;
		this._skeletonPoseDirty = true;
	}

	/**
	 * @inheritDoc
	 */
	public _pUpdatePositionDelta(): void {
		this._pPositionDeltaDirty = false;

		let delta: Vector3D;
		let weight: number;

		this.positionDelta.x = 0;
		this.positionDelta.y = 0;
		this.positionDelta.z = 0;

		for (let j: number = 0; j < this._skeletonAnimationNode.numInputs; ++j) {
			weight = this._blendWeights[j];

			if (weight) {
				delta = this._inputs[j].positionDelta;
				this.positionDelta.x += weight * delta.x;
				this.positionDelta.y += weight * delta.y;
				this.positionDelta.z += weight * delta.z;
			}
		}
	}

	/**
	 * Updates the output skeleton pose of the node based on the blend weight values given to the input nodes.
	 *
	 * @param skeleton The skeleton used by the animator requesting the ouput pose.
	 */
	private updateSkeletonPose(skeleton: Skeleton): void {
		this._skeletonPoseDirty = false;

		let weight: number;
		const endPoses: Array<JointPose> = this._skeletonPose.jointPoses;
		let poses: Array<JointPose>;
		let endPose: JointPose, pose: JointPose;
		let endTr: Vector3D, tr: Vector3D;
		let endQuat: Quaternion, q: Quaternion;
		let firstPose: Array<JointPose>;
		let i: number;
		let w0: number, x0: number, y0: number, z0: number;
		let w1: number, x1: number, y1: number, z1: number;
		const numJoints: number = skeleton.numJoints;

		// :s
		if (endPoses.length != numJoints)
			endPoses.length = numJoints;

		for (let j: number = 0; j < this._skeletonAnimationNode.numInputs; ++j) {
			weight = this._blendWeights[j];

			if (!weight)
				continue;

			poses = this._inputs[j].getSkeletonPose(skeleton).jointPoses;

			if (!firstPose) {
				firstPose = poses;
				for (i = 0; i < numJoints; ++i) {
					endPose = endPoses[i];

					if (endPose == null)
						endPose = endPoses[i] = new JointPose();

					pose = poses[i];
					q = pose.orientation;
					tr = pose.translation;

					endQuat = endPose.orientation;

					endQuat.x = weight * q.x;
					endQuat.y = weight * q.y;
					endQuat.z = weight * q.z;
					endQuat.w = weight * q.w;

					endTr = endPose.translation;
					endTr.x = weight * tr.x;
					endTr.y = weight * tr.y;
					endTr.z = weight * tr.z;
				}
			} else {
				for (i = 0; i < skeleton.numJoints; ++i) {
					endPose = endPoses[i];
					pose = poses[i];

					q = firstPose[i].orientation;
					x0 = q.x;
					y0 = q.y;
					z0 = q.z;
					w0 = q.w;

					q = pose.orientation;
					tr = pose.translation;

					x1 = q.x;
					y1 = q.y;
					z1 = q.z;
					w1 = q.w;
					// find shortest direction
					if (x0 * x1 + y0 * y1 + z0 * z1 + w0 * w1 < 0) {
						x1 = -x1;
						y1 = -y1;
						z1 = -z1;
						w1 = -w1;
					}
					endQuat = endPose.orientation;
					endQuat.x += weight * x1;
					endQuat.y += weight * y1;
					endQuat.z += weight * z1;
					endQuat.w += weight * w1;

					endTr = endPose.translation;
					endTr.x += weight * tr.x;
					endTr.y += weight * tr.y;
					endTr.z += weight * tr.z;
				}
			}
		}

		for (i = 0; i < skeleton.numJoints; ++i)
			endPoses[i].orientation.normalize();
	}
}