use core::fmt;
use std::hash::{DefaultHasher, Hasher};

use glam::{Mat2, Vec2, Vec4, u8vec2, vec2};
use parley::{Alignment, AlignmentOptions, Glyph, GlyphRun, Layout, PositionedLayoutItem};
use swash::scale::Scaler;
use swash::scale::image::Content;
use swash::zeno::Vector;

use crate::RRect;
use crate::common::upload_subtexture;
use crate::quadlas::Quadlas;
use crate::{Color, RotatingRRect};
use crate::{DrawCommand, RawImage};

use super::Renderer;
use super::batch::glyph_quad::GlyphQuad;
use super::quadlas::{GlyphKey, InsertGlyphError};

#[derive(Clone)]
pub struct TextLayout {
	layout: Layout<()>,
	text: String,
	alignment: Alignment,
	color: Color,
	real_size: Vec2,
}

impl fmt::Debug for TextLayout {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		write!(f, "TextLayout ({}x{})", self.real_size.x, self.real_size.y)
	}
}

impl TextLayout {
	pub fn new(layout: Layout<()>, text: String, alignment: Alignment, color: Color, scale_factor: f32) -> Self {
		let mut layout = Self {
			layout,
			text,
			alignment,
			color,
			real_size: Vec2::ZERO,
		};
		layout.set_max_advance(None, scale_factor);
		layout
	}

	pub fn set_max_advance(&mut self, max_advance: Option<f32>, scale_factor: f32) {
		// Perform layout (including bidi resolution and shaping) with start alignment
		let max_advance = max_advance.map(|x| x * scale_factor);
		self.layout.break_all_lines(max_advance);
		self.layout.align(
			max_advance,
			self.alignment,
			AlignmentOptions {
				align_when_overflowing: true,
			},
		);

		self.update_size(scale_factor);
	}

	/// Apply alignment to text layout.
	/// Doesn't do anything if the layout is the same as before.
	pub fn realign(&mut self, alignment: Alignment, scale_factor: f32) {
		if self.alignment != alignment {
			self.alignment = alignment;

			self.layout.align(
				None,
				self.alignment,
				AlignmentOptions {
					align_when_overflowing: true,
				},
			);
			self.update_size(scale_factor);
		}
	}

	fn update_size(&mut self, scale_factor: f32) {
		let width = self.layout.width();
		let height = if let Some(last_line) = self.layout.lines().last() {
			let metrics = last_line.metrics();
			metrics.baseline + metrics.descent
		} else {
			0.0
		};

		self.real_size = vec2(width, height) / scale_factor;
	}

	pub fn color(&self) -> Color {
		self.color
	}

	pub fn set_color(&mut self, color: Color) {
		self.color = color;
	}

	pub fn size(&self) -> Vec2 {
		self.real_size
	}

	pub fn width(&self) -> f32 {
		self.real_size.x
	}

	pub fn height(&self) -> f32 {
		self.real_size.y
	}

	pub fn layout(&self) -> &Layout<()> {
		&self.layout
	}

	pub fn text(&self) -> &str {
		&self.text
	}
}

impl Renderer {
	pub fn render_text(&mut self, text: &TextLayout, pos: Vec2, scale_factor: f32) {
		self.render_text_raw(text.layout(), text.real_size, text.color, pos, scale_factor)
	}

	pub fn render_text_rotated(
		&mut self,
		text: &TextLayout,
		pos: Vec2,
		origin: Vec2,
		rotation: f32,
		scale_factor: f32,
	) {
		self.render_text_raw_rotated(
			text.layout(),
			text.real_size,
			text.color,
			pos,
			origin,
			rotation,
			scale_factor,
		)
	}

	pub fn render_text_raw(
		&mut self,
		layout: &Layout<()>,
		real_size: Vec2,
		color: Color,
		pos: Vec2,
		scale_factor: f32,
	) {
		self.render_text_raw_rotated(layout, real_size, color, pos, Vec2::ZERO, 0.0, scale_factor)
	}

	pub fn render_text_raw_rotated(
		&mut self,
		layout: &Layout<()>,
		real_size: Vec2,
		color: Color,
		pos: Vec2,
		origin: Vec2,
		rotation: f32,
		scale_factor: f32,
	) {
		let px_advance = Mat2::from_angle(rotation);

		let tl_pos = pos - px_advance * (origin * real_size);
		let br_pos = pos + px_advance * ((1.0 - origin) * real_size);
		let min_boundary = tl_pos.min(br_pos);
		let max_boundary = tl_pos.max(br_pos);

		if (min_boundary.x >= self.viewport.x || max_boundary.x <= 0.0)
			|| (min_boundary.y >= self.viewport.y || max_boundary.y <= 0.0)
		{
			return;
		}

		for line in layout.lines() {
			for item in line.items() {
				match item {
					PositionedLayoutItem::GlyphRun(glyph_run) => {
						self.render_glyph_run(&glyph_run, color, tl_pos, px_advance, rotation, scale_factor);
					}
					PositionedLayoutItem::InlineBox(inline_box) => {
						self.push_draw_command(DrawCommand::RotatingRRect(RotatingRRect {
							pos: tl_pos + px_advance * vec2(inline_box.x, inline_box.y),
							size: vec2(inline_box.width, inline_box.height),
							border_radius: Vec4::ZERO,
							border_width: Vec4::ZERO,
							fill_color: 0x000000ff,
							stroke_color: 0x000000ff,
							origin: Vec2::ZERO,
							rotation,
							box_blur: 0.0,
						}));
					}
				};
			}
		}
	}

