import {
	BufferGeometry,
	Object3D,
	Mesh,
	Vector3,
	BufferAttribute,
	Float32BufferAttribute,
	Int32BufferAttribute,
	InstancedBufferAttribute,
} from 'three';
import {CoreObjectType, ObjectContent} from '../../ObjectContent';
import {CorePoint} from '../../entities/point/CorePoint';
import {PointAttributesDict} from '../../entities/point/Common';
import {Attribute} from '../../Attribute';
import {objectCloneDeep} from '../../../ObjectUtils';
import {NumericAttribValue} from '../../../../types/GlobalTypes';
import {markedAsInstance} from '../../GeometryUtils';
import {pointsCountFromBufferGeometry, positionAttributeNameFromBufferGeometry} from './CoreThreejsPointUtils';
import {attributeNumericValues, AttributeNumericValuesOptions} from '../../entities/utils/Common';
import {ThreejsVertex} from './ThreejsVertex';
import {pointsCountFromObject} from '../../entities/point/CorePointUtils';
import {TraversedRelatedEntityData} from '../../entities/utils/TraversedRelatedEntities';
import {CoreEntityWithObject} from '../../CoreEntity';

const INDEX_ATTRIB_VALUES = 'indexedAttribValues';
const target: AttributeNumericValuesOptions = {
	attributeAdded: false,
	values: [],
};

export class ThreejsPoint extends CorePoint<CoreObjectType.THREEJS> {
	protected _geometry?: BufferGeometry;

	constructor(object: Object3D, index: number) {
		super(object, index);
		this._updateGeometry();
	}

	override setIndex(index: number, object?: Object3D) {
		this._index = index;
		if (object) {
			this._object = object;
			this._updateGeometry();
		}
		return this;
	}
	private _updateGeometry() {
		const geometry = (this._object as Mesh).geometry as BufferGeometry | undefined;
		if (geometry) {
			this._geometry = geometry;
		}
	}
	geometry() {
		return this._geometry;
	}
	static override addAttribute(object: Object3D, attribName: string, attribute: BufferAttribute) {
		const geometry = (object as Mesh).geometry as BufferGeometry | undefined;
		if (!geometry) {
			return;
		}
		geometry.setAttribute(attribName, attribute);
	}
	static override attributes<T extends CoreObjectType>(object: ObjectContent<T>): PointAttributesDict | undefined {
		const geometry = (object as any as Mesh).geometry as BufferGeometry | undefined;
		if (!geometry) {
			return;
		}
		return geometry.attributes;
	}
	static override entitiesCount<T extends CoreObjectType>(object: ObjectContent<T>) {
		const geometry = (object as any as Mesh).geometry as BufferGeometry | undefined;
		if (!geometry) {
			return 0;
		}
		return pointsCountFromBufferGeometry(geometry);
	}
	static positionAttributeName<T extends CoreObjectType>(object: ObjectContent<T>) {
		const geometry = (object as any as Mesh).geometry as BufferGeometry | undefined;
		if (!geometry) {
			return null;
		}
		return positionAttributeNameFromBufferGeometry(geometry);
	}
	static position<T extends CoreObjectType>(object: ObjectContent<T>, index: number, target: Vector3) {
		const geometry = (object as any as Mesh).geometry as BufferGeometry | undefined;
		if (!geometry) {
			return null;
		}
		const {array} = geometry.getAttribute(Attribute.POSITION) as BufferAttribute;
		return target.fromArray(array, index * 3);
	}
	override position(target: Vector3): Vector3 {
		if (!this._geometry) {
			return target;
		}
		const {array} = this._geometry.getAttribute(Attribute.POSITION) as BufferAttribute;
		return target.fromArray(array, this._index * 3);
	}
	override normal(target: Vector3): Vector3 {
		if (!this._geometry) {
			return target;
		}
		const {array} = this._geometry.getAttribute(Attribute.NORMAL) as BufferAttribute;
		return target.fromArray(array, this._index * 3);
	}
	static override computeNormals<T extends CoreObjectType>(object: ObjectContent<T>) {
		const geometry = (object as any as Mesh).geometry as BufferGeometry | undefined;
		if (!geometry) {
			return null;
		}
		geometry.computeVertexNormals();
	}

	static override markAttribAsNeedsUpdate<T extends CoreObjectType>(object: ObjectContent<T>, attribName: string) {
		const geometry = (object as any as Mesh).geometry as BufferGeometry | undefined;
		if (!geometry) {
			return null;
		}
		const attribute = geometry.getAttribute(attribName);
		if (!attribute) {
			return;
		}
		attribute.needsUpdate = true;
	}

