/*
 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
 *
 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
 */

use super::{DavParser, RawElement, Token, XmlValueParser, tokenizer::Tokenizer};
use crate::schema::{
    Attribute, AttributeValue, Element, NamedElement, Namespace,
    property::{
        CalDavProperty, CalDavPropertyName, CalendarData, CardDavProperty, CardDavPropertyName,
        Comp, DavProperty, DavValue, PrincipalProperty, ResourceType, WebDavProperty,
    },
    request::{DavPropertyValue, VCardPropertyWithGroup},
    response::List,
};
use calcard::{
    Entry, Parser,
    common::{IanaParse, PartialDateTime},
    icalendar::{ICalendar, ICalendarComponentType, ICalendarParameterName, ICalendarProperty},
    vcard::{VCardParameterName, VCardProperty},
};
use mail_parser::DateTime;
use types::{TimeRange, dead_property::DeadProperty};

impl Tokenizer<'_> {
    pub(crate) fn collect_properties(
        &mut self,
        mut elements: Vec<DavProperty>,
    ) -> crate::parser::Result<Vec<DavProperty>> {
        loop {
            match self.token()? {
                Token::ElementStart {
                    name:
                        NamedElement {
                            ns: Namespace::CalDav,
                            element: Element::CalendarData,
                        },
                    ..
                } => {
                    elements.push(DavProperty::CalDav(CalDavProperty::CalendarData(
                        self.collect_calendar_data()?,
                    )));
                }
                Token::ElementStart {
                    name:
                        NamedElement {
                            ns: Namespace::CardDav,
                            element: Element::AddressData,
                        },
                    ..
                } => {
                    elements.push(DavProperty::CardDav(CardDavProperty::AddressData(
                        self.collect_address_data()?,
                    )));
                }
                Token::ElementStart { name, .. } => {
                    if let Some(property) = DavProperty::from_element(name) {
                        elements.push(property);
                    }
                    self.expect_element_end()?;
                }
                Token::ElementEnd => {
                    break;
                }
                Token::UnknownElement(name) => {
                    elements.push(DavProperty::DeadProperty((&name).into()));
                    self.expect_element_end()?;
                }
                token => return Err(token.into_unexpected()),
            }
        }

        Ok(elements)
    }

    pub(crate) fn collect_calendar_data(&mut self) -> crate::parser::Result<CalendarData> {
        let mut depth = 1;
        let mut data = CalendarData {
            properties: Vec::with_capacity(4),
            expand: None,
            limit_recurrence: None,
            limit_freebusy: None,
        };
        let mut components: Vec<ICalendarComponentType> = Vec::new();

        loop {
            match self.token()? {
                Token::ElementStart {
                    name:
                        NamedElement {
                            ns: Namespace::CalDav,
                            element: Element::Allcomp,
                        },
                    ..
                } => {
                    self.expect_element_end()?;
                }
                Token::ElementStart {
                    name:
                        NamedElement {
                            ns: Namespace::CalDav,
                            element: Element::Allprop,
                        },
                    ..
                } => {
                    if let Some(component) = components.last().cloned() {
                        data.properties.push(CalDavPropertyName {
                            component: Some(component),
                            name: None,
                            no_value: false,
                        });
                    }
                    self.expect_element_end()?;
                }
                Token::ElementStart {
                    name:
                        NamedElement {
                            ns: Namespace::CalDav,
                            element: Element::Comp,
                        },
                    raw,
                } => {
                    depth += 1;

                    for attribute in raw.attributes::<ICalendarComponentType>() {
                        if let Attribute::Name(name) = attribute? {
                            components.push(name);
                        }
                    }
                }
                Token::ElementStart {
                    name:
                        NamedElement {
                            ns: Namespace::CalDav,
                            element: Element::Prop,
                        },
                    raw,
                } => {
                    let mut name = None;
                    let mut no_value = false;

                    for attribute in raw.attributes::<ICalendarProperty>() {
                        match attribute? {
                            Attribute::Name(name_) => {
                                name = Some(name_);
                            }
                            Attribute::NoValue(no_value_) => {
                                no_value = no_value_;
                            }
                            _ => {}
                        }
                    }

                    if let Some(name) = name {
                        data.properties.push(CalDavPropertyName {
                            component: components.last().cloned(),
                            name: Some(name),
                            no_value,
                        });
                    }

                    self.expect_element_end()?;
                }
                Token::ElementStart {
                    name:
                        NamedElement {
                            ns: Namespace::CalDav,
                            element: Element::Expand,
                        },
                    raw,
                } => {
                    data.expand = TimeRange::from_raw(&raw)?;
                    self.expect_element_end()?;
                }
                Token::ElementStart {
                    name:
                        NamedElement {
                            ns: Namespace::CalDav,
                            element: Element::LimitRecurrenceSet,
                        },
                    raw,
                } => {
                    data.limit_recurrence = TimeRange::from_raw(&raw)?;
                    self.expect_element_end()?;
                }
                Token::ElementStart {
                    name:
                        NamedElement {
                            ns: Namespace::CalDav,
                            element: Element::LimitFreebusySet,
                        },
                    raw,
                } => {
                    data.limit_freebusy = TimeRange::from_raw(&raw)?;
                    self.expect_element_end()?;
                }
                Token::ElementEnd => {
                    depth -= 1;
                    if depth == 0 {
                        break;
                    }
                    if let Some(last_component) = components.pop()
                        && last_component != ICalendarComponentType::VCalendar
                        && !matches!(data.properties.last(), Some(CalDavPropertyName { component: Some(component), .. }) if component == &last_component)
                    {
                        data.properties.push(CalDavPropertyName {
                            component: Some(last_component),
                            name: None,
                            no_value: false,
                        });
                    }
                }
                Token::Eof => {
                    break;
                }
                token => return Err(token.into_unexpected()),
            }
        }

        Ok(data)
    }

    pub(crate) fn collect_address_data(
        &mut self,
    ) -> crate::parser::Result<Vec<CardDavPropertyName>> {
        let mut items = Vec::with_capacity(4);
        loop {
            match self.token()? {
                Token::ElementStart {
                    name:
                        NamedElement {
                            ns: Namespace::CardDav,
                            element: Element::Allprop,
                        },
                    ..
                } => {
                    self.expect_element_end()?;
                }
                Token::ElementStart {
                    name:
                        NamedElement {
                            ns: Namespace::CardDav,
                            element: Element::Prop,
                        },
                    raw,
                } => {
                    let mut name = None;
                    let mut group = None;
                    let mut no_value = false;

                    for attribute in raw.attributes::<VCardPropertyWithGroup>() {
                        match attribute? {
                            Attribute::Name(name_) => {
                                name = Some(name_.name);
                                group = name_.group;
                            }
                            Attribute::NoValue(no_value_) => {
                                no_value = no_value_;
                            }
                            _ => {}
                        }
                    }

                    if let Some(name) = name {
                        items.push(CardDavPropertyName {
                            name,
                            group,
                            no_value,
                        });
                    }

                    self.expect_element_end()?;
                }
                Token::ElementEnd | Token::Eof => {
                    break;
                }
                token => return Err(token.into_unexpected()),
            }
        }

        Ok(items)
    }
}

