//! An ISO 4217 currency code.

#[cfg(test)]
pub(crate) mod test;

use std::fmt;

use crate::{
    from_warning_all, json,
    warning::{self, GatherWarnings as _},
    IntoCaveat as _, Verdict,
};

/// The warnings that can happen when parsing or linting a currency code.
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Warning {
    /// The currency field does not require char escape codes.
    ContainsEscapeCodes,

    /// The field at the path could not be decoded.
    Decode(json::decode::Warning),

    /// The `country` is not a valid ISO 3166-1 country code because it's not uppercase.
    PreferUpperCase,

    /// The `currency` is not a valid ISO 4217 currency code.
    InvalidCode,

    /// The JSON value given is not a string.
    InvalidType { type_found: json::ValueKind },

    /// The `currency` is not a valid ISO 4217 currency code: it should be 3 chars.
    InvalidLength,

    /// The `currency` is not a valid ISO 4217 currency code because it's a test code.
    InvalidCodeXTS,

    /// The `currency` is not a valid ISO 4217 currency code because it's a code for `no-currency`.
    InvalidCodeXXX,
}

impl Warning {
    fn invalid_type(elem: &json::Element<'_>) -> Self {
        Self::InvalidType {
            type_found: elem.value().kind(),
        }
    }
}

impl fmt::Display for Warning {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::ContainsEscapeCodes => write!(
                f,
                "The currency-code contains escape-codes but it does not need them.",
            ),
            Self::Decode(warning) => fmt::Display::fmt(warning, f),
            Self::PreferUpperCase => write!(
                f,
                "The currency-code follows the ISO 4217 standard whch states: the chars should be uppercase.",
            ),
            Self::InvalidCode => {
                write!(f, "The currency-code is not a valid ISO 4217 code.")
            }
            Self::InvalidType { .. } => write!(f, "The currency-code should be a string."),
            Self::InvalidLength => write!(f, "The currency-code follows the ISO 4217 standard whch states: the code should be three chars."),
            Self::InvalidCodeXTS => write!(
                f,
                "The currency-code is `XTS`. This is a code for testing only",
            ),
            Self::InvalidCodeXXX => write!(
                f,
                "The currency-code is `XXX`. This means there is no currency",
            ),
        }
    }
}

impl crate::Warning for Warning {
    fn id(&self) -> warning::Id {
        match self {
            Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
            Self::Decode(kind) => kind.id(),
            Self::PreferUpperCase => warning::Id::from_static("prefer_upper_case"),
            Self::InvalidCode => warning::Id::from_static("invalid_code"),
            Self::InvalidType { type_found } => {
                warning::Id::from_string(format!("invalid_type({type_found})"))
            }
            Self::InvalidLength => warning::Id::from_static("invalid_length"),
            Self::InvalidCodeXTS => warning::Id::from_static("invalid_code_xts"),
            Self::InvalidCodeXXX => warning::Id::from_static("invalid_code_xxx"),
        }
    }
}

from_warning_all!(json::decode::Warning => Warning::Decode);

impl json::FromJson<'_> for Code {
    type Warning = Warning;

    fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::Warning> {
        let mut warnings = warning::Set::new();
        let value = elem.as_value();

        let Some(s) = value.to_raw_str() else {
            return warnings.bail(Warning::invalid_type(elem), elem);
        };

        let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);

        let s = match pending_str {
            json::decode::PendingStr::NoEscapes(s) => s,
            json::decode::PendingStr::HasEscapes(_) => {
                return warnings.bail(Warning::ContainsEscapeCodes, elem);
            }
        };

        let bytes = s.as_bytes();

        // ISO 4217 is expected to be 3 chars enclosed in quotes.
        let [a, b, c] = bytes else {
            return warnings.bail(Warning::InvalidLength, elem);
        };

        let triplet: [u8; 3] = [
            a.to_ascii_uppercase(),
            b.to_ascii_uppercase(),
            c.to_ascii_uppercase(),
        ];

        if triplet != bytes {
            warnings.insert(Warning::PreferUpperCase, elem);
        }

        let Some(code) = Code::from_alpha_3(triplet) else {
            return warnings.bail(Warning::InvalidCode, elem);
        };

        if matches!(code, Code::Xts) {
            warnings.insert(Warning::InvalidCodeXTS, elem);
        } else if matches!(code, Code::Xxx) {
            warnings.insert(Warning::InvalidCodeXXX, elem);
        }

        Ok(code.into_caveat(warnings))
    }
}

impl fmt::Display for Code {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.into_str())
    }
}

