import {TypeAssert} from './../../poly/Assert';
import {BaseSopOperation} from './_Base';
import {CoreGroup} from '../../../core/geometry/Group';
import {InputCloneMode} from '../../../engine/poly/InputCloneMode';
import {AttribClass, ATTRIBUTE_CLASSES} from '../../../core/geometry/Constant';
import {DefaultOperationParams} from '../../../core/operations/_Base';
import {coreObjectClassFactory, coreObjectInstanceFactory} from '../../../core/geometry/CoreObjectFactory';
import {filterObjectsFromCoreGroup} from '../../../core/geometry/Mask';
import {CoreEntity} from '../../../core/geometry/CoreEntity';
import {ENTITY_CLASS_FACTORY} from '../../../core/geometry/CoreObjectFactory';
import {arrayMin, arrayMax, ArrayToItemFunction} from '../../../core/ArrayUtils';
import {AttribValue, Constructor} from '../../../types/GlobalTypes';
import {isNumber, isString, isBoolean} from '../../../core/Type';
import {Vector2, Vector3, Vector4, Color} from 'three';
import {TraversedRelatedEntityData} from '../../../core/geometry/entities/utils/TraversedRelatedEntities';
import {pushOnArrayAtEntry} from '../../../core/MapUtils';
import {CorePoint} from '../../../core/geometry/entities/point/CorePoint';
import {CoreObjectType} from '../../../core/geometry/ObjectContent';
const _v2 = new Vector2();
const _v3 = new Vector3();
const _v4 = new Vector4();
const _c = new Color();

interface AttribPromoteSopParams extends DefaultOperationParams {
	group: string;
	classFrom: number;
	classTo: number;
	mode: number;
	name: string;
}

export enum AttribPromoteMode {
	MIN = 'min',
	MAX = 'max',
	FIRST_FOUND = 'first found',
}
export const ATTRIB_PROMOTE_MODES: AttribPromoteMode[] = [
	AttribPromoteMode.MIN,
	AttribPromoteMode.MAX,
	AttribPromoteMode.FIRST_FOUND,
];

export class AttribPromoteSopOperation extends BaseSopOperation {
	static override readonly DEFAULT_PARAMS: AttribPromoteSopParams = {
		group: '',
		classFrom: ATTRIBUTE_CLASSES.indexOf(AttribClass.POINT),
		classTo: ATTRIBUTE_CLASSES.indexOf(AttribClass.OBJECT),
		mode: ATTRIB_PROMOTE_MODES.indexOf(AttribPromoteMode.FIRST_FOUND),
		name: '',
	};
	static override readonly INPUT_CLONED_STATE = InputCloneMode.FROM_NODE;
	static override type(): Readonly<'attribPromote'> {
		return 'attribPromote';
	}

	override cook(inputCoreGroups: CoreGroup[], params: AttribPromoteSopParams) {
		const coreGroup = inputCoreGroups[0];

		const classFrom = ATTRIBUTE_CLASSES[params.classFrom];
		const classTo = ATTRIBUTE_CLASSES[params.classTo];
		const mode = ATTRIB_PROMOTE_MODES[params.mode];

		const objects = filterObjectsFromCoreGroup(coreGroup, params);
		for (const object of objects) {
			const factoryFrom = ENTITY_CLASS_FACTORY[classFrom];
			const factoryTo = ENTITY_CLASS_FACTORY[classTo];
			const attribNames = factoryFrom
				? factoryFrom(object).attributeNamesMatchingMask(object, params.name)
				: coreGroup.attributeNamesMatchingMask(params.name);

			// add attribute if it does not exist on the dest entities
			for (const attribName of attribNames) {
				const hasAttribute = factoryTo
					? factoryTo(object).hasAttribute(object, attribName)
					: coreGroup.hasAttribute(attribName);
				if (!hasAttribute) {
					const srcAttribSize: number | null = factoryFrom
						? factoryFrom(object).attribSize(object, attribName)
						: coreGroup.attribSize(attribName);
					if (srcAttribSize != null) {
						if (factoryTo) {
							factoryTo(object).addNumericAttribute(object, attribName, srcAttribSize);
						} else {
							coreGroup.addNumericAttribute(attribName, srcAttribSize);
						}
					}
				}
			}

			// promote attribute
			const destEntities: CoreEntity[] = [];
			const srcEntities: CoreEntity[] = [];
			const traversedRelatedEntityData: TraversedRelatedEntityData = {
				[AttribClass.CORE_GROUP]: {ids: []},
				[AttribClass.OBJECT]: {ids: []},
				[AttribClass.POINT]: {ids: []},
				[AttribClass.PRIMITIVE]: {ids: []},
				[AttribClass.VERTEX]: {ids: []},
			};
			const coreObjectClass = coreObjectClassFactory(object);
			coreObjectInstanceFactory(object).relatedEntities(
				classTo,
				coreGroup,
				destEntities,
				traversedRelatedEntityData
			);
			const traversedClassFromEntityIds = traversedRelatedEntityData[classFrom].ids;
			const classFromEntitiesByClassToEntitytIndex: Map<number, CoreEntity[]> = new Map();
			if (traversedClassFromEntityIds.length > 0 && classFrom != AttribClass.CORE_GROUP) {
				const entityClassFrom = coreObjectClass.relatedEntityClass(object, classFrom) as any as Constructor<
					CorePoint<CoreObjectType>
				>;
				for (const classFromEntityId of traversedClassFromEntityIds) {
					const classToEntities: CoreEntity[] = [];
					const classFromEntity = new entityClassFrom(object, classFromEntityId);
					classFromEntity.relatedEntities(classTo, coreGroup, classToEntities);
					for (const classToEntity of classToEntities) {
						const index = classToEntity.index();
						pushOnArrayAtEntry(classFromEntitiesByClassToEntitytIndex, index, classFromEntity);
					}
				}
			}

			for (const destEntity of destEntities) {
				const cachedEntities = classFromEntitiesByClassToEntitytIndex.get(destEntity.index());
				if (cachedEntities) {
					for (const attribName of attribNames) {
						this._promoteAttribute(attribName, cachedEntities, destEntity, mode);
					}
				} else {
					destEntity.relatedEntities(classFrom, coreGroup, srcEntities);
					for (const attribName of attribNames) {
						this._promoteAttribute(attribName, srcEntities, destEntity, mode);
					}
				}
			}
		}

		return coreGroup;
	}

