// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
#[cfg(feature = "client")] mod app;
mod net;
mod protocol;
mod server;
mod world;
mod utils;

use std::{process, io::Write, sync::Arc, path::PathBuf};

use clap::{Parser, Args, Subcommand, ValueEnum};
use log::Level;
use env_logger::{Env, Builder};
use rustls::crypto::aws_lc_rs;
use colored::Colorize;

use net::server::{PORT, init};
use utils::escape;
#[cfg(feature = "client")] use app::App;
#[cfg(feature = "client")] use net::{Address, request};
#[cfg(feature = "client")] use protocol::info::Info;
#[cfg(feature = "client")] use world::{colour::Colour, player::PlayerName};

// Heavily based on clap documentation: https://docs.rs/clap/latest/clap/_derive/_tutorial/chapter_0/index.html
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
	#[cfg(feature = "client")] #[command(subcommand)] command: Option<Command>,
	#[cfg(not(feature = "client"))] #[command(subcommand)] command: Command,
}

#[derive(Subcommand)]
enum Command {
	/// Starts a game immediately rather than opening the GUI
	#[cfg(feature = "client")]
	Play {
		/// The address of the server
		#[arg(default_value_t = Box::from("127.0.0.1"))] host: Box<str>,

		/// The game id of the server to join
		#[arg(default_value_t = Arc::from(""))] game_id: Arc<str>,

		/// The UDP port of the server
		#[arg(short, long, default_value_t = PORT)] port: u16,

		/// Requests to join as a spectator
		#[arg(short = 'e', long)] spectate: bool,

		/// Joins the game with this name
		#[arg(short, long)] name: Option<Arc<str>>,

		/// Joins the game with this colour
		#[arg(short, long)] colour: Option<Box<str>>,

		/// Plays a given level
		#[arg(value_name = "PATH", short, long, conflicts_with_all = ["host", "game_id", "port"])] level: Option<Box<str>>,

		/// Whether sanity checks are enabled for the level
		#[arg(long, requires = "level")] sanity_checks: bool,
	},

	/// Requests build information about a server or the client
	#[cfg(feature = "client")]
	Info {
		/// The address of the server, skip this to print info about the client
		host: Option<Box<str>>,

		/// The UDP port of the server
		#[arg(short, long, default_value_t = PORT, requires = "host")] port: u16,
	},

	/// Starts a server
	Server(ServerArgs),

	/// Saves the block map generated from a configuration file to an image
	SaveBlocks {
		/// Path of the input configuration file
		#[arg(value_name = "PATH", short = 'c', long = "config")] config_path: PathBuf,

		/// Path of the output image file of the blocks in the world
		#[arg(value_name = "PATH", short = 'o', long = "output")] output_path: PathBuf,

		/// Use a level config instead of a multiplayer config
		#[arg(short, long)] level: bool,
	},
}

#[derive(Args)]
struct ServerArgs {
	/// List of game servers to start
	game_servers: Vec<String>,

	/// The address to bind on
	#[arg(value_name = "ADDRESS", short = 'a', long = "address", default_value_t = String::from("::"))] addr: String,

	/// The UDP port to listen to
	#[arg(short, long, default_value_t = PORT)] port: u16,

	/// The path of the certificate chain in PEM format
	#[arg(value_name = "PATH", long, requires = "key_path")] cert_path: Option<Box<str>>,

	/// The path of the private key in PEM format
	#[arg(value_name = "PATH", long, requires = "cert_path")] key_path: Option<Box<str>>,

	/// Whether LAN discovery is enabled
	#[arg(value_name = "SETTING", value_enum, long, default_value_t = Setting::Enabled)] lan_discovery: Setting,

	/// Default game server privacy config
	#[arg(short = 'P', long, default_value_t = Box::from("local"))] privacy: Box<str>,

	/// Publishes game servers to this address
	#[arg(value_name = "ADDRESS", long = "publish")] publish_addr: Option<Box<str>>,

	/// Destination UDP port used when making the publication request
	#[arg(value_name = "PORT", long, default_value_t = PORT)] publish_port: u16,

	/// The server's hostname published to a publication server
	#[arg(value_name = "NAME", long)] server_name: Option<Arc<str>>,

	/// Whether to accept publications and include them in discovery results
	#[arg(long, default_value_t = false)] accept_publications: bool,

