/**********************************************************************
*
* .obj loader
*
* Copyright (c) 2021 Dario Deledda. All rights reserved.
* Use of this source code is governed by an MIT license
* that can be found in the LICENSE file.
*
* TODO:
**********************************************************************/
module obj

import sokol.gfx
import gg.m4
import math
import stbi

/******************************************************************************
* Texture functions
******************************************************************************/
pub fn create_texture(w int, h int, buf &u8) (gfx.Image, gfx.Sampler) {
	sz := w * h * 4
	mut img_desc := gfx.ImageDesc{
		width: w
		height: h
		num_mipmaps: 0
		// min_filter: .linear
		// mag_filter: .linear
		// usage: .dynamic
		// wrap_u: .clamp_to_edge
		// wrap_v: .clamp_to_edge
		label: &u8(0)
		d3d11_texture: 0
	}
	// comment if .dynamic is enabled
	img_desc.data.subimage[0][0] = gfx.Range{
		ptr: buf
		size: usize(sz)
	}

	sg_img := gfx.make_image(&img_desc)

	mut smp_desc := gfx.SamplerDesc{
		min_filter: .linear
		mag_filter: .linear
		wrap_u: .clamp_to_edge
		wrap_v: .clamp_to_edge
	}

	sg_smp := gfx.make_sampler(&smp_desc)
	return sg_img, sg_smp
}

pub fn destroy_texture(sg_img gfx.Image) {
	gfx.destroy_image(sg_img)
}

pub fn load_texture(file_name string) (gfx.Image, gfx.Sampler) {
	buffer := read_bytes_from_file(file_name)
	stbi.set_flip_vertically_on_load(true)
	img := stbi.load_from_memory(buffer.data, buffer.len) or {
		eprintln('Texure file: [${file_name}] ERROR!')
		exit(0)
	}
	sg_img, sg_smp := create_texture(int(img.width), int(img.height), img.data)
	img.free()
	return sg_img, sg_smp
}

/******************************************************************************
* Pipeline
******************************************************************************/
pub fn (mut obj_part ObjPart) create_pipeline(in_part []int, shader gfx.Shader, texture gfx.Image, sampler gfx.Sampler) Render_data {
	mut res := Render_data{}
	obj_buf := obj_part.get_buffer(in_part)
	res.n_vert = obj_buf.n_vertex
	res.material = obj_part.part[in_part[0]].material

	// vertex buffer
	mut vert_buffer_desc := gfx.BufferDesc{}
	unsafe { vmemset(&vert_buffer_desc, 0, int(sizeof(vert_buffer_desc))) }

	vert_buffer_desc.size = usize(obj_buf.vbuf.len * int(sizeof(Vertex_pnct)))
	vert_buffer_desc.data = gfx.Range{
		ptr: obj_buf.vbuf.data
		size: usize(obj_buf.vbuf.len * int(sizeof(Vertex_pnct)))
	}

	vert_buffer_desc.@type = .vertexbuffer
	vert_buffer_desc.label = 'vertbuf_part_${in_part:03}'.str
	vbuf := gfx.make_buffer(&vert_buffer_desc)

	// index buffer
	mut index_buffer_desc := gfx.BufferDesc{}
	unsafe { vmemset(&index_buffer_desc, 0, int(sizeof(index_buffer_desc))) }

	index_buffer_desc.size = usize(obj_buf.ibuf.len * int(sizeof(u32)))
	index_buffer_desc.data = gfx.Range{
		ptr: obj_buf.ibuf.data
		size: usize(obj_buf.ibuf.len * int(sizeof(u32)))
	}

	index_buffer_desc.@type = .indexbuffer
	index_buffer_desc.label = 'indbuf_part_${in_part:03}'.str
	ibuf := gfx.make_buffer(&index_buffer_desc)

	mut pipdesc := gfx.PipelineDesc{}
	unsafe { vmemset(&pipdesc, 0, int(sizeof(pipdesc))) }
	pipdesc.layout.buffers[0].stride = int(sizeof(Vertex_pnct))

	// the constants [C.ATTR_vs_a_Position, C.ATTR_vs_a_Color, C.ATTR_vs_a_Texcoord0] are generated by sokol-shdc
	pipdesc.layout.attrs[C.ATTR_vs_a_Position].format = .float3 // x,y,z as f32
	pipdesc.layout.attrs[C.ATTR_vs_a_Normal].format = .float3 // x,y,z as f32
	// pipdesc.layout.attrs[C.ATTR_vs_a_Color].format = .ubyte4n // color as u32
	pipdesc.layout.attrs[C.ATTR_vs_a_Texcoord0].format = .float2 // u,v as f32
	// pipdesc.layout.attrs[C.ATTR_vs_a_Texcoord0].format  = .short2n  // u,v as u16
	pipdesc.index_type = .uint32

	color_state := gfx.ColorTargetState{
		blend: gfx.BlendState{
			enabled: true
			src_factor_rgb: .src_alpha
			dst_factor_rgb: .one_minus_src_alpha
		}
	}
	pipdesc.colors[0] = color_state

	pipdesc.depth = gfx.DepthState{
		write_enabled: true
		compare: .less_equal
	}
	pipdesc.cull_mode = .front

	pipdesc.label = 'pip_part_${in_part:03}'.str

	// shader
	pipdesc.shader = shader

	res.bind.vertex_buffers[0] = vbuf
	res.bind.index_buffer = ibuf
	res.bind.fs.images[C.SLOT_tex] = texture
	res.bind.fs.samplers[C.SLOT_smp] = sampler
	res.pipeline = gfx.make_pipeline(&pipdesc)
	// println('Buffers part [$in_part] init done!')

	return res
}

