/**
 * Sets an attribute value for a specific point or object
 *
 * @remarks
 * While the same operation is possible using the sop/attribCreate
 * and its group parameter, this node is more performant.
 * Also, unlike the sop/attribCreate, it does not accept point specific variables such as @ptnum
 * in expressions.
 *
 */
import {TypedSopNode} from './_Base';
import {
	AttribClassMenuEntries,
	AttribTypeMenuEntries,
	AttribClass,
	AttribType,
	ATTRIBUTE_CLASSES,
	ATTRIBUTE_TYPES,
} from '../../../core/geometry/Constant';
import {CoreAttribute} from '../../../core/geometry/Attribute';
import {BaseCoreObject} from '../../../core/geometry/entities/object/BaseCoreObject';
import {CoreGroup} from '../../../core/geometry/Group';
import {TypeAssert} from '../../poly/Assert';
import {AttribSetAtIndexSopOperation} from '../../operations/sop/AttribSetAtIndex';
import {NodeParamsConfig, ParamConfig} from '../utils/params/ParamsConfig';
import {BufferAttribute} from 'three';
import {CoreObjectType, ObjectContent} from '../../../core/geometry/ObjectContent';
import {corePointClassFactory} from '../../../core/geometry/CoreObjectFactory';
import {pointsFromObject} from '../../../core/geometry/entities/point/CorePointUtils';
import {CorePoint} from '../../../core/geometry/entities/point/CorePoint';
const DEFAULT = AttribSetAtIndexSopOperation.DEFAULT_PARAMS;

const _allPoints: CorePoint<CoreObjectType>[] = [];
class AttribSetAtIndexSopParamsConfig extends NodeParamsConfig {
	/** @param the point or object index this applies to */
	index = ParamConfig.INTEGER(DEFAULT.index, {
		range: [0, 100],
		rangeLocked: [true, false],
	});
	/** @param the attribute class (geometry or object) */
	class = ParamConfig.INTEGER(DEFAULT.class, {
		menu: {
			entries: AttribClassMenuEntries,
		},
	});
	/** @param the attribute type (numeric or string) */
	type = ParamConfig.INTEGER(DEFAULT.type, {
		menu: {
			entries: AttribTypeMenuEntries,
		},
	});
	/** @param the attribute name */
	name = ParamConfig.STRING(DEFAULT.name);
	/** @param the attribute size (1 for float, 2 for vector2, 3 for vector3, 4 for vector4) */
	size = ParamConfig.INTEGER(DEFAULT.size, {
		range: [1, 4],
		rangeLocked: [true, true],
		visibleIf: {type: ATTRIBUTE_TYPES.indexOf(AttribType.NUMERIC)},
	});
	/** @param the value for a float attribute */
	value1 = ParamConfig.FLOAT(DEFAULT.value1, {
		visibleIf: {type: ATTRIBUTE_TYPES.indexOf(AttribType.NUMERIC), size: 1},
	});
	/** @param the value for a vector2 */
	value2 = ParamConfig.VECTOR2(DEFAULT.value2, {
		visibleIf: {type: ATTRIBUTE_TYPES.indexOf(AttribType.NUMERIC), size: 2},
	});
	/** @param the value for a vector3 */
	value3 = ParamConfig.VECTOR3(DEFAULT.value3, {
		visibleIf: {type: ATTRIBUTE_TYPES.indexOf(AttribType.NUMERIC), size: 3},
	});
	/** @param the value for a vector4 */
	value4 = ParamConfig.VECTOR4(DEFAULT.value4, {
		visibleIf: {type: ATTRIBUTE_TYPES.indexOf(AttribType.NUMERIC), size: 4},
	});
	/** @param the value for a string attribute */
	string = ParamConfig.STRING(DEFAULT.string, {
		visibleIf: {type: ATTRIBUTE_TYPES.indexOf(AttribType.STRING)},
	});
}
const ParamsConfig = new AttribSetAtIndexSopParamsConfig();
export class AttribSetAtIndexSopNode extends TypedSopNode<AttribSetAtIndexSopParamsConfig> {
	override paramsConfig = ParamsConfig;
	static override type() {
		return 'attribSetAtIndex';
	}

