/**
 * Copies a geometry onto every point from the right input.
 *
 * @remarks
 * This is different than the instance SOP, as the operation here is more expensive, but allows for more flexibility.
 *
 *
 */

import {TypedSopNode} from './_Base';
import {CoreGroup, Object3DWithGeometry} from '../../../core/geometry/Group';
import {CoreObject} from '../../../core/geometry/Object';
import {CoreInstancer} from '../../../core/geometry/Instancer';
import {CoreString} from '../../../core/String';
import {CopyStamp} from './utils/CopyStamp';
import {Matrix4} from 'three/src/math/Matrix4';
import {CorePoint} from '../../../core/geometry/Point';
import {NodeParamsConfig, ParamConfig} from '../utils/params/ParamsConfig';
import {InputCloneMode} from '../../poly/InputCloneMode';
import {Object3D} from 'three/src/core/Object3D';
import {Vector3} from 'three/src/math/Vector3';
import {Quaternion} from 'three/src/math/Quaternion';
import {ArrayUtils} from '../../../core/ArrayUtils';
class CopySopParamsConfig extends NodeParamsConfig {
	/** @param copies count, used when the second input is not given */
	count = ParamConfig.INTEGER(1, {
		range: [1, 20],
		rangeLocked: [true, false],
	});
	/** @param transforms every input object each on a single input point */
	transformOnly = ParamConfig.BOOLEAN(0);
	/** @param toggles on to copy attributes from the input points to the created objects. Note that the vertex attributes from the points become object attributes */
	copyAttributes = ParamConfig.BOOLEAN(0);
	/** @param names of attributes to copy */
	attributesToCopy = ParamConfig.STRING('', {
		visibleIf: {copyAttributes: true},
	});
	/** @param toggle on to use the `copy` expression, which allows to change how the left input is evaluated for each point */
	useCopyExpr = ParamConfig.BOOLEAN(0);
}
const ParamsConfig = new CopySopParamsConfig();

export class CopySopNode extends TypedSopNode<CopySopParamsConfig> {
	params_config = ParamsConfig;
	static type() {
		return 'copy';
	}

	private _attribute_names_to_copy: string[] = [];
	private _objects: Object3D[] = [];
	private _stamp_node!: CopyStamp;

	static displayedInputNames(): string[] {
		return ['geometry to be copied', 'points to copy to'];
	}

	initializeNode() {
		this.io.inputs.setCount(1, 2);
		this.io.inputs.initInputsClonedState([InputCloneMode.ALWAYS, InputCloneMode.NEVER]);
	}

	async cook(input_contents: CoreGroup[]) {
		const core_group0 = input_contents[0];
		if (!this.io.inputs.has_input(1)) {
			await this.cook_without_template(core_group0);
			return;
		}

		const core_group1 = input_contents[1];
		if (!core_group1) {
			this.states.error.set('second input invalid');
			return;
		}
		await this.cook_with_template(core_group0, core_group1);
	}

	private async cook_with_template(instance_core_group: CoreGroup, template_core_group: CoreGroup) {
		this._objects = [];

		const template_points = template_core_group.points();

		const instancer = new CoreInstancer(template_core_group);
		let instance_matrices = instancer.matrices();
		const t = new Vector3();
		const q = new Quaternion();
		const s = new Vector3();
		instance_matrices[0].decompose(t, q, s);

		this._attribute_names_to_copy = CoreString.attribNames(this.pv.attributesToCopy).filter((attrib_name) =>
			template_core_group.hasAttrib(attrib_name)
		);
		await this._copy_moved_objects_on_template_points(instance_core_group, instance_matrices, template_points);
		this.setObjects(this._objects);
	}

	// https://stackoverflow.com/questions/24586110/resolve-promises-one-after-another-i-e-in-sequence
	private async _copy_moved_objects_on_template_points(
		instance_core_group: CoreGroup,
		instance_matrices: Matrix4[],
		template_points: CorePoint[]
	) {
		for (let point_index = 0; point_index < template_points.length; point_index++) {
			await this._copy_moved_object_on_template_point(
				instance_core_group,
				instance_matrices,
				template_points,
				point_index
			);
		}
	}

