use std::{
	collections::HashMap,
};

use glam::{
	Quat,
	// Vec4,
};

use opengl_rust::{
	prelude::*,
	gl_state::{
		FrontFace,
		GlState,
		Pass,
	},
	renderable_model::{
		RenderableModel,
		attributes,
	},
	shader_closure::ShaderLookup,
	texture::Texture,
};

mod uniforms {
use iota::iota;
iota! {
pub const 
	  MVP: u32 = iota;
	, OBJECT_SPACE_LIGHT
	, OBJECT_SPACE_SKY
	, ALBEDO
	, MIN_ALBEDO
	, MIN_BRIGHT
	, TEXTURE
}
}

pub struct Graphics {
	passes: Vec <Pass>,
	
	shaders: Vec <ShaderClosure>,
	shader_lookup: HashMap <u32, usize>,
	
	// mesh_cube: RenderableModel,
	mesh_protag_kun: RenderableModel,
	mesh_sky: RenderableModel,
	mesh_sphere: RenderableModel,
	
	// text_stream: TriangleStream,
	
	texture_crate: Texture,
	texture_earth: Texture,
	texture_sky: Texture,
	texture_white: Texture,
	
	pub frames: u64,
}

impl ShaderLookup for Graphics {
	fn lookup <'a> (&'a self, id: u32) -> &'a ShaderClosure {
		&self.shaders [self.shader_lookup [&id]]
	}
}

impl Graphics {
	pub fn new () -> Self {
		let uniform_names = {
			vec! [
				(uniforms::MVP, "uni_mvp"),
				(uniforms::OBJECT_SPACE_LIGHT, "uni_object_space_light"),
				(uniforms::OBJECT_SPACE_SKY, "uni_object_space_sky"),
				(uniforms::ALBEDO, "uni_albedo"),
				(uniforms::MIN_ALBEDO, "uni_min_albedo"),
				(uniforms::MIN_BRIGHT, "uni_min_bright"),
				(uniforms::TEXTURE, "uni_texture"),
			]
		};
		
		let attr_names = {
			vec! [
				(attributes::POS, "attr_pos"),
				(attributes::UV, "attr_uv"),
				(attributes::NORMAL, "attr_normal"),
			]
		};
		
		let shaders: Vec <_> = [
			("shaders/pumpkin-vert.glsl", "shaders/pumpkin-frag.glsl"),
			("shaders/shadow-vert.glsl", "shaders/shadow-frag.glsl"),
			("shaders/terrain-vert.glsl", "shaders/terrain-frag.glsl"),
		].into_iter ()
		.map (|(v, f)| {
			ShaderClosure::new (shader_from_files (v, f), &uniform_names, &attr_names)
		})
		.collect ();
		
		// let mesh_cube = renderable_from_iqm_file ("cube.iqm");
		let mesh_protag_kun = renderable_from_iqm_file ("protag-kun.iqm");
		let mesh_sky = renderable_from_iqm_file ("sky-sphere.iqm");
		let mesh_sphere = renderable_from_iqm_file ("sphere.iqm");
		
		let passes = vec![
			// Clear everything
			Pass::default ()
				.color_mask ([1, 1, 1, 1])
				.depth_mask (1)
			.clone (),
			// Draw world
			Pass::default ()
				.shader (&shaders [0])
				.flags ([
					(gl::CULL_FACE, false),
					(gl::DEPTH_TEST, true),
					(gl::TEXTURE_2D, true),
					(gl::STENCIL_TEST, false),
				].into_iter ())
				.front_face (FrontFace::Ccw)
				.color_mask ([1, 1, 1, 1])
				.depth_mask (1)
			.clone (),
		];
		/*
		let text_stream = TriangleStream {
			verts: gpu_buffers::VertexBuffer::streaming (4 * 5 * 6 * 1024),
			indices: {
				let quad = [
					0, 1, 2,
					0, 2, 3,
				];
				
				let v: Vec <u32> = (0u32..1024).map (|i| {
					quad.iter ().map (move |j| 4 * i + j)
				}).flatten ().collect ();
				gpu_buffers::IndexBuffer::from_slice_u32 (&v)
			}
		};
		*/
		let shader_lookup = HashMap::from_iter (shaders.iter ().enumerate ()
		.map (|(i, s)| {
			(s.get_id (), i)
		}));
		
		shaders [0].with (None, |shader_vars| {
			let attrs = shader_vars.attrs;
			glezz::enable_vertex_attrib_array (attrs [attributes::POS]);
			glezz::enable_vertex_attrib_array (attrs [attributes::UV]);
			glezz::enable_vertex_attrib_array (attrs [attributes::NORMAL]);
		});
		
		Graphics {
			// mesh_cube,
			mesh_protag_kun,
			mesh_sky,
			mesh_sphere,
			passes,
			shader_lookup,
			shaders,
			// text_stream,
			texture_crate: Texture::from_file ("crate.png"),
			texture_earth: Texture::from_file ("earth.png"),
			texture_sky: Texture::from_file ("sky.png"),
			texture_white: Texture::from_file ("white.png"),
			frames: 0,
		}
	}
	