/// Macro to specify a list of valid ISO 4217 alpha-3 currency codes.
macro_rules! currency_codes {
    [$(($name:ident, $alph3:literal, $symbol:literal)),*] => {
        /// An ISO 4217 currency code.
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash,  PartialOrd, Ord)]
        pub enum Code {
            $($name),*
        }

        impl Code {
            /// Try creating a `Code` from three upper ASCII bytes.
            const fn from_alpha_3(code: [u8; 3]) -> Option<Self> {
                match &code {
                    $($alph3 => Some(Self::$name),)*
                    _ => None
                }
            }

            /// Return enum as three byte uppercase &str.
            pub fn into_str(self) -> &'static str {
                let bytes = match self {
                    $(Self::$name => $alph3),*
                };
                std::str::from_utf8(bytes).expect("The currency code bytes are known to be valid UTF8 as they are embedded into the binary")
            }

            /// Return a str symbol of the [Code].
            pub fn into_symbol(self) -> &'static str {
                match self {
                    $(Self::$name => $symbol),*
                }
            }
        }
    };
}

currency_codes![
    (Aed, b"AED", "D"),
    (Afn, b"AFN", "Afs"),
    (All, b"ALL", "L"),
    (Amd, b"AMD", "֏"),
    (Ang, b"ANG", "NAƒ"),
    (Aoa, b"AOA", "Kz"),
    (Ars, b"ARS", "$"),
    (Aud, b"AUD", "$"),
    (Awg, b"AWG", "Aƒ"),
    (Azn, b"AZN", "₼"),
    (Bam, b"BAM", "KM"),
    (Bbd, b"BBD", "$"),
    (Bdt, b"BDT", "৳"),
    (Bgn, b"BGN", "лв"),
    (Bhd, b"BHD", "BD"),
    (Bif, b"BIF", "FBu"),
    (Bmd, b"BMD", "$"),
    (Bnd, b"BND", "$"),
    (Bob, b"BOB", "Bs"),
    (Bov, b"BOV", "BOV"),
    (Brl, b"BRL", "$"),
    (Bsd, b"BSD", "$"),
    (Btn, b"BTN", "Nu."),
    (Bwp, b"BWP", "p"),
    (Byn, b"BYN", "BYN"),
    (Bzd, b"BZD", "$"),
    (Cad, b"CAD", "$"),
    (Cdf, b"CDF", "FC"),
    (Che, b"CHE", "CHE"),
    (Chf, b"CHF", "CHF"),
    (Chw, b"CHW", "CHW"),
    (Clf, b"CLF", "UF"),
    (Clp, b"CLP", "$"),
    (Cny, b"CNY", "¥"),
    (Cop, b"COP", "$"),
    (Cou, b"COU", "COU"),
    (Crc, b"CRC", "₡"),
    (Cuc, b"CUC", "$"),
    (Cup, b"CUP", "$"),
    (Cve, b"CVE", "$"),
    (Czk, b"CZK", "Kč"),
    (Djf, b"DJF", "Fdj"),
    (Dkk, b"DKK", "kr."),
    (Dop, b"DOP", "$"),
    (Dzd, b"DZD", "DA"),
    (Egp, b"EGP", "£"),
    (Ern, b"ERN", "Nfk"),
    (Etb, b"ETB", "Br"),
    (Eur, b"EUR", "€"),
    (Fjd, b"FJD", "$"),
    (Fkp, b"FKP", "£"),
    (Gbp, b"GBP", "£"),
    (Gel, b"GEL", "₾"),
    (Ghs, b"GHS", "GH₵"),
    (Gip, b"GIP", "£"),
    (Gmd, b"GMD", "D"),
    (Gnf, b"GNF", "FG"),
    (Gtq, b"GTQ", "Q"),
    (Gyd, b"GYD", "$"),
    (Hkd, b"HKD", "$"),
    (Hnl, b"HNL", "L"),
    (Hrk, b"HRK", "lp"),
    (Htg, b"HTG", "G"),
    (Huf, b"HUF", "Ft"),
    (Idr, b"IDR", "Rp"),
    (Ils, b"ILS", "₪"),
    (Inr, b"INR", "₹"),
    (Iqd, b"IQD", "IQD"),
    (Irr, b"IRR", "Rl"),
    (Isk, b"ISK", "kr"),
    (Jmd, b"JMD", "$"),
    (Jod, b"JOD", "د.أ"),
    (Jpy, b"JPY", "¥"),
    (Kes, b"KES", "KES"),
    (Kgs, b"KGS", "⃀"),
    (Khr, b"KHR", "៛"),
    (Kmf, b"KMF", "KMF"),
    (Kpw, b"KPW", "₩"),
    (Krw, b"KRW", "₩"),
    (Kwd, b"KWD", "KD"),
    (Kyd, b"KYD", "$"),
    (Kzt, b"KZT", "₸"),
    (Lak, b"LAK", "₭"),
    (Lbp, b"LBP", "LL"),
    (Lkr, b"LKR", "₨"),
    (Lrd, b"LRD", "$"),
    (Lsl, b"LSL", "L"),
    (Lyd, b"LYD", "LD"),
    (Mad, b"MAD", "DH"),
    (Mdl, b"MDL", "L"),
    (Mga, b"MGA", "Ar"),
    (Mkd, b"MKD", "den"),
    (Mmk, b"MMK", "Ks."),
    (Mnt, b"MNT", "₮"),
    (Mop, b"MOP", "$"),
    (Mru, b"MRU", "UM"),
    (Mur, b"MUR", "₨"),
    (Mvr, b"MVR", "Rf"),
    (Mwk, b"MWK", "K"),
    (Mxn, b"MXN", "$"),
    (Mxv, b"MXV", "MXV"),
    (Myr, b"MYR", "RM"),
    (Mzn, b"MZN", "MT"),
    (Nad, b"NAD", "$"),
    (Ngn, b"NGN", "₦"),
    (Nio, b"NIO", "C$"),
    (Nok, b"NOK", "kr"),
    (Npr, b"NPR", "रु"),
    (Nzd, b"NZD", "$"),
    (Omr, b"OMR", "OMR"),
    (Pab, b"PAB", "฿"),
    (Pen, b"PEN", "S/"),
    (Pgk, b"PGK", "K"),
    (Php, b"PHP", "₱"),
    (Pkr, b"PKR", "Re"),
    (Pln, b"PLN", "zł"),
    (Pyg, b"PYG", "₲"),
    (Qar, b"QAR", "QR"),
    (Ron, b"RON", "lei"),
    (Rsd, b"RSD", "DIN"),
    (Rub, b"RUB", "₽"),
    (Rwf, b"RWF", "R₣"),
    (Sar, b"SAR", "\u{20C1}"),
    (Sbd, b"SBD", "$"),
    (Scr, b"SCR", "R"),
    (Sdg, b"SDG", "PT"),
    (Sek, b"SEK", "kr"),
    (Sgd, b"SGD", "$"),
    (Shp, b"SHP", "£"),
    (Sle, b"SLE", "Le"),
    (Sll, b"SLL", "Le"),
    (Sos, b"SOS", "Sh.So."),
    (Srd, b"SRD", "$"),
    (Ssp, b"SSP", "£"),
    (Stn, b"STN", "Db"),
    (Svc, b"SVC", "₡"),
    (Syp, b"SYP", "LS"),
    (Szl, b"SZL", "L"),
    (Thb, b"THB", "฿"),
    (Tjs, b"TJS", "SM"),
    (Tmt, b"TMT", "m"),
    (Tnd, b"TND", "DT"),
    (Top, b"TOP", "$"),
    (Try, b"TRY", "₺"),
    (Ttd, b"TTD", "$"),
    (Twd, b"TWD", "$"),
    (Tzs, b"TZS", "TZS"),
    (Uah, b"UAH", "₴"),
    (Ugx, b"UGX", "UGX"),
    (Usd, b"USD", "$"),
    (Usn, b"USN", "USN"),
    (Uyi, b"UYI", "UYI"),
    (Uyu, b"UYU", "$"),
    (Uyw, b"UYW", "UYW"),
    (Uzs, b"UZS", "UZS"),
    (Ved, b"VED", "Bs.D"),
    (Ves, b"VES", "Bs.S"),
    (Vnd, b"VND", "₫"),
    (Vuv, b"VUV", "Vt"),
    (Wst, b"WST", "$"),
    (Xaf, b"XAF", "F.CFA"),
    (Xag, b"XAG", "XAG"),
    (Xau, b"XAU", "XAU"),
    (Xba, b"XBA", "XBA"),
    (Xbb, b"XBB", "XBB"),
    (Xbc, b"XBC", "XBC"),
    (Xbd, b"XBD", "XBD"),
    (Xcd, b"XCD", "$"),
    (Xdr, b"XDR", "SDR"),
    (Xof, b"XOF", "F.CFA"),
    (Xpd, b"XPD", "XPD"),
    (Xpf, b"XPF", "F"),
    (Xpt, b"XPT", "XPT"),
    (Xsu, b"XSU", "sucre"),
    (Xts, b"XTS", "XTS"),
    (Xua, b"XUA", "XUA"),
    (Xxx, b"XXX", "XXX"),
    (Yer, b"YER", "﷼"),
    (Zar, b"ZAR", "R"),
    (Zmw, b"ZMW", "K"),
    (Zwl, b"ZWL", "$")
];
