use std::mem::offset_of;
use std::rc::Rc;

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

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

use super::{SRC_FRAG_RRECT, SRC_VERT_RRECT};

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

	vertices: Vec<[RRectVertex; 4]>,
	indices: Vec<[u32; 6]>,

	prev_vertices_count: usize,
	gl_idx_buffer_count: i32,
	gl_idx_buffer_capacity: i32,
}

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

			let shader = create_shader_program(&gl, SRC_VERT_RRECT, SRC_FRAG_RRECT);

			let u_viewport = gl.get_uniform_location(shader, "u_viewport").unwrap();
			gl.uniform_2_f32(Some(&u_viewport), viewport.x, viewport.y);
			let u_smooth_delta = gl.get_uniform_location(shader, "u_smooth_delta").unwrap();
			gl.uniform_1_f32(Some(&u_smooth_delta), 0.5 / scale_factor);

			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::<RRectVertex>() as i32;

			#[rustfmt::skip]
            {
                let a_position      = gl.get_attrib_location(shader, "position"     ).unwrap();
                let a_size          = gl.get_attrib_location(shader, "size"         ).unwrap();
                let a_border_radius = gl.get_attrib_location(shader, "border_radius").unwrap();
                let a_border_width  = gl.get_attrib_location(shader, "border_width" ).unwrap();
                let a_fill_color    = gl.get_attrib_location(shader, "fill_color"   ).unwrap();
                let a_stroke_color  = gl.get_attrib_location(shader, "stroke_color" ).unwrap();
                let a_box_blur      = gl.get_attrib_location(shader, "box_blur"     ).unwrap();

                gl.vertex_attrib_pointer_f32(a_position,      2, glow::FLOAT, false, size_vertex, offset_of!(RRectVertex, pos)           as _);
                gl.vertex_attrib_pointer_f32(a_size,          2, glow::FLOAT, false, size_vertex, offset_of!(RRectVertex, size)          as _);
                gl.vertex_attrib_pointer_f32(a_border_radius, 4, glow::FLOAT, false, size_vertex, offset_of!(RRectVertex, border_radius) as _);
                gl.vertex_attrib_pointer_f32(a_border_width,  4, glow::FLOAT, false, size_vertex, offset_of!(RRectVertex, border_width)  as _);
                gl.vertex_attrib_pointer_i32(a_fill_color,    1, glow::INT,          size_vertex, offset_of!(RRectVertex, fill_color)    as _);
                gl.vertex_attrib_pointer_i32(a_stroke_color,  1, glow::INT,          size_vertex, offset_of!(RRectVertex, stroke_color)  as _);
                gl.vertex_attrib_pointer_f32(a_box_blur,      1, glow::FLOAT, false, size_vertex, offset_of!(RRectVertex, box_blur)      as _);

                gl.enable_vertex_attrib_array(a_position);
                gl.enable_vertex_attrib_array(a_size);
                gl.enable_vertex_attrib_array(a_border_radius);
                gl.enable_vertex_attrib_array(a_border_width);
                gl.enable_vertex_attrib_array(a_fill_color);
                gl.enable_vertex_attrib_array(a_stroke_color);
                gl.enable_vertex_attrib_array(a_box_blur);
            };

			pop_debug_group(&gl);

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

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

				prev_vertices_count: 0,
				gl_idx_buffer_count: 0,
				gl_idx_buffer_capacity: 0,
			}
		}
	}

	pub fn clear_rrects(&mut self) {
		self.vertices.clear();
	}

	pub fn push_rrect(&mut self, rrect: RRect) {
		self.vertices.push(rrect.vertices());
	}

	pub fn push_rotating_rrect(&mut self, rotating_rrect: RotatingRRect) {
		self.vertices.push(rotating_rrect.vertices());
	}

	pub fn upload_rrects(&mut self) {
		let gl = &self.gl;
		unsafe {
			push_debug_group(gl, "RRectBatchRenderer: upload rrects");

			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));
			}

			self.gl_idx_buffer_count = (self.vertices.len() * 6) as i32;
			self.gl_idx_buffer_capacity = self.gl_idx_buffer_capacity.max(self.gl_idx_buffer_count);

			pop_debug_group(gl);
		}
	}

	/// Returns the number of executed draw calls.
	pub fn draw(&self) -> usize {
		let gl = &self.gl;
		unsafe {
			push_debug_group(gl, "RRectBatchRenderer: 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));

			gl.draw_elements(glow::TRIANGLES, self.gl_idx_buffer_count, glow::UNSIGNED_INT, 0);

			pop_debug_group(gl);

			1
		}
	}

	pub fn resize(&mut self, viewport: Vec2) {
		let gl = &self.gl;
		unsafe {
			push_debug_group(gl, "RRectBatchRenderer: 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 RRectBatchRenderer {
	fn drop(&mut self) {
		let gl = &self.gl;
		unsafe {
			push_debug_group(gl, "RRectBatchRenderer: 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 RotatingRRect {
	fn vertices(self) -> [RRectVertex; 4] {
		let Self {
			pos: position,
			size,
			origin,
			rotation,
			border_radius,
			border_width,
			fill_color,
			stroke_color,
			box_blur,
		} = self;

		let r = Vec2::from_angle(rotation);

		let (pmin, pmax) = (Vec2::ZERO - origin, Vec2::ONE - origin);

		let pos_dims = [
			(vec2(pmin.x, pmin.y) * size).rotate(r) + position,
			(vec2(pmin.x, pmax.y) * size).rotate(r) + position,
			(vec2(pmax.x, pmax.y) * size).rotate(r) + position,
			(vec2(pmax.x, pmin.y) * size).rotate(r) + position,
		];

		pos_dims.map(|position| RRectVertex {
			pos: position,
			size,
			fill_color: i32::from_ne_bytes(fill_color.to_le_bytes()),
			stroke_color: i32::from_ne_bytes(stroke_color.to_le_bytes()),
			border_radius,
			border_width,
			box_blur,
		})
	}
}

impl RRect {
	fn vertices(self) -> [RRectVertex; 4] {
		let Self {
			pos,
			size,
			border_radius,
			border_width,
			fill_color,
			stroke_color,
			box_blur,
		} = self;

		let pos_dims = [
			vec2(0.0, 0.0) * size + pos,
			vec2(0.0, 1.0) * size + pos,
			vec2(1.0, 1.0) * size + pos,
			vec2(1.0, 0.0) * size + pos,
		];

		pos_dims.map(|pos| RRectVertex {
			pos,
			size,
			border_radius,
			border_width,
			fill_color: i32::from_ne_bytes(fill_color.to_le_bytes()),
			stroke_color: i32::from_ne_bytes(stroke_color.to_le_bytes()),
			box_blur,
		})
	}
}

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 RRectVertex {
	pub pos: Vec2,
	pub size: Vec2,
	pub border_radius: Vec4,
	pub border_width: Vec4,
	pub fill_color: i32,
	pub stroke_color: i32,
	pub box_blur: f32,
}