/******************************************************************************
* Render functions
******************************************************************************/
// aggregate all the part by materials
pub fn (mut obj_part ObjPart) init_render_data(texture gfx.Image, sampler gfx.Sampler) {
	// create shader
	// One shader for all the model
	shader := gfx.make_shader(C.gouraud_shader_desc(gfx.query_backend()))

	mut part_dict := map[string][]int{}
	for i, p in obj_part.part {
		if p.faces.len > 0 {
			part_dict[p.material] << i
		}
	}
	obj_part.rend_data.clear()
	// println("Material dict: ${obj_part.mat_map.keys()}")

	for k, v in part_dict {
		// println("$k => Parts $v")

		mut txt := texture
		mut smp := sampler

		if k in obj_part.mat_map {
			mat_map := obj_part.mat[obj_part.mat_map[k]]
			if 'map_Kd' in mat_map.maps {
				file_name := mat_map.maps['map_Kd']
				if file_name in obj_part.texture {
					txt = obj_part.texture[file_name]
					// println("Texture [${file_name}] => from CACHE")
				} else {
					txt, smp = load_texture(file_name)
					obj_part.texture[file_name] = txt
					obj_part.sampler[file_name] = smp
					// println("Texture [${file_name}] => LOADED")
				}
			}
		}
		// key := obj_part.texture.keys()[0]
		// obj_part.rend_data << obj_part.create_pipeline(v, shader, obj_part.texture[key])
		obj_part.rend_data << obj_part.create_pipeline(v, shader, txt, smp)
	}
	// println("Texture array len: ${obj_part.texture.len}")
	// println("Calc bounding box.")
	obj_part.calc_bbox()
	println('init_render_data DONE!')
}

pub fn (obj_part ObjPart) bind_and_draw(rend_data_index int, in_data Shader_data) u32 {
	// apply the pipeline and bindings
	mut part_render_data := obj_part.rend_data[rend_data_index]

	// pass light position
	mut tmp_fs_params := Tmp_fs_param{}
	tmp_fs_params.light = in_data.fs_data.light

	if part_render_data.material in obj_part.mat_map {
		mat_index := obj_part.mat_map[part_render_data.material]
		mat := obj_part.mat[mat_index]

		// ambient
		tmp_fs_params.ka = in_data.fs_data.ka
		if 'Ka' in mat.ks {
			tmp_fs_params.ka = mat.ks['Ka']
		}

		// specular
		tmp_fs_params.ks = in_data.fs_data.ks
		if 'Ks' in mat.ks {
			tmp_fs_params.ks = mat.ks['Ks']
		}

		//  specular exponent Ns
		if 'Ns' in mat.ns {
			tmp_fs_params.ks.e[3] = mat.ns['Ns'] / 1000.0
		} else {
			// default value is 10
			tmp_fs_params.ks.e[3] = f32(10) / 1000.0
		}

		// diffuse
		tmp_fs_params.kd = in_data.fs_data.kd
		if 'Kd' in mat.ks {
			tmp_fs_params.kd = mat.ks['Kd']
		}

		// alpha/transparency
		if 'Tr' in mat.ns {
			tmp_fs_params.kd.e[3] = mat.ns['Tr']
		}
	}

	gfx.apply_pipeline(part_render_data.pipeline)
	gfx.apply_bindings(part_render_data.bind)

	vs_uniforms_range := gfx.Range{
		ptr: in_data.vs_data
		size: usize(in_data.vs_len)
	}
	fs_uniforms_range := gfx.Range{
		ptr: unsafe { &tmp_fs_params }
		size: usize(in_data.fs_len)
	}

	gfx.apply_uniforms(.vs, C.SLOT_vs_params, &vs_uniforms_range)
	gfx.apply_uniforms(.fs, C.SLOT_fs_params, &fs_uniforms_range)
	gfx.draw(0, int(part_render_data.n_vert), 1)
	return part_render_data.n_vert
}

pub fn (obj_part ObjPart) bind_and_draw_all(in_data Shader_data) u32 {
	mut n_vert := u32(0)
	// println("Parts: ${obj_part.rend_data.len}")
	for i, _ in obj_part.rend_data {
		n_vert += obj_part.bind_and_draw(i, in_data)
	}
	return n_vert
}

pub fn (mut obj_part ObjPart) calc_bbox() {
	obj_part.max = m4.Vec4{
		e: [f32(-math.max_f32), -math.max_f32, -math.max_f32, 0]!
	}
	obj_part.min = m4.Vec4{
		e: [f32(math.max_f32), math.max_f32, math.max_f32, 0]!
	}
	for v in obj_part.v {
		if v.e[0] > obj_part.max.e[0] {
			obj_part.max.e[0] = v.e[0]
		}
		if v.e[1] > obj_part.max.e[1] {
			obj_part.max.e[1] = v.e[1]
		}
		if v.e[2] > obj_part.max.e[2] {
			obj_part.max.e[2] = v.e[2]
		}

		if v.e[0] < obj_part.min.e[0] {
			obj_part.min.e[0] = v.e[0]
		}
		if v.e[1] < obj_part.min.e[1] {
			obj_part.min.e[1] = v.e[1]
		}
		if v.e[2] < obj_part.min.e[2] {
			obj_part.min.e[2] = v.e[2]
		}
	}
	val1 := obj_part.max.mod3()
	val2 := obj_part.min.mod3()
	if val1 > val2 {
		obj_part.radius = f32(val1)
	} else {
		obj_part.radius = f32(val2)
	}
	// println("BBox: ${obj_part.min} <=> ${obj_part.max}\nRadius: ${obj_part.radius}")
}