	fn render_glyph_run(
		&mut self,
		glyph_run: &GlyphRun<'_, ()>,
		color: Color,
		offset_pos: Vec2,
		px_advance: Mat2,
		rotation: f32,
		scale_factor: f32,
	) {
		let run_pos = vec2(glyph_run.offset(), glyph_run.baseline());

		let run = glyph_run.run();
		let run_metrics = run.metrics();

		let font = run.font();
		let font_size = run.font_size();
		let normalized_coords = run.normalized_coords();

		{
			let pos = offset_pos + px_advance * (run_pos - vec2(0.0, run_metrics.ascent)) / scale_factor;
			let size = vec2(glyph_run.advance(), font_size) / scale_factor;

			let pos_end = pos + px_advance * size;
			let min_boundary = pos.min(pos_end);
			let max_boundary = pos.max(pos_end);

			if (min_boundary.x >= self.viewport.x || max_boundary.x <= 0.0)
				|| (min_boundary.y >= self.viewport.y || max_boundary.y <= 0.0)
			{
				return;
			}
		}

		let font_ref = swash::FontRef::from_index(font.data.as_ref(), font.index as usize).unwrap();

		// To make sure that glyphs with a different font weight have a different hash, we
		// create a hash of the current style of the font and embed it into each glyph key.
		let style_hash = {
			let (stretch, weight, style) = font_ref.attributes().parts();

			let mut hasher = DefaultHasher::new();
			hasher.write_u64(font.data.id());
			hasher.write_u16(stretch.raw());
			hasher.write_u16(weight.0);
			hasher.write_u32(u32::from_le_bytes(style.to_degrees().to_le_bytes()));
			hasher.write_u32(u32::from_ne_bytes(font_size.to_ne_bytes()));

			(hasher.finish() % (u32::MAX as u64 + 1)) as u32
		};

		for attempt in 0..=10 {
			if attempt == 10 {
				panic!("There have been 10 failed attempts at drawing a single glyph run. What the fuck.");
			}

			let mut no_space_left = false;
			let prev_command_count = self.command_buffer.len();

			// I wish I didn't have to recreate the scaler on every failure,
			// but the Rust borrow checker is getting in my way for this one.
			let mut scaler = (self.text.scale_cx)
				.builder(font_ref)
				.size(font_size)
				.hint(false)
				.normalized_coords(normalized_coords)
				.build();

			// Iterate over the glyphs in the GlyphRun
			for glyph in glyph_run.positioned_glyphs() {
				let glyph_pos = vec2(glyph.x, glyph.y);

				let render_result = render_glyph(
					&self.gl,
					&mut self.text.quadlas,
					self.text.quadlas_texture,
					&mut self.text.scale_render,
					&mut self.command_buffer,
					&mut scaler,
					self.current_frame,
					color,
					style_hash,
					glyph,
					glyph_pos,
					offset_pos,
					px_advance,
					rotation,
					scale_factor,
				);

				match render_result {
					Ok(()) => {}
					Err(InsertGlyphError::NoSpaceLeft) => {
						no_space_left = true;
						break;
					}
					Err(InsertGlyphError::EmptyImage) => {}
					Err(InsertGlyphError::TooBig) => {}
				}
			}

			if no_space_left {
				// No space left: draw what we already have, double the texture atlas's size if we can, and try again
				self.command_buffer.truncate(prev_command_count);
				self.draw();
				self.text.quadlas.grow();
				self.reupload_glyph_atlas_texture();
			} else {
				break;
			}
		}

		// Draw decorations: underline & strikethrough
		let style = glyph_run.style();
		if let Some(decoration) = &style.underline {
			let offset = (decoration.offset).unwrap_or(run_metrics.underline_offset);
			let size = decoration.size.unwrap_or(run_metrics.underline_size);
			render_decoration(self, glyph_run, color, offset, size, offset_pos, scale_factor);
		}

		if let Some(decoration) = &style.strikethrough {
			let offset = (decoration.offset).unwrap_or(run_metrics.strikethrough_offset);
			let size = decoration.size.unwrap_or(run_metrics.strikethrough_size);
			render_decoration(self, glyph_run, color, offset, size, offset_pos, scale_factor);
		}
	}
}

