// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
use std::time::Instant;

use glium::Frame as GliumFrame;
use egui::CursorIcon;

use super::{common::{Common, CommonGs}, discovery as discovery_gui};

use crate::app::{App, config};
use crate::app::gui::{GuiState, style::UiExt};
use crate::net::{Address, request};
use crate::protocol::discovery::{DiscoveryMessage, DiscoveryServer, Source, UnofficialSource};
use crate::utils::task::Task;

pub struct PsOnline {
	common: Common,
	task: Task<(Box<str>, DiscoveryMessage)>,
	servers: Option<Servers>,
}

struct Servers {
	/*
	 * Not using the config's online discovery host because the user can search
	 * online games, go to the settings and change that host and then go back and
	 * view the online games. After this, if I didn't store the host in this
	 * field, connecting to the official servers would use the incorrect newly
	 * updated host rather than the old host the discovery request was first made
	 * to.
	 */
	official_host: Box<str>,
	servers: Vec<(Source, DiscoveryServer)>,
	init_time: Instant,
}

impl PsOnline {
	pub fn new() -> PsOnline {
		PsOnline {
			common: Common::new(),
			task: Task::new(),
			servers: None,
		}
	}
}

impl CommonGs for PsOnline {
	fn common(&mut self) -> &mut Common { &mut self.common }

	fn disable(&mut self) {
		self.task.stop();
	}

	fn loop_iter(&mut self, app: &mut App, frame: &mut GliumFrame) -> bool {
		let config = &mut app.config.borrow_mut();

		let res = GuiState::update(&mut app.gui, "ps-online", &app.window, |ctx, ui| {
			ui.h1("Online Servers").add();

			if ui.b2_enabled("Search", self.task.empty()).clicked() {
				let endpoint = app.endpoint.get();
				let host = config.online_discovery.host.clone().into_boxed_str();
				let addr = Address { host: host.clone(), port: config.online_discovery.port };
				self.task.set(async {
					request::discover(endpoint, addr).await.map(|msg| (host, msg))
				});
			}

			if let Some(res) = self.task.result() {
				match res {
					Ok((official_host, servers)) => {
						let mut servers = servers.into_pairs();
						servers.sort_by_key(|(_src, server)| !server.player_count);
						self.servers = Some(Servers { official_host, servers, init_time: Instant::now() });
					},
					Err(err) => self.common.set_error(format!("failed discovering servers: {err}")),
				}
			}

			ui.add_space(30.0);
			config::set_changed!(config, ui.checkbox(&mut config.gui.spectate, "Join as Spectator").changed());
			ui.add_space(30.0);

			if let Some(servers) = &self.servers {
				for (src, server) in &servers.servers {
					let host = match src {
						Source::Official => servers.official_host.as_ref(),
						Source::Unofficial(UnofficialSource::Ip(addr)) => &addr.to_canonical().to_string(),
						Source::Unofficial(UnofficialSource::Hostname(host)) => host.get(),
					};

					discovery_gui::add_entry(ui, app.endpoint.get(), config, self.common.connection_starter(), server, host, servers.init_time);
				}
			}

			self.common.finish_update(ctx, ui, self.task.running().then_some(CursorIcon::Wait));
		});
		self.common.after_gui_update(res);

		app.gui.render(&app.display, frame);

		self.task.running()
	}
}
