use std::{
    cmp::max,
    ffi::OsStr,
    net::{IpAddr, SocketAddr},
};

use curseofrust::state::{BasicOpts, MultiplayerOpts};

use wrapper::{DifficultyWrapper as Difficulty, SpeedWrapper as Speed, StencilWrapper as Stencil};

mod wrapper;

const DEFAULT_SERVER_PORT: u16 = 19140;
const DEFAULT_CLIENT_PORT: u16 = 19150;

/// Parses the command line arguments.
#[deprecated(note = "use `parse_to_options` instead")]
pub fn parse(
    args: impl IntoIterator<Item = impl Into<std::ffi::OsString>>,
) -> Result<(BasicOpts, MultiplayerOpts), Error> {
    let options = parse_to_options(args)?;
    if options.exit {
        std::process::exit(0);
    }
    Ok((options.basic, options.multiplayer))
}

#[cfg(feature = "net-proto")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Protocol {
    Tcp,
    #[default]
    Udp,
    WebSocket,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ControlMode {
    /// Control mode designed for termux.
    Termux,
    /// Pure-keyboard controlling. Same as curseofwar.
    #[default]
    Keyboard,
    /// Basic cursor controlling and full-featured
    /// keyboard control.
    Hybrid,
}

#[cfg(feature = "net-proto")]
impl std::str::FromStr for Protocol {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(match s {
            "tcp" => Protocol::Tcp,
            "udp" => Protocol::Udp,
            "ws" | "websocket" => Protocol::WebSocket,
            _ => {
                return Err(Error::UnknownVariant {
                    ty: "protocol",
                    variants: &["tcp", "udp", "ws or websocket"],
                    value: s.to_owned(),
                })
            }
        })
    }
}

impl std::str::FromStr for ControlMode {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(match s {
            "termux" => Self::Termux,
            "keyboard" => Self::Keyboard,
            "hybrid" => Self::Hybrid,
            _ => {
                return Err(Error::UnknownVariant {
                    ty: "control_mode",
                    variants: &["termux", "keyboard", "hybrid"],
                    value: s.to_owned(),
                })
            }
        })
    }
}

struct ServerAddr(SocketAddr);

impl std::str::FromStr for ServerAddr {
    type Err = std::net::AddrParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        s.parse::<SocketAddr>()
            .or_else(|_| {
                s.parse::<IpAddr>()
                    .map(|addr| (addr, DEFAULT_SERVER_PORT).into())
            })
            .map(Self)
    }
}

#[derive(Default)]
struct ModifiedMarkers {
    width: bool,
}

