package iron.object;

import kha.graphics4.Graphics;
import kha.graphics4.TextureFormat;
import kha.graphics4.DepthStencilFormat;
import kha.graphics4.CubeMap;
import iron.data.ProbeData;
import iron.data.CameraData;
import iron.data.SceneFormat;
import iron.math.Vec4;
import iron.math.Mat4;

class ProbeObject extends Object {

#if rp_probes

	public var data: ProbeData;
	public var renderTarget: kha.Image = null;
	public var camera: CameraObject = null;
	public var ready = false;

	// Cubemap update
	public var perFrame = false; // Update probe every frame
	public var redraw = true; // Update probe next frame

	var m1: Mat4;
	var m2: Mat4;
	var proben: Vec4;
	var probep: Vec4;
	// static var v = new Vec4();
	static var p = new Vec4();
	static var q = new Vec4();

	public function new(data: ProbeData) {
		super();
		this.data = data;
		Scene.active.probes.push(this);
		iron.App.notifyOnInit(init);
	}

	public override function remove() {
		if (Scene.active != null) Scene.active.probes.remove(this);
		// if (camera != null) camera.remove();
		super.remove();
	}

	function init() {
		probep = transform.world.getLoc();
		proben = transform.up().normalize();
		proben.w = -probep.dot(proben);

		if (data.raw.type == "planar") {
			m1 = Mat4.identity();
			m2 = Mat4.identity();
			reflect(m1, proben, probep);
			reflect(m2, new Vec4(0, 1, 0), probep);

			transform.scale.z = 1.0; // Only take dim.z into account
			transform.buildMatrix();

			// var aspect = transform.scale.x / transform.scale.y;
			var aspect = iron.App.w() / iron.App.h(); // TODO
			var craw: TCameraData = {
				name: raw.name + "_Camera",
				near_plane: Scene.active.camera.data.raw.near_plane,
				far_plane: Scene.active.camera.data.raw.far_plane,
				fov: Scene.active.camera.data.raw.fov,
				aspect: aspect
			};
			new CameraData(craw, function(cdata: CameraData) {
				camera = new CameraObject(cdata);
				camera.renderTarget = kha.Image.createRenderTarget(
					iron.App.w(), // TODO
					iron.App.h(),
					TextureFormat.RGBA32,
					DepthStencilFormat.NoDepthAndStencil
				);
				camera.name = craw.name;
				camera.setParent(iron.Scene.active.root);
				// Make target bindable from render path
				var rt = new RenderPath.RenderTarget(new RenderPath.RenderTargetRaw());
				rt.raw.name = raw.name;
				rt.image = camera.renderTarget;
				RenderPath.active.renderTargets.set(rt.raw.name, rt);
				ready = true;
			});
		}
		else if (data.raw.type == "cubemap") {
			transform.scale.x *= transform.dim.x;
			transform.scale.y *= transform.dim.y;
			transform.scale.z *= transform.dim.z;
			transform.buildMatrix();

			var craw: TCameraData = {
				name: data.raw.name + "_Camera",
				near_plane: Scene.active.camera.data.raw.near_plane,
				far_plane: Scene.active.camera.data.raw.far_plane,
				fov: 1.5708, // pi/2
				aspect: 1.0
			};
			new CameraData(craw, function(cdata: CameraData) {
				camera = new CameraObject(cdata);
				camera.renderTargetCube = CubeMap.createRenderTarget(
					1024, // TODO
					TextureFormat.RGBA32,
					DepthStencilFormat.NoDepthAndStencil
				);
				camera.name = craw.name;
				camera.setParent(iron.Scene.active.root);
				// Make target bindable from render path
				var rt = new RenderPath.RenderTarget(new RenderPath.RenderTargetRaw());
				rt.raw.name = raw.name;
				rt.raw.is_cubemap = true;
				rt.isCubeMap = true;
				rt.cubeMap = camera.renderTargetCube;
				RenderPath.active.renderTargets.set(rt.raw.name, rt);
				ready = true;
			});
		}
	}

	static function reflect(m: Mat4, n: Vec4, p: Vec4) {
		var c = -p.dot(n);
		m._00 = 1 - 2 * n.x * n.x;
		m._10 =   - 2 * n.x * n.y;
		m._20 =   - 2 * n.x * n.z;
		m._30 =   - 2 * n.x * c;
		m._01 =   - 2 * n.x * n.y;
		m._11 = 1 - 2 * n.y * n.y;
		m._21 =   - 2 * n.y * n.z;
		m._31 =   - 2 * n.y * c;
		m._02 =   - 2 * n.x * n.z;
		m._12 =   - 2 * n.y * n.z;
		m._22 = 1 - 2 * n.z * n.z;
		m._32 =   - 2 * n.z * c;
		m._03 = 0;
		m._13 = 0;
		m._23 = 0;
		m._33 = 1;
	}

	static inline function sign(f: Float): Float {
		return f > 0.0 ? 1.0 : f < 0.0 ? -1.0 : 0.0;
	}

	static function obliqueProjection(m: Mat4, plane: Vec4) {
		// http://www.terathon.com/code/oblique.html
		p.x = (sign(plane.x) + m._20) / m._00;
		p.y = (sign(plane.y) + m._21) / m._11;
		p.z = -1.0;
		p.w = (1.0 + m._22) / m._32;
		q.setFrom(plane).mult(2.0 / plane.dot(p));
		m._02 = q.x;
		m._12 = q.y;
		m._22 = q.z + 1.0;
		m._32 = q.w;
	}

	function cullProbe(camera: CameraObject): Bool {
		if (camera.data.raw.frustum_culling) {
			if (!CameraObject.sphereInFrustum(camera.frustumPlanes, transform, 1.0)) {
				culled = true;
				return culled;
			}
		}
		culled = false;
		return culled;
	}

	public function render(g: Graphics, activeCamera: CameraObject) {
		if (camera == null || !ready || !RenderPath.active.ready || !visible || cullProbe(activeCamera)) return;

		if (data.raw.type == "planar") {
			camera.V.setFrom(m1);
			camera.V.multmat(activeCamera.V);
			camera.V.multmat(m2);
			camera.transform.local.getInverse(camera.V);
			camera.transform.decompose();
			// Skip objects below the reflection plane
			// v.setFrom(proben).applyproj(camera.V);
			// obliqueProjection(#if (lnx_taa) camera.noJitterP #else camera.P #end, v);
			camera.renderFrame(g);
		}
		else if (data.raw.type == "cubemap") {
			if (perFrame || redraw) {
				for (i in 0...6) {
					camera.currentFace = i;
					#if (!kha_opengl && !kha_webgl)
					var flip = (i == 2 || i == 3) ? true : false; // Flip +Y, -Y
					#else
					var flip = false;
					#end
					CameraObject.setCubeFace(camera.V, probep, i, flip);
					camera.transform.local.getInverse(camera.V);
					camera.transform.decompose();
					camera.renderFrame(g);
				}
			}
		}

		redraw = false;
	}

#end
}