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

/**
 * Using different implementations of the Filesystem struct which abstracts
 * access to the filesystem based on how the game is distributed. Right now the
 * distributions released to the average user are the standalone and Flatpak
 * distributions.
 *
 * If the `standalone` flag is set, then this is a version released to the
 * average user. If the `standalone` flag isn't set, then this is used during
 * development.
 *
 * For the standalone distribution, the `data` directory is found relative to
 * the path of the Spaceships binary. This makes things easy to use for the
 * average user as they don't need to worry about their working directory.
 *
 * However, this isn't ideal for use during development as the Spaceships binary
 * is located in `./target/{debug,release}/spaceships`, and I cannot put the
 * data in that directory. Instead for development, the current working
 * directory is used.
 */
mod filesystem;
mod final_tasks;
mod playing;
mod gui;

use std::{any::Any, rc::Rc, sync::Arc, cell::RefCell, num::NonZeroU32, path::Path};

use glium::{Display, backend::glutin::simple_window_builder::GliumEventLoop};
use glutin::{config::GlConfig, display::{GlDisplay, GetGlDisplay}, surface::{WindowSurface, SurfaceAttributesBuilder}, config::ConfigTemplateBuilder, context::{NotCurrentGlContext, ContextAttributesBuilder}};
use winit::{window::{Window, WindowAttributes, Fullscreen}, event_loop::EventLoop, event::{Event, WindowEvent}, dpi::PhysicalSize};
use glutin_winit::DisplayBuilder;
use raw_window_handle::HasWindowHandle;
use quinn::{Endpoint, VarInt};
use tokio::{task, fs};

use state::{GameStateId, StateManager, Status};
use filesystem::Filesystem;
use config::{Config, save_task::ConfigSaveTask, action::{Action, ActionEvent, ActionState}};
use player_gfx::PlayerGfxCache;
use final_tasks::FinalTasks;

use crate::app::playing::{Playing, model::client::Client};
use crate::app::gui::{Gui, title::Title, play_selection::{PlaySelection, online::PsOnline, local::PsLocal, enter_address::PsEnterAddress, create_local::PsCreateLocal, story_mode::PsStoryMode}, settings::Settings, about::About};
use crate::net::{Address, serp::request::PlayRequest, utils::{WaitIdleTimeout, quic}};
use crate::protocol::discovery::GameId;
use crate::world::{colour::Colour, player::PlayerName};
use crate::utils::{ctrl_c::CtrlCHandler, ron};

struct EndpointContainer(Option<Endpoint>);

impl EndpointContainer {
	fn get(&mut self) -> Result<Endpoint, String> {
		match &self.0 {
			Some(endpoint) => Ok(endpoint.clone()),
			None => {
				let ret = App::create_endpoint()?;
				self.0 = Some(ret.clone());
				Ok(ret)
			},
		}
	}
}

pub struct App {
	/*
	 * NOTE: Do not change the order of these fields!
	 *
	 * There exists a weird unsound bug in one of the windowing or GUI crates
	 * where a segmentation fault can occur when the game closes in some
	 * orderings of these fields. This is to do with the order in which these
	 * fields are dropped.
	 *
	 * Also this ordering fixes another bug where the window closes but the
	 * process doesn't exit, in effect causing an OS-wide memory leak if the user
	 * doesn't notice. This bug only occurred on X11 on some (probably older)
	 * distros, including Debian 12.
	 */
	player_gfx: PlayerGfxCache,
	gui: Gui,
	display: Display<WindowSurface>,
	window: Window,

	final_tasks: FinalTasks,
	endpoint: EndpointContainer,
	fs: Filesystem,
	config: Rc<RefCell<Config>>,
}

impl App {
	async fn create_fs_and_config() -> Result<(Filesystem, Rc<RefCell<Config>>), String> {
		let fs = Filesystem::try_new().await?;
		let config = Rc::new(RefCell::new(Config::new(&fs).await));
		Ok((fs, config))
	}

	#[tokio::main]
	pub async fn title() -> Result<(), String> {
		let (fs, config) = App::create_fs_and_config().await?;
		App::run(GameStateId::Title, None, None, fs, config).await;
		Ok(())
	}

