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

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

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

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

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

/**
 *
 */
export class SkeletonClipState extends AnimationClipState implements ISkeletonAnimationState {
	private _rootPos: Vector3D = new Vector3D();
	private _frames: Array<SkeletonPose>;
	private _skeletonClipNode: SkeletonClipNode;
	private _skeletonPose: SkeletonPose = new SkeletonPose();
	private _skeletonPoseDirty: boolean = true;
	private _currentPose: SkeletonPose;
	private _nextPose: SkeletonPose;

	/**
	 * Returns the current skeleton pose frame of animation in the clip based on the internal playhead position.
	 */
	public get currentPose(): SkeletonPose {
		if (this._pFramesDirty)
			this._pUpdateFrames();

		return this._currentPose;
	}

	/**
	 * Returns the next skeleton pose frame of animation in the clip based on the internal playhead position.
	 */
	public get nextPose(): SkeletonPose {
		if (this._pFramesDirty)
			this._pUpdateFrames();

		return this._nextPose;
	}

	constructor(animator: AnimatorBase, skeletonClipNode: SkeletonClipNode) {
		super(animator, skeletonClipNode);

		this._skeletonClipNode = skeletonClipNode;
		this._frames = this._skeletonClipNode.frames;
	}

	/**
	 * 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;
	}

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

		super._pUpdateTime(time);
	}

	/**
	 * @inheritDoc
	 */
	public _pUpdateFrames(): void {
		super._pUpdateFrames();

		this._currentPose = this._frames[this._pCurrentFrame];

		if (this._skeletonClipNode.looping && this._pNextFrame >= this._skeletonClipNode.lastFrame) {
			this._nextPose = this._frames[0];
			(<SkeletonAnimator> this._pAnimator).dispatchCycleEvent();
		} else
			this._nextPose = this._frames[this._pNextFrame];
	}

	/**
	 * Updates the output skeleton pose of the node based on the internal playhead position.
	 *
	 * @param skeleton The skeleton used by the animator requesting the ouput pose.
	 */
	private updateSkeletonPose(skeleton: Skeleton): void {
		this._skeletonPoseDirty = false;

		if (!this._skeletonClipNode.totalDuration)
			return;

		if (this._pFramesDirty)
			this._pUpdateFrames();

		const currentPose: Array<JointPose> = this._currentPose.jointPoses;
		const nextPose: Array<JointPose> = this._nextPose.jointPoses;
		const numJoints: number = skeleton.numJoints;
		let p1: Vector3D, p2: Vector3D;
		let pose1: JointPose, pose2: JointPose;
		const endPoses: Array<JointPose> = this._skeletonPose.jointPoses;
		let endPose: JointPose;
		let tr: Vector3D;

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

		if ((numJoints != currentPose.length) || (numJoints != nextPose.length))
			throw new Error('joint counts don\'t match!');

		for (let i: number = 0; i < numJoints; ++i) {
			endPose = endPoses[i];

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

			pose1 = currentPose[i];
			pose2 = nextPose[i];
			p1 = pose1.translation;
			p2 = pose2.translation;

			if (this._skeletonClipNode.highQuality)
				endPose.orientation.slerp(pose1.orientation, pose2.orientation, this._pBlendWeight);
			else
				endPose.orientation.lerp(pose1.orientation, pose2.orientation, this._pBlendWeight);

			if (i > 0) {
				tr = endPose.translation;
				tr.x = p1.x + this._pBlendWeight * (p2.x - p1.x);
				tr.y = p1.y + this._pBlendWeight * (p2.y - p1.y);
				tr.z = p1.z + this._pBlendWeight * (p2.z - p1.z);
			}
		}
	}

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

		if (this._pFramesDirty)
			this._pUpdateFrames();

		let p1: Vector3D, p2: Vector3D, p3: Vector3D;
		const totalDelta: Vector3D = this._skeletonClipNode.totalDelta;

		// jumping back, need to reset position
		if ((this._pTimeDir > 0 && this._pNextFrame < this._pOldFrame) || (this._pTimeDir < 0 && this._pNextFrame > this._pOldFrame)) {
			this._rootPos.x -= totalDelta.x * this._pTimeDir;
			this._rootPos.y -= totalDelta.y * this._pTimeDir;
			this._rootPos.z -= totalDelta.z * this._pTimeDir;
		}

		const dx: number = this._rootPos.x;
		const dy: number = this._rootPos.y;
		const dz: number = this._rootPos.z;

		if (this._skeletonClipNode.stitchFinalFrame && this._pNextFrame == this._skeletonClipNode.lastFrame) {
			p1 = this._frames[0].jointPoses[0].translation;
			p2 = this._frames[1].jointPoses[0].translation;
			p3 = this._currentPose.jointPoses[0].translation;

			this._rootPos.x = p3.x + p1.x + this._pBlendWeight * (p2.x - p1.x);
			this._rootPos.y = p3.y + p1.y + this._pBlendWeight * (p2.y - p1.y);
			this._rootPos.z = p3.z + p1.z + this._pBlendWeight * (p2.z - p1.z);
		} else {
			p1 = this._currentPose.jointPoses[0].translation;
			p2 = this._frames[this._pNextFrame].jointPoses[0].translation; //cover the instances where we wrap the pose but still want the final frame translation values
			this._rootPos.x = p1.x + this._pBlendWeight * (p2.x - p1.x);
			this._rootPos.y = p1.y + this._pBlendWeight * (p2.y - p1.y);
			this._rootPos.z = p1.z + this._pBlendWeight * (p2.z - p1.z);
		}

		this._pRootDelta.x = this._rootPos.x - dx;
		this._pRootDelta.y = this._rootPos.y - dy;
		this._pRootDelta.z = this._rootPos.z - dz;

		this._pOldFrame = this._pNextFrame;
	}
}