// SPDX-License-Identifier: GPL-3.0-or-later
use std::fmt;

use gettextrs::*;
use gtk::{glib, prelude::*};
use url::Url;

mod parser {
    use nom::{
        IResult, Parser,
        branch::{alt, permutation},
        bytes::complete::{tag, tag_no_case, take, take_till, take_until},
        combinator::opt,
        number::complete::double,
        sequence::separated_pair,
    };
    use nom_permutation::permutation_opt;

    use super::*;

    fn pair_f64(input: &str) -> IResult<&str, (f64, f64)> {
        separated_pair(double, tag(","), double).parse(input)
    }

    fn email_parse(input: &str) -> IResult<&str, Mail> {
        alt((mailto_parse, mailmsg_parse)).parse(input)
    }

    pub fn tel_parse(input: &str) -> IResult<&str, &str> {
        tag_no_case("tel:").parse(input)
    }

    fn opt_tagged<'a>(
        i: &'a str,
        tag_name: &str,
        stop_char: &str,
    ) -> IResult<&'a str, Option<&'a str>> {
        let parse_tagged_cl = |i| parse_tagged(i, tag_name, stop_char);

        opt(parse_tagged_cl).parse(i)
    }

    fn tagged<'a>(i: &'a str, tag_name: &'a str, stop_char: &'a str) -> IResult<&'a str, &'a str> {
        parse_tagged(i, tag_name, stop_char)
    }

    fn parse_tagged<'a>(
        input: &'a str,
        tag_name: &str,
        stop_tag: &str,
    ) -> IResult<&'a str, &'a str> {
        let (input, _) = tag_no_case(tag_name).parse(input)?;
        let (input, _) = tag(":").parse(input)?;
        let (input, data) = take_until(stop_tag)(input)?;

        let (input, _) = take(stop_tag.len())(input)?;

        Ok((input, data))
    }

    pub fn location_parse(input: &str) -> IResult<&str, Location> {
        let (input, _) = tag_no_case("geo:")(input)?;
        let (input, (latitude, longitude)) = pair_f64(input)?;

        Ok((
            input,
            Location {
                latitude,
                longitude,
            },
        ))
    }

    fn mailto_parse(input: &str) -> IResult<&str, Mail> {
        let (input, _) = tag_no_case("mailto:")(input)?;

        Ok((
            input,
            Mail {
                to: input.to_string(),
                body: None,
                subject: None,
            },
        ))
    }

    pub fn mailmsg_parse(input: &str) -> IResult<&str, Mail> {
        let (input, _) = tag_no_case("MATMSG:")(input)?;

        let (input, (email, subject, body)) = permutation_opt((
            |i| tagged(i, "TO", ";"),
            |i| tagged(i, "SUB", ";"),
            |i| tagged(i, "BODY", ";"),
        ))(input)?;

        let Some(email) = email else {
            return Err(nom::Err::Error(nom::error::Error::new(
                input,
                nom::error::ErrorKind::IsA,
            )));
        };

        Ok((
            input,
            Mail {
                to: email.to_string(),
                body: body.map(|s| s.to_string()),
                subject: subject.map(|s| s.to_string()),
            },
        ))
    }

    fn sms_parse(input: &str) -> IResult<&str, Sms> {
        let (input, _) = tag_no_case("smsto:")(input)?;

        let (input, phone) = take_till(|c| c == ':')(input)?;
        let (input, _) = take(1usize)(input)?;

        Ok((
            input,
            Sms {
                phone: phone.to_string(),
                content: input.to_string(),
            },
        ))
    }

    pub fn wifi_parse(input: &str) -> IResult<&str, WiFi> {
        let (input, _) = tag_no_case("wifi:")(input)?;

        let (input, (encryption, network, password, visible)) = permutation((
            |i| parse_tagged(i, "T", ";"),
            |i| parse_tagged(i, "S", ";"),
            |i| parse_tagged(i, "P", ";"),
            |i| opt_tagged(i, "H", ";"),
        ))
        .parse(input)?;

        Ok((
            input,
            WiFi {
                encryption: encryption.into(),
                network: network.to_string(),
                password: password.to_string(),
                visible: visible.map(|v| &v.to_lowercase() == "true"),
            },
        ))
    }

    pub fn datetime_parse(input: &str) -> IResult<&str, chrono::NaiveDate> {
        // TODO RFC 5545 also supports a third format that we do not support at
        // the moment: TZID=America/New_York:19980119T020000
        //
        // Additionally, we don't really use the UTC timezone if specified by
        // the Z at the end.
        let (input, date) = take(8_usize)(input)?;
        let (input, time) = opt(|i| {
            let (i, _) = tag("T").parse(i)?;
            let (i, time) = take(6_usize).parse(i)?;
            let (i, _) = opt(tag("Z")).parse(i)?;
            Ok((i, time))
        })
        .parse(input)?;

        let datetime = if let Some(time) = time {
            chrono::NaiveDate::parse_from_str(&format!("{date}{time}"), "%Y%m%d%H%M%S")
        } else {
            chrono::NaiveDate::parse_from_str(date, "%Y%m%d")
        };

        if let Ok(datetime) = datetime {
            Ok((input, datetime))
        } else {
            Err(nom::Err::Error(nom::error::Error::new(
                input,
                nom::error::ErrorKind::IsA,
            )))
        }
    }

    fn event_parse(input: &str) -> IResult<&str, Event> {
        let (input, _) = tag_no_case("BEGIN:VEVENT")(input)?;
        let (input, _) = nom::character::complete::multispace0(input)?;

        let (input, (summary, start_at, end_at, location, description)) = permutation_opt((
            |i| tagged(i, "SUMMARY", "\n"),
            |i| tagged(i, "DTSTART", "\n"),
            |i| tagged(i, "DTEND", "\n"),
            |i| tagged(i, "LOCATION", "\n"),
            |i| tagged(i, "DESCRIPTION", "\n"),
        ))(input)?;

        let (Some(summary), Some(start_at), Some(end_at)) = (summary, start_at, end_at) else {
            return Err(nom::Err::Error(nom::error::Error::new(
                input,
                nom::error::ErrorKind::IsA,
            )));
        };

        let (_, start_at) = datetime_parse(start_at.trim_end())?;
        let (_, end_at) = datetime_parse(end_at.trim_end())?;
        Ok((
            input,
            Event {
                summary: summary.trim_end().to_string(),
                start_at,
                end_at,
                description: description.map(|s| s.trim_end().to_string()),
                location: location.map(|s| s.trim_end().to_string()),
            },
        ))
    }

    pub fn parse_qrcode(content: &str) -> Option<QRCodeKind> {
        let uri = Url::parse(content).ok()?;
        let scheme = uri.scheme();
        if scheme == "mailto" || scheme == "matmsg" {
            let (_, mail) = email_parse(content).ok()?;
            Some(QRCodeKind::Mail(mail))
        } else if scheme == "wifi" {
            let (_, wifi) = wifi_parse(content).ok()?;
            Some(QRCodeKind::WiFi(wifi))
        } else if scheme == "geo" {
            let (_, location) = location_parse(content).ok()?;
            Some(QRCodeKind::Location(location))
        } else if scheme == "tel" {
            let (tel, _) = tel_parse(content).ok()?;
            Some(QRCodeKind::Telephone(tel.to_string()))
        } else if scheme == "smsto" {
            let (_, sms) = sms_parse(content).ok()?;
            Some(QRCodeKind::Sms(sms))
        } else if scheme == "http" || scheme == "https" {
            Some(QRCodeKind::Url(uri))
        } else if content.starts_with("BEGIN:VEVENT") {
            let (_, event) = event_parse(content).ok()?;
            Some(QRCodeKind::Event(event))
        } else {
            None
        }
    }
}
#[derive(Debug, Clone)]
pub struct Location {
    pub latitude: f64,
    pub longitude: f64,
}

