// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
//
// This file is copied from egui to implement a colour picker with some
// modifications of the original code. egui is created by Emil Ernerfeldt and
// licensed under either the Apache-2.0 or MIT licenses.
//
// Source for egui: https://github.com/emilk/egui
// File I copied: https://github.com/emilk/egui/blob/0.29.1/crates/egui/src/widgets/color_picker.rs
use egui::{
	Window, DragValue, Painter, Response, Sense, Context, Ui, WidgetInfo, Id,
	WidgetType, CursorIcon, Widget, epaint, lerp, remap_clamp,
};

use epaint::{
	Mesh, Rect, Shape, CircleShape, Stroke, Vec2, pos2, vec2, ecolor::{Color32,
	Hsva, HsvaGamma, Rgba},
};

use super::player_preview::PreviewInfo;

pub struct ColourPicker {
	colour: Hsva,
	open: bool,
	show_preview: bool,
	title: &'static str,
}

impl ColourPicker {
	pub fn new(colour: [u8; 3], show_preview: bool, title: &'static str) -> ColourPicker {
		ColourPicker {
			colour: Hsva::from_srgb(colour),
			open: false,
			show_preview,
			title,
		}
	}

	pub fn show(&mut self, ctx: &Context, ui: &mut Ui, id: Option<Id>) -> (Response, Option<PreviewInfo>) {
		const COLOUR_SLIDER_WIDTH: f32 = 256.0;

		let mut button_response = colour_button(ui, self.colour.into(), self.open);
		if ui.style().explanation_tooltips {
			button_response = button_response.on_hover_text("Click to edit colour");
		}

		if button_response.clicked() {
			self.open = !self.open;
		}

		let mut preview = None;
		let mut window = Window::new(self.title);

		if let Some(id) = id {
			window = window.id(id);
		}

		window.open(&mut self.open)
			.resizable(false)
			.show(ctx, |ui| {
				ui.horizontal(|ui| {
					ui.vertical(|ui| {
						ui.spacing_mut().slider_width = COLOUR_SLIDER_WIDTH;
						if colour_picker_hsva_2d(ui, &mut self.colour) {
							button_response.mark_changed();
						}
					});

					if self.show_preview {
						let used_space = ui.min_rect();
						let preview_size = used_space.width().max(used_space.height());
						let (rect, response) = ui.allocate_exact_size(Vec2::splat(preview_size), Sense::hover());
						ui.painter().rect_filled(rect, 0.0, Color32::BLACK);
						preview = Some(PreviewInfo { rect, alpha: ui.opacity() });
						response.on_hover_cursor(CursorIcon::Crosshair);
					}
				});
			});

		(button_response, preview)
	}

	pub fn get(&self) -> [u8; 3] {
		self.colour.to_srgb()
	}

	pub fn set(&mut self, colour: [u8; 3]) {
		self.colour = Hsva::from_srgb(colour);
	}

	pub fn is_open(&self) -> bool {
		self.open
	}

	pub fn set_open(&mut self, open: bool) {
		self.open = open;
	}

	pub fn close(&mut self) {
		self.open = false;
	}
}

fn contrast_colour(colour: impl Into<Rgba>) -> Color32 {
	if colour.into().intensity() < 0.5 {
		Color32::WHITE
	} else {
		Color32::BLACK
	}
}

/// Number of vertices per dimension in the colour sliders.
/// We need at least 6 for hues, and more for smooth 2D areas.
/// Should always be a multiple of 6 to hit the peak hues in HSV/HSL (every 60°).
const N: u32 = 6 * 6;

/// Show a colour with background checkers to demonstrate transparency (if any).
fn show_colour(ui: &mut Ui, colour: impl Into<Color32>, desired_size: Vec2) -> Response {
	show_color32(ui, colour.into(), desired_size)
}

fn show_color32(ui: &mut Ui, colour: Color32, desired_size: Vec2) -> Response {
	let (rect, response) = ui.allocate_at_least(desired_size, Sense::hover());
	if ui.is_rect_visible(rect) {
		show_colour_at(ui.painter(), colour, rect);
	}
	response
}

/// Show a colour with background checkers to demonstrate transparency (if any).
fn show_colour_at(painter: &Painter, colour: Color32, rect: Rect) {
	painter.rect_filled(rect, 0.0, colour);
}

