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

use std::{borrow::Cow, path::Path, fmt::Write, collections::BTreeSet};

use serde::{Serialize, Deserialize};
use ron::{ser::{self, PrettyConfig}, de};
use fs4::tokio::AsyncFileExt;
use tokio::{fs::File, io::{AsyncReadExt, AsyncWriteExt, ErrorKind}, task};
use nonempty::nonempty;
use glam::Vec3;
use strum_macros::EnumIter;
use kira::Decibels;

use action::ActionManager;
use sky::{SkyConfig, Gradient, Bezier};

use super::filesystem::{Filesystem, FsBase};

use crate::world::{colour::Colour, player::{PlayerNameRef, PlayerNameMut, HumanStyle}};
use crate::net::server::PORT;
use crate::protocol::discovery::GameIdMut;
use crate::utils::ToStr;

#[derive(Serialize, Deserialize)]
pub struct Config {
	#[serde(skip)] changed: bool,

	/*
	 * Need to use default to allow upgrading versions without any config errors
	 * (needed this after that Flatpak distribution).
	 */
	#[serde(default = "default_name")] pub name: PlayerNameMut,
	#[serde(default)] pub colour: Colour,
	#[serde(rename = "input_bindings")] pub action_manager: ActionManager,
	#[serde(default)] pub gui: Gui,
	pub graphics: Graphics,
	#[serde(default)] pub sound: Sound,
	#[serde(default)] pub online_discovery: OnlineDiscovery,
	#[serde(default)] pub accessibility: Accessibility,
	#[serde(default)] pub story_mode: StoryMode,
}

#[derive(Serialize, Deserialize)]
pub struct Gui {
	pub spectate: bool,
	pub host: String,
	pub port: u16,
	pub game_id: GameIdMut,
}

impl Gui {
	pub fn play_button_text(&self) -> &'static str {
		if self.spectate { "Spectate" } else { "Play" }
	}
}

impl Default for Gui {
	fn default() -> Gui {
		Gui { spectate: false, host: String::new(), port: PORT, game_id: GameIdMut::new() }
	}
}

#[derive(Serialize, Deserialize)]
pub struct Graphics {
	#[serde(default)] pub sky: Sky,
	#[serde(default)] pub custom_sky: SkyConfig,
	#[serde(default)] pub sky_perf: SkyPerformance,
	pub particles: Particles,
	#[serde(default = "default_forcefield_lightning")] pub forcefield_lightning: bool,
}

impl Graphics {
	pub fn get_sky_config(&self) -> Cow<'_, SkyConfig> {
		match self.sky {
			Sky::Preset(preset) => Cow::Owned(preset.to_config()),
			Sky::Custom => Cow::Borrowed(&self.custom_sky),
		}
	}
}

#[derive(Serialize, Deserialize, PartialEq, Eq)]
pub enum Sky {
	Preset(SkyPreset),
	Custom,
}

impl Default for Sky {
	fn default() -> Sky {
		Sky::Preset(SkyPreset::default())
	}
}

#[derive(Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
pub enum SkyPreset {
	#[default] Night,
	DarkNight,
	LightNight,
	Galaxy,
	DarkGalaxy,
	LightGalaxy,
	Fire,
	Forest,
	Ocean,
}

