use byteorder::{ByteOrder, LittleEndian};

use std::convert::TryInto;
use std::ffi::c_void;

use crate::gpu_buffers::*;
use crate::iqm;

// Takes ownership of mesh stuff in an opaque way that's abstract
// from the IQM model. IQM is zero-copy, but this is not.
// Since it's opaque, I can drop in a VBO/IBO setup when I'm not lazy.

pub struct RenderableMesh {
	first_triangle: usize,
	num_triangles: i32,
	pub name: Vec <u8>,
}

pub struct RenderableModel {
	num_pos: usize,
	num_uv: usize,
	num_normal: usize,
	
	vertexes: VertexBuffer,
	indexes: IndexBuffer,
	
	pub meshes: Vec <RenderableMesh>,
}

unsafe fn vertex_attrib_pointer (id: Option <u32>, num_coords: i32, float_offset: usize) {
	const FALSE_U8: u8 = 0;
	const FLOAT_SIZE: i32 = 4;
	
	if let Some (id) = id {
		gl::VertexAttribPointer (id, num_coords, gl::FLOAT, FALSE_U8, FLOAT_SIZE * num_coords, (float_offset * 4) as *const u8 as *const c_void);
	}
}

pub type AttrMap = Vec <Option <u32>>;

pub mod attributes {
use iota::iota;
iota! {
pub const 
	  POS: usize = iota;
	, UV
	, NORMAL
}
}

impl RenderableModel {
	pub fn from_iqm (model: &iqm::Model) -> RenderableModel {
		let pos_bytes = model.get_vertex_slice (iqm::types::POSITION);
		let uv_bytes = model.get_vertex_slice (iqm::types::TEXCOORD);
		let normal_bytes = model.get_vertex_slice (iqm::types::NORMAL);
		
		let num_pos = pos_bytes.len () / 4;
		let num_uv = uv_bytes.len () / 4;
		let num_normal = normal_bytes.len () / 4;
		
		let num_vertexes = num_pos / 3;
		assert_eq! (num_vertexes * 2, num_uv);
		assert_eq! (num_vertexes * 3, num_normal);
		
		let mut vertex_vec = vec! [0.0; num_pos + num_uv + num_normal];
		
		LittleEndian::read_f32_into (pos_bytes, &mut vertex_vec [0..num_pos]);
		LittleEndian::read_f32_into (uv_bytes, &mut vertex_vec [num_pos..num_pos + num_uv]);
		LittleEndian::read_f32_into (normal_bytes, &mut vertex_vec [num_pos + num_uv..num_pos + num_uv + num_normal]);
		
		let vertexes = VertexBuffer::from_slice (&vertex_vec);
		
		let index_slice = model.get_all_indexes ();
		let indexes = IndexBuffer::from_slice (index_slice);
		
		let max_index: usize = indexes.max ().try_into ().unwrap ();
		
		assert! (max_index * 3 < num_pos);
		assert! (max_index * 2 < num_uv);
		assert! (max_index * 3 < num_normal);
		
		let meshes = model.meshes.iter ().enumerate ()
		.map (|(i, mesh)| RenderableMesh {
			first_triangle: mesh.first_triangle.try_into ().unwrap (),
			num_triangles: mesh.num_triangles.try_into ().unwrap (),
			name: model.get_mesh_name (i).to_owned (),
		})
		.collect ();
		
		Self {
			num_pos,
			num_uv,
			num_normal,
			
			vertexes,
			indexes,
			meshes,
		}
	}
	
	pub fn draw_ranged <F> (&self, 
		attrs: &AttrMap, callback: F, 
		start: usize, end: usize
	) 
	where F: Fn (usize) -> bool
	{
		self.vertexes.bind ();
		self.indexes.bind ();
		
		unsafe {
			use attributes::*;
			vertex_attrib_pointer (attrs [POS], 3, 0);
			vertex_attrib_pointer (attrs [UV], 2, self.num_pos);
			vertex_attrib_pointer (attrs [NORMAL], 3, self.num_pos + self.num_uv);
		}
		
		for mesh_num in start..end {
			if ! callback (mesh_num) {
				continue;
			}
			
			let mesh = &self.meshes [mesh_num];
			
			unsafe {
				gl::DrawRangeElements (gl::TRIANGLES, 0, self.indexes.max (), mesh.num_triangles * 3, gl::UNSIGNED_INT, (mesh.first_triangle * 3 * 4) as *const u8 as *const c_void);
			}
		}
	}
	
	pub fn draw_all <F> (&self, attrs: &AttrMap, callback: F)
	where F: Fn (usize) -> bool
	{
		self.draw_ranged (attrs, callback, 0, self.meshes.len ());
	}
	
	pub fn draw (&self, attrs: &AttrMap, mesh_num: usize) 
	{
		self.draw_ranged (attrs, |_| true, mesh_num, mesh_num + 1);
	}
	
	pub fn num_meshes (&self) -> usize {
		self.meshes.len ()
	}
}

pub fn renderable_from_iqm_file <P> (filename: P) -> RenderableModel
where P: AsRef <std::path::Path>
{
	let data = crate::file::load_small_file (filename, 1024 * 1024).unwrap ();
	let model = crate::iqm::Model::from_slice (&data).unwrap ();
	
	RenderableModel::from_iqm (&model)
}
