// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
use std::{rc::Rc, time::Instant, net::SocketAddr, fmt::Write};

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::lan_discovery::client::LanDiscoveryClient;
use crate::protocol::discovery::DiscoveryServer;

pub struct PsLocal {
	common: Common,
	lan_discovery: LanDiscoveryClient,
	servers: Vec<LocalDiscoveryServer>,
}

struct LocalDiscoveryServer {
	server: DiscoveryServer,
	host: Rc<str>,
	init_time: Instant,
}

impl PsLocal {
	pub fn new() -> PsLocal {
		PsLocal {
			common: Common::new(),
			lan_discovery: LanDiscoveryClient::new(),
			servers: Vec::new(),
		}
	}

	fn socket_addr_without_port(socket_addr: SocketAddr) -> Rc<str> {
		Rc::from(match socket_addr {
			SocketAddr::V4(addr) => addr.ip().to_string(),
			SocketAddr::V6(addr) => {
				let mut addr_str = addr.ip().to_canonical().to_string();
				if addr.scope_id() != 0 { // A scope id of zero in the socket address doesn't get printed
					let _ = write!(addr_str, "%{}", addr.scope_id());
				}
				addr_str
			}
		}.as_ref())
	}
}

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

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

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

		let new_messages = self.lan_discovery.update(app.endpoint.get());
		let has_new_messages = !new_messages.is_empty();
		for (addr, msg) in new_messages {
			let host = Self::socket_addr_without_port(addr);
			let init_time = Instant::now();
			for server in msg.into_official_servers() {
				self.servers.push(LocalDiscoveryServer { server, host: Rc::clone(&host), init_time });
			}
		}

		if has_new_messages {
			self.servers.sort_by_key(|server| !server.server.player_count);
		}

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

			if ui.b2_enabled("Search LAN", !self.lan_discovery.open()).clicked() {
				self.servers.clear();

				if let Err(err) = self.lan_discovery.discover() {
					self.common.set_error(format!("failed searching LAN: {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);

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

			/*
			 * Using the `Wait` cursor whenever there are no servers, in which the
			 * user can't do much, and the `Progress` cursor when there's at least
			 * one server, in which the user can do something (play one of the
			 * existing games). This is based on the recommendation by
			 * https://developer.mozilla.org/en-US/docs/Web/CSS/cursor for whether
			 * the user can interact with the interface.
			 *
			 * Technically I'm not following that recommendation perfectly as even
			 * with the `Wait` cursor, the user can exit the LAN discovery state.
			 */
			self.common.finish_update(ctx, ui, self.lan_discovery.open().then_some(if self.servers.is_empty() { CursorIcon::Wait } else { CursorIcon::Progress }));
		});
		self.common.after_gui_update(res);

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

		self.lan_discovery.open()
	}
}
