package iron.object;

import haxe.ds.Vector;
import kha.graphics4.Graphics;
import kha.graphics4.PipelineState;
import iron.math.Vec4;
import iron.math.Mat4;
import iron.data.MeshData;
import iron.data.MaterialData;
import iron.data.ShaderData;
import iron.data.SceneFormat;

class MeshObject extends Object {

	public var data: MeshData = null;
	public var materials: Vector<MaterialData>;
	public var materialIndex = 0;
	public var depthRead(default, null) = false;
	#if lnx_particles
	public var particleSystems: Array<ParticleSystem> = null; // Particle owner
	public var particleChildren: Array<MeshObject> = null;
	public var particleOwner: MeshObject = null; // Particle object
	public var particleIndex = -1;
	#end
	public var cameraDistance: Float;
	public var screenSize = 0.0;
	public var frustumCulling = true;
	public var activeTilesheet: Tilesheet = null;
	public var tilesheets: Array<Tilesheet> = null;
	public var skip_context: String = null; // Do not draw this context
	public var force_context: String = null; // Draw only this context
	static var lastPipeline: PipelineState = null;
	#if lnx_morph_target
	public var morphTarget: MorphTarget = null;
	#end

	#if lnx_veloc
	public var prevMatrix = Mat4.identity();
	#end

	public function new(data: MeshData, materials: Vector<MaterialData>) {
		super();

		this.materials = materials;
		setData(data);
		Scene.active.meshes.push(this);
	}

	public function setData(data: MeshData) {
		this.data = data;
		data.refcount++;

		#if (!lnx_batch)
		data.geom.build();
		#end

		// Scale-up packed (-1,1) mesh coords
		transform.scaleWorld = data.scalePos;
	}

	#if lnx_batch
	@:allow(iron.Scene)
	function batch(isLod: Bool) {
		var batched = Scene.active.meshBatch.addMesh(this, isLod);
		if (!batched) data.geom.build();
	}
	#end

	override public function remove() {
		#if lnx_batch
		Scene.active.meshBatch.removeMesh(this);
		#end
		#if lnx_particles
		if (particleChildren != null) {
			for (c in particleChildren) c.remove();
			particleChildren = null;
		}
		if (particleSystems != null) {
			for (psys in particleSystems) psys.remove();
			particleSystems = null;
		}
		#end
		if (activeTilesheet != null) activeTilesheet.remove();
		if (tilesheets != null) for (ts in tilesheets) { ts.remove(); }
		if (Scene.active != null) Scene.active.meshes.remove(this);
		data.refcount--;
		super.remove();
	}

	override public function setupAnimation(oactions: Array<TSceneFormat> = null) {
		#if lnx_skin
		var hasAction = parent != null && parent.raw != null && parent.raw.bone_actions != null;
		if (hasAction) {
			var armatureUid = parent.uid;
			animation = getBoneAnimation(armatureUid);
			if (animation == null) animation = new BoneAnimation(armatureUid, parent);
			if (data.isSkinned) cast(animation, BoneAnimation).setSkin(this);
		}
		#end
		super.setupAnimation(oactions);
	}

	#if lnx_morph_target
	override public function setupMorphTargets() {
		if (data.raw.morph_target != null) {
			morphTarget = new MorphTarget(data.raw.morph_target);
		}
	}
	#end

	#if lnx_particles
	public function setupParticleSystem(sceneName: String, pref: TParticleReference) {
		if (particleSystems == null) particleSystems = [];
		var psys = new ParticleSystem(sceneName, pref);
		particleSystems.push(psys);
	}
	#end

	public function setupTilesheet(sceneName: String, tilesheet_ref: String, tilesheet_action_ref: String) {
		activeTilesheet = new Tilesheet(sceneName, tilesheet_ref, tilesheet_action_ref);
		if(tilesheets == null) tilesheets = new Array<Tilesheet>();
		tilesheets.push(activeTilesheet);
	}

	public function setActiveTilesheet(sceneName: String, tilesheet_ref: String, tilesheet_action_ref: String) {
		var set = false;
		// Check if tilesheet already created
		if (tilesheets != null) {
			for (ts in tilesheets) {
				if (ts.raw.name == tilesheet_ref) {
					activeTilesheet = ts;
					activeTilesheet.play(tilesheet_action_ref);
					set = true;
					break;
				}
			}
		}
		// If not already created
		if (!set) {
			setupTilesheet(sceneName, tilesheet_ref, tilesheet_action_ref);
		}

	}