impl Tokenizer<'_> {
    pub(crate) fn collect_property_values(
        &mut self,
        elements: &mut Vec<DavPropertyValue>,
    ) -> crate::parser::Result<()> {
        loop {
            match self.token()? {
                Token::ElementStart { name, .. } => {
                    if let Some(property) = DavProperty::from_element(name) {
                        let value = match property {
                            DavProperty::WebDav(WebDavProperty::ResourceType) => {
                                DavValue::ResourceTypes(List(self.collect_elements()?))
                            }
                            DavProperty::WebDav(WebDavProperty::CreationDate) => {
                                match self.parse_value::<DateTime>()? {
                                    Some(Ok(value)) => DavValue::Timestamp(value.to_timestamp()),
                                    Some(Err(value)) => DavValue::String(value),
                                    None => DavValue::Null,
                                }
                            }
                            DavProperty::CalDav(CalDavProperty::CalendarTimezone) => {
                                match self
                                    .collect_string_value()?
                                    .map(|v| ICalendar::parse(&v).map_err(|_| v))
                                {
                                    Some(Ok(value)) => DavValue::ICalendar(value),
                                    Some(Err(value)) => DavValue::String(value),
                                    None => DavValue::Null,
                                }
                            }
                            DavProperty::CalDav(CalDavProperty::SupportedCalendarComponentSet) => {
                                let mut components = Vec::new();

                                loop {
                                    match self.token()? {
                                        Token::ElementStart { name, raw } => {
                                            if name.ns == Namespace::CalDav
                                                && name.element == Element::Comp
                                            {
                                                for component in
                                                    raw.attributes::<ICalendarComponentType>()
                                                {
                                                    if let Attribute::Name(name) = component? {
                                                        components.push(Comp(name));
                                                    }
                                                }
                                            }
                                            self.seek_element_end()?;
                                        }
                                        Token::UnknownElement(_) => {
                                            // Ignore unknown elements
                                            self.seek_element_end()?;
                                        }
                                        Token::ElementEnd | Token::Eof => {
                                            break;
                                        }
                                        _ => {}
                                    }
                                }

                                DavValue::Components(List(components))
                            }
                            DavProperty::CalDav(
                                CalDavProperty::MaxInstances
                                | CalDavProperty::MaxAttendeesPerInstance,
                            ) => match self.parse_value()? {
                                Some(Ok(value)) => DavValue::Uint64(value),
                                Some(Err(value)) => DavValue::String(value),
                                None => DavValue::Null,
                            },
                            _ => self
                                .collect_string_value()?
                                .map(DavValue::String)
                                .unwrap_or(DavValue::Null),
                        };

                        elements.push(DavPropertyValue { property, value });
                    } else {
                        // Ignore unknown elements
                        self.seek_element_end()?;
                    }
                }
                Token::ElementEnd | Token::Eof => {
                    break;
                }
                Token::UnknownElement(raw) => {
                    elements.push(DavPropertyValue {
                        property: DavProperty::DeadProperty((&raw).into()),
                        value: DavValue::DeadProperty(DeadProperty::parse(self)?),
                    });
                }
                token => return Err(token.into_unexpected()),
            }
        }

        Ok(())
    }
}