pub fn parse_to_options<I, S>(args: I) -> Result<Options, Error>
where
    I: IntoIterator<Item = S>,
    S: Into<std::ffi::OsString>,
{
    let mut basic_opts = BasicOpts::default();
    let mut multiplayer_opts = MultiplayerOpts::default();
    let mut exit = false;
    let mut cm = ControlMode::default();
    let mut mm = ModifiedMarkers::default();

    #[cfg(feature = "net-proto")]
    let mut protocol = Protocol::default();

    let args = clap_lex::RawArgs::new(args);
    let mut cursor = args.cursor();
    args.next(&mut cursor); // skip bin
    let mut short_buf = String::new();
    while let Some(arg) = args.next(&mut cursor) {
        if let Some(mut s) = arg.to_short() {
            while let Some(Ok(flag)) = s.next() {
                macro_rules! parse {
                    ($a:expr, $t:expr, $vt:ty) => {{
                        short_buf.clear();
                        short_buf.extend(s.by_ref().map_while(Result::ok));
                        let v: Result<$vt, _> = (!short_buf.is_empty())
                            .then_some(&*short_buf)
                            .or_else(|| args.next(&mut cursor).and_then(|a| a.to_value().ok()))
                            .ok_or_else(|| Error::MissingValue { arg: $a, ty: $t })
                            .and_then(|a| a.parse().map_err(From::from));
                        v
                    }};
                    ($a:expr, $t:expr) => {
                        parse!($a, $t, _)
                    };
                }
                match flag {
                    'W' => {
                        basic_opts.width = {
                            mm.width = true;
                            max(parse!("-W", "integer")?, 15)
                        }
                    }
                    // Minimum height.
                    'H' => basic_opts.height = max(parse!("-H", "integer")?, 5),
                    'S' => basic_opts.shape = parse!("-S", "shape", Stencil)?.0,
                    'l' => basic_opts.locations = parse!("-l", "integer")?,
                    'i' => basic_opts.inequality = Some(parse!("-i", "integer")?),
                    'q' => basic_opts.conditions = Some(parse!("-q", "integer")?),
                    'r' => basic_opts.keep_random = true,
                    'd' => basic_opts.difficulty = parse!("-d", "difficulty", Difficulty)?.0,
                    's' => basic_opts.speed = parse!("-s", "speed", Speed)?.0,
                    'R' => basic_opts.seed = parse!("-R", "integer")?,
                    'T' => basic_opts.timeline = true,
                    'E' => {
                        basic_opts.clients = parse!("-E", "integer")?;
                        if matches!(multiplayer_opts, MultiplayerOpts::None) {
                            multiplayer_opts = MultiplayerOpts::Server {
                                port: DEFAULT_SERVER_PORT,
                            };
                        }
                    }
                    'e' => {
                        multiplayer_opts = MultiplayerOpts::Server {
                            port: parse!("-e", "integer")?,
                        };
                    }
                    'C' => {
                        let ServerAddr(parsed) = parse!("-C", "SocketAddr")?;
                        if let MultiplayerOpts::Client { ref mut server, .. } = multiplayer_opts {
                            *server = parsed;
                        } else {
                            multiplayer_opts = MultiplayerOpts::Client {
                                server: parsed,
                                port: DEFAULT_CLIENT_PORT,
                            }
                        }
                    }
                    'c' => {
                        let parsed = parse!("-c", "integer")?;
                        if let MultiplayerOpts::Client { ref mut port, .. } = multiplayer_opts {
                            *port = parsed
                        } else {
                            multiplayer_opts = MultiplayerOpts::Client {
                                server: SocketAddr::from((
                                    std::net::Ipv4Addr::LOCALHOST,
                                    DEFAULT_SERVER_PORT,
                                )),
                                port: parsed,
                            };
                        }
                    }
                    'v' => {
                        println!("curseofrust");
                        exit = true
                    }
                    'h' => {
                        println!("{HELP_MSG}");
                        exit = true
                    }

                    #[cfg(feature = "net-proto")]
                    'p' => protocol = parse!("-p", "protocol", Protocol)?,

                    'm' => cm = parse!("-m", "control mode", ControlMode)?,

                    f => return Err(Error::UnknownFlag { flag: f }),
                }
            }
        }
    }

    if !mm.width && basic_opts.shape == curseofrust::grid::Stencil::Rect {
        // make rect more elegant this way
        basic_opts.width += 10;
    }

    Ok(Options {
        basic: basic_opts,
        multiplayer: multiplayer_opts,
        exit,

        #[cfg(feature = "net-proto")]
        protocol,
        control_mode: cm,
    })
}

/// The options for the program.
#[derive(Debug)]
#[non_exhaustive]
pub struct Options {
    pub basic: BasicOpts,
    pub multiplayer: MultiplayerOpts,
    pub exit: bool,
    pub control_mode: ControlMode,

    #[cfg(feature = "net-proto")]
    pub protocol: Protocol,
}

