import {Object3D,Quaternion,Color,Vector2,Vector3,Vector4} from 'three';
import {} from 'three';
import {BaseNodeType} from '../../engine/nodes/_Base';
import {BaseParamType} from '../../engine/params/_Base';
import {ParamType} from '../../engine/poly/ParamType';
import {Vector2Param} from '../../engine/params/Vector2';
import {Vector3Param} from '../../engine/params/Vector3';
import {Vector4Param} from '../../engine/params/Vector4';
import {Poly} from '../../engine/Poly';
import {isNumber,isEuler,isVector,isColor,isQuaternion} from '../Type';
import {ColorParam} from '../../engine/params/Color';
import {IntegerParam} from '../../engine/params/Integer';
import {RegisterableProperty} from './AnimatedPropertiesRegister';
import {animBuilderCommonVars} from './vars/Common';
import {AddToTimelineOptions, Operation} from './vars/AnimBuilderTypes';
import {animBuilderStartTimeline} from './vars/StartTimeline';
import {populateVarsForParamVector4} from './vars/type/Vector4';
import {populateVarsForColor, populateVarsForParamColor} from './vars/type/Color';
import {populateVarsForParamVector3} from './vars/type/Vector3';
import {populateVarsForParamVector2} from './vars/type/Vector2';
import {populateVarsForNumber, populateVarsForSingleNumber} from './vars/type/Number';
import {populateVarsForVector} from './vars/type/Vector';
import {populateVarsAndCreateProxyForQuaternion} from './vars/type/Quaternion';
import {populateVarsForEuler} from './vars/type/Euler';
import type {GsapTweenVars} from '../thirdParty/gsap/gsapFactory';

export type AnimPropertyTargetValue = number | Vector2 | Vector3 | Color | Vector4 | Quaternion;

interface Object3DProps {
	targetProperty: AnimPropertyTargetValue;
	toTarget: object;
	propertyNames: string[];
}
interface PopulateVarsOptions {
	vars: GsapTweenVars;
	targetValue: AnimPropertyTargetValue;
	targetProperty: AnimPropertyTargetValue;
	propertyNames: string[];
	operation: Operation;
}

const PROPERTY_SEPARATOR = '.';

export class TimelineBuilderProperty {
	private _propertyName: string | undefined;
	private _targetValue: AnimPropertyTargetValue | undefined;
	constructor() {}
	setName(name: string) {
		this._propertyName = name;
	}
	setTargetValue(value: AnimPropertyTargetValue) {
		this._targetValue = value;
	}
	name() {
		return this._propertyName;
	}
	targetValue() {
		return this._targetValue;
	}

	private _debug = false;
	setDebug(debug: boolean) {
		this._debug = debug;
	}
	private _printDebug(message: any) {
		if (!this._debug) {
			return;
		}
		console.log(message);
	}

	clone() {
		const cloned = new TimelineBuilderProperty();
		if (this._propertyName) {
			cloned.setName(this._propertyName);
		}
		if (this._targetValue != null) {
			const newTargetValue = isNumber(this._targetValue) ? this._targetValue : this._targetValue.clone();
			cloned.setTargetValue(newTargetValue);
		}

		return cloned;
	}