impl From<Location> for String {
    fn from(val: Location) -> Self {
        format!("geo:{}:{}", val.latitude, val.longitude)
    }
}
#[derive(Debug, Clone, PartialEq)]
pub struct Mail {
    pub to: String,
    pub subject: Option<String>,
    pub body: Option<String>,
}

impl From<Mail> for String {
    fn from(val: Mail) -> Self {
        let mut output = String::new();
        output.push_str("mailto:");
        output.push_str(&val.to);
        if let Some(body) = val.body {
            output.push_str("&body=");
            output.push_str(&body);
        }
        if let Some(subject) = val.subject {
            output.push_str("&subject=");
            output.push_str(&subject);
        }
        output
    }
}

#[derive(Debug, Clone)]
pub struct Sms {
    pub phone: String,
    pub content: String,
}

#[derive(Debug, Copy, Clone, glib::Enum, Default, PartialEq)]
#[repr(u32)]
#[enum_type(name = "WiFiEncryption")]
pub enum WiFiEncryption {
    #[default]
    #[enum_type(name = "WPA")]
    Wpa = 0,
    #[enum_type(name = "WEP")]
    Wep = 1,
    #[enum_type(name = "SAE")]
    Sae = 2,
    #[enum_type(name = "NoPass")]
    NoPass = 3,
}

