// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
use std::{any::Any, future::Future, sync::Arc, path::PathBuf};

use glium::Frame as GliumFrame;
use winit::event::Event;
use egui::{Context, Ui, Window, Id, Align2, CursorIcon};
use quinn::Endpoint;

use crate::net::{Address, serp::request::PlayRequest, server::StreamSender};
use crate::app::{App, config::action::ActionEvent, state::{GameState, GameStateId, Status}};
use crate::app::gui::{state::{GuiState, GuiUpdateResult}, style::UiExt};
use crate::server::level::config::Config;
use crate::world::player::HumanStyle;
use crate::app::playing::model::client::{Client, ClientRes};
use crate::utils::task::Task;

/// Common components shared by the PlaySelection sub-states.
pub(super) struct Common {
	gui_state: GuiState,
	connection_task: Task<ClientRes>,
	popup_error: Option<Box<str>>,
	exit_now: bool,
}

pub(super) struct ConnectionStarter<'a>(&'a mut Common);

impl ConnectionStarter<'_> {
	pub fn connect(self, endpoint: Result<Endpoint, String>, addr: Address, request: PlayRequest) {
		self.connect_with(Client::multiplayer(endpoint, addr, request));
	}

	pub fn connect_local(self, request: PlayRequest, stream_sender: StreamSender) {
		self.connect_with(Client::local_gui(request, stream_sender));
	}

	pub fn connect_story_mode(self, config: Arc<Config>, config_path: PathBuf, style: HumanStyle, index: usize) {
		self.connect_with(async move { Client::story_mode(config, config_path, style, Some(index), false, false) });
	}

	fn connect_with(self, future: impl Future<Output = Result<ClientRes, String>> + Send + 'static) {
		debug_assert!(self.0.connection_task.empty());
		self.0.connection_task.set(future);
	}
}

impl Common {
	pub fn new() -> Common {
		Common {
			gui_state: GuiState::new(),
			connection_task: Task::new(),
			popup_error: None,
			exit_now: false,
		}
	}

	/**
	 * If a connection isn't already attempted, returns a handle that allows a
	 * connection to be started. Otherwise returns `None` if a connection is
	 * being waited for.
	 */
	pub fn connection_starter(&mut self) -> Option<ConnectionStarter<'_>> {
		self.connection_task.empty().then_some(ConnectionStarter(self))
	}

	pub fn is_connecting(&self) -> bool {
		self.connection_task.running()
	}

	/**
	 * Returns the result of the attempted connection if the connection task
	 * exists and it has finished. Otherwise returns `None`.
	 */
	fn connection_finished(&mut self) -> Option<Result<ClientRes, String>> {
		self.connection_task.result()
	}

	pub fn after_gui_update(&mut self, res: GuiUpdateResult) {
		self.gui_state.after_update(res);
	}

	pub fn get_gui_state(&mut self) -> &mut GuiState {
		&mut self.gui_state
	}

	pub fn finish_update(&mut self, ctx: &Context, ui: &mut Ui, cursor: Option<CursorIcon>) {
		if ui.b_back().clicked() {
			self.exit_now = true;
		}

		if let Some(msg) = self.popup_error.as_deref() {
			let mut open = true;
			Window::new("Error")
				.id(Id::new("multiplayer-error"))
				.open(&mut open)
				.collapsible(false)
				.anchor(Align2::CENTER_CENTER, (0.0, 0.0))
				.resizable(false)
				.show(ctx, |ui| ui.label(msg));

			if !open {
				self.popup_error = None;
			}
		}

		if self.is_connecting() {
			ctx.set_cursor_icon(CursorIcon::Wait);
		} else if let Some(cursor) = cursor {
			ctx.set_cursor_icon(cursor);
		}
	}

	pub fn set_error(&mut self, error: String) {
		log::warn!("{error}");
		self.popup_error = Some(error.into_boxed_str());
	}
}

pub(super) trait CommonGs: GameState {
	fn common(&mut self) -> &mut Common;
	fn disable(&mut self) {}
	fn loop_iter(&mut self, app: &mut App, frame: &mut GliumFrame) -> bool;
}

impl<T> GameState for T where T: CommonGs {
	fn enable(&mut self, app: &mut App) {
		self.common().gui_state.enable(&mut app.gui);
	}

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

	fn disable(&mut self, _app: &mut App) {
		let common = self.common();
		common.connection_task.stop();
		common.popup_error = None;
		CommonGs::disable(self);
	}

	fn action(&mut self, action: ActionEvent) {
		self.common().gui_state.action(action);
	}

	fn event(&mut self, app: &mut App, event: &Event<()>) {
		self.common().gui_state.event(app, event);
	}

	fn loop_iter(&mut self, app: &mut App, frame: &mut GliumFrame) -> Status {
		let should_repaint = CommonGs::loop_iter(self, app, frame);

		let common = self.common();
		if common.is_connecting() || should_repaint {
			/*
			 * Want to repaint while waiting as the game could start at any moment
			 * even when there's no user input.
			 */
			common.gui_state.should_repaint();
		}

		if common.exit_now {
			common.exit_now = false;
			Status::PopState
		} else if common.gui_state.should_exit() {
			if common.popup_error.is_some() {
				common.popup_error = None;
				Status::Ok
			} else {
				Status::PopState
			}
		} else if let Some(res) = common.connection_finished() {
			match res {
				Ok(res) => Status::PushState(GameStateId::Playing, Some(Box::new(res))),
				Err(err) => {
					common.set_error(format!("failed starting multiplayer: {err}"));
					Status::Ok
				}
			}
		} else {
			Status::Ok
		}
	}
}