impl SkyPreset {
	pub fn to_config(self) -> SkyConfig {
		match self {
			SkyPreset::Night => SkyConfig {
				gradients: [
					Gradient::Bezier(Bezier {
						pre_power: 0.9375,
						points: nonempty![Vec3::ZERO, Vec3::new(0.375, 0.0, 0.75), Vec3::new(0.5, 0.125, 0.875), Vec3::new(0.625, 0.1875, 1.0)],
						post_power: 1.0,
					}),
					Gradient::Bezier(Bezier {
						pre_power: 0.9375,
						points: nonempty![Vec3::ZERO, Vec3::new(0.25, 0.125, 1.0)],
						post_power: 1.0,
					}),
				],
				cloud_brightness: 0.109375,
				star_colour_offset: 0.625,
				.. SkyConfig::default()
			},
			SkyPreset::DarkNight => SkyConfig {
				gradients: [
					Gradient::Bezier(Bezier {
						pre_power: 0.9375,
						points: nonempty![Vec3::ZERO, Vec3::new(0.125, 0.0, 0.5), Vec3::new(0.375, 0.125, 0.75), Vec3::new(0.625, 0.1875, 1.0)],
						post_power: 1.0,
					}),
					Gradient::Bezier(Bezier {
						pre_power: 0.9375,
						points: nonempty![Vec3::ZERO, Vec3::new(0.375, 0.125, 1.0)],
						post_power: 1.0,
					}),
				],
				cloud_brightness: 0.0625,
				star_density: 300.0,
				star_intensity: 1.25,
				.. SkyConfig::default()
			},
			SkyPreset::LightNight => SkyConfig {
				gradients: [
					Gradient::Bezier(Bezier {
						pre_power: 0.9375,
						points: nonempty![Vec3::ZERO, Vec3::new(0.375, 0.0, 0.75), Vec3::new(0.5, 0.125, 0.875), Vec3::new(0.625, 0.1875, 1.0)],
						post_power: 1.0,
					}),
					Gradient::Bezier(Bezier {
						pre_power: 0.9375,
						points: nonempty![Vec3::ZERO, Vec3::new(0.125, 0.25, 0.75), Vec3::new(0.25, 0.5, 1.0)],
						post_power: 1.0,
					}),
				],
				cloud_brightness: 0.25,
				star_colour_brightness: 0.75,
				star_colour_offset: 0.625,
				.. SkyConfig::default()
			},
			SkyPreset::Galaxy => SkyConfig {
				gradients: [
					Gradient::Bezier(Bezier {
						pre_power: 1.0,
						points: nonempty![Vec3::ZERO, Vec3::new(0.75, 0.0625, 0.375), Vec3::new(1.0, 0.125, 0.5), Vec3::new(1.0, 0.25, 1.0)],
						post_power: 1.25,
					}),
					Gradient::Bezier(Bezier {
						pre_power: 1.0,
						points: nonempty![Vec3::ZERO, Vec3::new(0.375, 0.0625, 0.75), Vec3::new(0.5, 0.125, 1.0), Vec3::new(1.0, 0.25, 1.0)],
						post_power: 1.5,
					}),
				],
				.. SkyConfig::default()
			},
			SkyPreset::DarkGalaxy => SkyConfig {
				gradients: [
					Gradient::Bezier(Bezier {
						pre_power: 1.0,
						points: nonempty![Vec3::ZERO, Vec3::new(0.75, 0.0625, 0.375), Vec3::new(1.0, 0.125, 0.5), Vec3::new(1.0, 0.25, 1.0)],
						post_power: 1.5,
					}),
					Gradient::Bezier(Bezier {
						pre_power: 1.0,
						points: nonempty![Vec3::ZERO, Vec3::new(0.375, 0.0625, 0.75), Vec3::new(0.5, 0.125, 1.0), Vec3::new(1.0, 0.25, 1.0)],
						post_power: 1.5,
					}),
				],
				cloud_brightness: 0.125,
				.. SkyConfig::default()
			},
			SkyPreset::LightGalaxy => SkyConfig {
				gradients: [
					Gradient::Bezier(Bezier {
						pre_power: 1.0,
						points: nonempty![Vec3::ZERO, Vec3::new(0.75, 0.0625, 0.375), Vec3::new(1.0, 0.125, 0.5), Vec3::new(1.0, 0.25, 1.0)],
						post_power: 1.0,
					}),
					Gradient::Bezier(Bezier {
						pre_power: 1.0,
						points: nonempty![Vec3::ZERO, Vec3::new(0.375, 0.0625, 0.75), Vec3::new(0.5, 0.125, 1.0), Vec3::new(1.0, 0.25, 1.0)],
						post_power: 1.0,
					}),
				],
				.. SkyConfig::default()
			},
			SkyPreset::Fire => SkyConfig {
				gradients: [
					Gradient::Bezier(Bezier {
						pre_power: 1.25,
						points: nonempty![Vec3::ZERO, Vec3::new(0.75, 0.0, 0.0), Vec3::new(1.0, 0.5, 0.0)],
						post_power: 1.0,
					}),
					Gradient::Bezier(Bezier {
						pre_power: 1.5,
						points: nonempty![Vec3::ZERO, Vec3::new(1.0, 0.0, 0.0), Vec3::new(1.0, 0.75, 0.0)],
						post_power: 1.0,
					}),
				],
				cloud_brightness: 0.5,
				star_density: 1000.0,
				.. SkyConfig::default()
			},
			SkyPreset::Forest => SkyConfig {
				gradients: [
					Gradient::Bezier(Bezier {
						pre_power: 1.125,
						points: nonempty![Vec3::ZERO, Vec3::new(0.0625, 0.5, 0.125), Vec3::new(0.125, 0.75, 0.25), Vec3::new(0.25, 1.0, 0.5)],
						post_power: 1.0,
					}),
					Gradient::Bezier(Bezier {
						pre_power: 1.125,
						points: nonempty![Vec3::ZERO, Vec3::new(0.125, 0.5, 0.0625), Vec3::new(0.25, 0.75, 0.125), Vec3::new(0.5, 1.0, 0.25)],
						post_power: 1.0,
					}),
				],
				.. SkyConfig::default()
			},
			SkyPreset::Ocean => SkyConfig {
				gradients: [
					Gradient::Bezier(Bezier {
						pre_power: 1.0,
						points: nonempty![Vec3::ZERO, Vec3::new(0.1875, 0.5, 0.875), Vec3::new(0.25, 0.75, 1.0), Vec3::new(0.5, 1.0, 0.9375)],
						post_power: 1.25,
					}),
					Gradient::Bezier(Bezier {
						pre_power: 1.0,
						points: nonempty![Vec3::ZERO, Vec3::new(0.25, 0.3125, 0.75), Vec3::new(0.4375, 0.375, 0.9375), Vec3::new(0.5625, 0.4375, 1.0)],
						post_power: 1.25,
					}),
				],
				cloud_brightness: 0.25,
				star_intensity: 1.5,
				.. SkyConfig::default()
			},
		}
	}
}