	//
	//
	//
	//
	//
	static override userDataAttribs<T extends CoreObjectType>(object: ObjectContent<T>) {
		const geometry = (object as any as Mesh).geometry as BufferGeometry | undefined;
		if (!geometry) {
			return {};
		}
		return (geometry.userData[INDEX_ATTRIB_VALUES] = geometry.userData[INDEX_ATTRIB_VALUES] || {});
	}
	static override setIndexedAttribute<T extends CoreObjectType>(
		object: ObjectContent<T>,
		attribName: string,
		values: string[],
		indices: number[]
	) {
		const geometry = (object as any as Mesh).geometry as BufferGeometry | undefined;
		if (!geometry) {
			return;
		}
		this.setIndexedAttributeValues(object, attribName, values);
		geometry.setAttribute(attribName, new Int32BufferAttribute(indices, 1));
		geometry.getAttribute(attribName).needsUpdate = true;
	}
	static override attribValueIndex<T extends CoreObjectType>(
		object: ObjectContent<T>,
		index: number,
		attribName: string
	): number {
		if (this.isAttribIndexed(object, attribName)) {
			const geometry = (object as any as Mesh).geometry as BufferGeometry | undefined;
			if (geometry) {
				return (geometry.getAttribute(attribName) as BufferAttribute).array[index];
			}
		}
		return -1;
	}
	//
	//
	//
	//
	//
	static override renameAttribute<T extends CoreObjectType>(
		object: ObjectContent<T>,
		oldName: string,
		newName: string
	) {
		const geometry = (object as any as Mesh).geometry as BufferGeometry | undefined;
		if (!geometry) {
			return;
		}
		if (this.isAttribIndexed(object, oldName)) {
			this.userDataAttribs(object)[newName] = objectCloneDeep(this.userDataAttribs(object)[oldName]);
			delete this.userDataAttribs(object)[oldName];
		}

		const oldAttrib = geometry.getAttribute(oldName) as BufferAttribute;
		geometry.setAttribute(newName, new Float32BufferAttribute(oldAttrib.array, oldAttrib.itemSize));
		return geometry.deleteAttribute(oldName);
	}

	static override deleteAttribute<T extends CoreObjectType>(object: ObjectContent<T>, attribName: string) {
		const geometry = (object as any as Mesh).geometry as BufferGeometry | undefined;
		if (!geometry) {
			return;
		}
		if (this.isAttribIndexed(object, attribName)) {
			delete this.userDataAttribs(object)[attribName];
		}

		return geometry.deleteAttribute(attribName);
	}

	static override addNumericAttribute<T extends CoreObjectType>(
		object: ObjectContent<T>,
		attribName: string,
		size: number = 1,
		defaultValue: NumericAttribValue = 0
	) {
		const geometry = (object as any as Mesh).geometry as BufferGeometry | undefined;
		if (!geometry) {
			return;
		}
		attributeNumericValues(object, pointsCountFromObject, size, defaultValue, target);

		if (target.attributeAdded) {
			if (markedAsInstance(geometry)) {
				const valuesAsTypedArray = new Float32Array(target.values);
				geometry.setAttribute(attribName.trim(), new InstancedBufferAttribute(valuesAsTypedArray, size));
			} else {
				geometry.setAttribute(attribName.trim(), new Float32BufferAttribute(target.values, size));
			}
		} else {
			console.warn(defaultValue);
			throw `CoreThreejsPoint.addNumericAttrib error: no other default value allowed for now (default given: ${defaultValue})`;
		}
	}
	//
	//
	// RELATED ENTITIES
	//
	//
	static override relatedVertexIds<T extends CoreObjectType>(
		object: ObjectContent<T>,
		pointIndex: number,
		target: number[],
		traversedRelatedEntityData?: TraversedRelatedEntityData
	): void {
		const geometry = (object as any as Mesh).geometry as BufferGeometry | undefined;
		if (!geometry) {
			return;
		}
		const index = geometry.getIndex();
		if (!index) {
			return;
		}
		const indexArray = index.array;
		let i = 0;
		for (const indexValue of indexArray) {
			if (indexValue == pointIndex) {
				target.push(i);
			}
			i++;
		}
	}
	static override relatedVertexClass<T extends CoreObjectType>(object: ObjectContent<T>) {
		return ThreejsVertex as any as typeof CoreEntityWithObject<T>;
	}
}