fn colour_button(ui: &mut Ui, colour: Color32, open: bool) -> Response {
	let size = ui.spacing().interact_size;
	let (rect, response) = ui.allocate_exact_size(size, Sense::click());
	response.widget_info(|| WidgetInfo::new(WidgetType::ColorButton));

	if ui.is_rect_visible(rect) {
		let visuals = if open {
			&ui.visuals().widgets.open
		} else {
			ui.style().interact(&response)
		};
		let rect = rect.expand(visuals.expansion);

		show_colour_at(ui.painter(), colour, rect);

		let rounding = visuals.rounding.at_most(2.0); // Can't do more rounding because the background grid doesn't do any rounding
		ui.painter()
			.rect_stroke(rect, rounding, (2.0, visuals.bg_fill)); // fill is intentional, because default style has no border
	}

	response
}

fn colour_slider_1d(ui: &mut Ui, value: &mut f32, colour_at: impl Fn(f32) -> Color32) -> Response {
	#![allow(clippy::identity_op)]

	let desired_size = vec2(ui.spacing().slider_width, ui.spacing().interact_size.y);
	let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag());

	if let Some(mpos) = response.interact_pointer_pos() {
		*value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0);
	}

	if ui.is_rect_visible(rect) {
		let visuals = ui.style().interact(&response);

		{
			// fill colour:
			let mut mesh = Mesh::default();
			for i in 0..=N {
				let t = i as f32 / (N as f32);
				let colour = colour_at(t);
				let x = lerp(rect.left()..=rect.right(), t);
				mesh.colored_vertex(pos2(x, rect.top()), colour);
				mesh.colored_vertex(pos2(x, rect.bottom()), colour);
				if i < N {
					mesh.add_triangle(2 * i + 0, 2 * i + 1, 2 * i + 2);
					mesh.add_triangle(2 * i + 1, 2 * i + 2, 2 * i + 3);
				}
			}
			ui.painter().add(Shape::mesh(mesh));
		}

		ui.painter().rect_stroke(rect, 0.0, visuals.bg_stroke); // outline

		{
			// Show where the slider is at:
			let x = lerp(rect.left()..=rect.right(), *value);
			let r = rect.height() / 4.0;
			let picked_colour = colour_at(*value);
			ui.painter().add(Shape::convex_polygon(
				vec![
					pos2(x, rect.center().y),   // tip
					pos2(x + r, rect.bottom()), // right bottom
					pos2(x - r, rect.bottom()), // left bottom
				],
				picked_colour,
				Stroke::new(visuals.fg_stroke.width, contrast_colour(picked_colour)),
			));
		}
	}

	response
}

fn colour_slider_2d(
	ui: &mut Ui,
	x_value: &mut f32,
	y_value: &mut f32,
	colour_at: impl Fn(f32, f32) -> Color32,
) -> Response {
	let desired_size = Vec2::splat(ui.spacing().slider_width);
	let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag());

	if let Some(mpos) = response.interact_pointer_pos() {
		*x_value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0);
		*y_value = remap_clamp(mpos.y, rect.bottom()..=rect.top(), 0.0..=1.0);
	}

	if ui.is_rect_visible(rect) {
		let visuals = ui.style().interact(&response);
		let mut mesh = Mesh::default();

		for xi in 0..=N {
			for yi in 0..=N {
				let xt = xi as f32 / (N as f32);
				let yt = yi as f32 / (N as f32);
				let colour = colour_at(xt, yt);
				let x = lerp(rect.left()..=rect.right(), xt);
				let y = lerp(rect.bottom()..=rect.top(), yt);
				mesh.colored_vertex(pos2(x, y), colour);

				if xi < N && yi < N {
					let x_offset = 1;
					let y_offset = N + 1;
					let tl = yi * y_offset + xi;
					mesh.add_triangle(tl, tl + x_offset, tl + y_offset);
					mesh.add_triangle(tl + x_offset, tl + y_offset, tl + y_offset + x_offset);
				}
			}
		}
		ui.painter().add(Shape::mesh(mesh)); // fill

		ui.painter().rect_stroke(rect, 0.0, visuals.bg_stroke); // outline

		// Show where the slider is at:
		let x = lerp(rect.left()..=rect.right(), *x_value);
		let y = lerp(rect.bottom()..=rect.top(), *y_value);
		let picked_colour = colour_at(*x_value, *y_value);

		let prev_clip_rect = ui.clip_rect();
		let stroke_width = visuals.fg_stroke.width;

		/*
		 * Resolves problem of circle being cut off by the borders.
		 *
		 * This is only needed for the left border as the top border is well
		 * within the window (sliders above) and the right border has enough
		 * distance to the black background that a radius of four is good enough.
		 */
		ui.set_clip_rect(Rect { min: pos2(-f32::INFINITY, -f32::INFINITY), max: pos2(f32::INFINITY, f32::INFINITY) });

		ui.painter().add(CircleShape {
			center: pos2(x, y),
			radius: 4.0,
			fill: picked_colour,
			stroke: Stroke::new(stroke_width, contrast_colour(picked_colour)),
		});
		ui.set_clip_rect(prev_clip_rect);
	}

	response
}

