use std::convert::TryInto;
use std::ffi::{CStr, CString};

pub struct ShaderObject {
	id: u32,
}

impl ShaderObject {
	pub fn id (&self) -> u32 {
		self.id
	}
	
	pub fn from_source (shader_type: u32, source: &[u8])
	-> Result <ShaderObject, String>
	{
		let id = unsafe {
			gl::CreateShader (shader_type)
		};
		
		let sources = [
			source.as_ptr () as *const i8,
		];
		
		let lengths = [
			source.len ().try_into ().unwrap (),
		];
		
		let success = unsafe {
			gl::ShaderSource (id, sources.len ().try_into ().unwrap (), sources.as_ptr (), lengths.as_ptr ());
			gl::CompileShader (id);
			
			let mut success = 0;
			gl::GetShaderiv (id, gl::COMPILE_STATUS, &mut success);
			success == 1
		};
		
		if success {
			Ok (ShaderObject {
				id,
			})
		}
		else {
			let mut info_log = vec! [0u8; 4096];
			let mut log_length = 0;
			
			unsafe {
				gl::GetShaderInfoLog (id, (info_log.len () - 1).try_into ().unwrap (), &mut log_length, info_log.as_mut_ptr () as *mut i8);
			}
			
			info_log.truncate (log_length.try_into ().unwrap ());
			
			let info = String::from_utf8 (info_log).unwrap ();
			
			Err (info)
		}
	}
	
	pub fn from_file <P> (shader_type: u32, filename: P)
	-> Result <ShaderObject, String>
	where P: AsRef <std::path::Path>
	{
		let src = crate::file::load_small_file (filename, 1024 * 1024).unwrap ();
		Self::from_source (shader_type, &src)
	}
}

impl Drop for ShaderObject {
	fn drop (&mut self) {
		unsafe {
			gl::DeleteShader (self.id);
		}
	}
}

#[derive (PartialEq, Eq)]
pub struct ShaderProgram {
	id: u32,
}

impl ShaderProgram {
	pub fn new (vert: &ShaderObject, frag: &ShaderObject) 
	-> Result <ShaderProgram, String> 
	{
		let id = unsafe {
			gl::CreateProgram ()
		};
		
		unsafe {
			gl::AttachShader (id, vert.id ());
			gl::AttachShader (id, frag.id ());
			
			gl::LinkProgram (id);
		}
		
		let success = unsafe {
			let mut success = 0;
			gl::GetProgramiv (id, gl::LINK_STATUS, &mut success);
			success == 1
		};
		
		if success {
			Ok (ShaderProgram {
				id,
			})
		}
		else {
			let mut info_log = vec! [0u8; 4096];
			let mut log_length = 0;
			
			unsafe {
				gl::GetProgramInfoLog (id, (info_log.len () - 1).try_into ().unwrap (), &mut log_length, info_log.as_mut_ptr () as *mut i8);
			}
			
			info_log.truncate (log_length.try_into ().unwrap ());
			
			let info = String::from_utf8 (info_log).unwrap ();
			
			Err (info)
		}
	}
	
	pub fn get_id (&self) -> u32 {
		self.id
	}
	
	// 'use' is a keyword
	
	pub fn use_program (&self) {
		unsafe {
			gl::UseProgram (self.id);
		}
	}
	
	pub fn get_uniform_location (&self, name: &CStr) -> i32 {
		self.use_program ();
		
		unsafe {
			gl::GetUniformLocation (self.id, name.as_ptr ())
		}
	}
	
	pub fn get_uniform_locations <'a, I> (&self, names: I)
	-> Vec <i32>
	where I: Iterator <Item = &'a str>
	{
		self.use_program ();
		
		names
		.map (|name| {
			let c_str = CString::new (name.as_bytes ()).unwrap ();
			let loc = unsafe {
				gl::GetUniformLocation (self.id, c_str.as_ptr ())
			};
			
			loc
		})
		.collect ()
	}
	
	pub fn get_attribute_location (&self, name: &CStr) -> i32 {
		self.use_program ();
		
		unsafe {
			gl::GetAttribLocation (self.id, name.as_ptr ())
		}
	}
	
	// This could be an iterator adapter but the code is too long
	// and it's only called during startup so I don't care.
	
	pub fn get_attribute_locations <'a, I> (&self, names: I)
	-> Vec <Option <u32>>
	where I: Iterator <Item = &'a str>
	{
		self.use_program ();
		
		names
		.map (|name| {
			let c_str = CString::new (name.as_bytes ()).unwrap ();
			let loc = unsafe {
				gl::GetAttribLocation (self.id, c_str.as_ptr ())
			};
			
			match loc.try_into () {
				Ok (loc) => Some (loc),
				_ => None,
			}
		})
		.collect ()
	}
}

impl Drop for ShaderProgram {
	fn drop (&mut self) {
		unsafe {
			gl::DeleteProgram (self.id);
		}
	}
}

pub fn shader_from_files <P> (vert: P, frag: P) -> ShaderProgram
where P: AsRef <std::path::Path>
{
	let vert_shader = ShaderObject::from_file (gl::VERTEX_SHADER, vert).unwrap ();
	let frag_shader = ShaderObject::from_file (gl::FRAGMENT_SHADER, frag).unwrap ();
	
	ShaderProgram::new (&vert_shader, &frag_shader).unwrap ()
}