	override initializeNode() {
		this.io.inputs.setCount(1);
		this.io.inputs.initInputsClonedState(AttribSetAtIndexSopOperation.INPUT_CLONED_STATE);
	}

	private _operation: AttribSetAtIndexSopOperation | undefined;
	override cook(inputCoreGroups: CoreGroup[]) {
		// cannot yet convert to an operation, as expressions may be used in this node
		// but we can still use one when no expression is required
		const attribName = this.pv.name;

		if (this._isUsingExpression()) {
			if (attribName && attribName.trim() != '') {
				this._addAttribute(ATTRIBUTE_CLASSES[this.pv.class], inputCoreGroups[0]);
			} else {
				this.states.error.set('attribute name is not valid');
			}
		} else {
			this._operation = this._operation || new AttribSetAtIndexSopOperation(this.scene(), this.states, this);
			const coreGroup = this._operation.cook(inputCoreGroups, this.pv);
			this.setCoreGroup(coreGroup);
		}
	}
	private _addAttribute(attribClass: AttribClass, coreGroup: CoreGroup) {
		const attribType = ATTRIBUTE_TYPES[this.pv.type];
		switch (attribClass) {
			case AttribClass.POINT:
				this._addPointAttribute(attribType, coreGroup);
				return this.setCoreGroup(coreGroup);
			case AttribClass.VERTEX:
				this.states.error.set('vertex attributes are not supported');
				return this.setCoreGroup(coreGroup);
			case AttribClass.PRIMITIVE:
				this.states.error.set('primitive attributes are not supported');
				return this.setCoreGroup(coreGroup);
			case AttribClass.OBJECT:
				this._addObjectAttribute(attribType, coreGroup);
				return this.setCoreGroup(coreGroup);
			case AttribClass.CORE_GROUP:
				this._addCoreGroupAttribute(attribType, coreGroup);
				return this.setCoreGroup(coreGroup);
		}
		TypeAssert.unreachable(attribClass);
	}

	private _addPointAttribute(attribType: AttribType, coreGroup: CoreGroup) {
		const objects = coreGroup.allObjects();
		switch (attribType) {
			case AttribType.NUMERIC: {
				for (const object of objects) {
					this._addNumericAttributeToPoints(object);
				}
				return;
			}
			case AttribType.STRING: {
				for (const object of objects) {
					this._addStringAttributeToPoints(object);
				}
				return;
			}
		}
		TypeAssert.unreachable(attribType);
	}
	private _addObjectAttribute(attribType: AttribType, coreGroup: CoreGroup) {
		const allCoreObjects = coreGroup.allCoreObjects();

		// add attrib if non existent
		const attribName = this.pv.name;
		const defaultValue = AttribSetAtIndexSopOperation.defaultAttribValue(this.pv);
		if (defaultValue != null) {
			for (const coreObject of allCoreObjects) {
				if (!coreObject.hasAttribute(attribName)) {
					coreObject.setAttribValue(attribName, defaultValue);
				}
			}
		}

		const coreObject = allCoreObjects[this.pv.index];
		if (!coreObject) {
			return;
		}

		switch (attribType) {
			case AttribType.NUMERIC:
				this._addNumericAttributeToObject(coreObject);
				return;
			case AttribType.STRING:
				this._addStringAttributeToObject(coreObject);
				return;
		}
		TypeAssert.unreachable(attribType);
	}
	private _addCoreGroupAttribute(attribType: AttribType, coreGroup: CoreGroup) {
		switch (attribType) {
			case AttribType.NUMERIC:
				this._addNumericAttributeToCoreGroup(coreGroup);
				return;
			case AttribType.STRING:
				this._addStringAttributeToCoreGroup(coreGroup);
				return;
		}
		TypeAssert.unreachable(attribType);
	}