	inline function isLodMaterial(): Bool {
		return (raw != null && raw.lod_material != null && raw.lod_material == true);
	}

	function setCulled(isShadow: Bool, b: Bool): Bool {
		isShadow ? culledShadow = b : culledMesh = b;
		culled = culledMesh && culledShadow;
		#if lnx_debug
		if (b) RenderPath.culled++;
		#end
		return b;
	}

	public function cullMaterial(context: String): Bool {
		// Skip render if material does not contain current context
		var mats = materials;
		if (!isLodMaterial() && !validContext(mats, context)) return true;

		var isShadow = context == "shadowmap";
		if (!visibleMesh && !isShadow) return setCulled(isShadow, true);
		if (!visibleShadow && isShadow) return setCulled(isShadow, true);

		if (skip_context == context) return setCulled(isShadow, true);
		if (force_context != null && force_context != context) return setCulled(isShadow, true);

		return setCulled(isShadow, false);
	}

	function cullMesh(context: String, camera: CameraObject, light: LightObject): Bool {
		if (camera == null) return false;

		if (camera.data.raw.frustum_culling && frustumCulling) {
			// Scale radius for skinned mesh and particle system
			// TODO: define skin & particle bounds
			var radiusScale = data.isSkinned ? 2.0 : 1.0;
			#if lnx_particles
			// particleSystems for update, particleOwner for render
			if (particleSystems != null || particleOwner != null) radiusScale *= 1000;
			#end
			if (context == "voxel") radiusScale *= 100;
			if (data.geom.instanced) radiusScale *= 100;
			var isShadow = context == "shadowmap";
			var frustumPlanes = isShadow ? light.frustumPlanes : camera.frustumPlanes;

			if (isShadow && light.data.raw.type != "sun") { // Non-sun light bounds intersect camera frustum
				light.transform.radius = light.data.raw.far_plane;
				if (!CameraObject.sphereInFrustum(camera.frustumPlanes, light.transform)) {
					return setCulled(isShadow, true);
				}
			}

			if (!CameraObject.sphereInFrustum(frustumPlanes, transform, radiusScale)) {
				return setCulled(isShadow, true);
			}
		}

		culled = false;
		return culled;
	}

	function skipContext(context: String, mat: MaterialData): Bool {
		if (mat.raw.skip_context != null &&
			mat.raw.skip_context == context) {
			return true;
		}
		return false;
	}

	function getContexts(context: String, materials: Vector<MaterialData>, materialContexts: Array<MaterialContext>, shaderContexts: Array<ShaderContext>) {
		for (mat in materials) {
			var found = false;
			for (i in 0...mat.raw.contexts.length) {
				if (mat.raw.contexts[i].name.substr(0, context.length) == context) {
					materialContexts.push(mat.contexts[i]);
					shaderContexts.push(mat.shader.getContext(context));
					found = true;
					break;
				}
			}
			if (!found) {
				materialContexts.push(null);
				shaderContexts.push(null);
			}
		}
	}

