/**
 * Snaps points onto one another.
 *
 * @remarks
 * Based on a distance threshold.
 *
 */
import {TypedSopNode} from './_Base';
import {CoreGroup} from '../../../core/geometry/Group';
import {BufferAttribute, BufferGeometry, Object3D, Vector2, Vector3, Vector4, Mesh} from 'three';
import type {Points, LineSegments} from 'three';
import {NodeParamsConfig, ParamConfig} from '../utils/params/ParamsConfig';
import {InputCloneMode} from '../../poly/InputCloneMode';
import {pushOnArrayAtEntry} from '../../../core/MapUtils';
import {ObjectType, objectTypeFromObject} from '../../../core/geometry/Constant';
import {arrayUniq} from '../../../core/ArrayUtils';
import {mergeFaces, mergeFacesWithUnsharedEdges} from '../../../core/geometry/operation/Fuse';
import {CoreMask} from '../../../core/geometry/Mask';
import {SopType} from '../../poly/registers/nodes/types/Sop';
import {Attribute} from '../../../core/geometry/Attribute';
const roundedPosition = new Vector3();
const vector2 = new Vector2();
const vector3 = new Vector3();
const vector4 = new Vector4();

function clearAttributes(geometry: BufferGeometry) {
	const attributeNames = Object.keys(geometry.attributes);
	for (const attributeName of attributeNames) {
		const attribute = geometry.getAttribute(attributeName);
		if (attribute instanceof BufferAttribute) {
			const newAttribValues: number[] = [];
			geometry.setAttribute(
				attributeName,
				new BufferAttribute(new Float32Array(newAttribValues), attribute.itemSize)
			);
		}
	}
}

class FuseSopParamsConfig extends NodeParamsConfig {
	/** @param group to assign the material to */
	group = ParamConfig.STRING('', {
		objectMask: true,
	});
	/** @param distance threshold */
	dist = ParamConfig.FLOAT(0.001, {
		range: [0, 1],
		rangeLocked: [true, false],
		step: 0.001,
	});
	/** @param fuse only open edges */
	onlyOpenEdges = ParamConfig.BOOLEAN(false);
	/** @param recompute normals */
	computeNormals = ParamConfig.BOOLEAN(1);
}
const ParamsConfig = new FuseSopParamsConfig();

export class FuseSopNode extends TypedSopNode<FuseSopParamsConfig> {
	override paramsConfig = ParamsConfig;
	static override type() {
		return SopType.FUSE;
	}

	override initializeNode() {
		this.io.inputs.setCount(1);
		this.io.inputs.initInputsClonedState(InputCloneMode.FROM_NODE);
	}

	override cook(inputCoreGroups: CoreGroup[]) {
		const inputCoreGroup = inputCoreGroups[0];

		const selectedObjects = CoreMask.filterThreejsObjects(inputCoreGroup, this.pv);

		for (const object of selectedObjects) {
			this._filterObject(object);
		}
		this.setCoreGroup(inputCoreGroup);
	}

	private _filterObject(object: Object3D) {
		const objectType = objectTypeFromObject(object);
		switch (objectType) {
			case ObjectType.MESH: {
				return this._filterMesh(object as Mesh);
			}
			case ObjectType.LINE_SEGMENTS: {
				this._fuseGeometry((object as LineSegments).geometry);
				return this._filterLineSegments(object as LineSegments);
			}
			case ObjectType.POINTS: {
				this._fuseGeometry((object as Points).geometry);
				return this._filterPoints(object as Points);
			}
		}
	}

	private _filterMesh(object: Mesh) {
		const geometry = object.geometry;
		const index = geometry.getIndex();
		if (!index) {
			return;
		}
		if (this.pv.onlyOpenEdges == true) {
			mergeFacesWithUnsharedEdges(geometry, this.pv.dist);
		} else {
			mergeFaces(geometry, this.pv.dist);
		}
	}
	private _filterLineSegments(object: LineSegments) {
		const geometry = object.geometry;
		const index = geometry.getIndex();
		if (!index) {
			return;
		}
		const newIndices: number[] = [];
		const indexArray = index.array;
		const segmentsCount = indexArray.length / 2;
		for (let i = 0; i < segmentsCount; i++) {
			vector2.fromArray(indexArray, i * 2);
			const a = vector2.x;
			const b = vector2.y;
			const segmentSnapped = a == b;
			if (!segmentSnapped) {
				vector2.toArray(newIndices, newIndices.length);
			}
		}
		geometry.setIndex(newIndices);
		if (newIndices.length == 0) {
			clearAttributes(geometry);
		}
	}