	private _addNumericAttributeToPoints<T extends CoreObjectType>(object: ObjectContent<T>) {
		const corePointClass = corePointClassFactory(object);

		const attribName = CoreAttribute.remapName(this.pv.name);
		if (!corePointClass.hasAttribute(object, attribName)) {
			corePointClass.addNumericAttribute(object, attribName, this.pv.size, 0);
		}
		const attrib = corePointClass.attribute(object, attribName) as BufferAttribute;
		const array = attrib.array;
		const {index, size} = this.pv;
		switch (size) {
			case 1: {
				if (index < array.length) {
					array[index] = this.pv.value1;
					attrib.needsUpdate = true;
				}
				break;
			}
			case 2: {
				const i2 = index * 2;
				if (i2 < array.length) {
					this.pv.value2.toArray(array, i2);
					attrib.needsUpdate = true;
				}
				break;
			}
			case 3: {
				const i3 = index * 3;
				if (i3 < array.length) {
					this.pv.value3.toArray(array, i3);
					attrib.needsUpdate = true;
				}
				break;
			}
			case 4: {
				const i4 = index * 4;
				if (i4 < array.length) {
					this.pv.value4.toArray(array, i4);
					attrib.needsUpdate = true;
				}
				break;
			}
		}

		// const point = coreObject.points()[this.pv.index];

		// if (!point) {
		// 	return;
		// }

		// const param = [this.p.value1, this.p.value2, this.p.value3, this.p.value4][this.pv.size - 1];
		// point.setAttribValue(attribName, param.value);
	}

	private _addNumericAttributeToObject(coreObject: BaseCoreObject<CoreObjectType>) {
		const param = [this.p.value1, this.p.value2, this.p.value3, this.p.value4][this.pv.size - 1];
		const attribName = this.pv.name;
		coreObject.setAttribValue(attribName, param.value);
	}
	private _addNumericAttributeToCoreGroup(coreGroup: CoreGroup) {
		const param = [this.p.value1, this.p.value2, this.p.value3, this.p.value4][this.pv.size - 1];
		const attribName = this.pv.name;
		coreGroup.setAttribValue(attribName, param.value);
	}

	private _addStringAttributeToPoints<T extends CoreObjectType>(object: ObjectContent<T>) {
		const corePointClass = corePointClassFactory(object);

		const attribName = this.pv.name;
		if (!corePointClass.hasAttribute(object, attribName)) {
			const tmpIndexData = CoreAttribute.arrayToIndexedArrays(['']);
			corePointClass.setIndexedAttribute(object, attribName, tmpIndexData['values'], tmpIndexData['indices']);
		}

		pointsFromObject(object, _allPoints);

		const param = this.p.string;

		const stringValues: string[] = new Array(_allPoints.length);
		for (const point of _allPoints) {
			let currentValue = point.stringAttribValue(attribName);
			if (currentValue == null) {
				currentValue = '';
			}
			stringValues[point.index()] = currentValue;
		}

		const indexPoint = _allPoints[this.pv.index];
		if (indexPoint) {
			stringValues[indexPoint.index()] = param.value;
		}

		const indexData = CoreAttribute.arrayToIndexedArrays(stringValues);
		// const geometry = coreObject.coreGeometry();
		// if (geometry) {
		corePointClass.setIndexedAttribute(object, attribName, indexData['values'], indexData['indices']);
		// }
	}

	private _addStringAttributeToObject(coreObject: BaseCoreObject<CoreObjectType>) {
		const param = this.p.string;
		const attribName = this.pv.name;
		coreObject.setAttribValue(attribName, param.value);
	}
	private _addStringAttributeToCoreGroup(coreGroup: CoreGroup) {
		const param = this.p.string;
		const attribName = this.pv.name;
		coreGroup.setAttribValue(attribName, param.value);
	}

	//
	//
	// CHECK IF EXPRESSION IS BEING USED, TO ALLOW EASY SWITCH TO OPERATION
	//
	//
	private _isUsingExpression(): boolean {
		const attribType = ATTRIBUTE_TYPES[this.pv.type];
		switch (attribType) {
			case AttribType.NUMERIC:
				const param = [this.p.value1, this.p.value2, this.p.value3, this.p.value4][this.pv.size - 1];
				return param.hasExpression();
			case AttribType.STRING:
				return this.p.string.hasExpression();
		}
	}

	//
	//
	// API UTILS
	//
	//
	setAttribClass(attribClass: AttribClass) {
		this.p.class.set(ATTRIBUTE_CLASSES.indexOf(attribClass));
	}
	attribClass() {
		return ATTRIBUTE_CLASSES[this.pv.class];
	}
	setAttribType(type: AttribType) {
		this.p.type.set(ATTRIBUTE_TYPES.indexOf(type));
	}
	attribType() {
		return ATTRIBUTE_TYPES[this.pv.type];
	}
}