	private async _copy_moved_object_on_template_point(
		instance_core_group: CoreGroup,
		instance_matrices: Matrix4[],
		template_points: CorePoint[],
		point_index: number
	) {
		const matrix = instance_matrices[point_index];
		const template_point = template_points[point_index];
		this.stamp_node.set_point(template_point);

		const moved_objects = await this._get_moved_objects_for_template_point(instance_core_group, point_index);

		for (let moved_object of moved_objects) {
			if (this.pv.copyAttributes) {
				this._copyAttributes_from_template(moved_object, template_point);
			}

			// TODO: that node is getting inconsistent...
			// should I always only move the object?
			// and have a toggle to bake back to the geo?
			// or just enfore the use of a merge?
			if (this.pv.transformOnly) {
				moved_object.applyMatrix4(matrix);
			} else {
				const geometry = moved_object.geometry;
				if (geometry) {
					moved_object.geometry.applyMatrix4(matrix);
				} //else {
				//moved_object.applyMatrix4(matrix);
				//}
			}

			this._objects.push(moved_object);
		}
	}

	private async _get_moved_objects_for_template_point(
		instance_core_group: CoreGroup,
		point_index: number
	): Promise<Object3DWithGeometry[]> {
		const stamped_instance_core_group = await this._stamp_instance_group_if_required(instance_core_group);
		if (stamped_instance_core_group) {
			// duplicate or select from instance children
			const moved_objects = this.pv.transformOnly
				? // TODO: why is doing a transform slower than cloning the input??
				  ArrayUtils.compact([stamped_instance_core_group.objectsWithGeo()[point_index]])
				: stamped_instance_core_group.clone().objectsWithGeo();

			return moved_objects;
		} else {
			return [];
		}
	}

	private async _stamp_instance_group_if_required(instance_core_group: CoreGroup): Promise<CoreGroup | undefined> {
		if (this.pv.useCopyExpr) {
			const container0 = await this.container_controller.requestInputContainer(0);
			if (container0) {
				const core_group0 = container0.coreContent();
				if (core_group0) {
					return core_group0;
				} else {
					return;
				}
			} else {
				this.states.error.set(`input failed for index ${this.stamp_value()}`);
				return;
			}
		} else {
			return instance_core_group;
		}
	}

	private async _copy_moved_objects_for_each_instance(instance_core_group: CoreGroup) {
		for (let i = 0; i < this.pv.count; i++) {
			await this._copy_moved_objects_for_instance(instance_core_group, i);
		}
	}

	private async _copy_moved_objects_for_instance(instance_core_group: CoreGroup, i: number) {
		this.stamp_node.set_global_index(i);

		const stamped_instance_core_group = await this._stamp_instance_group_if_required(instance_core_group);
		if (stamped_instance_core_group) {
			stamped_instance_core_group.objects().forEach((object) => {
				// TODO: I should use the Core Group, to ensure that material.linewidth is properly cloned
				const new_object = CoreObject.clone(object);
				this._objects.push(new_object);
			});
		}
	}

	// TODO: what if I combine both param_count and stamping?!
	private async cook_without_template(instance_core_group: CoreGroup) {
		this._objects = [];
		await this._copy_moved_objects_for_each_instance(instance_core_group);

		this.setObjects(this._objects);
	}

	private _copyAttributes_from_template(object: Object3D, template_point: CorePoint) {
		this._attribute_names_to_copy.forEach((attrib_name, i) => {
			const attrib_value = template_point.attribValue(attrib_name);
			const object_wrapper = new CoreObject(object, i);
			object_wrapper.addAttribute(attrib_name, attrib_value);
		});
	}

	//
	//
	// STAMP
	//
	//
	stamp_value(attrib_name?: string) {
		return this.stamp_node.value(attrib_name);
	}
	get stamp_node() {
		return (this._stamp_node = this._stamp_node || this.create_stamp_node());
	}
	private create_stamp_node() {
		const stamp_node = new CopyStamp(this.scene());
		this.dirtyController.set_forbidden_trigger_nodes([stamp_node]);
		return stamp_node;
	}
	dispose() {
		super.dispose();
		if (this._stamp_node) {
			this._stamp_node.dispose();
		}
	}

	// private set_dirty_allowed(original_trigger_graph_node: CoreGraphNode): boolean {
	// 	return original_trigger_graph_node.graphNodeId() !== this.stamp_node.graphNodeId();
	// }
}
