use std::collections::HashMap;
use glium::{Display, Frame, Surface, DrawParameters, Program,
	VertexBuffer, IndexBuffer, texture::{Texture2d, SrgbTexture2d}, uniforms::{Uniforms, UniformValue}};
use crate::math::{mult_matrix, mult_matrix3};
use crate::textures;
use serde::Deserialize;

pub const MAX_LIGHTS: usize = 24;

#[derive(Debug, Copy, Clone)]
pub struct Vertex {
	pub position: [f32; 3],
	pub normal: [f32; 3],
	pub texcoords: [f32; 2]
}

implement_vertex!(Vertex, position, normal, texcoords);

#[derive(Clone)]
pub struct MtlInfo {
	pub diffuse_texture: Option<String>,
	pub color: [f32; 3]
}

impl Default for MtlInfo {
	fn default() -> Self {
		Self {
			color: [1., 1., 1.],
			diffuse_texture: None
		}
	}
}

pub struct EnvDrawInfo<'a> {
	pub view_mat: [[f32; 4]; 4],
	pub perspective_mat: [[f32; 4]; 4],
	pub params: &'a DrawParameters<'a>,
	pub lights: [Light; MAX_LIGHTS],
	pub light_count: usize,
	pub textures: &'a HashMap<String, Texture2d>
}

pub struct ObjDrawInfo {
	pub position: [f32; 3],
	pub rotation: [f32; 3],
	pub scale: [f32; 3],
	pub model_mat: Option<[[f32; 4]; 4]>,
	pub color: [f32; 3]
}

impl Default for ObjDrawInfo {
	fn default() -> Self {
		let mut result = Self {
			position: [0., 0., 0.],
			rotation: [0., 0., 0.],
			scale: [1., 1., 1.],
			color: [1., 1., 1.],
			model_mat: None
		};
		result.generate_matrix();
		result
	}
}

pub struct UIDrawInfo {
	pub position: (f32, f32),
	pub scale: (f32, f32),
	pub left_clip: f32,
	pub screen_left_clip: f32,
	pub screen_dim: (u32, u32),
	pub color: [f32; 4],
	pub model_mat: Option<[[f32; 3]; 3]>,
	pub translate_after_scale: bool
}

pub struct ObjDef {
	pub position: [f32; 3],
	pub vertices: VertexBuffer<Vertex>,
	pub indices: IndexBuffer<u32>,
	pub material: Option<MtlInfo>
}

#[derive(Copy, Clone, Deserialize)]
#[serde(default)]
pub struct Light {
	pub position: [f32; 3],
	pub att_linear: f32,
	pub att_quad: f32,
	pub att_constant: f32
}

impl Default for Light {
	fn default() -> Self {
		Self {
			position: [0., 0., 0.],
			att_linear: 0.,
			att_constant: 0.,
			att_quad: 1.
		}
	}
}

struct BasicDrawUniforms<'a> {
	lights: [Light; MAX_LIGHTS],
	light_count: i32,
	model: [[f32; 4]; 4],
	view: [[f32; 4]; 4],
	perspective: [[f32; 4]; 4],
	mtl_color: [f32; 3],
	obj_color: [f32; 3],
	texcoord_displacement: [f32; 2],
	min_text_val: [f32; 4],
	tex: &'a Texture2d
}

impl Uniforms for BasicDrawUniforms<'_> {
	fn visit_values<'a, F: FnMut(&str, UniformValue<'a>)>(&'a self, mut func: F) {
		for i in 0..MAX_LIGHTS {
			func(&format!("lights[{}].position", i).to_string(), UniformValue::Vec3(self.lights[i].position));
			func(&format!("lights[{}].att_constant", i).to_string(), UniformValue::Float(self.lights[i].att_constant));
			func(&format!("lights[{}].att_linear", i).to_string(), UniformValue::Float(self.lights[i].att_linear));
			func(&format!("lights[{}].att_quad", i).to_string(), UniformValue::Float(self.lights[i].att_quad));
		}
		func("light_count", UniformValue::SignedInt(self.light_count));
		func("model", UniformValue::Mat4(self.model));
		func("view", UniformValue::Mat4(self.view));
		func("perspective", UniformValue::Mat4(self.perspective));
		func("mtl_color", UniformValue::Vec3(self.mtl_color));
		func("obj_color", UniformValue::Vec3(self.obj_color));
		func("texcoord_displacement", UniformValue::Vec2(self.texcoord_displacement));
		func("tex", UniformValue::Texture2d(self.tex, None));
		func("min_text_val", UniformValue::Vec4(self.min_text_val));
	}
}

