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

use anyhow::Result;
use maplit::hashmap;

use opengl_rust::{
	prelude::*,
	gl_state::*,
	physics::PhysicsBody,
	renderable_model::{
		attributes,
		RenderableModel,
	},
	shader_closure::ShaderLookup,
	texture::Texture,
};

mod graphics;

use graphics::Graphics;

#[derive (Default)]
pub struct GameState {
	player: PhysicsBody,
	aabbs: Vec <opengl_rust::physics::Aabb>,
	phys_tris: Vec <opengl_rust::physics::Triangle>,
}

impl GameState {
	fn reset_level (&mut self, level: &LoadedLevel) {
		self.player = Default::default ();
		self.player.pos = level.player_spawn;
	}
}

#[tokio::main]
async fn main () -> Result <()> {
	tracing_subscriber::fmt::fmt ()
	.with_env_filter (tracing_subscriber::EnvFilter::from_default_env())
	.with_span_events (tracing_subscriber::fmt::format::FmtSpan::CLOSE)
	.init ();
	
	let sdl_context = sdl2::init ().map_err (|e| anyhow! ("Can't init SDL: {}", e))?;
	let video_subsystem = sdl_context.video ().map_err (|e| anyhow! ("Can't get SDL video subsystem: {}", e))?;
	let window = video_subsystem.window ("3D platformer", 320 * 2, 240 * 2)
		.position_centered ()
		.opengl ()
		.build ()
	?;
	
	gl::load_with (|s| {
		video_subsystem.gl_get_proc_address (s) as *const _
	});
	
	if ! gl::ClearColor::is_loaded () {
		bail! ("gl::ClearColor didn't load, something is wrong with OpenGL");
	}
	
	let gl_ctx = window.gl_create_context ().map_err (|e| anyhow! ("Can't create OpenGL context: {}", e))?;
	
	window.gl_make_current (&gl_ctx).map_err (|e| anyhow! ("Can't make OpenGL context current: {}", e))?;
	
	let mut event_pump = sdl_context.event_pump ().unwrap ();
	
	let mut time_step = TimeStep::new (60, 1000);
	
	let level = LoadedLevel::from_path ("gltf/level-00.glb")?;
	let scene = level.level.scenes ().next ().unwrap ();
	
	let mut camera = None;
	let mut phys_tris: Vec <opengl_rust::physics::Triangle> = Vec::new ();
	
	for node in scene.nodes () {
		if let Some (c) = node.camera () {
			camera = Some (Camera {
				mat: gltf_node_get_mat4 (&node).inverse (),
			});
		}
		
		let mesh = match node.mesh () {
			None => continue,
			Some (x) => x,
		};
		
		let m = gltf_node_get_mat4 (&node);
		
		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 indices = match prim.indices () {
				None => continue,
				Some (x) => x,
			};
			let indices_view = match indices.view () {
				None => continue,
				Some (x) => x,
			};
			
			let idx_start = indices.offset () + indices_view.offset ();
			let idx_slice = &level.buffer [idx_start..idx_start + indices.count () * 2];
			let vert_start = positions.offset () + pos_view.offset ();
			let vert_slice = &level.buffer [vert_start..vert_start + positions.count () * 4 * 3];
			
			for chunk in idx_slice.chunks_exact (2 * 3) {
				let read_idx = |i: usize| u16::from_le_bytes ([chunk [i * 2 + 0], chunk [i * 2 + 1]]);
				
				let idxs = [
					read_idx (0),
					read_idx (1),
					read_idx (2),
				];
				
				let read_pos_coord = |i| f32::from_le_bytes ([
					vert_slice [i * 4 + 0],
					vert_slice [i * 4 + 1],
					vert_slice [i * 4 + 2],
					vert_slice [i * 4 + 3],
				]);
				
				let read_pos = move |i| {
					let i = usize::try_from (i).unwrap ();
					m.transform_point3 (Vec3::new (
						read_pos_coord (i * 3 + 0),
						read_pos_coord (i * 3 + 1),
						read_pos_coord (i * 3 + 2),
					))
				};
				
				// glTF triangle winding is backwards from what I expected
				// no biggie
				let verts = [
					read_pos (idxs [0]),
					read_pos (idxs [2]),
					read_pos (idxs [1]),
				];
				
				phys_tris.push (opengl_rust::physics::Triangle {
					verts,
				});
			}
		}
	}
	
	let camera = match camera {
		None => bail! ("Couldn't find camera node in glTF file"),
		Some (x) => x,
	};
	
	let mut graphics = Graphics::new ();
	
	let mut gl_state = Default::default ();
	let mut game_state = GameState::default ();
	let phys_params = opengl_rust::physics::Params {
		dt: 1.0 / 60.0,
		gravity: (0.0, 0.0, -0.5).into (),
		margin: 0.00125,
	};
	
	let player_acc = 1.0;
	let player_acc_air = 0.125;
	let player_max_speed = 4.0;
	let player_max_speed_air = 8.0;
	let player_jump_speed = 12.0;
	let kill_z = -3.0;
	let mut player_jump_vec: Option <Vec3> = None;
	
	game_state.reset_level (&level);
	game_state.phys_tris = phys_tris;
	
	'running: loop {
		let _frames_to_do = time_step.step ();
		let mut player_wants_to_jump = false;
		
		for event in event_pump.poll_iter () {
			match event {
				Event::Quit {..} |
				Event::KeyDown { keycode: Some (Keycode::Escape), .. } => {
					break 'running
				},
				Event::KeyDown { keycode: Some (Keycode::R), .. } => {
					game_state.reset_level (&level);
				},
				Event::KeyDown { keycode: Some (Keycode::Space), .. } => {
					player_wants_to_jump = true;
				},
				
				_ => (),
			}
		}
		
		let kb_state = event_pump.keyboard_state ();
		
		let mut wanted_dir = Vec3::default ();
		
		if kb_state.is_scancode_pressed (Scancode::Left) {
			wanted_dir.x -= 1.0;
		}
		if kb_state.is_scancode_pressed (Scancode::Right) {
			wanted_dir.x += 1.0;
		}
		if kb_state.is_scancode_pressed (Scancode::Up) {
			wanted_dir.y += 1.0;
		}
		if kb_state.is_scancode_pressed (Scancode::Down) {
			wanted_dir.y -= 1.0;
		}
		
		let wanted_dir = if wanted_dir.length_squared () >= 1.0 {
			wanted_dir.normalize ()
		}
		else {
			wanted_dir
		};
		
		let old_vel = game_state.player.vel;
		let old_vel_2 = old_vel * Vec3::new (1.0, 1.0, 0.0);
		
		let new_vel_2 = match player_jump_vec.as_ref () {
			Some (v) => {
				// Ground
				let acc = player_acc * v.z * v.z;
				let wanted_vel = wanted_dir * player_max_speed;
				let diff = wanted_vel - old_vel_2;
				
				if diff.length_squared () < acc * acc {
					// We're near the wanted velocity, so snap to it
					wanted_vel
				}
				else {
					// We're not near the wanted velocity, so accelerate
					old_vel_2 + diff.normalize_or_zero () * acc
				}
			},
			// Air
			None => {
				let proposed_vel = old_vel_2 + wanted_dir * player_acc_air;
				
				if old_vel_2.length_squared () < player_max_speed_air * player_max_speed_air {
					// Air control is normal below player_max_speed
					proposed_vel
				}
				else {
					// If the player's input would push them beyond player_max_speed,
					// apply drag to preserve overall energy
					proposed_vel * old_vel_2.length () / proposed_vel.length ()
				}
			},
		};
		
		game_state.player.vel = new_vel_2;
		game_state.player.vel.z = old_vel.z;
		
		// dbg! (game_state.player.vel);
		
		if player_wants_to_jump {
			if let Some (normal) = player_jump_vec.clone () {
				game_state.player.vel.z = 0.0;
				game_state.player.vel += normal * player_jump_speed;
			}
		}
		
		let phys_result = opengl_rust::physics::step (&phys_params, &game_state.phys_tris, &[], 0.5, &game_state.player);
		
		game_state.player = phys_result.body;
		if game_state.player.pos.z < kill_z {
			game_state.reset_level (&level);
		}
		// tracing::debug! ("player pos: {}", game_state.player.pos);
		
		player_jump_vec = None;
		for normal in &phys_result.normals_hit {
			player_jump_vec = Some (match player_jump_vec {
				None => *normal,
				Some (old) => {
					if normal.z > old.z {
						*normal
					}
					else {
						old
					}
				},
			});
		}
		
		dbg! (game_state.player.pos);
		
		window.gl_make_current (&gl_ctx).unwrap ();
		
		graphics.draw (&game_state, &mut gl_state, &level, &camera);
		graphics.frames += 1;
		
		window.gl_swap_window ();
		
		tokio::time::sleep (Duration::from_millis (15)).await;
	}
	
	Ok (())
}