	#[tokio::main]
	pub async fn multiplayer(addr: Address, game_id: Arc<str>, spectate: bool, name: Option<PlayerName>, colour: Option<Colour>) -> Result<(), String> {
		let (fs, config) = App::create_fs_and_config().await?;
		let endpoint = App::create_endpoint();
		let request = PlayRequest::new(GameId::try_from(game_id).map_err(|err| format!("invalid game id: {err}"))?, spectate, config.borrow().get_style().override_with(name, colour));
		let res = Client::multiplayer(endpoint.clone(), addr, request).await?;
		let endpoint = endpoint?;
		App::run(GameStateId::Playing, Some(Box::new(res)), Some(endpoint), fs, config).await;
		Ok(())
	}

	#[tokio::main]
	pub async fn story_mode(level_path: Box<str>, sanity_checks: bool, spectate: bool, name: Option<PlayerName>, colour: Option<Colour>) -> Result<(), String> {
		let (fs, config) = App::create_fs_and_config().await?;
		let style = config.borrow().get_style().override_with(name, colour);
		let level_data = fs::read(Path::new(level_path.as_ref())).await.map_err(|err| format!("failed loading level: {err}"))?;
		let level_config = ron::deserialise(&level_data).map_err(|err| format!("failed deserialising level: {err}"))?;
		let res = Client::story_mode(Arc::new(level_config), String::from(level_path).into(), style, None, sanity_checks, spectate)?;
		App::run(GameStateId::Playing, Some(Box::new(res)), None, fs, config).await;
		Ok(())
	}

	fn build_window(event_loop: &EventLoop<()>) -> (Window, Display<WindowSurface>) { // Copied from SimpleWindowBuilder::build as I want multisampling enabled
		let attributes = WindowAttributes::default().with_title("Spaceships").with_inner_size(PhysicalSize::new(1280, 720));

		// First we start by opening a new Window
		let display_builder = DisplayBuilder::new().with_window_attributes(Some(attributes));
		let config_template_builder = ConfigTemplateBuilder::new().with_stencil_size(1);
		let (window, gl_config) = event_loop.build(display_builder, config_template_builder, |configs| {
			/*
			 * Behaviour differing from glium's SimpleWindowBuilder::build method by
			 * choosing the configuration with the most samples.
			 *
			 * I got inspiration that I could do this from:
			 * 	https://github.com/rust-windowing/glutin/blob/master/glutin_examples/examples/egl_device.rs
			 */
			configs.max_by_key(GlConfig::num_samples).unwrap()
		}).unwrap();

		let window = window.unwrap();

		// Now we get the window size to use as the initial size of the Surface
		let (width, height): (u32, u32) = window.inner_size().into();
		let attrs = SurfaceAttributesBuilder::<WindowSurface>::new()
			.build(window.window_handle().expect("couldn't obtain raw window handle").into(), NonZeroU32::new(width).unwrap(), NonZeroU32::new(height).unwrap());

		// Finally we can create a Surface, use it to make a PossiblyCurrentContext and create the glium Display
		// SAFETY: Copied from glium's SimpleWindowBuilder, should be fine
		let surface = unsafe { gl_config.display().create_window_surface(&gl_config, &attrs).unwrap() };
		let context_attributes = ContextAttributesBuilder::new().build(Some(window.window_handle().expect("couldn't obtain raw window handle").into()));

		// SAFETY: Copied from glium's SimpleWindowBuilder, should be fine
		let current_context = unsafe {
			gl_config.display().create_context(&gl_config, &context_attributes).expect("failed to create context")
		}.make_current(&surface).unwrap();

		let display = Display::from_context_surface(current_context, surface).unwrap();

		(window, display)
	}