	private _filterPoints(object: Points) {
		const geometry = object.geometry;
		const index = geometry.getIndex();
		if (!index) {
			return;
		}
		const indexArray = [...index.array];
		const newIndices: number[] = [];
		arrayUniq(indexArray, newIndices);
		newIndices.sort((a, b) => a - b);
		geometry.setIndex(newIndices);
		if (newIndices.length == 0) {
			clearAttributes(geometry);
		}
	}

	private _fuseGeometry(geometry: BufferGeometry) {
		const index = geometry.getIndex();
		if (!index) {
			return;
		}
		const indexArray = index.array;
		const precision = this.pv.dist;
		const position = geometry.getAttribute(Attribute.POSITION) as BufferAttribute;
		const srcPointsCount = position.array.length / 3;

		function roundedPos(index: number, target: Vector3) {
			target.fromBufferAttribute(position, index);

			if (precision > 0) {
				target.x = Math.round(target.x / precision) * precision;
				target.y = Math.round(target.y / precision) * precision;
				target.z = Math.round(target.z / precision) * precision;
			}
		}

		const indicesByPosKey: Map<string, Array<number>> = new Map();
		const posKeyByIndex: Map<number, string> = new Map();
		for (let index = 0; index < srcPointsCount; index++) {
			roundedPos(index, roundedPosition);
			const posKey = `${roundedPosition.x},${roundedPosition.y},${roundedPosition.z}`;
			pushOnArrayAtEntry(indicesByPosKey, posKey, index);
			posKeyByIndex.set(index, posKey);
		}

		indicesByPosKey.forEach((indices, posKey) => {
			indices.sort((a, b) => a - b);
		});

		const newIndicesAfterGapsCreated: Map<number, number> = new Map();
		let nextAvailableIndex = 0;
		for (let index = 0; index < srcPointsCount; index++) {
			const posKey = posKeyByIndex.get(index)!;
			const indices = indicesByPosKey.get(posKey)!;
			if (indices.length <= 1 || indices[0] == index) {
				newIndicesAfterGapsCreated.set(index, nextAvailableIndex);
				nextAvailableIndex++;
			}
		}

		const newIndexByOldIndex: Map<number, number> = new Map();
		indicesByPosKey.forEach((indices, posKey) => {
			const firstIndex = indices[0];
			for (let i = 1; i < indices.length; i++) {
				const index = indices[i];
				newIndexByOldIndex.set(index, firstIndex);
			}
		});

		const newIndices: number[] = [];
		const newIndexByOldIndexAfterAssignment: Map<number, number> = new Map();
		for (let i = 0; i < indexArray.length; i++) {
			const index = indexArray[i];
			const targetIndex = newIndexByOldIndex.get(index);
			// const offsetIndex = newIndicesAfterGapsCreated.get(index);
			const targetOffset =
				targetIndex != null
					? newIndicesAfterGapsCreated.get(targetIndex)
					: newIndicesAfterGapsCreated.get(index);
			// const offset2 = offsetIndex != null ? newIndexByOldIndex.get(offsetIndex) : undefined;
			let newIndex = index;
			if (targetOffset != null) {
				newIndex = targetOffset;
			} else {
				if (targetIndex != null) {
					newIndex = targetIndex;
				}
			}
			newIndices.push(newIndex);
			newIndexByOldIndexAfterAssignment.set(index, newIndex);
		}

		const attributeNames = Object.keys(geometry.attributes);
		for (const attributeName of attributeNames) {
			const attribute = geometry.getAttribute(attributeName);
			if (attribute instanceof BufferAttribute) {
				const itemSize = attribute.itemSize;
				const newAttribValues: number[] = [];

				function getVector() {
					if (itemSize == 2) {
						return vector2;
					}
					if (itemSize == 3) {
						return vector3;
					}
					if (itemSize == 4) {
						return vector4;
					}
				}
				const vector = getVector();

				const visitedIndex: Set<number> = new Set();
				for (let i = 0; i < srcPointsCount; i++) {
					const index = newIndexByOldIndexAfterAssignment.get(i);

					if (index != null) {
						if (!visitedIndex.has(index)) {
							visitedIndex.add(index);
							if (vector) {
								vector.fromBufferAttribute(attribute, i);
								vector.toArray(newAttribValues, index * itemSize);
							} else {
								const currentVal = attribute.array[i];
								newAttribValues[index] = currentVal;
							}
						}
					}
				}
				geometry.setAttribute(attributeName, new BufferAttribute(new Float32Array(newAttribValues), itemSize));
			}
		}
		geometry.setIndex(newIndices);
	}
}