#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
    MissingValue {
        arg: &'static str,
        ty: &'static str,
    },
    InvalidIntValueFmt(std::num::ParseIntError),
    InvalidIpAddrValueFmt(std::net::AddrParseError),
    NonUnicodeValue {
        content: Box<OsStr>,
    },
    UnknownFlag {
        flag: char,
    },
    UnknownVariant {
        ty: &'static str,
        variants: &'static [&'static str],
        value: String,
    },
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Error::MissingValue { arg, ty } => {
                write!(
                    f,
                    "missing value for argument '{arg}', expected type '{ty}'"
                )
            }
            Error::InvalidIntValueFmt(err) => write!(f, "invalid integer formatting: {err}"),
            Error::InvalidIpAddrValueFmt(err) => write!(f, "invalid IP-address formatting: {err}"),
            Error::NonUnicodeValue { content } => {
                write!(f, "non-unicode value: {content:?}")
            }
            Error::UnknownFlag { flag } => write!(f, "unknown flag: {flag}"),
            Error::UnknownVariant {
                ty,
                variants,
                value,
            } => write!(
                f,
                "unknown variant '{value}' for type '{ty}', expected one of: {variants:?}",
            ),
        }
    }
}

impl<'a> From<&'a OsStr> for Error {
    #[inline]
    fn from(value: &'a OsStr) -> Self {
        Error::NonUnicodeValue {
            content: value.into(),
        }
    }
}

impl From<std::num::ParseIntError> for Error {
    #[inline]
    fn from(value: std::num::ParseIntError) -> Self {
        Error::InvalidIntValueFmt(value)
    }
}

impl From<std::net::AddrParseError> for Error {
    #[inline]
    fn from(value: std::net::AddrParseError) -> Self {
        Error::InvalidIpAddrValueFmt(value)
    }
}

impl std::error::Error for Error {}

macro_rules! help_msg_basic {
    () => {
        r#"                                __
   ____                        /  ]  ________             __
  / __ \_ _ ___ ___ ___    __ _| |_  |  ___  \__  __ ___ _| |__
_/ /  \/ | |X _/ __/ __\  /   \   /  | |___| | | |  / __/_  __/
\ X    | | | | |__ | __X  | X || |   | X_  __/ |_|  X__ | | X
 \ \__/\ __X_| \___/___/  \___/| |   | | \ \_ X__ /___ /  \__/
  \____/                       |/    |__\ \__/

  Made by DM Earth in 2024-2025.

  Command line arguments:

-W width
  Map width (default is 21)

-H height
  Map height (default is 21)

-S [rhombus|rect|hex]
  Map shape (rectangle is default). Max number of countries N=4 for rhombus and rectangle, and N=6 for the hexagon.

-l [2|3| ... N]
  Sets L, the number of countries (default is N).

-i [0|1|2|3|4]
  Inequality between the countries (0 is the lowest, 4 in the highest).

-q [1|2| ... L]
  Choose player's location by its quality (1 = the best available on the map, L = the worst). Only in the singleplayer mode.

-r
  Absolutely random initial conditions, overrides options -l, -i, and -q.

-d [ee|e|n|h|hh]
  Difficulty level (AI) from the easiest to the hardest (default is normal).

-s [p|sss|ss|s|n|f|ff|fff]
  Game speed from the slowest to the fastest (default is normal).

-R seed
  Specify a random seed (unsigned integer) for map generation.

-T
  Show the timeline.

-E [1|2| ... L]
  Start a server for not more than L clients.

-e port
  Server's port (19140 is default).

-C IP
  Start a client and connect to the provided server's IP-address.

-c port
  Clients's port (19150 is default).

-m [keyboard|termux|hybrid]
  Control method.

-v
  Display the version number

-h
  Display this help"#
    };
}

#[cfg(feature = "net-proto")]
macro_rules! help_msg_net_proto {
    () => {
        r#"

-p [tcp|udp|ws|websocket]
  Specify the networking protocol to use in multiplayer (udp is default)."#
    };
}

#[cfg(not(feature = "net-proto"))]
macro_rules! help_msg_net_proto {
    () => {
        ""
    };
}

/// The help message for the program.
pub const HELP_MSG: &str = concat![help_msg_basic!(), help_msg_net_proto!()];