impl ToStr for SkyPreset {
	fn to_str(self) -> &'static str {
		match self {
			SkyPreset::Night => "Night",
			SkyPreset::DarkNight => "Dark Night",
			SkyPreset::LightNight => "Light Night",
			SkyPreset::Galaxy => "Galaxy",
			SkyPreset::DarkGalaxy => "Dark Galaxy",
			SkyPreset::LightGalaxy => "Light Galaxy",
			SkyPreset::Fire => "Fire",
			SkyPreset::Forest => "Forest",
			SkyPreset::Ocean => "Ocean",
		}
	}
}

#[derive(Clone, Serialize, Deserialize)]
pub struct SkyPerformance {
	pub clouds: usize,
	pub stars: bool,
	pub framebuffer: bool,
}

pub const CLOUD_LAYERS: usize = 3;

impl Default for SkyPerformance {
	fn default() -> SkyPerformance {
		SkyPerformance {
			clouds: CLOUD_LAYERS,
			stars: true,
			framebuffer: true,
		}
	}
}

#[derive(Serialize, Deserialize)]
pub struct Particles {
	pub thrust: bool,
	pub powerup: bool,
	pub death: bool,
	pub hit: bool,
	#[serde(default = "default_forcefield_deflection_particles")] pub forcefield_deflection: bool,
	#[serde(default = "default_checkpoint_particles")] pub checkpoint: bool,
}

#[derive(Serialize, Deserialize)]
pub struct Sound {
	pub amplitude: f32,
	pub mute: bool,
}

impl Sound {
	pub fn get_volume(&self) -> Decibels {
		let amplitude = if self.mute { 0.0 } else { self.amplitude };
		let mut volume = 20.0 * amplitude.log10();
		if volume.is_nan() { volume = f32::NEG_INFINITY }
		Decibels(volume.clamp(Decibels::SILENCE.0, Decibels::IDENTITY.0))
	}
}

impl Default for Sound {
	fn default() -> Sound {
		Sound {
			amplitude: 1.0,
			mute: false,
		}
	}
}

#[derive(Serialize, Deserialize)]
pub struct OnlineDiscovery {
	pub host: String,
	pub port: u16,
}

impl Default for OnlineDiscovery {
	fn default() -> OnlineDiscovery {
		OnlineDiscovery { host: String::from("game.spaceships.me"), port: PORT }
	}
}

#[derive(Serialize, Deserialize)]
pub struct Accessibility {
	pub scale_factor: f32,
	pub powerup_labels: bool,
	#[serde(default)] pub powerup_labels_hud_hidden: bool,
}

impl Default for Accessibility {
	fn default() -> Accessibility {
		Accessibility { scale_factor: 1.0, powerup_labels: false, powerup_labels_hud_hidden: false }
	}
}

#[derive(Default, Serialize, Deserialize)]
pub struct StoryMode {
	pub completed_levels: BTreeSet<usize>,
	pub unlock_all_levels: bool,
}

const PATH: &str = "config.ron";

impl Config {
	pub async fn new(fs: &Filesystem) -> Config {
		match Config::load(fs).await {
			Ok(config) => config,
			Err(err) => {
				let config = Config::default();
				if let Some(err) = err {
					log::warn!("failed loading config: {err}");
					/*
					 * Don't want to save the config file on error, as it might be a
					 * syntax error from manual editing and I don't want to overwrite
					 * the user's changes.
					 */
				} else {
					log::info!("Config file doesn't exist, creating one");

					if let Err(err) = config.serialise_and_save(fs).await {
						log::warn!("{err}");
					}
				}

				config
			},
		}
	}

	pub fn get_style(&self) -> HumanStyle {
		HumanStyle { name: self.name.to_shared(), colour: self.colour }
	}

