import { BoundingBox } from '../core/shape/bounding-box.js';
import { BoundingSphere } from '../core/shape/bounding-sphere.js';
import { BindGroup } from '../platform/graphics/bind-group.js';
import { UniformBuffer } from '../platform/graphics/uniform-buffer.js';
import { SHADOW_CASCADE_ALL, LAYER_WORLD, RENDERSTYLE_SOLID, MASK_AFFECT_DYNAMIC, SHADERDEF_UV0, SHADERDEF_UV1, SHADERDEF_VCOLOR, SHADERDEF_TANGENTS, SHADERDEF_NOSHADOW, SHADERDEF_BATCH, SHADERDEF_SKIN, SHADERDEF_MORPH_POSITION, SHADERDEF_MORPH_NORMAL, SHADERDEF_MORPH_TEXTURE_BASED_INT, SHADERDEF_SCREENSPACE, SHADERDEF_INSTANCING, MASK_AFFECT_LIGHTMAPPED } from './constants.js';
import { getDefaultMaterial } from './materials/default-material.js';
import { LightmapCache } from './graphics/lightmap-cache.js';
import { hash32Fnv1a } from '../core/hash.js';

let id = 0;
const _tmpAabb = new BoundingBox();
const _tempBoneAabb = new BoundingBox();
const _tempSphere = new BoundingSphere();
const _meshSet = new Set();
const lookupHashes = new Uint32Array(4);
class InstancingData {
		constructor(numObjects){
				this.vertexBuffer = null;
				this._destroyVertexBuffer = false;
				this.count = numObjects;
		}
		destroy() {
				if (this._destroyVertexBuffer) {
						this.vertexBuffer?.destroy();
				}
				this.vertexBuffer = null;
		}
}
class IndirectData {
		get(camera) {
				return this.map.get(camera) ?? this.map.get(null);
		}
		constructor(){
				this.map = new Map();
				this.meshMetaData = new Int32Array(4);
		}
}
class ShaderInstance {
		getBindGroup(device) {
				if (!this.bindGroup) {
						const shader = this.shader;
						const bindGroupFormat = shader.meshBindGroupFormat;
						this.bindGroup = new BindGroup(device, bindGroupFormat);
				}
				return this.bindGroup;
		}
		getUniformBuffer(device) {
				if (!this.uniformBuffer) {
						const shader = this.shader;
						const ubFormat = shader.meshUniformBufferFormat;
						this.uniformBuffer = new UniformBuffer(device, ubFormat, false);
				}
				return this.uniformBuffer;
		}
		destroy() {
				this.bindGroup?.destroy();
				this.bindGroup = null;
				this.uniformBuffer?.destroy();
				this.uniformBuffer = null;
		}
		constructor(){
				this.bindGroup = null;
				this.uniformBuffer = null;
		}
}
class MeshInstance {
		constructor(mesh, material, node = null){
				this.castShadow = false;
				this.shadowCascadeMask = SHADOW_CASCADE_ALL;
				this.cull = true;
				this.drawOrder = 0;
				this._drawBucket = 127;
				this.visible = true;
				this.visibleThisFrame = false;
				this.flipFacesFactor = 1;
				this.gsplatInstance = null;
				this.id = id++;
				this.isVisibleFunc = null;
				this.instancingData = null;
				this.indirectData = null;
				this.parameters = {};
				this.pick = true;
				this.stencilFront = null;
				this.stencilBack = null;
				this.transparent = false;
				this._aabb = new BoundingBox();
				this._aabbVer = -1;
				this._aabbMeshVer = -1;
				this._customAabb = null;
				this._updateAabb = true;
				this._updateAabbFunc = null;
				this._sortKeyShadow = 0;
				this._sortKeyForward = 0;
				this._sortKeyDynamic = 0;
				this._layer = LAYER_WORLD;
				this._material = null;
				this._skinInstance = null;
				this._morphInstance = null;
				this._receiveShadow = true;
				this._renderStyle = RENDERSTYLE_SOLID;
				this._screenSpace = false;
				this._shaderCache = new Map();
				this._shaderDefs = MASK_AFFECT_DYNAMIC << 16;
				this._calculateSortDistance = null;
				this.node = node;
				this._mesh = mesh;
				mesh.incRefCount();
				this.material = material;
				if (mesh.vertexBuffer) {
						const format = mesh.vertexBuffer.format;
						this._shaderDefs |= format.hasUv0 ? SHADERDEF_UV0 : 0;
						this._shaderDefs |= format.hasUv1 ? SHADERDEF_UV1 : 0;
						this._shaderDefs |= format.hasColor ? SHADERDEF_VCOLOR : 0;
						this._shaderDefs |= format.hasTangents ? SHADERDEF_TANGENTS : 0;
				}
				this.updateKey();
		}
		set drawBucket(bucket) {
				this._drawBucket = Math.floor(bucket) & 0xff;
				this.updateKey();
		}
		get drawBucket() {
				return this._drawBucket;
		}
		set renderStyle(renderStyle) {
				this._renderStyle = renderStyle;
				this.mesh.prepareRenderState(renderStyle);
		}
		get renderStyle() {
				return this._renderStyle;
		}
		set mesh(mesh) {
				if (mesh === this._mesh) {
						return;
				}
				if (this._mesh) {
						this._mesh.decRefCount();
				}
				this._mesh = mesh;
				if (mesh) {
						mesh.incRefCount();
				}
		}
		get mesh() {
				return this._mesh;
		}
		set aabb(aabb) {
				this._aabb = aabb;
		}
		get aabb() {
				if (!this._updateAabb) {
						return this._aabb;
				}
				if (this._updateAabbFunc) {
						return this._updateAabbFunc(this._aabb);
				}
				let localAabb = this._customAabb;
				let toWorldSpace = !!localAabb;
				if (!localAabb) {
						localAabb = _tmpAabb;
						if (this.skinInstance) {
								if (!this.mesh.boneAabb) {
										const morphTargets = this._morphInstance ? this._morphInstance.morph._targets : null;
										this.mesh._initBoneAabbs(morphTargets);
								}
								const boneUsed = this.mesh.boneUsed;
								let first = true;
								for(let i = 0; i < this.mesh.boneAabb.length; i++){
										if (boneUsed[i]) {
												_tempBoneAabb.setFromTransformedAabb(this.mesh.boneAabb[i], this.skinInstance.matrices[i]);
												if (first) {
														first = false;
														localAabb.center.copy(_tempBoneAabb.center);
														localAabb.halfExtents.copy(_tempBoneAabb.halfExtents);
												} else {
														localAabb.add(_tempBoneAabb);
												}
										}
								}
								toWorldSpace = true;
						} else if (this.node._aabbVer !== this._aabbVer || this.mesh._aabbVer !== this._aabbMeshVer) {
								if (this.mesh) {
										localAabb.center.copy(this.mesh.aabb.center);
										localAabb.halfExtents.copy(this.mesh.aabb.halfExtents);
								} else {
										localAabb.center.set(0, 0, 0);
										localAabb.halfExtents.set(0, 0, 0);
								}
								if (this.mesh && this.mesh.morph) {
										const morphAabb = this.mesh.morph.aabb;
										localAabb._expand(morphAabb.getMin(), morphAabb.getMax());
								}
								toWorldSpace = true;
								this._aabbVer = this.node._aabbVer;
								this._aabbMeshVer = this.mesh._aabbVer;
						}
				}
				if (toWorldSpace) {
						this._aabb.setFromTransformedAabb(localAabb, this.node.getWorldTransform());
				}
				return this._aabb;
		}
		clearShaders() {
				this._shaderCache.forEach((shaderInstance)=>{
						shaderInstance.destroy();
				});
				this._shaderCache.clear();
		}
		getShaderInstance(shaderPass, lightHash, scene, cameraShaderParams, viewUniformFormat, viewBindGroupFormat, sortedLights) {
				const shaderDefs = this._shaderDefs;
				lookupHashes[0] = shaderPass;
				lookupHashes[1] = lightHash;
				lookupHashes[2] = shaderDefs;
				lookupHashes[3] = cameraShaderParams.hash;
				const hash = hash32Fnv1a(lookupHashes);
				let shaderInstance = this._shaderCache.get(hash);
				if (!shaderInstance) {
						const mat = this._material;
						shaderInstance = new ShaderInstance();
						shaderInstance.shader = mat.variants.get(hash);
						shaderInstance.hashes = new Uint32Array(lookupHashes);
						if (!shaderInstance.shader) {
								const shader = mat.getShaderVariant({
										device: this.mesh.device,
										scene: scene,
										objDefs: shaderDefs,
										cameraShaderParams: cameraShaderParams,
										pass: shaderPass,
										sortedLights: sortedLights,
										viewUniformFormat: viewUniformFormat,
										viewBindGroupFormat: viewBindGroupFormat,
										vertexFormat: this.mesh.vertexBuffer?.format
								});
								mat.variants.set(hash, shader);
								shaderInstance.shader = shader;
						}
						this._shaderCache.set(hash, shaderInstance);
				}
				return shaderInstance;
		}
		set material(material) {
				this.clearShaders();
				const prevMat = this._material;
				if (prevMat) {
						prevMat.removeMeshInstanceRef(this);
				}
				this._material = material;
				if (material) {
						material.addMeshInstanceRef(this);
						this.transparent = material.transparent;
						this.updateKey();
				}
		}
		get material() {
				return this._material;
		}
		_updateShaderDefs(shaderDefs) {
				if (shaderDefs !== this._shaderDefs) {
						this._shaderDefs = shaderDefs;
						this.clearShaders();
				}
		}
		set calculateSortDistance(calculateSortDistance) {
				this._calculateSortDistance = calculateSortDistance;
		}
		get calculateSortDistance() {
				return this._calculateSortDistance;
		}
		set receiveShadow(val) {
				if (this._receiveShadow !== val) {
						this._receiveShadow = val;
						this._updateShaderDefs(val ? this._shaderDefs & -2 : this._shaderDefs | SHADERDEF_NOSHADOW);
				}
		}
		get receiveShadow() {
				return this._receiveShadow;
		}
		set batching(val) {
				this._updateShaderDefs(val ? this._shaderDefs | SHADERDEF_BATCH : this._shaderDefs & -16385);
		}
		get batching() {
				return (this._shaderDefs & SHADERDEF_BATCH) !== 0;
		}
		set skinInstance(val) {
				this._skinInstance = val;
				this._updateShaderDefs(val ? this._shaderDefs | SHADERDEF_SKIN : this._shaderDefs & -3);
				this._setupSkinUpdate();
		}
		get skinInstance() {
				return this._skinInstance;
		}
		set morphInstance(val) {
				this._morphInstance?.destroy();
				this._morphInstance = val;
				let shaderDefs = this._shaderDefs;
				shaderDefs = val && val.morph.morphPositions ? shaderDefs | SHADERDEF_MORPH_POSITION : shaderDefs & -1025;
				shaderDefs = val && val.morph.morphNormals ? shaderDefs | SHADERDEF_MORPH_NORMAL : shaderDefs & -2049;
				shaderDefs = val && val.morph.intRenderFormat ? shaderDefs | SHADERDEF_MORPH_TEXTURE_BASED_INT : shaderDefs & -8193;
				this._updateShaderDefs(shaderDefs);
		}
		get morphInstance() {
				return this._morphInstance;
		}
		set screenSpace(val) {
				if (this._screenSpace !== val) {
						this._screenSpace = val;
						this._updateShaderDefs(val ? this._shaderDefs | SHADERDEF_SCREENSPACE : this._shaderDefs & -257);
				}
		}
		get screenSpace() {
				return this._screenSpace;
		}
		set key(val) {
				this._sortKeyForward = val;
		}
		get key() {
				return this._sortKeyForward;
		}
		set mask(val) {
				const toggles = this._shaderDefs & 0x0000FFFF;
				this._updateShaderDefs(toggles | val << 16);
		}
		get mask() {
				return this._shaderDefs >> 16;
		}
		set instancingCount(value) {
				if (this.instancingData) {
						this.instancingData.count = value;
				}
		}
		get instancingCount() {
				return this.instancingData ? this.instancingData.count : 0;
		}
		destroy() {
				const mesh = this.mesh;
				if (mesh) {
						this.mesh = null;
						if (mesh.refCount < 1) {
								mesh.destroy();
						}
				}
				this.setRealtimeLightmap(MeshInstance.lightmapParamNames[0], null);
				this.setRealtimeLightmap(MeshInstance.lightmapParamNames[1], null);
				this._skinInstance?.destroy();
				this._skinInstance = null;
				this.morphInstance?.destroy();
				this.morphInstance = null;
				this.clearShaders();
				this.material = null;
				this.instancingData?.destroy();
		}
		static{
				this.lightmapParamNames = [
						'texture_lightMap',
						'texture_dirLightMap'
				];
		}
		static _prepareRenderStyleForArray(meshInstances, renderStyle) {
				if (meshInstances) {
						for(let i = 0; i < meshInstances.length; i++){
								meshInstances[i]._renderStyle = renderStyle;
								const mesh = meshInstances[i].mesh;
								if (!_meshSet.has(mesh)) {
										_meshSet.add(mesh);
										mesh.prepareRenderState(renderStyle);
								}
						}
						_meshSet.clear();
				}
		}
		_isVisible(camera) {
				if (this.visible) {
						if (this.isVisibleFunc) {
								return this.isVisibleFunc(camera);
						}
						_tempSphere.center = this.aabb.center;
						_tempSphere.radius = this._aabb.halfExtents.length();
						return camera.frustum.containsSphere(_tempSphere) > 0;
				}
				return false;
		}
		updateKey() {
				const { material } = this;
				this._sortKeyForward = this._drawBucket << 23 | (material.alphaToCoverage || material.alphaTest ? 0x400000 : 0) | material.id & 0x3fffff;
		}
		setInstancing(vertexBuffer, cull = false) {
				if (vertexBuffer) {
						this.instancingData = new InstancingData(vertexBuffer.numVertices);
						this.instancingData.vertexBuffer = vertexBuffer;
						vertexBuffer.format.instancing = true;
						this.cull = cull;
				} else {
						this.instancingData = null;
						this.cull = true;
				}
				this._updateShaderDefs(vertexBuffer ? this._shaderDefs | SHADERDEF_INSTANCING : this._shaderDefs & -33);
		}
		setIndirect(camera, slot) {
				this._allocIndirectData();
				this.indirectData.map.set(camera?.camera ?? null, slot);
				const device = this.mesh.device;
				device.mapsToClear.add(this.indirectData.map);
		}
		getIndirectMetaData() {
				this._allocIndirectData();
				const prim = this.mesh?.primitive[this.renderStyle];
				const data = this.indirectData.meshMetaData;
				data[0] = prim.count;
				data[1] = prim.base;
				data[2] = prim.baseVertex;
				return data;
		}
		_allocIndirectData() {
				if (!this.indirectData) {
						this.indirectData = new IndirectData();
				}
		}
		ensureMaterial(device) {
				if (!this.material) {
						this.material = getDefaultMaterial(device);
				}
		}
		clearParameters() {
				this.parameters = {};
		}
		getParameters() {
				return this.parameters;
		}
		getParameter(name) {
				return this.parameters[name];
		}
		setParameter(name, data, passFlags = 0xFFFFFFFF) {
				const param = this.parameters[name];
				if (param) {
						param.data = data;
						param.passFlags = passFlags;
				} else {
						this.parameters[name] = {
								scopeId: null,
								data: data,
								passFlags: passFlags
						};
				}
		}
		setRealtimeLightmap(name, texture) {
				const old = this.getParameter(name);
				if (old === texture) {
						return;
				}
				if (old) {
						LightmapCache.decRef(old.data);
				}
				if (texture) {
						LightmapCache.incRef(texture);
						this.setParameter(name, texture);
				} else {
						this.deleteParameter(name);
				}
		}
		deleteParameter(name) {
				if (this.parameters[name]) {
						delete this.parameters[name];
				}
		}
		setParameters(device, passFlag) {
				const parameters = this.parameters;
				for(const paramName in parameters){
						const parameter = parameters[paramName];
						if (parameter.passFlags & passFlag) {
								if (!parameter.scopeId) {
										parameter.scopeId = device.scope.resolve(paramName);
								}
								parameter.scopeId.setValue(parameter.data);
						}
				}
		}
		setLightmapped(value) {
				if (value) {
						this.mask = (this.mask | MASK_AFFECT_LIGHTMAPPED) & -6;
				} else {
						this.setRealtimeLightmap(MeshInstance.lightmapParamNames[0], null);
						this.setRealtimeLightmap(MeshInstance.lightmapParamNames[1], null);
						this._shaderDefs &= -4289;
						this.mask = (this.mask | MASK_AFFECT_DYNAMIC) & -7;
				}
		}
		setCustomAabb(aabb) {
				if (aabb) {
						if (this._customAabb) {
								this._customAabb.copy(aabb);
						} else {
								this._customAabb = aabb.clone();
						}
				} else {
						this._customAabb = null;
						this._aabbVer = -1;
				}
				this._setupSkinUpdate();
		}
		_setupSkinUpdate() {
				if (this._skinInstance) {
						this._skinInstance._updateBeforeCull = !this._customAabb;
				}
		}
}

export { MeshInstance };
