import {
	Color,
	Vector2,
	Vector3,
	Vector4,
	BufferAttribute,
	InstancedBufferAttribute,
	InterleavedBufferAttribute,
} from 'three';
import {AttribValue, PolyDictionary} from '../../types/GlobalTypes';
import {arrayUniq} from '../ArrayUtils';
import {stringToAttribNames, stringMatchMask} from '../String';
import {isString,isNumber,isArray} from '../Type';
import {AttribSize, GroupString} from './Constant';

export enum Attribute {
	POINT_INDEX = 'ptnum',
	VERTEX_INDEX = 'vtxnum',
	PRIMITIVE_INDEX = 'primnum',
	OBJECT_INDEX = 'objnum',
	OBJECT_NAME = 'objname',
	COLOR = 'color',
	NORMAL = 'normal',
	POSITION = 'position',
	PSCALE = 'pscale',
	UP = 'up',
	UV = 'uv',
	SCALE = 'scale',
	TANGENT = 'tangent',
	ID = 'id',
}
export enum ObjectAttribute {
	HOVERED = 'hovered',
}

const ATTRIB_NAME_MAP: PolyDictionary<string> = {
	P: Attribute.POSITION,
	N: Attribute.NORMAL,
	Cd: Attribute.COLOR,
};
const _matchingAttribNames: string[] = [];
const _masks: string[] = [];

export class CoreAttribute {
	static remapName(name: string): string {
		return ATTRIB_NAME_MAP[name] || name;
	}

	static arrayToIndexedArrays(array: string[]) {
		const index_by_value: PolyDictionary<number> = {};
		let current_index = 0;
		const indices = [];
		const values = [];

		let i = 0;
		while (i < array.length) {
			//(value = array[i++])?
			const value = array[i];
			const index = index_by_value[value];
			if (index != null) {
				indices.push(index);
			} else {
				values.push(value);
				indices.push(current_index);
				index_by_value[value] = current_index;
				current_index += 1;
			}

			i++;
		}

		return {
			indices,
			values,
		};
	}

	static defaultValue(size: number) {
		switch (size) {
			case 1:
				return 0;
			case 2:
				return new Vector2(0, 0);
			case 3:
				return new Vector3(0, 0, 0);
			default:
				throw `size ${size} not yet implemented`;
		}
	}

	static copy(src: BufferAttribute, dest: BufferAttribute, markAsNeedsUpdate = true) {
		const srcArray = src?.array;
		const destArray = dest?.array;
		if (srcArray && destArray) {
			const min_length = Math.min(srcArray.length, destArray.length);
			for (let i = 0; i < min_length; i++) {
				destArray[i] = srcArray[i];
			}

			if (markAsNeedsUpdate) {
				dest.needsUpdate = true;
			}
		}
	}

	static attribSizeFromValue(val: AttribValue): AttribSize | null {
		if (isString(val) || isNumber(val)) {
			return AttribSize.FLOAT;
		}
		if (isArray(val)) {
			return val.length;
		}

		switch (val.constructor) {
			case Color:
				return AttribSize.VECTOR3;
			case Vector2:
				return AttribSize.VECTOR2;
			case Vector3:
				return AttribSize.VECTOR3;
			case Vector4:
				return AttribSize.VECTOR4;
		}
		return null;
	}
	static attribNamesMatchingMask(masksString: GroupString, existingAttribNames: string[]) {
		stringToAttribNames(masksString, _masks);

		_matchingAttribNames.length = 0;
		for (const mask of _masks) {
			for (const attribName of existingAttribNames) {
				if (stringMatchMask(attribName, mask)) {
					_matchingAttribNames.push(attribName);
				} else {
					const remapped = CoreAttribute.remapName(mask);
					if (attribName == remapped) {
						_matchingAttribNames.push(attribName);
					}
				}
			}
		}
		const uniqAttributeNames: string[] = [];
		return arrayUniq(_matchingAttribNames, uniqAttributeNames);
	}
}

export function markAttributeAsNeedsUpdateForFrame(
	attribute: BufferAttribute | InstancedBufferAttribute | InterleavedBufferAttribute,
	frame: number
) {
	if (attribute instanceof BufferAttribute || attribute instanceof InstancedBufferAttribute) {
		(attribute as BufferAttribute | InstancedBufferAttribute).version = frame;
	} else {
		if (attribute.data) {
			attribute.data.version = frame;
		}
	}
}