struct LoadedLevel {
	player_spawn: Vec3,
	buffer: Vec <u8>,
	level: gltf::Document,
}

impl LoadedLevel {
	fn from_path (path: &str) -> Result <Self> {
		use gltf::{
			Semantic,
			scene::Transform,
		};
		
		let (level, buffers, _) = gltf::import (path)?;
		let buffer = match buffers.get (0) {
			None => bail! ("gltf didn't load any buffers"),
			Some (x) => x.0.to_vec (),
		};
		
		let scene = match level.scenes ().next () {
			None => bail! ("No scenes in glTF file"),
			Some (x) => x,
		};
		
		let mut player_spawn = None;
		
		for node in scene.nodes () {
			if node.name () == Some ("Player Spawn") {
				let (translation, _, _) = node.transform ().decomposed ();
				player_spawn = Some (Vec3::from (translation));
			}
			else if let Some (camera) = node.camera () {
				
			}
			else if let Some (mesh) = node.mesh () {
				for (i, prim) in mesh.primitives ().enumerate () {
					let positions = match prim.get (&Semantic::Positions) {
						None => continue,
						Some (x) => x,
					};
					let normals = match prim.get (&Semantic::Normals) {
						None => continue,
						Some (x) => x,
					};
					let indices = match prim.indices () {
						None => continue,
						Some (x) => x,
					};
					let pos_view = match positions.view () {
						None => continue,
						Some (x) => x,
					};
					let norm_view = match normals.view () {
						None => continue,
						Some (x) => x,
					};
					let indices_view = match indices.view () {
						None => continue,
						Some (x) => x,
					};
				}
			}
		}
		
		let player_spawn = match player_spawn {
			None => bail! ("glTF file must have `Player Spawn` node"),
			Some (x) => x,
		};
		
		Ok (Self {
			buffer,
			level,
			player_spawn,
		})
	}
}

struct ShaderLocations {
	attr_pos: u32,
	attr_normal: u32,
	attr_color: u32,
	uni_mvp: i32,
}

impl ShaderLocations {
	pub fn new (shader_program: &opengl_rust::shader::ShaderProgram) -> anyhow::Result <Self> 
	{
		let attr = |name: &str| shader_program.get_attribute_location (&CString::new (name.as_bytes ())?).try_into ().context ("Attribute location negative");
		let uni = |name: &str| shader_program.get_uniform_location (&CString::new (name.as_bytes ())?).try_into ().context ("Uniform location bad");
		
		Ok (Self {
			attr_pos:    attr ("attr_pos")?,
			attr_normal: attr ("attr_normal")?,
			attr_color:  attr ("attr_color")?,
			uni_mvp:     uni ("uni_mvp")?,
		})
	}
}

struct TriangleStream {
	pub verts: gpu_buffers::VertexBuffer,
	pub indices: gpu_buffers::IndexBuffer,
}

struct Camera {
	mat: Mat4,
}

fn gltf_node_get_mat4 (node: &gltf::Node) -> Mat4 {
	Mat4::from_cols_array_2d (&node.transform ().matrix ())
}