	addToTimeline(options: AddToTimelineOptions) {
		const target = options.propertyTarget || options.target;
		const objects = target.objects() as Object3D[];
		const node = target.node();
		this._printDebug(['addToTimeline', target, objects, node]);
		if (objects) {
			this._populateWithObjects(objects, options);
		}
		if (node) {
			this._populateWithNode(node, options);
		}
	}
	private _populateWithObjects(objects: Object3D[], options: AddToTimelineOptions) {
		const {timelineBuilder} = options;
		this._printDebug(['_populateWithObjects', objects]);
		if (!this._propertyName) {
			Poly.warn('no property name given');
			return;
		}
		if (this._targetValue == null) {
			Poly.warn('no target value given');
			return;
		}
		const operation = timelineBuilder.operation();
		const updateCallback = timelineBuilder.updateCallback();

		for (const object3d of objects) {
			// const target_property = (object3d as any)[this._property_name as any] as TargetValue;
			// let to_target: object | null = null;
			const props = this._sceneGraphProps(object3d, this._propertyName);
			if (props) {
				const registerableProp: RegisterableProperty = {
					object: object3d,
					propertyName: this._propertyName,
				};
				let {targetProperty, toTarget, propertyNames} = props;
				const vars = animBuilderCommonVars(timelineBuilder);

				// add update_matrix
				if (updateCallback && updateCallback.updateMatrix()) {
					const oldMatrixAutoUpdate = object3d.matrixAutoUpdate;
					// matrixAutoUpdate should be set to true during onUpdate and not onStart
					// in case another timeline completes and sets it to false
					vars.onUpdate = () => {
						object3d.matrixAutoUpdate = true;
					};
					vars.onComplete = () => {
						object3d.matrixAutoUpdate = oldMatrixAutoUpdate;
						// we still need to update the matrix manually here,
						// as (it is specially noticeable on short animations)
						// the last step of the timeline will not have the matrix updated
						// and the object will therefore look like it has not fully completed
						// its animation.
						if (!object3d.matrixAutoUpdate) {
							object3d.updateMatrix();
						}
					};
				}
				// handle quaternions as a special case
				if (targetProperty instanceof Quaternion && this._targetValue instanceof Quaternion) {
					toTarget = populateVarsAndCreateProxyForQuaternion({
						targetValue: this._targetValue,
						vars,
						targetProperty,
					});
				}

				this._populateVarsForObjectProperty({
					targetValue: this._targetValue,
					vars,
					targetProperty,
					propertyNames,
					operation,
				});

				if (toTarget) {
					animBuilderStartTimeline({...options, vars, target: toTarget, registerableProp});
				}
			}
		}
	}
	private _populateVarsForObjectProperty(options: PopulateVarsOptions) {
		const {vars, targetValue, targetProperty, propertyNames, operation} = options;
		function warnMismatch(expectedType: string) {
			Poly.warn(
				`mismatch between targetValue and targetProperty (expected ${expectedType})`,
				targetValue,
				targetProperty
			);
		}
		// number
		if (isNumber(targetProperty)) {
			if (isNumber(targetValue)) {
				return populateVarsForNumber({targetValue, vars, targetProperty, propertyNames, operation});
			}
			return warnMismatch('number');
		}
		// euler (needs to be positioned before the isVector, as it would other be caught in the isVector )
		// note that for euler, we first test targetProperty, as otherwise a 'position' property would test true here
		if (isEuler(targetProperty)) {
			if (targetValue instanceof Vector3) {
				return populateVarsForEuler({targetValue, vars, targetProperty, propertyNames, operation});
			}
			return warnMismatch('euler');
		}
		// vector
		if (isVector(targetProperty)) {
			if (isVector(targetValue)) {
				return populateVarsForVector({targetValue, vars, targetProperty, propertyNames, operation});
			}
			return warnMismatch('vector');
		}
		// color
		if (isColor(targetProperty)) {
			if (isColor(targetValue)) {
				return populateVarsForColor({targetValue, vars, targetProperty, propertyNames, operation});
			}
			return warnMismatch('color');
		}

		if (isQuaternion(targetProperty)) {
			//
		}
		Poly.warn(`targetValue and targetProp are not recognized types`, targetValue, targetProperty);
	}

	private _sceneGraphProps(object: object, propertyName: string): Object3DProps | undefined {
		const elements = propertyName.split(PROPERTY_SEPARATOR);
		if (elements.length > 1) {
			const firstElement = elements.shift() as string;
			const subObject = (object as any)[firstElement as any] as object;
			if (subObject) {
				const subPropertyName = elements.join(PROPERTY_SEPARATOR);
				return this._sceneGraphProps(subObject, subPropertyName);
			} else {
				Poly.warn(`property ${firstElement} not found on object`, object);
			}
		} else {
			const targetProperty = (object as any)[propertyName as any] as AnimPropertyTargetValue;
			let toTarget: object | null = null;
			const propertyNames: string[] = [];
			if (isNumber(targetProperty)) {
				toTarget = object;
				propertyNames.push(propertyName);
			} else {
				toTarget = targetProperty;
				if (this._targetValue instanceof Vector2) {
					propertyNames.push('x', 'y');
				}
				if (this._targetValue instanceof Vector3) {
					propertyNames.push('x', 'y', 'z');
				}
				if (this._targetValue instanceof Vector4) {
					propertyNames.push('x', 'y', 'z', 'w');
				}
				if (this._targetValue instanceof Color) {
					propertyNames.push('r', 'g', 'b');
				}
				if (this._targetValue instanceof Quaternion) {
					// is_quaternion = true;
				}
			}
			return {
				targetProperty: targetProperty,
				toTarget: toTarget,
				propertyNames: propertyNames,
			};
		}
	}

	private _populateWithNode(node: BaseNodeType, options: AddToTimelineOptions) {
		this._printDebug(['_populateWithNode', node]);
		const targetParam = node.p[this._propertyName as any] as BaseParamType;
		this._printDebug(['targetParam', targetParam]);
		if (!targetParam) {
			Poly.warn(`${this._propertyName} not found on node ${node.path()}`);
			return;
		}

		if (targetParam) {
			this._populateVarsForParam(targetParam, options);
		}
	}

	private _populateVarsForParam(param: BaseParamType, options: AddToTimelineOptions) {
		this._printDebug(['_populateVarsForParam', param]);
		if (this._targetValue == null) {
			return;
		}
		switch (param.type()) {
			case ParamType.INTEGER:
			case ParamType.FLOAT: {
				return populateVarsForSingleNumber(param as IntegerParam, this._targetValue, options);
			}
			case ParamType.VECTOR2: {
				return populateVarsForParamVector2(param as Vector2Param, this._targetValue, options);
			}
			case ParamType.VECTOR3: {
				return populateVarsForParamVector3(param as Vector3Param, this._targetValue, options);
			}
			case ParamType.COLOR: {
				return populateVarsForParamColor(param as ColorParam, this._targetValue, options);
			}
			case ParamType.VECTOR4: {
				return populateVarsForParamVector4(param as Vector4Param, this._targetValue, options);
			}
		}
		Poly.warn(`param type cannot be animated (yet): '${param.type()}' '${param.path()}'`);
	}
}