#[allow(clippy::too_many_arguments)]
fn render_glyph(
	gl: &glow::Context,
	quadlas: &mut Quadlas,
	quadlas_texture: glow::Texture,
	scale_render: &mut swash::scale::Render<'_>,
	draw_commands: &mut Vec<DrawCommand>,
	scaler: &mut Scaler<'_>,
	current_frame: u64,
	color: Color,
	style_hash: u32,
	glyph: Glyph,
	glyph_pos: Vec2,
	offset_pos: Vec2,
	px_advance: Mat2,
	rotation: f32,
	scale: f32,
) -> Result<(), InsertGlyphError> {
	// Quantization
	let quantize = u8vec2(2, 1);
	let fquantize = quantize.as_vec2();
	let offset_fract = (offset_pos.fract() * fquantize).floor() / fquantize;
	let offset_fract_yinv = vec2(offset_fract.x, 1.0 - offset_fract.y);

	let glyph_key = GlyphKey {
		id: (glyph.id % (u16::MAX as u32)) as u16, // TODO: what do we lose here?
		quantization: (offset_fract_yinv * fquantize).floor().as_u8vec2(),
		style_hash,
	};

	if let Some((land, gce)) = quadlas.get_cached_glyph(glyph_key, current_frame) {
		let placement = gce.placement;
		let glyph_size = vec2(placement.width as f32, placement.height as f32) / scale;
		let glyph_pos = (glyph_pos + vec2(placement.left as f32, -placement.top as f32)) / scale - offset_fract;
		let land_uv = quadlas.to_land_uv(land);

		let color = match gce.is_colored {
			true => 0xffffffff,
			false => color,
		};

		draw_commands.push(DrawCommand::GlyphQuad(
			GlyphQuad {
				pos: offset_pos + px_advance * glyph_pos,
				size: glyph_size,
				uv_pos: land_uv.pos,
				uv_size: land_uv.size,
				color,
				rotation,
			},
			quadlas_texture,
		));
	} else {
		let fractional_offset = Vector::new(offset_fract_yinv.x, offset_fract_yinv.y);

		// Render the glyph using swash
		let Some(rendered_glyph) = scale_render.offset(fractional_offset).render(scaler, glyph_key.id) else {
			// No image, nothing to render
			return Ok(());
		};

		let placement = rendered_glyph.placement;
		let glyph_size = vec2(placement.width as f32, placement.height as f32) / scale;
		let glyph_pos = (glyph_pos + vec2(placement.left as f32, -placement.top as f32)) / scale - offset_fract;

		let (land, color) = match rendered_glyph.content {
			Content::Color => {
				let glyph_image = RawImage {
					width: placement.width,
					height: placement.height,
					pixels: &rendered_glyph.data,
				};

				let (land, _) = quadlas.insert_glyph_unless_cached(
					glyph_key,
					rendered_glyph.placement,
					true,
					glyph_image,
					current_frame,
				)?;

				// upload image to GPU
				unsafe {
					upload_subtexture(
						gl,
						quadlas_texture,
						land.pos.x,
						land.pos.y,
						land.size.x,
						land.size.y,
						Some(glyph_image.pixels),
					);
				}

				(land, 0xffffffff)
			}

			Content::Mask => {
				let rgba_data = (rendered_glyph.data.iter())
					.flat_map(|&a| [0xff, 0xff, 0xff, a])
					.collect::<Vec<_>>();

				let glyph_image = RawImage {
					width: placement.width,
					height: placement.height,
					pixels: &rgba_data,
				};

				let (land, _) = quadlas.insert_glyph_unless_cached(
					glyph_key,
					rendered_glyph.placement,
					false,
					RawImage {
						width: placement.width,
						height: placement.height,
						pixels: &rgba_data,
					},
					current_frame,
				)?;

				// upload image to GPU
				unsafe {
					upload_subtexture(
						gl,
						quadlas_texture,
						land.pos.x,
						land.pos.y,
						land.size.x,
						land.size.y,
						Some(glyph_image.pixels),
					);
				}

				(land, color)
			}

			Content::SubpixelMask => unimplemented!(),
		};

		let land_uv = quadlas.to_land_uv(land);

		draw_commands.push(DrawCommand::GlyphQuad(
			GlyphQuad {
				pos: offset_pos + px_advance * glyph_pos,
				size: glyph_size,
				uv_pos: land_uv.pos,
				uv_size: land_uv.size,
				color,
				rotation,
			},
			quadlas_texture,
		));
	}

	Ok(())
}

fn render_decoration(
	renderer: &mut Renderer,
	glyph_run: &GlyphRun<'_, ()>,
	color: Color,
	offset: f32,
	thickness: f32,
	offset_pos: Vec2,
	scale_factor: f32,
) {
	let pos = offset_pos + vec2(glyph_run.offset(), glyph_run.baseline() - offset) / scale_factor;
	let size = vec2(glyph_run.advance(), thickness * 2.0) / scale_factor;
	if pos.x >= renderer.viewport.x || pos.x + size.x <= 0.0 || pos.y >= renderer.viewport.y || pos.y + size.y <= 0.0 {
		return;
	}

	renderer.push_draw_command(DrawCommand::RRect(RRect {
		pos,
		size,
		border_radius: Vec4::splat(thickness) / 1.5,
		border_width: Vec4::ZERO,
		fill_color: color,
		stroke_color: 0x00000000,
		box_blur: 0.0,
	}));
}