	private _promoteAttribute(
		attribName: string,
		srcEntities: CoreEntity[],
		destEntity: CoreEntity,
		mode: AttribPromoteMode
	) {
		const srcValues = srcEntities.map((entity) => entity.attribValue(attribName)!);
		const destValue = this._convertSrcValues(srcValues, mode);
		destEntity.setAttribValue(attribName, destValue as any);
	}
	private _convertSrcValues(srcValues: AttribValue[], mode: AttribPromoteMode): AttribValue {
		switch (mode) {
			case AttribPromoteMode.MIN: {
				return this._convertValuesMin(srcValues);
			}
			case AttribPromoteMode.MAX: {
				return this._convertValuesMax(srcValues);
			}
			case AttribPromoteMode.FIRST_FOUND: {
				return this._convertValuesFirstFound(srcValues);
			}
		}
		TypeAssert.unreachable(mode);
	}
	private _convertValuesMin(srcValues: AttribValue[]): AttribValue {
		return this._convertValuesMinMax(srcValues, arrayMin);
	}
	private _convertValuesMax(srcValues: AttribValue[]): AttribValue {
		return this._convertValuesMinMax(srcValues, arrayMax);
	}
	private _convertValuesMinMax(srcValues: AttribValue[], arrayFunc: ArrayToItemFunction<number>): AttribValue {
		const firstValue = srcValues[0];
		if (isNumber(firstValue) || isString(firstValue) || isBoolean(firstValue)) {
			return arrayFunc(srcValues as number[]);
		}

		if (firstValue instanceof Vector2) {
			_v2.x = arrayFunc((srcValues as Vector2[]).map((v) => v.x));
			_v2.y = arrayFunc((srcValues as Vector2[]).map((v) => v.y));
			return _v2;
		}
		if (firstValue instanceof Vector3) {
			_v3.x = arrayFunc((srcValues as Vector3[]).map((v) => v.x));
			_v3.y = arrayFunc((srcValues as Vector3[]).map((v) => v.y));
			_v3.z = arrayFunc((srcValues as Vector3[]).map((v) => v.z));
			return _v3;
		}
		if (firstValue instanceof Color) {
			_c.r = arrayFunc((srcValues as Color[]).map((c) => c.r));
			_c.g = arrayFunc((srcValues as Color[]).map((c) => c.g));
			_c.b = arrayFunc((srcValues as Color[]).map((c) => c.b));
			return _c;
		}
		if (firstValue instanceof Vector4) {
			_v4.x = arrayFunc((srcValues as Vector4[]).map((v) => v.x));
			_v4.y = arrayFunc((srcValues as Vector4[]).map((v) => v.y));
			_v4.z = arrayFunc((srcValues as Vector4[]).map((v) => v.z));
			_v4.w = arrayFunc((srcValues as Vector4[]).map((v) => v.w));
			return _v4;
		}
		return 0;
	}
	private _convertValuesFirstFound(srcValues: AttribValue[]): AttribValue {
		return srcValues[0];
	}
}
