use std::ops::Range;
use std::rc::Rc;

use glam::{Vec2, vec2};
use glow::HasContext;

use crate::Quad;
use crate::common::{create_shader_program, pop_debug_group, push_debug_group, slice_as_bytes};

use super::{SRC_FRAG_TEXTURE, SRC_VERT_QUAD};

pub struct TextureQuadBatchRenderer {
	gl: Rc<glow::Context>,
	shader: glow::Program,
	u_viewport: glow::UniformLocation,
	vao: glow::VertexArray,
	vbo: glow::Buffer,
	ebo: glow::Buffer,

	batch_slices: Vec<(glow::Texture, Range<usize>)>,
	quads: Vec<Quad>,
	vertices: Vec<[QuadVertex; 4]>,
	indices: Vec<[u32; 6]>,

	prev_vertices_count: usize,
}

impl TextureQuadBatchRenderer {
	pub fn new(gl: Rc<glow::Context>, viewport: Vec2) -> Self {
		unsafe {
			push_debug_group(&gl, "TextureQuadBatchRenderer: init");

			let shader = create_shader_program(&gl, SRC_VERT_QUAD, SRC_FRAG_TEXTURE);

			let u_viewport = gl.get_uniform_location(shader, "u_viewport").unwrap();
			gl.uniform_2_f32(Some(&u_viewport), viewport.x, viewport.y);

			let vao = gl.create_vertex_array().unwrap();
			gl.bind_vertex_array(Some(vao));

			let vbo = gl.create_buffer().unwrap();
			gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));

			let ebo = gl.create_buffer().unwrap();
			gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(ebo));

			let size_vertex = size_of::<QuadVertex>() as i32;
			let size_f32 = size_of::<f32>() as i32;

			#[rustfmt::skip]
            {
				let a_position = gl.get_attrib_location(shader, "position").unwrap();
				let a_uv       = gl.get_attrib_location(shader, "uv").unwrap();

				gl.vertex_attrib_pointer_f32(a_position, 2, glow::FLOAT, false, size_vertex, 0);
				gl.vertex_attrib_pointer_f32(a_uv,       2, glow::FLOAT, false, size_vertex, 2 * size_f32);

				gl.enable_vertex_attrib_array(a_position);
				gl.enable_vertex_attrib_array(a_uv);
            };

			pop_debug_group(&gl);

			Self {
				gl,
				shader,
				u_viewport,
				vao,
				vbo,
				ebo,

				batch_slices: Vec::new(),
				quads: Vec::new(),
				vertices: Vec::new(),
				indices: Vec::new(),

				prev_vertices_count: 0,
			}
		}
	}

	pub fn clear_quads(&mut self) {
		self.quads.clear();
		self.batch_slices.clear();
		self.vertices.clear();
	}

	pub fn push_quad(&mut self, quad: Quad, texture: glow::Texture) {
		if let Some((last_texture, last_batch_slice)) = self.batch_slices.last_mut() {
			if texture == *last_texture {
				last_batch_slice.end += 1;
			} else {
				let slice = self.quads.len()..(self.quads.len() + 1);
				self.batch_slices.push((texture, slice));
			}
		} else {
			// there are no quads
			let slice = 0..(self.quads.len() + 1);
			self.batch_slices.push((texture, slice));
		}

		self.quads.push(quad);
	}

	pub fn upload_quads(&mut self) {
		unsafe {
			let gl = &self.gl;
			push_debug_group(gl, "TextureQuadBatchRenderer: upload quads");

			self.vertices.clear();
			for quad in &self.quads {
				self.vertices.push(quad.vertices());
			}

			if self.vertices.len() > self.prev_vertices_count {
				gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vbo));
				gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, slice_as_bytes(&self.vertices), glow::DYNAMIC_DRAW);

				for i in self.prev_vertices_count..self.vertices.len() {
					self.indices.push(quad_indices(i as u32));
				}

				gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(self.ebo));
				gl.buffer_data_u8_slice(
					glow::ELEMENT_ARRAY_BUFFER,
					slice_as_bytes(&self.indices),
					glow::STATIC_DRAW,
				);

				self.prev_vertices_count = self.vertices.len();
			} else {
				gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vbo));
				gl.buffer_sub_data_u8_slice(glow::ARRAY_BUFFER, 0, slice_as_bytes(&self.vertices));
			}

			pop_debug_group(gl);
		}
	}

	/// Returns number of executed draw calls.
	pub fn draw(&self) -> usize {
		unsafe {
			let gl = &self.gl;
			push_debug_group(gl, "TextureQuadBatchRenderer: draw");

			gl.bind_vertex_array(Some(self.vao));
			gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vbo));
			gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(self.ebo));
			gl.use_program(Some(self.shader));

			for (texture, batch_slice) in &self.batch_slices {
				gl.bind_texture(glow::TEXTURE_2D, Some(*texture));
				gl.draw_elements(
					glow::TRIANGLES,
					(batch_slice.end - batch_slice.start) as i32 * 6,
					glow::UNSIGNED_INT,
					(batch_slice.start * 6 * size_of::<u32>()) as i32,
				);
			}

			pop_debug_group(gl);

			self.batch_slices.len()
		}
	}

	pub fn resize(&mut self, viewport: Vec2) {
		unsafe {
			let gl = &self.gl;
			push_debug_group(gl, "TextureQuadBatchRenderer: resize");

			gl.use_program(Some(self.shader));
			gl.uniform_2_f32(Some(&self.u_viewport), viewport.x, viewport.y);

			pop_debug_group(gl);
		}
	}
}

impl Drop for TextureQuadBatchRenderer {
	fn drop(&mut self) {
		unsafe {
			let gl = &self.gl;
			push_debug_group(gl, "TextureQuadBatchRenderer: drop");

			gl.delete_program(self.shader);
			gl.delete_vertex_array(self.vao);

			gl.delete_buffer(self.vbo);
			gl.delete_buffer(self.ebo);

			pop_debug_group(gl);
		}
	}
}

impl Quad {
	fn vertices(self) -> [QuadVertex; 4] {
		let r = Vec2::from_angle(self.rotation);
		let (pmin, pmax) = (Vec2::ZERO - self.origin, Vec2::ONE - self.origin);

		[
			(vec2(pmin.x, pmin.y), vec2(0.0, 0.0)),
			(vec2(pmin.x, pmax.y), vec2(0.0, 1.0)),
			(vec2(pmax.x, pmax.y), vec2(1.0, 1.0)),
			(vec2(pmax.x, pmin.y), vec2(1.0, 0.0)),
		]
		.map(|(pos, uv)| QuadVertex {
			pos: (pos * self.size).rotate(r) + self.pos,
			uv: self.uv_pos + uv * self.uv_size,
		})
	}
}

fn quad_indices(index: u32) -> [u32; 6] {
	let i = index * 4;
	[i, 1 + i, 2 + i, i, 2 + i, 3 + i]
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
struct QuadVertex {
	pub pos: Vec2,
	pub uv: Vec2,
}