	public function render(g: Graphics, context: String, bindParams: Array<String>) {
		if (data == null || !data.geom.ready) return; // Data not yet streamed
		if (!visible) return; // Skip render if object is hidden
		if (cullMesh(context, Scene.active.camera, RenderPath.active.light)) return;
		var meshContext = raw != null ? context == "mesh" : false;

		#if lnx_particles
		if (raw != null && raw.is_particle && particleOwner == null) return; // Instancing not yet set-up by particle system owner
		if (particleSystems != null && meshContext) {
			if (particleChildren == null) {
				particleChildren = [];
				for (psys in particleSystems) {
					// var c: MeshObject = cast Scene.active.getChild(psys.data.raw.instance_object);
					Scene.active.spawnObject(psys.data.raw.instance_object, null, function(o: Object) {
						if (o != null) {
							var c: MeshObject = cast o;
							particleChildren.push(c);
							c.particleOwner = this;
							c.particleIndex = particleChildren.length - 1;
						}
					});
				}
			}
			for (i in 0...particleSystems.length) {
				particleSystems[i].update(particleChildren[i], this);
			}
		}
		if (particleSystems != null && particleSystems.length > 0 && !raw.render_emitter) return;
		#end

		if (cullMaterial(context)) return;

		// Get lod
		var mats = materials;
		var lod = this;
		if (raw != null && raw.lods != null && raw.lods.length > 0) {
			computeScreenSize(Scene.active.camera);
			initLods();
			if (context == "voxel") {
				// Voxelize using the lowest lod
				lod = cast lods[lods.length - 1];
			}
			else {
				// Select lod
				for (i in 0...raw.lods.length) {
					// Lod found
					if (screenSize > raw.lods[i].screen_size) break;
					lod = cast lods[i];
					if (isLodMaterial()) mats = lod.materials;
				}
			}
			if (lod == null) return; // Empty object
		}
		#if lnx_debug
		else computeScreenSize(Scene.active.camera);
		#end
		if (isLodMaterial() && !validContext(mats, context)) return;

		// Get context
		var materialContexts: Array<MaterialContext> = [];
		var shaderContexts: Array<ShaderContext> = [];
		getContexts(context, mats, materialContexts, shaderContexts);

		Uniforms.posUnpack = data.scalePos;
		Uniforms.texUnpack = data.scaleTex;
		transform.update();

		// Render mesh
		var ldata = lod.data;
		for (i in 0...ldata.geom.indexBuffers.length) {

			var mi = ldata.geom.materialIndices[i];
			if (shaderContexts.length <= mi || shaderContexts[mi] == null) continue;
			materialIndex = mi;

			// Check context skip
			if (materials.length > mi && skipContext(context, materials[mi])) continue;

			var scontext = shaderContexts[mi];
			if (scontext == null) continue;
			var elems = scontext.raw.vertex_elements;

			// Uniforms
			if (scontext.pipeState != lastPipeline) {
				g.setPipeline(scontext.pipeState);
				lastPipeline = scontext.pipeState;
				// Uniforms.setContextConstants(g, scontext, bindParams);
			}
			Uniforms.setContextConstants(g, scontext, bindParams); //
			Uniforms.setObjectConstants(g, scontext, this);
			if (materialContexts.length > mi) {
				Uniforms.setMaterialConstants(g, scontext, materialContexts[mi]);
			}

			// VB / IB
			#if lnx_deinterleaved
			g.setVertexBuffers(ldata.geom.get(elems));
			#else
			if (ldata.geom.instancedVB != null) {
				g.setVertexBuffers([ldata.geom.get(elems), ldata.geom.instancedVB]);
			}
			else {
				g.setVertexBuffer(ldata.geom.get(elems));
			}
			#end

			g.setIndexBuffer(ldata.geom.indexBuffers[i]);

			// Draw
			if (ldata.geom.instanced) {
				g.drawIndexedVerticesInstanced(ldata.geom.instanceCount, ldata.geom.start, ldata.geom.count);
			}
			else {
				g.drawIndexedVertices(ldata.geom.start, ldata.geom.count);
			}
		}

		#if lnx_debug
		var isShadow = context == "shadowmap";
		if (meshContext) RenderPath.numTrisMesh += ldata.geom.numTris;
		else if (isShadow) RenderPath.numTrisShadow += ldata.geom.numTris;
		RenderPath.drawCalls++;
		#end

		#if lnx_veloc
		prevMatrix.setFrom(transform.worldUnpack);
		#end
	}

	function validContext(mats: Vector<MaterialData>, context: String): Bool {
		for (mat in mats) if (mat.getContext(context) != null) return true;
		return false;
	}

	public inline function computeCameraDistance(camX: Float, camY: Float, camZ: Float) {
		// Render path mesh sorting
		cameraDistance = Vec4.distancef(camX, camY, camZ, transform.worldx(), transform.worldy(), transform.worldz());
	}

	public inline function computeDepthRead() {
		#if rp_depth_texture
		depthRead = false;
		for (material in materials) {
			for (context in material.contexts) {
				if (context.raw.depth_read == true) {
					depthRead = true;
					break;
				}
			}
		}
		#end
	}

	public inline function computeScreenSize(camera: CameraObject) {
		// Approx..
		// var rp = camera.renderPath;
		// var screenVolume = rp.currentW * rp.currentH;
		var tr = transform;
		var volume = tr.dim.x * tr.dim.y * tr.dim.z;
		screenSize = volume * (1.0 / cameraDistance);
		screenSize = screenSize > 1.0 ? 1.0 : screenSize;
	}

	inline function initLods() {
		if (lods == null) {
			lods = [];
			for (l in raw.lods) {
				if (l.object_ref == "") lods.push(null); // Empty
				else lods.push(Scene.active.getChild(l.object_ref));
			}
		}
	}
}