// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
mod action;
mod colour_picker;
mod player_preview;

use std::{any::Any, collections::BTreeSet};

use egui::{RichText, Button, Ui, Window, CollapsingHeader, TextEdit, ComboBox, Slider, SliderClamping, Id};
use strum::IntoEnumIterator;
use glium::Frame as GliumFrame;
use glam::Vec3;
use winit::event::Event;

use action::Category;
use colour_picker::ColourPicker;
use player_preview::PlayerPreview;

use super::layout_job_helper::LayoutJobHelper;

use crate::app::{App, config::{Sky, SkyPreset, CLOUD_LAYERS, self, sky::{Gradient, GRADIENT_COUNT, DEFAULT_STAR_DENSITY}, action::{Action, ActionEvent, ActionState, ActionManager, Input}}, state::{GameState, Status}};
use crate::app::gui::{GuiState, style::{UiExt, FontSize, LABEL_SIZE, H3_SIZE, H4_SIZE, STYLE_SETTINGS_BIND_BUTTON}, port_edit::PortEdit};
use crate::utils::ToStr;

struct Section {
	header: &'static str,
	actions: Vec<Action>,
}

pub struct Settings {
	gui_state: GuiState,
	player_colour_picker: ColourPicker,
	player_preview: PlayerPreview,
	sections: Box<[Section]>,
	opened_action_windows: BTreeSet<Action>,
	ignore_next_action: bool,
	selecting: bool,
	selected_input: Option<Input>,
	selected_input_string: String,
	gpcp: ColourPicker,
	gpcp_open_for: [Vec<bool>; GRADIENT_COUNT],
	online_discovery_port: PortEdit,
	staging_scale_factor: f32,
}

const SELECTING_OPACITY: f32 = 0.375;

impl Settings {
	pub fn new(app: &mut App) -> Settings {
		let mut sections: Box<[Section]> = Category::iter()
			.map(|category| Section {
				header: category.to_str(),
				actions: Vec::new(),
			}).collect();

		for action in Action::iter() {
			sections[Category::from(action) as usize].actions.push(action);
		}

		let config = app.config.borrow();
		let player_colour = config.colour.into();
		let online_discovery_port = config.online_discovery.port;
		let staging_scale_factor = config.accessibility.scale_factor;
		drop(config);

		Settings {
			gui_state: GuiState::new(),
			player_colour_picker: ColourPicker::new(player_colour, true, "Change Player Colour"),
			player_preview: PlayerPreview::new(app),
			sections,
			opened_action_windows: BTreeSet::new(),
			ignore_next_action: false,
			selecting: false,
			selected_input: None,
			selected_input_string: String::new(),
			gpcp: ColourPicker::new([0, 0, 0], false, "Change Gradient Point Colour"),
			gpcp_open_for: Default::default(),
			online_discovery_port: PortEdit::new(online_discovery_port),
			staging_scale_factor,
		}
	}

	fn add_input_list(action_manager: &ActionManager, action: Action, separator: &str) -> String {
		let mut string = String::new();
		if let Some(inputs) = action_manager.inputs_of_action(action) {
			for (i, input) in inputs.enumerate() {
				if i != 0 {
					string.push_str(separator);
				}
				string.push_str(&input.to_string());
			}
		}
		string
	}

	#[must_use]
	fn bind_button_clicked(ui: &mut Ui, text: &str, enabled: bool) -> bool {
		ui.add_enabled(enabled, Button::new(RichText::new(text).font_size(STYLE_SETTINGS_BIND_BUTTON))).clicked()
	}
}

impl GameState for Settings {
	fn enable(&mut self, app: &mut App) {
		self.gui_state.enable(&mut app.gui);
		debug_assert!(!self.selecting);
	}

	fn push(&mut self, _app: &mut App, msg: Option<Box<dyn Any>>) {
		debug_assert!(msg.is_none());
	}

	fn disable(&mut self, _app: &mut App) {
		debug_assert!(!self.selecting);
	}

	fn action(&mut self, action: ActionEvent) {
		if !self.ignore_next_action {
			self.gui_state.action(action);
		}
	}

	fn event(&mut self, app: &mut App, event: &Event<()>) {
		if let Event::WindowEvent { event, .. } = event {
			if self.selecting && let Some((input, ActionState::Pressed)) = Input::from_event(event) {
				self.selecting = false;
				self.selected_input = Some(input);
				self.selected_input_string = input.to_string();
				app.config.borrow_mut().action_manager.set_enabled(true);
				self.gui_state.should_repaint();

				/*
				 * Fixes bug when pressing an action bound to `GuiLeave` results
				 * in the action binding window closing.
				 */
				self.ignore_next_action = true;
			}

			self.player_preview.event(event);
		}

		self.gui_state.event(app, event);
	}