	/**
	 * Tries to load the config from the file.
	 *
	 * Returns `Ok` on success, `Err(None)` when the file isn't found (so falling
	 * back to default) or `Err(Some(String))` on another error worth logging.
	 */
	async fn load(fs: &Filesystem) -> Result<Config, Option<String>> {
		let path = fs.get(FsBase::Config, Path::new(PATH));
		let mut file = match File::open(&path).await {
			Ok(file) => file,
			Err(err) if err.kind() == ErrorKind::NotFound => return Err(None),
			Err(err) => return Err(Some(format!("cannot open file \"{}\": {err}", path.display()))),
		};
		task::block_in_place(|| file.lock_shared()).map_err(|err| Some(format!("cannot lock (shared) file: {err}")))?;
		let mut data = Vec::new();
		file.read_to_end(&mut data).await.map_err(|err| Some(format!("cannot read file: {err}")))?;
		file.unlock_async().await.map_err(|err| format!("cannot unlock file: {err}"))?;
		Config::deserialise(&data).map_err(Some)
	}

	fn deserialise(data: &[u8]) -> Result<Config, String> {
		// Not using `utils::ron` because the extensions fail to deserialise existing configs
		de::from_bytes(data).map_err(|err| format!("failed deserialising: {err}"))
	}

	pub fn serialise_if_changed(&mut self) -> Option<SerialisedConfig> {
		if self.changed {
			self.changed = false;

			match self.serialise() {
				Ok(data) => Some(data),
				Err(err) => {
					log::warn!("cannot serialise config: {err}");
					None
				},
			}
		} else { None }
	}

	fn serialise(&self) -> Result<SerialisedConfig, String> {
		let mut data = String::from("// Warning: All comments made will be automatically overwritten when changing these in the GUI\n");
		ser::to_writer_pretty(&mut data, self, PrettyConfig::new().indentor(String::from("\t"))).map_err(|err| format!("cannot serialise: {err}"))?;
		let _ = writeln!(data); // Need to add a final newline
		Ok(SerialisedConfig(data.into_bytes()))
	}

	async fn serialise_and_save(&self, fs: &Filesystem) -> Result<(), String> {
		let config = self.serialise().map_err(|err| format!("cannot serialise config: {err}"))?;
		config.save(fs).await.map_err(|err| format!("cannot save config: {err}"))
	}

	pub fn changed(&mut self) {
		self.changed = true;
	}

	pub fn set_changed(&mut self, changed: bool) {
		self.changed |= changed;
	}
}

pub struct SerialisedConfig(Vec<u8>);

impl SerialisedConfig {
	async fn save(&self, fs: &Filesystem) -> Result<(), String> {
		/*
		 * Cannot call `File::create` because that truncates the file, which
		 * breaks the locking. Instead I am truncating the file but only after
		 * locking it.
		 */
		let path = fs.get(FsBase::Config, Path::new(PATH));
		let mut file = File::options().write(true).create(true).truncate(false).open(&path).await.map_err(|err| format!("cannot create file \"{}\": {err}", path.display()))?;
		task::block_in_place(|| file.lock_exclusive()).map_err(|err| format!("cannot lock (exclusive) file: {err}"))?;
		file.set_len(0).await.map_err(|err| format!("cannot truncate file: {err}"))?;
		file.write_all(&self.0).await.map_err(|err| format!("cannot write to file: {err}"))?;
		file.unlock_async().await.map_err(|err| format!("cannot unlock file: {err}"))
	}
}

/*
 * Prevents borrow checker problems with `config.set_changed(egui_stuff(&mut
 * config.stuff).changed())`. I don't need this macro but the alternative is too
 * much boilerplate (it's just an extra line, but that adds up to a lot of
 * lines).
 */
macro_rules! set_changed {
	( $config:expr, $x:expr ) => {
		{
			let changed = $x;
			$config.set_changed(changed);
		}
	};
}

pub(crate) use set_changed;

impl Default for Config {
	fn default() -> Config {
		Config {
			changed: false,
			name: default_name(),
			colour: Colour::RED,
			action_manager: ActionManager::default(),
			gui: Gui::default(),
			graphics: Graphics {
				sky: Sky::default(),
				custom_sky: SkyPreset::default().to_config(),
				sky_perf: SkyPerformance::default(),
				particles: Particles { thrust: true, powerup: true, death: true, hit: true, forcefield_deflection: default_forcefield_deflection_particles(), checkpoint: default_checkpoint_particles() },
				forcefield_lightning: default_forcefield_lightning(),
			},
			sound: Sound::default(),
			online_discovery: OnlineDiscovery::default(),
			accessibility: Accessibility::default(),
			story_mode: StoryMode::default(),
		}
	}
}

fn default_name() -> PlayerNameMut { const { PlayerNameRef::new("Player").unwrap() }.owned() }
fn default_forcefield_deflection_particles() -> bool { true }
fn default_checkpoint_particles() -> bool { true }
fn default_forcefield_lightning() -> bool { true }