impl ObjDrawInfo {
	pub fn generate_matrix(&mut self) {
		let rotation_matrix = [
			[
				self.rotation[2].cos() * self.rotation[1].cos(),
				(self.rotation[2].cos() * self.rotation[1].sin() * self.rotation[0].sin())
					- (self.rotation[2].sin() * self.rotation[0].cos()),
				(self.rotation[2].cos() * self.rotation[1].sin() * self.rotation[0].cos())
					+ (self.rotation[2].sin() * self.rotation[0].sin()),
				0.0
			],
			[
				self.rotation[2].sin() * self.rotation[1].cos(),
				(self.rotation[2].sin() * self.rotation[1].sin() * self.rotation[0].sin())
					+ (self.rotation[2].cos() * self.rotation[0].cos()),
				(self.rotation[2].sin() * self.rotation[1].sin() * self.rotation[0].cos())
					- (self.rotation[2].cos() * self.rotation[0].sin()),
				0.0
			],
			[
				-self.rotation[1].sin(),
				self.rotation[1].cos() * self.rotation[0].sin(),
				self.rotation[1].cos() * self.rotation[0].cos(),
				0.0
			],
			[0.0, 0.0, 0.0, 1.0f32]
		];

		let scale_matrix = [
			[self.scale[0], 0.0, 0.0, 0.0],
			[0.0, self.scale[1], 0.0, 0.0],
			[0.0, 0.0, self.scale[2], 0.0],
			[0.0, 0.0, 0.0, 1.0]
		];

		let translate_matrix = [
			[1.0, 0.0, 0.0, 0.0],
			[0.0, 1.0, 0.0, 0.0],
			[0.0, 0.0, 1.0, 0.0],
			[self.position[0], self.position[1], self.position[2], 1.0f32]
		];

		self.model_mat = Some(mult_matrix(&mult_matrix(&rotation_matrix, &scale_matrix), &translate_matrix));
	}
}

impl UIDrawInfo {
	pub fn new(position: (f32, f32), scale: (f32, f32)) -> UIDrawInfo {
		UIDrawInfo {
			model_mat: None,
			position: position,
			scale: scale,
			color: [1., 1., 1., 1.],
			left_clip: -4.,
			screen_left_clip: -4.,
			screen_dim: (0, 0),
			translate_after_scale: true
		}
	}

	pub fn generate_matrix(&mut self, target: &mut Frame) {
		if self.screen_dim == target.get_dimensions() {
			return;
		}
		self.screen_dim = target.get_dimensions();
		let x_scale = self.screen_dim.1 as f32 / self.screen_dim.0 as f32;
		let mut translate_mat = [
			[1., 0., 0.],
			[0., 1., 0.],
			[self.position.0, self.position.1, 1.0f32]
		];
		let scale_mat = [
			[self.scale.0 * x_scale, 0., 0.],
			[0., self.scale.1, 0.],
			[0., 0., 1.]
		];
		self.screen_left_clip = self.left_clip * x_scale;
		self.model_mat = Some(if !self.translate_after_scale {
			mult_matrix3(&translate_mat, &scale_mat)
		} else {
			translate_mat[2][0] *= x_scale;
			mult_matrix3(&scale_mat, &translate_mat)
		});
	}
}

pub fn load_data_to_gpu(display: &Display, vertices: &[Vertex], indices: &[u32]) -> ObjDef {
	ObjDef {
		vertices: glium::VertexBuffer::new(display, &vertices).unwrap(),
		indices: glium::IndexBuffer::new(display, glium::index::PrimitiveType::TrianglesList, &indices).unwrap(),
		position: vertices[0].position,
		material: None
	}
}

pub fn basic_render(target: &mut Frame, env_info: &EnvDrawInfo, obj_info: &ObjDrawInfo, obj_def: &ObjDef,
	program: &Program, texcoord_displacement: Option<[f32; 2]>) {
	let uniforms = BasicDrawUniforms {
		model: *obj_info.model_mat.as_ref().unwrap(),
		view: env_info.view_mat,
		perspective: env_info.perspective_mat,
		lights: env_info.lights,
		light_count: env_info.light_count as i32,
		mtl_color: obj_def.material.as_ref().unwrap().color,
		obj_color: obj_info.color,
		texcoord_displacement: texcoord_displacement.unwrap_or([0., 0.]),
		min_text_val: if obj_def.material.as_ref().unwrap().diffuse_texture.is_none() { [1., 1., 1., 1.0f32] } else { [0., 0., 0., 0.0f32] },
		tex: env_info.textures.get(
			obj_def.material.as_ref().unwrap().diffuse_texture.as_ref().unwrap_or(&textures::WHITE.to_string())
		).unwrap()
	};
	target.draw(&obj_def.vertices, &obj_def.indices, program, &uniforms, env_info.params).unwrap();
}

pub fn ui_draw(target: &mut Frame, obj_def: &ObjDef, ui_draw_info: &UIDrawInfo, program: &Program, texture: &SrgbTexture2d) {
	let uniforms = uniform! {
		tex: texture.sampled()
			.magnify_filter(glium::uniforms::MagnifySamplerFilter::Linear)
			.minify_filter(glium::uniforms::MinifySamplerFilter::Linear),
		ui_color: ui_draw_info.color,
		model: ui_draw_info.model_mat.unwrap(),
		left_clip: ui_draw_info.screen_left_clip
	};
	let params = DrawParameters {
		blend: glium::draw_parameters::Blend::alpha_blending(),
		clip_planes_bitmask: 1,
		..Default::default()
	};
	target.draw(&obj_def.vertices, &obj_def.indices, program, &uniforms, &params).unwrap();
}