pub(crate) trait TimeRangeFromRaw {
    fn from_raw(raw: &RawElement<'_>) -> super::Result<Option<TimeRange>>;
}

impl TimeRangeFromRaw for TimeRange {
    fn from_raw(raw: &RawElement<'_>) -> super::Result<Option<Self>> {
        let mut range = TimeRange {
            start: i64::MIN,
            end: i64::MAX,
        };

        for attribute in raw.attributes::<ICalendarDateTime>() {
            match attribute? {
                Attribute::Start(start) => {
                    range.start = start.0;
                }
                Attribute::End(end) => {
                    range.end = end.0;
                }
                _ => {}
            }
        }

        if range.end < range.start {
            range.end = i64::MAX;
        }

        if range.start != i64::MIN || range.end != i64::MAX {
            Ok(Some(range))
        } else {
            Ok(None)
        }
    }
}

impl DavProperty {
    pub(crate) fn from_element(element: NamedElement) -> Option<Self> {
        match (element.ns, element.element) {
            (Namespace::Dav, Element::Creationdate) => {
                Some(DavProperty::WebDav(WebDavProperty::CreationDate))
            }
            (Namespace::Dav, Element::Displayname) => {
                Some(DavProperty::WebDav(WebDavProperty::DisplayName))
            }
            (Namespace::Dav, Element::Getcontentlanguage) => {
                Some(DavProperty::WebDav(WebDavProperty::GetContentLanguage))
            }
            (Namespace::Dav, Element::Getcontentlength) => {
                Some(DavProperty::WebDav(WebDavProperty::GetContentLength))
            }
            (Namespace::Dav, Element::Getcontenttype) => {
                Some(DavProperty::WebDav(WebDavProperty::GetContentType))
            }
            (Namespace::Dav, Element::Getetag) => {
                Some(DavProperty::WebDav(WebDavProperty::GetETag))
            }
            (Namespace::Dav, Element::Getlastmodified) => {
                Some(DavProperty::WebDav(WebDavProperty::GetLastModified))
            }
            (Namespace::Dav, Element::Resourcetype) => {
                Some(DavProperty::WebDav(WebDavProperty::ResourceType))
            }
            (Namespace::Dav, Element::Lockdiscovery) => {
                Some(DavProperty::WebDav(WebDavProperty::LockDiscovery))
            }
            (Namespace::Dav, Element::Supportedlock) => {
                Some(DavProperty::WebDav(WebDavProperty::SupportedLock))
            }
            (Namespace::Dav, Element::CurrentUserPrincipal) => {
                Some(DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal))
            }
            (Namespace::Dav, Element::QuotaAvailableBytes) => {
                Some(DavProperty::WebDav(WebDavProperty::QuotaAvailableBytes))
            }
            (Namespace::Dav, Element::QuotaUsedBytes) => {
                Some(DavProperty::WebDav(WebDavProperty::QuotaUsedBytes))
            }
            (Namespace::Dav, Element::SupportedReportSet) => {
                Some(DavProperty::WebDav(WebDavProperty::SupportedReportSet))
            }
            (Namespace::Dav, Element::SyncToken) => {
                Some(DavProperty::WebDav(WebDavProperty::SyncToken))
            }
            (Namespace::Dav, Element::AlternateUriSet) => {
                Some(DavProperty::Principal(PrincipalProperty::AlternateURISet))
            }
            (Namespace::Dav, Element::PrincipalUrl) => {
                Some(DavProperty::Principal(PrincipalProperty::PrincipalURL))
            }
            (Namespace::Dav, Element::GroupMemberSet) => {
                Some(DavProperty::Principal(PrincipalProperty::GroupMemberSet))
            }
            (Namespace::Dav, Element::GroupMembership) => {
                Some(DavProperty::Principal(PrincipalProperty::GroupMembership))
            }
            (Namespace::Dav, Element::Owner) => Some(DavProperty::WebDav(WebDavProperty::Owner)),
            (Namespace::Dav, Element::Group) => Some(DavProperty::WebDav(WebDavProperty::Group)),
            (Namespace::Dav, Element::SupportedPrivilegeSet) => {
                Some(DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet))
            }
            (Namespace::Dav, Element::CurrentUserPrivilegeSet) => {
                Some(DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet))
            }
            (Namespace::Dav, Element::Acl) => Some(DavProperty::WebDav(WebDavProperty::Acl)),
            (Namespace::Dav, Element::AclRestrictions) => {
                Some(DavProperty::WebDav(WebDavProperty::AclRestrictions))
            }
            (Namespace::Dav, Element::InheritedAclSet) => {
                Some(DavProperty::WebDav(WebDavProperty::InheritedAclSet))
            }
            (Namespace::Dav, Element::PrincipalCollectionSet) => {
                Some(DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet))
            }
            (Namespace::CardDav, Element::AddressbookDescription) => Some(DavProperty::CardDav(
                CardDavProperty::AddressbookDescription,
            )),
            (Namespace::CardDav, Element::SupportedAddressData) => {
                Some(DavProperty::CardDav(CardDavProperty::SupportedAddressData))
            }
            (Namespace::CardDav, Element::SupportedCollationSet) => {
                Some(DavProperty::CardDav(CardDavProperty::SupportedCollationSet))
            }
            (Namespace::CardDav, Element::AddressbookHomeSet) => Some(DavProperty::Principal(
                PrincipalProperty::AddressbookHomeSet,
            )),
            (Namespace::CardDav, Element::PrincipalAddress) => {
                Some(DavProperty::Principal(PrincipalProperty::PrincipalAddress))
            }
            (Namespace::CardDav, Element::AddressData) => Some(DavProperty::CardDav(
                CardDavProperty::AddressData(Default::default()),
            )),
            (Namespace::CardDav, Element::MaxResourceSize) => {
                Some(DavProperty::CardDav(CardDavProperty::MaxResourceSize))
            }
            (Namespace::CalDav, Element::CalendarDescription) => {
                Some(DavProperty::CalDav(CalDavProperty::CalendarDescription))
            }
            (Namespace::CalDav, Element::CalendarTimezone) => {
                Some(DavProperty::CalDav(CalDavProperty::CalendarTimezone))
            }
            (Namespace::CalDav, Element::SupportedCalendarComponentSet) => Some(
                DavProperty::CalDav(CalDavProperty::SupportedCalendarComponentSet),
            ),
            (Namespace::CalDav, Element::SupportedCollationSet) => {
                Some(DavProperty::CalDav(CalDavProperty::SupportedCollationSet))
            }
            (Namespace::CalDav, Element::SupportedCalendarData) => {
                Some(DavProperty::CalDav(CalDavProperty::SupportedCalendarData))
            }
            (Namespace::CalDav, Element::MaxResourceSize) => {
                Some(DavProperty::CalDav(CalDavProperty::MaxResourceSize))
            }
            (Namespace::CalDav, Element::MinDateTime) => {
                Some(DavProperty::CalDav(CalDavProperty::MinDateTime))
            }
            (Namespace::CalDav, Element::MaxDateTime) => {
                Some(DavProperty::CalDav(CalDavProperty::MaxDateTime))
            }
            (Namespace::CalDav, Element::MaxInstances) => {
                Some(DavProperty::CalDav(CalDavProperty::MaxInstances))
            }
            (Namespace::CalDav, Element::MaxAttendeesPerInstance) => {
                Some(DavProperty::CalDav(CalDavProperty::MaxAttendeesPerInstance))
            }
            (Namespace::CalDav, Element::ScheduleDefaultCalendarUrl) => Some(DavProperty::CalDav(
                CalDavProperty::ScheduleDefaultCalendarURL,
            )),
            (Namespace::CalDav, Element::ScheduleTag) => {
                Some(DavProperty::CalDav(CalDavProperty::ScheduleTag))
            }
            (Namespace::CalDav, Element::ScheduleCalendarTransp) => {
                Some(DavProperty::CalDav(CalDavProperty::ScheduleCalendarTransp))
            }
            (Namespace::CalDav, Element::CalendarHomeSet) => {
                Some(DavProperty::Principal(PrincipalProperty::CalendarHomeSet))
            }
            (Namespace::CalDav, Element::CalendarUserAddressSet) => Some(DavProperty::Principal(
                PrincipalProperty::CalendarUserAddressSet,
            )),
            (Namespace::CalDav, Element::CalendarUserType) => {
                Some(DavProperty::Principal(PrincipalProperty::CalendarUserType))
            }
            (Namespace::CalDav, Element::ScheduleInboxUrl) => {
                Some(DavProperty::Principal(PrincipalProperty::ScheduleInboxURL))
            }
            (Namespace::CalDav, Element::ScheduleOutboxUrl) => {
                Some(DavProperty::Principal(PrincipalProperty::ScheduleOutboxURL))
            }
            (Namespace::CalDav, Element::CalendarData) => Some(DavProperty::CalDav(
                CalDavProperty::CalendarData(Default::default()),
            )),
            (Namespace::CalDav, Element::TimezoneServiceSet) => {
                Some(DavProperty::CalDav(CalDavProperty::TimezoneServiceSet))
            }
            (Namespace::CalDav, Element::CalendarTimezoneId) => {
                Some(DavProperty::CalDav(CalDavProperty::TimezoneId))
            }
            (Namespace::CalendarServer, Element::Getctag) => {
                Some(DavProperty::WebDav(WebDavProperty::GetCTag))
            }
            _ => None,
        }
    }
}