	async fn run(
		init_state: GameStateId, arg_to_push: Option<Box<dyn Any>>, endpoint: Option<Endpoint>,
		fs: Filesystem, config: Rc<RefCell<Config>>,
	) {
		let event_loop = EventLoop::new().unwrap();
		let ctrl_c_handler = CtrlCHandler::new();
		{
			let ctrl_c_handler = ctrl_c_handler.clone();
			let sender = event_loop.create_proxy();

			/*
			 * Wakes up the event loop with a useless user event to check the
			 * Ctrl+C handler and then exit.
			 *
			 * This is needed if in the GUI and the window hasn't been focused
			 * within a second (or whatever the time is set to), as no events
			 * arrive during that time (as the GUI doesn't need to always be
			 * rendering).
			 */
			tokio::spawn(async move { ctrl_c_handler.listen_with(|| _ = sender.send_event(())).await });
		}

		let (window, display, gui) = task::block_in_place(|| {
			let (window, display) = App::build_window(&event_loop);
			let gui = Gui::new(&display, &window, &fs, &event_loop, &config.borrow());
			(window, display, gui)
		});

		let mut app = App {
			window, display, gui,
			endpoint: EndpointContainer(endpoint), final_tasks: FinalTasks::new(),
			fs, config, player_gfx: PlayerGfxCache::default(),
		};

		let mut stack = vec![init_state];

		let mut sman = StateManager::new();
		let mut enable = true;
		let mut push_msg = Some(arg_to_push);

		let config_save_task = ConfigSaveTask::new(app.fs.clone());

		#[allow(deprecated)] // Refactoring is too much work
		task::block_in_place(|| event_loop.run(|event, window_target| {
			let Some(id) = stack.last() else {
				window_target.exit();
				return;
			};

			let state = sman.get(*id, || match id {
				GameStateId::Title => Box::new(Title::new()),
				GameStateId::PlaySelection => Box::new(PlaySelection::new()),
				GameStateId::PsOnline => Box::new(PsOnline::new()),
				GameStateId::PsLocal => Box::new(PsLocal::new()),
				GameStateId::PsEnterAddress => Box::new(PsEnterAddress::new(&app.config.borrow())),
				GameStateId::PsCreateLocal => Box::new(PsCreateLocal::new(&app)),
				GameStateId::PsStoryMode => Box::new(PsStoryMode::new(&app.fs)),
				GameStateId::Playing => Box::new(Playing::new(&mut app)),
				GameStateId::Settings => Box::new(Settings::new(&mut app)),
				GameStateId::About => Box::new(About::new()),
			});

			if enable {
				if let Some(msg) = push_msg.take() {
					state.push(&mut app, msg);
				}
				state.enable(&mut app);
				enable = false;
			} else {
				assert!(push_msg.is_none());
			}

			if ctrl_c_handler.should_exit() {
				window_target.exit();
				return;
			}

			state.event(&mut app, &event);

			if let Event::WindowEvent { event, .. } = &event {
				match event {
					WindowEvent::CloseRequested => {
						window_target.exit();
						return;
					},
					WindowEvent::Resized(size) => app.display.resize((*size).into()),
					WindowEvent::RedrawRequested => {
						let mut frame = app.display.draw();

						match state.loop_iter(&mut app, &mut frame) {
							Status::Ok => (),
							Status::PushState(id, args) => {
								config_save_task.save(app.config.borrow_mut().serialise_if_changed());
								state.disable(&mut app);
								stack.push(id);
								push_msg = Some(args);
								enable = true;
							},
							Status::PopState => {
								config_save_task.save(app.config.borrow_mut().serialise_if_changed());
								state.disable(&mut app);
								stack.pop();
								enable = true;
							},
						}
						app.window.pre_present_notify(); // Apparently makes things better on Wayland, though I don't notice any difference
						frame.finish().unwrap();
						return; // Don't want to propagate this to the state's event method
					},
					_ => (),
				}

				app.config.borrow_mut().action_manager.event(event, |action| {
					if matches!(action, ActionEvent::Action(Action::ToggleFullscreen, ActionState::Pressed)) {
						app.window.set_fullscreen(if app.window.fullscreen().is_some() { None } else { Some(Fullscreen::Borderless(None)) });
					} else {
						state.action(action);
					}
				});
			}
		})).unwrap();

		if let Some(state) = stack.last().copied() {
			sman.finish(state, &mut app);
			stack.clear();
		} else {
			drop(sman);
		}

		config_save_task.finish(Rc::into_inner(app.config).unwrap().into_inner().serialise_if_changed()).await;

		if let Some(endpoint) = &app.endpoint.0 {
			endpoint.close(VarInt::from_u32(0), &[]);
			endpoint.wait_idle_timeout().await;
		}

		app.final_tasks.finish().await;
	}

	fn scale_factor(&self) -> f32 {
		self.window.scale_factor() as f32 * self.gui.zoom_factor()
	}

	fn create_endpoint() -> Result<Endpoint, String> {
		quic::client_endpoint()
	}
}