	fn loop_iter(&mut self, app: &mut App, frame: &mut GliumFrame) -> Status {
		self.ignore_next_action = false;

		let config = &mut *app.config.borrow_mut();

		let mut change: Option<(Action, bool)> = None;
		let mut back_button_clicked = false;

		let decrease_opacity = self.selecting;

		let mut player_preview = None;

		let res = GuiState::update(&mut app.gui, "settings", &app.window, |ctx, ui| {
			if decrease_opacity {
				ui.set_opacity(SELECTING_OPACITY);
			}

			ui.h1("Settings").no_bottom_padding().add();
			ui.h2("Player").no_bottom_padding().add();

			ui.h3("Name").add();
			config::set_changed!(config, ui.add(TextEdit::singleline(&mut config.name).font_size(H4_SIZE)).changed());

			ui.h3("Colour").add();
			let (response, info) = self.player_colour_picker.show(ctx, ui, None);
			if response.changed() {
				config.colour = self.player_colour_picker.get().into();
				config.changed();
			}
			player_preview = info;

			ui.h2("Input").add();

			for section in &self.sections {
				ui.collapsing(RichText::new(section.header).font_size(H3_SIZE), |ui| {
					ui.horizontal_wrapped(|ui| {
						for &action in &section.actions {
							let response = ui.add(Button::new(RichText::new(action.to_str()).font_size(H3_SIZE)));

							if response.clicked() {
								self.opened_action_windows.insert(action);
							}

							response.on_hover_ui_at_pointer(|ui| {
								ui.label(RichText::new("Bound to:").strong());
								ui.label(Settings::add_input_list(&config.action_manager, action, "\n"));
							});
						}
					});

					ui.add_space(10.0);
				});
			}

			ui.h2("Graphics").no_bottom_padding().add();
			ui.h3("Sky").add();

			let selected = match &config.graphics.sky {
				Sky::Preset(preset) => preset.to_str(),
				Sky::Custom => "Custom",
			};

			ComboBox::new("settings-sky-preset", "Preset")
				.selected_text(selected)
				.show_ui(ui, |ui| {
					for preset in SkyPreset::iter() {
						let text: &'static str = preset.to_str();
						if ui.selectable_label(config.graphics.sky == Sky::Preset(preset), text).clicked() {
							config.graphics.sky = Sky::Preset(preset);
							config.changed();
						}
					}

					if ui.selectable_label(config.graphics.sky == Sky::Custom, "Custom").clicked() {
						config.graphics.sky = Sky::Custom;
						config.changed();
					}
				});

			match config.graphics.sky {
				Sky::Preset(preset) => {
					if ui.button("Use as Custom Preset").clicked() {
						config.graphics.sky = Sky::Custom;
						config.graphics.custom_sky = preset.to_config();
					}
				},
				Sky::Custom => {
					let mut changed = false; // Borrow checker problems

					ui.add_space(H4_SIZE);
					for (i, (grad, open_for)) in config.graphics.custom_sky.gradients.iter_mut().zip(self.gpcp_open_for.iter_mut()).enumerate() {
						enum Action {
							Add(usize),
							Remove(usize),
						}

						let Gradient::Bezier(grad) = grad;

						let mut action = None;
						open_for.resize(grad.points.len(), false);

						CollapsingHeader::new(RichText::new(format!("Gradient {}", i + 1)).font_size(H4_SIZE)).default_open(true).show(ui, |ui| {
							changed |= ui.add(Slider::new(&mut grad.pre_power, 0.0..=2.0).text("Pre Power").clamping(SliderClamping::Never)).changed();

							let remove_enabled = !grad.points.tail.is_empty();
							for (j, (colour, open)) in grad.points.iter_mut().zip(open_for.iter_mut()).enumerate() {
								ui.horizontal(|ui| {
									if ui.button("+").clicked() {
										action = Some(Action::Add(j));
									}

									if ui.add_enabled(remove_enabled, Button::new("−")).clicked() {
										action = Some(Action::Remove(j));
									}

									let u8_colour = colour.to_array().map(|x| (x * 255.0) as u8);
									self.gpcp.set(u8_colour);

									self.gpcp.set_open(*open);

									if self.gpcp.show(ctx, ui, Some(Id::new((i, j)))).0.changed() {
										*colour = Vec3::from(self.gpcp.get().map(|x| x as f32 / 255.0));
										changed = true;
									}

									*open = self.gpcp.is_open();
								});
							}

							if let Some(action) = action {
								match action {
									Action::Add(j) => {
										grad.points.insert(j + 1, grad.points[j]);
										open_for.insert(j + 1, false);
									},
									Action::Remove(j) => {
										if j > 0 {
											grad.points.tail.remove(j - 1);
										} else if let Some(second) = grad.points.tail.first() { // Should always be Some
											grad.points.head = *second;
											grad.points.tail.remove(0);
										}

										open_for.remove(j);
									},
								}
								changed = true;
							}

							changed |= ui.add(Slider::new(&mut grad.post_power, 0.0..=2.0).text("Post Power").clamping(SliderClamping::Never)).changed();

							ui.add_space(10.0);
						});
					}

					changed |= ui.add(Slider::new(&mut config.graphics.custom_sky.cloud_brightness, 0.0..=1.0).text("Cloud Brightness").clamping(SliderClamping::Never)).changed();

					let mut show_stars = config.graphics.custom_sky.star_density > 0.0;
					let prev_show_stars = show_stars;
					ui.vertical(|ui| ui.checkbox(&mut show_stars, "Stars"));

					if prev_show_stars && !show_stars {
						config.graphics.custom_sky.star_density = 0.0;
						changed = true;
					} else if !prev_show_stars && show_stars {
						config.graphics.custom_sky.star_density = DEFAULT_STAR_DENSITY;
						changed = true;
					}

					let min = if show_stars { 1.0 } else { 0.0 };
					let slider = Slider::new(&mut config.graphics.custom_sky.star_density, min..=10000.0)
						.text("Star Density (per Megapixel)")
						.clamping(SliderClamping::Edits)
						.logarithmic(show_stars /* Avoids decimal places when zero */);

					changed |= ui.add_enabled(show_stars, slider).changed();

					changed |= ui.add(Slider::new(&mut config.graphics.custom_sky.star_intensity, 0.0..=2.0).text("Star Intensity").clamping(SliderClamping::Never)).changed();
					changed |= ui.add(Slider::new(&mut config.graphics.custom_sky.star_colour_brightness, 0.0..=2.0).text("Star Colour Brightness").clamping(SliderClamping::Never)).changed();
					changed |= ui.add(Slider::new(&mut config.graphics.custom_sky.star_colour_offset, 0.0..=1.0).text("Star Colour Offset").clamping(SliderClamping::Never)).changed();
					config.set_changed(changed);
				},
			}

			ui.h3("Sky Performance").add();
			ui.vertical(|ui| {
				ui.label("Here are some settings you might want to disable for performance reasons.");

				let slider = Slider::new(&mut config.graphics.sky_perf.clouds, 0..=CLOUD_LAYERS).text("Cloud Layers").clamping(SliderClamping::Edits);

				config::set_changed!(config, ui.add(slider).changed());
				config::set_changed!(config, ui.checkbox(&mut config.graphics.sky_perf.stars, "Stars").changed());

				ui.add_space(10.0);
				ui.label("Enabling the framebuffer renders clouds to a framebuffer, which is then used when rendering the stars so that they are coloured based on the clouds behind them. Disabling the framebuffer instead uses a constant colour for stars, and so is recommended if not rendering any cloud layers or if it's found to improve performance.");
				config::set_changed!(config, ui.checkbox(&mut config.graphics.sky_perf.framebuffer, "Framebuffer").changed());
			});

			ui.h3("Particles").add();

			// Don't want vertical centred to be consistent with the other buttons
			ui.vertical(|ui| {
				config::set_changed!(config, ui.checkbox(&mut config.graphics.particles.thrust, "Thrust").changed());
				config::set_changed!(config, ui.checkbox(&mut config.graphics.particles.powerup, "Powerup").changed());
				config::set_changed!(config, ui.checkbox(&mut config.graphics.particles.death, "Death").changed());
				config::set_changed!(config, ui.checkbox(&mut config.graphics.particles.hit, "Hit").changed());
				config::set_changed!(config, ui.checkbox(&mut config.graphics.particles.forcefield_deflection, "Forcefield Deflection").changed());
				config::set_changed!(config, ui.checkbox(&mut config.graphics.particles.checkpoint, "Checkpoint").changed());
			});

			ui.h3("Forcefield").add();
			ui.vertical(|ui| config::set_changed!(config, ui.checkbox(&mut config.graphics.forcefield_lightning, "Forcefield Lightning").changed()));

			ui.h2("Sound").add();

			let mut amplitude_percent = config.sound.amplitude * 100.0;
			let slider = Slider::new(&mut amplitude_percent, 0.0..=100.0)
				.text("Volume")
				.suffix("%")
				.integer(); // "0.0%" and "100.0%" look ugly

			if ui.add_enabled(!config.sound.mute, slider).changed() {
				config.sound.amplitude = amplitude_percent / 100.0;
				config.changed();
			}

			ui.vertical(|ui| config::set_changed!(config, ui.checkbox(&mut config.sound.mute, "Mute").changed()));

			ui.h2("Online Servers").add();
			ui.label("Configure which server is used to search for online games.");

			ui.h3("Host").add();
			config::set_changed!(config, ui.add(TextEdit::singleline(&mut config.online_discovery.host).font_size(H4_SIZE)).changed());

			ui.h3("Port").add();
			if let Some(port) = self.online_discovery_port.add(ui) && port != config.online_discovery.port {
				config.online_discovery.port = port;
				config.changed();
			}

			ui.h2("Accessibility").add();

			// Can't update the scaling factor while moving the slider
			ui.add(Slider::new(&mut self.staging_scale_factor, 0.5..=3.0).text("Scale Factor").logarithmic(true).clamping(SliderClamping::Edits));
			ui.add_enabled_ui(self.staging_scale_factor != config.accessibility.scale_factor, |ui| {
				ui.add_space(10.0);

				if ui.button("Apply").clicked() {
					config.accessibility.scale_factor = self.staging_scale_factor;
					ctx.set_zoom_factor(config.accessibility.scale_factor);
					config.changed();
				}
			});

			ui.add_space(10.0);

			ui.label("Colourblindness might make it hard to distinguish between the powerups. Enabling this adds a label above each powerup which describes it.");
			ui.vertical(|ui| config::set_changed!(config, ui.checkbox(&mut config.accessibility.powerup_labels, "Powerup Labels").changed()));
			ui.add_enabled_ui(config.accessibility.powerup_labels, |ui| {
				ui.horizontal(|ui| {
					ui.add_space(16.0);
					config::set_changed!(config, ui.checkbox(&mut config.accessibility.powerup_labels_hud_hidden, "Show when HUD is hidden").changed());
				});
			});

			back_button_clicked |= ui.b_back().clicked();

			let (text_colour, strong_colour) = {
				let style = ctx.style();
				(style.visuals.text_color(), style.visuals.strong_text_color())
			};

			self.opened_action_windows.retain(|&action| {
				let mut open = true;
				Window::new(format!("Change Bindings for {}", action.to_str()))
					.open(&mut open)
					.resizable(false)
					.show(ctx, |ui| {
						if decrease_opacity {
							ui.set_opacity(SELECTING_OPACITY);
						}

						// One part strong and another part normal
						ui.label(LayoutJobHelper::new(LABEL_SIZE)
							.add("Bindings: ", strong_colour)
							.add(&Settings::add_input_list(&config.action_manager, action, ", "), text_colour)
							.build());

						ui.add_space(10.0);

						if Settings::bind_button_clicked(ui, &format!("Select {}", self.selected_input_string), !decrease_opacity) {
							self.selecting = true;
							self.selected_input_string = String::from("(…)");
							config.action_manager.set_enabled(false);
						}

						ui.horizontal(|ui| {
							let input_bound = self.selected_input.map(|input| config.action_manager.is_bound_to(input, action));
							if Settings::bind_button_clicked(ui, "Bind", input_bound == Some(false) && !decrease_opacity) {
								change = Some((action, true));
							}

							if Settings::bind_button_clicked(ui, "Unbind", input_bound == Some(true) && !decrease_opacity) {
								change = Some((action, false));
							}
						});
					});

				open
			});
		});
		self.gui_state.after_update(res);

		if let Some((action, bind)) = change {
			let func = if bind { ActionManager::bind } else { ActionManager::unbind };
			func(&mut config.action_manager, self.selected_input.unwrap(), action);
			config.changed();
		}

		app.gui.render(&app.display, frame);
		if let Some(info) = player_preview {
			self.player_preview.render(&app.window, frame, info, config.colour);
		}

		if back_button_clicked {
			Status::PopState
		} else if self.gui_state.should_exit() {
			for open_for in &mut self.gpcp_open_for {
				for open in open_for {
					if *open {
						*open = false;
						return Status::Ok;
					}
				}
			}

			if self.opened_action_windows.pop_first().is_some() {
				Status::Ok
			} else if self.player_colour_picker.is_open() {
				self.player_colour_picker.close();
				Status::Ok
			} else {
				Status::PopState
			}
		} else { Status::Ok }
	}
}