impl TryFrom<NamedElement> for ResourceType {
    type Error = ();

    fn try_from(value: NamedElement) -> Result<Self, Self::Error> {
        match (value.ns, value.element) {
            (Namespace::Dav, Element::Collection) => Ok(ResourceType::Collection),
            (Namespace::Dav, Element::Principal) => Ok(ResourceType::Principal),
            (Namespace::CardDav, Element::Addressbook) => Ok(ResourceType::AddressBook),
            (Namespace::CalDav, Element::Calendar) => Ok(ResourceType::Calendar),
            (Namespace::CalDav, Element::ScheduleInbox) => Ok(ResourceType::ScheduleInbox),
            (Namespace::CalDav, Element::ScheduleOutbox) => Ok(ResourceType::ScheduleOutbox),
            _ => Err(()),
        }
    }
}

struct ICalendarDateTime(i64);

impl AttributeValue for ICalendarDateTime {
    fn from_str(s: &str) -> Option<Self>
    where
        Self: Sized,
    {
        let mut dt = PartialDateTime::default();
        dt.parse_timestamp(&mut s.as_bytes().iter().peekable(), true);
        dt.to_timestamp().map(ICalendarDateTime)
    }
}

impl AttributeValue for ICalendarComponentType {
    fn from_str(s: &str) -> Option<Self>
    where
        Self: Sized,
    {
        ICalendarComponentType::parse(s.as_bytes())
    }
}