fn colour_picker_hsvag_2d(ui: &mut Ui, hsvag: &mut HsvaGamma) {
	use egui::style::NumericColorSpace;

	match ui.style().visuals.numeric_color_space {
		NumericColorSpace::GammaByte => {
			let mut srgba_unmultiplied = Hsva::from(*hsvag).to_srgba_unmultiplied();
			// Only update if changed to avoid rounding issues.
			if srgba_edit_ui(ui, &mut srgba_unmultiplied) {
				*hsvag = HsvaGamma::from(Hsva::from_srgba_unmultiplied(srgba_unmultiplied));
			}
		}

		NumericColorSpace::Linear => {
			let mut rgba_unmultiplied = Hsva::from(*hsvag).to_rgba_unmultiplied();
			// Only update if changed to avoid rounding issues.
			if rgba_edit_ui(ui, &mut rgba_unmultiplied) {
				// Normal blending.
				*hsvag = HsvaGamma::from(Hsva::from_rgba_unmultiplied(
					rgba_unmultiplied[0],
					rgba_unmultiplied[1],
					rgba_unmultiplied[2],
					rgba_unmultiplied[3],
				));
			}
		}
	}

	let current_colour_size = vec2(ui.spacing().slider_width, ui.spacing().interact_size.y);
	show_colour(ui, *hsvag, current_colour_size).on_hover_text("Selected colour");

	let colour = *hsvag;
	let HsvaGamma { h, s, v, a: _ } = hsvag;

	colour_slider_1d(ui, h, |h| HsvaGamma { h, ..colour }.into()).on_hover_text("Hue");
	colour_slider_1d(ui, s, |s| HsvaGamma { s, ..colour }.into()).on_hover_text("Saturation");
	colour_slider_1d(ui, v, |v| HsvaGamma { v, ..colour }.into()).on_hover_text("Value");

	colour_slider_2d(ui, s, v, |s, v| HsvaGamma { s, v, ..colour }.into());
}

fn input_type_button_ui(ui: &mut Ui) {
	let mut input_type = ui.ctx().style().visuals.numeric_color_space;
	if input_type.toggle_button_ui(ui).changed() {
		ui.ctx().all_styles_mut(|s| {
			s.visuals.numeric_color_space = input_type;
		});
	}
}

/// Shows 4 `DragValue` widgets to be used to edit the RGBA u8 values.
/// Alpha's `DragValue` is hidden when `Alpha::Opaque`.
///
/// Returns `true` on change.
fn srgba_edit_ui(ui: &mut Ui, [r, g, b, _a]: &mut [u8; 4]) -> bool {
	let mut edited = false;

	ui.horizontal(|ui| {
		input_type_button_ui(ui);

		edited |= DragValue::new(r).speed(0.5).prefix("R ").ui(ui).changed();
		edited |= DragValue::new(g).speed(0.5).prefix("G ").ui(ui).changed();
		edited |= DragValue::new(b).speed(0.5).prefix("B ").ui(ui).changed();
	});

	edited
}

/// Shows 4 `DragValue` widgets to be used to edit the RGBA f32 values.
/// Alpha's `DragValue` is hidden when `Alpha::Opaque`.
///
/// Returns `true` on change.
fn rgba_edit_ui(ui: &mut Ui, [r, g, b, _a]: &mut [f32; 4]) -> bool {
	fn drag_value(ui: &mut Ui, prefix: &str, value: &mut f32) -> Response {
		DragValue::new(value)
			.speed(0.003)
			.prefix(prefix)
			.range(0.0..=1.0)
			.custom_formatter(|n, _| format!("{n:.03}"))
			.ui(ui)
	}

	let mut edited = false;

	ui.horizontal(|ui| {
		input_type_button_ui(ui);

		edited |= drag_value(ui, "R ", r).changed();
		edited |= drag_value(ui, "G ", g).changed();
		edited |= drag_value(ui, "B ", b).changed();
	});

	edited
}

/// Shows a colour picker where the user can change the given [`Hsva`] colour.
///
/// Returns `true` on change.
fn colour_picker_hsva_2d(ui: &mut Ui, hsva: &mut Hsva) -> bool {
	let mut hsvag = HsvaGamma::from(*hsva);
	ui.vertical(|ui| {
		colour_picker_hsvag_2d(ui, &mut hsvag);
	});
	let new_hasva = Hsva::from(hsvag);
	if *hsva == new_hasva {
		false
	} else {
		*hsva = new_hasva;
		true
	}
}