	pub fn draw (
		&self,
		phys_tris: &[opengl_rust::physics::Triangle],
		logic: &crate::LogicState,
		gl_state: &mut GlState,
		level: &crate::LoadedLevel,
		camera: &crate::level_loader::Camera,
	) {
		// use serde::Deserialize;
		use uniforms as u;
		/*
		#[derive (Default, Deserialize)]
		struct Config {
			camera: CameraCfg,
		}
		
		#[derive (Default, Deserialize)]
		struct CameraCfg {
			#[serde(default)]
			rotate_mix: f32,
			#[serde(default)]
			translate_mix: f32,
		}
		
		let cfg: Option <Config> = std::fs::read_to_string ("config.json").ok ()
		.map (|s| serde_json::from_str (s.as_str ()).ok ())
		.flatten ();
		
		let cfg = cfg.unwrap_or_else (Config::default);
		*/
		let white = color_from_255 ((255.0, 255.0, 255.0));
		let black = color_from_255 ((0.0, 0.0, 0.0));
		let shadow_blue = color_from_255 ((68.0, 36.0, 52.0));
		
		let screen_size = (320.0, 240.0);
		
		let fov = 39.6f32 * screen_size.1 / screen_size.0;
		
		let proj_mat = Mat4::perspective_rh_gl (fov.to_radians (), screen_size.0 / screen_size.1, 0.125, 200.0);
		
		let (
			cam_trans,
			cam_rot,
			_
		) = camera.transform.clone ().decomposed ();
		
		let cam_trans = Vec3::from (cam_trans);
		let cam_trans = Vec3::new (cam_trans.x.max (logic.player.pos.x + 4.0), cam_trans.y, cam_trans.z);
		
		let view_mat = 
		Mat4::from_quat (Quat::from_array (cam_rot).inverse ()) *
		Mat4::from_translation (-cam_trans);
		
		// println! ("Camera is at {:?}", view_mat.inverse ().transform_point3 ((0.0, 0.0, 0.0).into ()));
		
		let view_mat = proj_mat * view_mat;
		
		self.passes [0].with (gl_state, || {
			glezz::clear_color (109f32 / 255.0, 194f32 / 255.0, 202f32 / 255.0, 1.0f32);
			glezz::clear (gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT | gl::STENCIL_BUFFER_BIT);
		});
		
		self.passes [1].with_shader (gl_state, self, |shader_vars| {
			let attrs = shader_vars.attrs;
			let unis = shader_vars.unis;
			
			glezz::uniform_3fv (unis [&u::ALBEDO], &white);
			glezz::uniform_3fv (unis [&u::MIN_BRIGHT], &white);
			
			if true {
				self.texture_earth.bind ();
				
				let mvp = view_mat * 
				Mat4::from_translation (logic.player.pos) * 
				Mat4::from_scale ((0.5, 0.5, 0.5).into ());
				glezz::uniform_matrix_4fv (unis [&u::MVP], &mvp);
				glezz::uniform_3fv (unis [&u::ALBEDO], &white);
				
				self.mesh_protag_kun.draw_all (attrs, |_| {
					true
				});
			}
			
			self.texture_crate.bind ();
			glezz::uniform_3fv (unis [&u::ALBEDO], &white);
			
			if true {
				use opengl_rust::renderable_model::attributes as a;
				
				let scene = level.level.scenes ().next ().unwrap ();
				
				unsafe {
					gl::BindBuffer (gl::ARRAY_BUFFER, 0);
					gl::BindBuffer (gl::ELEMENT_ARRAY_BUFFER, 0);
				}
				
				for node in scene.nodes () {
					let mesh = match node.mesh () {
						None => continue,
						Some (x) => x,
					};
					
					let mvp = view_mat * 
					Mat4::from_cols_array_2d (&node.transform ().matrix ());
					glezz::uniform_matrix_4fv (unis [&u::MVP], &mvp);
					
					for prim in mesh.primitives () {
						use gltf::{
							Semantic,
						};
						
						let positions = match prim.get (&Semantic::Positions) {
							None => continue,
							Some (x) => x,
						};
						let pos_view = match positions.view () {
							None => continue,
							Some (x) => x,
						};
						let uv_coords = match prim.get (&Semantic::TexCoords (0)) {
							None => continue,
							Some (x) => x,
						};
						let uv_view = match uv_coords.view () {
							None => continue,
							Some (x) => x,
						};
						let indices = match prim.indices () {
							None => continue,
							Some (x) => x,
						};
						let indices_view = match indices.view () {
							None => continue,
							Some (x) => x,
						};
						
						unsafe {
							gl::VertexAttribPointer (
								attrs [a::POS].unwrap (),
								3,
								gl::FLOAT,
								0,
								4 * 3,
								&level.buffer [positions.offset () + pos_view.offset ()] as *const u8 as *const c_void,
							);
							gl::VertexAttribPointer (
								attrs [a::UV].unwrap (),
								2,
								gl::FLOAT,
								0,
								4 * 2,
								&level.buffer [uv_coords.offset () + uv_view.offset ()] as *const u8 as *const c_void,
							);
							gl::DrawElements (
								gl::TRIANGLES,
								indices.count ().try_into ().unwrap (),
								gl::UNSIGNED_SHORT,
								&level.buffer [indices.offset () + indices_view.offset ()] as *const u8 as *const c_void,
							);
						}
					}
				}
			}
			
			glezz::uniform_3fv (unis [&u::ALBEDO], &shadow_blue);
			
			if true {
				// Raycast for player shadow
				let coll = opengl_rust::physics::get_candidate (
					&phys_tris, &[],
					logic.player.pos,
					logic.player.pos + Vec3::new (0.0, 0.0, -100.0),
					0.0
				);
				
				self.texture_white.bind ();
				
				// kill_z
				if coll.t <= 1.0 && coll.p_impact.z > -3.0 {
					let mvp = view_mat * 
					Mat4::from_translation (coll.p_impact + Vec3::new (0.0, 0.0, 0.0625)) * 
					Mat4::from_scale ((0.5, 0.5, 0.0).into ());
					glezz::uniform_matrix_4fv (unis [&u::MVP], &mvp);
					
					self.mesh_sphere.draw_all (attrs, |_| {
						true
					});
				}
			}
			
			if true {
				let sky_mvp_mat = view_mat * Mat4::from_scale ((64.0, 64.0, 64.0).into ());
				
				self.texture_sky.bind ();
				glezz::uniform_matrix_4fv (unis [&u::MVP], &sky_mvp_mat);
				glezz::uniform_3fv (unis [&u::ALBEDO], &white);
				glezz::uniform_3fv (unis [&u::MIN_BRIGHT], &white);
				glezz::uniform_3fv (unis [&u::MIN_ALBEDO], &black);
				glezz::uniform_1i (unis [&u::TEXTURE], 0);
				
				self.mesh_sky.draw_all (attrs, |_| true);
			}
		});
	}
}

fn color_from_255 <V> (rgb: V) -> Vec3
where V: Into <Vec3>
{
	let rgb: Vec3 = rgb.into ();
	
	Vec3::from ((
		rgb.x / 255.0,
		rgb.y / 255.0,
		rgb.z / 255.0
	))
}