impl AttributeValue for ICalendarProperty {
    fn from_str(s: &str) -> Option<Self>
    where
        Self: Sized,
    {
        ICalendarProperty::parse(s.as_bytes())
            .unwrap_or_else(|| ICalendarProperty::Other(s.to_string()))
            .into()
    }
}

impl AttributeValue for ICalendarParameterName {
    fn from_str(s: &str) -> Option<Self>
    where
        Self: Sized,
    {
        ICalendarParameterName::parse(s).into()
    }
}

impl AttributeValue for VCardPropertyWithGroup {
    fn from_str(s: &str) -> Option<Self>
    where
        Self: Sized,
    {
        if let Some((group, s)) = s.split_once('.') {
            VCardPropertyWithGroup {
                name: VCardProperty::parse(s.as_bytes())
                    .unwrap_or_else(|| VCardProperty::Other(s.to_string())),
                group: group.to_string().into(),
            }
            .into()
        } else {
            VCardPropertyWithGroup {
                name: VCardProperty::parse(s.as_bytes())
                    .unwrap_or_else(|| VCardProperty::Other(s.to_string())),
                group: None,
            }
            .into()
        }
    }
}

impl AttributeValue for VCardParameterName {
    fn from_str(s: &str) -> Option<Self>
    where
        Self: Sized,
    {
        VCardParameterName::parse(s).into()
    }
}