impl From<u32> for WiFiEncryption {
    fn from(u: u32) -> Self {
        match u {
            0 => Self::Wpa,
            1 => Self::Wep,
            2 => Self::Sae,
            3 => Self::NoPass,
            _ => Self::default(),
        }
    }
}

impl fmt::Display for WiFiEncryption {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let s = match &self {
            Self::Wpa => "WPA",
            Self::Wep => "WEP",
            Self::Sae => "SAE",
            Self::NoPass => "nopass",
        };
        write!(f, "{s}")
    }
}

impl From<&str> for WiFiEncryption {
    fn from(s: &str) -> Self {
        match s {
            "WPA" => Self::Wpa,
            "WEP" => Self::Wep,
            "SAE" => Self::Sae,
            _ => Self::NoPass,
        }
    }
}

impl WiFiEncryption {
    pub fn to_translatable_string(self) -> String {
        match self {
            Self::Wpa => "WPA".to_string(),
            Self::Wep => "WEP".to_string(),
            Self::Sae => "SAE".to_string(),
            // NOTE This is in the context of selecting a wifi encryption
            // algorithm.
            Self::NoPass => gettext("None"),
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct WiFi {
    pub network: String,
    pub encryption: WiFiEncryption,
    pub password: String,
    pub visible: Option<bool>,
}

impl fmt::Display for WiFi {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // WiFi Codes are of the following form
        // WIFI:T:<ALGORITHM>;S:<SSID>;P:<PASSWORD>;H:<VISIBLE>;;
        // cf. https://github.com/zxing/zxing/blob/master/core/src/main/java/com/google/zxing/client/result/WifiResultParser.java

        let visible = match self.visible {
            Some(b) => format!(";H:{b}"),
            None => "".into(),
        };
        write!(
            f,
            "WIFI:T:{};S:{};P:{}{};;",
            self.encryption, self.network, self.password, visible,
        )
    }
}

#[derive(Debug, Clone)]
pub struct Event {
    pub summary: String,
    pub start_at: chrono::NaiveDate,
    pub end_at: chrono::NaiveDate,
    pub description: Option<String>,
    pub location: Option<String>,
}

#[derive(Debug, Clone)]
pub enum QRCodeKind {
    Url(Url),
    Telephone(String),
    Location(Location),
    Sms(Sms),
    Mail(Mail),
    WiFi(WiFi),
    Event(Event),
    // The default one
    Text(String),
}

impl QRCodeKind {
    pub fn widget(self) -> gtk::Widget {
        use crate::widgets::qrcode::kind;
        match self {
            QRCodeKind::Url(url) => kind::QRCodeUrl::new(url).upcast(),
            QRCodeKind::WiFi(wifi) => kind::QRCodeWiFi::new(wifi).upcast(),
            QRCodeKind::Location(location) => kind::QRCodeLocation::new(location).upcast(),
            QRCodeKind::Sms(sms) => kind::QRCodeSms::new(sms).upcast(),
            QRCodeKind::Mail(mail) => kind::QRCodeMail::new(mail).upcast(),
            QRCodeKind::Event(event) => kind::QRCodeEvent::new(event).upcast(),
            QRCodeKind::Text(content) => kind::QRCodeText::new(content).upcast(),
            QRCodeKind::Telephone(tel) => kind::QRCodeTelephone::new(tel).upcast(),
        }
    }
}

impl From<&str> for QRCodeKind {
    fn from(content: &str) -> Self {
        let kind = parser::parse_qrcode(content);

        kind.unwrap_or_else(|| Self::Text(content.to_string()))
    }
}

impl From<QRCodeKind> for String {
    fn from(val: QRCodeKind) -> Self {
        match val {
            // TODO: WiFi , SMS, Event
            QRCodeKind::Location(location) => location.into(),
            QRCodeKind::Url(url) => url.into(),
            QRCodeKind::Telephone(tel) => format!("tel:{tel}"),
            QRCodeKind::Text(content) => content,
            QRCodeKind::Mail(mail) => mail.into(),
            _ => "hey".to_string(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_wifi_parse_order() {
        let mut wifi = WiFi {
            network: String::from("SOME_SSID"),
            encryption: WiFiEncryption::Sae,
            password: String::from("SOME_PASS"),
            visible: Some(false),
        };
        assert_eq!(
            parser::wifi_parse("WIFI:S:SOME_SSID;T:SAE;P:SOME_PASS;H:false;;"),
            Ok((";", wifi.clone()))
        );
        assert_eq!(
            parser::wifi_parse("WIFI:S:SOME_SSID;T:SAE;P:SOME_PASS;H:false;;"),
            Ok((";", wifi.clone()))
        );
        assert_eq!(
            parser::wifi_parse("WIFI:T:SAE;S:SOME_SSID;P:SOME_PASS;H:false;;"),
            Ok((";", wifi.clone()))
        );
        assert_eq!(
            parser::wifi_parse("WIFI:H:false;S:SOME_SSID;T:SAE;P:SOME_PASS;;"),
            Ok((";", wifi.clone()))
        );
        wifi.visible = None;
        wifi.encryption = WiFiEncryption::Wep;
        assert_eq!(
            parser::wifi_parse("WIFI:S:SOME_SSID;T:WEP;P:SOME_PASS;;"),
            Ok((";", wifi.clone()))
        );

        wifi.visible = Some(false);
        assert_eq!(
            parser::wifi_parse("WIFI:S:SOME_SSID;H:false;T:WEP;P:SOME_PASS;;"),
            Ok((";", wifi.clone()))
        );
        assert_eq!(
            parser::wifi_parse("WIFI:H:false;S:SOME_SSID;T:WEP;P:SOME_PASS;;"),
            Ok((";", wifi))
        );
    }

    #[test]
    fn test_datetime_parse() {
        let datetime = "20231025T000000";
        let naive = chrono::NaiveDate::parse_from_str("20231025000000", "%Y%m%d%H%M%S").unwrap();

        let (input, dt) = parser::datetime_parse(datetime).unwrap();
        assert!(input.is_empty());
        assert_eq!(dt, naive);
    }

    #[test]
    fn test_mailmsg_parse() {
        let mail = Mail {
            to: "SOMEONE".to_string(),
            subject: Some("SOME_SUBJECT".to_string()),
            body: Some("SOME EMAIL".to_string()),
        };

        let (_, parsed_mail) =
            parser::mailmsg_parse("MATMSG:TO:SOMEONE;SUB:SOME_SUBJECT;BODY:SOME EMAIL;;").unwrap();
        assert_eq!(mail, parsed_mail);

        let (_, parsed_mail) =
            parser::mailmsg_parse("MATMSG:TO:SOMEONE;BODY:SOME EMAIL;SUB:SOME_SUBJECT;;").unwrap();
        assert_eq!(mail, parsed_mail);

        let (_, parsed_mail) =
            parser::mailmsg_parse("MATMSG:BODY:SOME EMAIL;TO:SOMEONE;SUB:SOME_SUBJECT;;").unwrap();
        assert_eq!(mail, parsed_mail);

        let (_, parsed_mail) =
            parser::mailmsg_parse("MATMSG:BODY:SOME EMAIL;SUB:SOME_SUBJECT;TO:SOMEONE;;").unwrap();
        assert_eq!(mail, parsed_mail);
    }
}
