#[cfg(test)]
mod tests;

use crate::types::Str;
use std::error::Error;
use std::fmt::{Display, Formatter, Result as FmtResult};

#[derive(Clone, Copy, PartialEq)]
#[cfg_attr(test, derive(Debug))]
pub enum Mode {
    Detect,
    Stats,
    Details,
}

#[derive(Clone, Copy, PartialEq, PartialOrd)]
#[cfg_attr(test, derive(Debug))]
pub enum Output {
    Quiet,
    Normal,
    Verbose,
}

#[derive(Default)]
#[cfg_attr(test, derive(Debug, PartialEq))]
pub struct Config {
    file_path: Option<Str>,
    mode: Option<Mode>,
    output: Option<Output>,
}

impl Config {
    pub fn file_path(&self) -> Option<&str> {
        self.file_path.as_deref()
    }

    pub fn mode(&self) -> Option<Mode> {
        self.mode
    }

    pub fn output(&self) -> Option<Output> {
        self.output
    }
}

#[derive(Debug)]
pub enum CliError {
    NoValue(Str),
    InvalidValue(Str, Str),
    Unknown(Str),
}

impl Error for CliError {}

impl Display for CliError {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        use CliError::*;
        match self {
            NoValue(arg) => write!(f, "Option '{arg}' requires a value"),
            InvalidValue(arg, value) => write!(f, "Invalid value '{value}' for option '{arg}'"),
            Unknown(arg) => write!(f, "Unknown option '{arg}'"),
        }
    }
}

macro_rules! err {
    ($e: expr) => {{
        use CliError::*;
        return Err($e);
    }};
}

macro_rules! from {
    ($s: expr) => {
        From::from($s.as_ref())
    };
}

pub fn read_args(
    mut args: impl Iterator<Item = impl AsRef<str>>,
) -> Result<Option<Config>, CliError> {
    let mut config = Config::default();
    let mut escape = false;

    while let Some(arg) = args.next() {
        let arg = arg.as_ref();

        if escape {
            config.file_path = Some(from!(arg));
            break;
        }

        if arg.is_empty() {
            continue;
        }

        if !arg.starts_with('-') {
            config.file_path = Some(from!(arg));
            continue;
        }

        macro_rules! next {
            () => {
                match args.next() {
                    Some(value) => value,
                    _ => err!(NoValue(from!(arg))),
                }
            };
        }

        match arg {
            "--mode" => {
                let value = next!();
                use Mode::*;
                config.mode = Some(match value.as_ref() {
                    "detect" => Detect,
                    "stats" => Stats,
                    "details" => Details,
                    _ => err!(InvalidValue(from!(arg), from!(value))),
                });
            }
            "-s" | "--stats" => {
                config.mode = Some(Mode::Stats);
            }
            "-d" | "--details" => {
                config.mode = Some(Mode::Details);
            }

            "--output" => {
                let value = next!();
                use Output::*;
                config.output = Some(match value.as_ref() {
                    "quiet" => Quiet,
                    "normal" => Normal,
                    "verbose" => Verbose,
                    _ => err!(InvalidValue(from!(arg), from!(value))),
                });
            }
            "-v" | "--verbose" => {
                config.output = Some(Output::Verbose);
            }
            "-q" | "--quiet" => {
                config.output = Some(Output::Quiet);
            }

            "-h" | "--help" => return Ok(None),

            "--" => {
                escape = true;
            }

            _ => err!(Unknown(from!(arg))),
        }
    }

    Ok(Some(config))
}