impl XmlValueParser for ICalendar {
    fn parse_bytes(bytes: &[u8]) -> Option<Self> {
        let text = String::from_utf8_lossy(bytes);
        let mut parser = Parser::new(&text);
        if let Entry::ICalendar(ical) = parser.entry() {
            Some(ical)
        } else {
            None
        }
    }

    fn parse_str(text: &str) -> Option<Self> {
        let mut parser = Parser::new(text);
        if let Entry::ICalendar(ical) = parser.entry() {
            Some(ical)
        } else {
            None
        }
    }
}

impl XmlValueParser for u64 {
    fn parse_bytes(bytes: &[u8]) -> Option<Self> {
        std::str::from_utf8(bytes).ok().and_then(|s| s.parse().ok())
    }

    fn parse_str(text: &str) -> Option<Self> {
        text.parse().ok()
    }
}

impl XmlValueParser for u32 {
    fn parse_bytes(bytes: &[u8]) -> Option<Self> {
        std::str::from_utf8(bytes).ok().and_then(|s| s.parse().ok())
    }

    fn parse_str(text: &str) -> Option<Self> {
        text.parse().ok()
    }
}

impl XmlValueParser for DateTime {
    fn parse_bytes(bytes: &[u8]) -> Option<Self> {
        std::str::from_utf8(bytes)
            .ok()
            .and_then(DateTime::parse_rfc3339)
    }

    fn parse_str(text: &str) -> Option<Self> {
        DateTime::parse_rfc3339(text)
    }
}