	/// Whether to periodically send copies of the world to allow clients to perform sanity checks
	#[arg(long, default_value_t = false)] sanity_checks: bool,

	/// Whether the server should shut down when the console receives an EOF
	#[arg(value_name = "SETTING", value_enum, long, default_value_t = Setting::Enabled)] exit_on_eof: Setting,
}

#[derive(Clone, Copy, ValueEnum)]
enum Setting {
	Enabled,
	Disabled,
}

impl From<Setting> for bool {
	fn from(config: Setting) -> bool {
		match config {
			Setting::Enabled => true,
			Setting::Disabled => false,
		}
	}
}

fn main() {
	let command = if cfg!(feature = "client") {
		Cli::parse().command
	} else {
		Cli::try_parse().unwrap_or_else(|err| {
			let _ = err.print();
			let server_only_note = "\nNOTE: Using the server-only build. Some features are disabled.";
			eprintln!("{}", server_only_note.yellow().bold());

			process::exit(err.exit_code());
		}).command
	};
	let mut builder = Builder::from_env(Env::default().default_filter_or("spaceships=info"));
	builder.format_timestamp_millis();
	builder.format(|buf, record| { // Reproduces the same log format used by env_logger but with escaping
		let body = escape::singleline(&record.args().to_string());
		let level = match record.level() {
			Level::Error => "ERROR".red().bold(),
			Level::Warn => "WARN".yellow(),
			Level::Info => "INFO".green(),
			Level::Debug => "DEBUG".blue(),
			Level::Trace => "TRACE".cyan(),
		};
		let open = "[".bright_black();
		let close = "]".bright_black();
		writeln!(buf, "{open}{} {level}  {}{close} {body}", buf.timestamp_millis(), record.module_path().unwrap_or_default())
	});
	builder.init();

	/*
	 * Not sure if to use ring or aws_lc_rs, but Rustls uses aws_lc_rs as the
	 * default, so I will use it as well. A bit of research suggests that
	 * aws_lc_rs is slightly faster.
	 *
	 * Source for this bit of code:
	 * https://github.com/rustls/rustls/issues/1938#issuecomment-2099920256
	 */
	aws_lc_rs::default_provider().install_default().unwrap();

	if let Err(err) = run(command) {
		log::error!("{err}");
		process::exit(1);
	}
}

#[cfg(feature = "client")]
fn run(command: Option<Command>) -> Result<(), String> {
	fn convert_colour(colour: Option<Box<str>>) -> Result<Option<Colour>, String> {
		match colour.map(|s| Colour::try_from(s.as_ref())) {
			Some(Ok(colour)) => Ok(Some(colour)),
			Some(Err(err)) => Err(format!("invalid colour: {err}")),
			None => Ok(None),
		}
	}

	fn convert_name(name: Option<Arc<str>>) -> Result<Option<PlayerName>, String> {
		match name.map(PlayerName::try_from) {
			Some(Ok(name)) => Ok(Some(name)),
			Some(Err(err)) => Err(format!("invalid name: {err}")),
			None => Ok(None),
		}
	}

	match command {
		Some(Command::Play { host, game_id, port, spectate, name, colour, level: None, sanity_checks: _ }) => {
			let addr = Address { host, port };
			App::multiplayer(addr, game_id, spectate, convert_name(name)?, convert_colour(colour)?)?;
		},
		Some(Command::Play { host: _, game_id: _, port: _, spectate, name, colour, level: Some(level), sanity_checks }) => App::story_mode(level, sanity_checks, spectate, convert_name(name)?, convert_colour(colour)?)?,
		Some(Command::Info { host: Some(host), port }) => {
			match request::info(host.as_ref(), port) {
				Ok(info) => info.print(),
				Err(err) => return Err(format!("failed receiving server info: {err}")),
			}
		},
		Some(Command::Info { host: None, port: _ }) => Info::new().print(),
		Some(Command::Server(args)) => init::run(args)?,
		Some(Command::SaveBlocks { config_path, output_path, level }) => server::save_blocks(config_path, &output_path, level)?,
		None => App::title()?,
	}

	Ok(())
}

#[cfg(not(feature = "client"))]
fn run(command: Command) -> Result<(), String> {
	match command {
		Command::Server(args) => init::run(args)?,
		Command::SaveBlocks { config_path, output_path, level } => server::save_blocks(config_path, &output_path, level)?,
	}

	Ok(())
}
