// Copyright (c) 2024 Jonas Schäfer <jonas@zombofant.net>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

//! # Convert data to and from XML text
//!
//! This module provides traits and types related to conversion of XML text
//! data to and from Rust types, as well as the [`AsXmlText`],
//! [`AsOptionalXmlText`][`crate::AsOptionalXmlText`] and [`FromXmlText`]
//! implementations for foreign and standard-library types.
//!
//! ## Support for types from third-party crates
//!
//! Beyond the standard library types, the following additional types are
//! supported:
//!
//! | Feature gate | Types |
//! | --- | --- |
//! | `jid` | `jid::Jid`, `jid::BareJid`, `jid::FullJid` |
//! | `serde_json` | `serde_json::Value` |
//! | `uuid` | `uuid::Uuid` |
//!
//! ### Adding support for more types
//!
//! Due to the orphan rule, it is not possible for applications to implement
//! [`AsXmlText`], [`AsOptionalXmlText`][`crate::AsOptionalXmlText`] or
//! [`FromXmlText`] on types which originate from third-party crates. Because
//! of that, we are **extremely liberal** at accepting merge requests for
//! implementations of these traits for types from third-party crates.
//!
//! The only requirement is that the implementation is gated behind a feature
//! flag which is disabled-by-default.
//!
//! ### Workaround for unsupported types
//!
//! If making a merge request against `xso` and waiting for a release is not
//! an option, you can use newtype wrappers in almost all cases, for example:
//!
#![cfg_attr(
    not(all(feature = "std", feature = "macros")),
    doc = "Because the std or macros feature was not enabled at doc build time, the example cannot be tested.\n\n```ignore\n"
)]
#![cfg_attr(all(feature = "std", feature = "macros"), doc = "\n```\n")]
//! # use xso::{AsXml, FromXml, AsXmlText, FromXmlText, error::Error};
//! # use std::borrow::Cow;
//! use std::process::ExitCode;
//!
//! struct MyExitCode(ExitCode);
//!
//! impl AsXmlText for MyExitCode {
//!     fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
//!         match self.0 {
//!             ExitCode::FAILURE => Ok(Cow::Borrowed("failure")),
//!             ExitCode::SUCCESS => Ok(Cow::Borrowed("success")),
//!             _ => Err(Error::Other("unknown exit code")),
//!         }
//!     }
//! }
//!
//! impl FromXmlText for MyExitCode {
//!     fn from_xml_text(s: String) -> Result<Self, Error> {
//!         match s.as_str() {
//!             "failure" => Ok(Self(ExitCode::FAILURE)),
//!             "success" => Ok(Self(ExitCode::SUCCESS)),
//!             _ => Err(Error::Other("unknown exit code")),
//!         }
//!     }
//! }
//!
//! #[derive(AsXml, FromXml)]
//! #[xml(namespace = "urn:example", name = "process-result")]
//! struct ProcessResult {
//!     #[xml(attribute)]
//!     code: MyExitCode,
//!     #[xml(text)]
//!     stdout: String,
//! }
//! ```
//!
//! Of course, such an approach reduces the usability of your struct (and
//! comes with issues once references are needed), so making a merge request
//! against `xso` is generally preferable.

use core::marker::PhantomData;

use alloc::{
    borrow::{Cow, ToOwned},
    boxed::Box,
    format,
    string::{String, ToString},
    vec::Vec,
};

use crate::{error::Error, AsOptionalXmlText, AsXmlText, FromXmlText};

#[cfg(feature = "base64")]
use base64::engine::general_purpose::STANDARD as StandardBase64Engine;

/// # Generate `AsXmlText` and `FromXmlText` implementations
///
/// This macro generates an `AsXmlText` implementation which uses
/// [`Display`][`core::fmt::Display`] and an `FromXmlText` which uses
/// [`FromStr`][`core::str::FromStr`] for the types it is called on.
///
/// ## Syntax
///
/// The macro accepts a comma-separated list of types. Optionally, each type
/// can be preceded by a `#[cfg(..)]` attribute to make the implementations
/// conditional on a feature.
///
/// ## Example
///
#[cfg_attr(
    not(feature = "macros"),
    doc = "Because the macros feature was not enabled at doc build time, the example cannot be tested.\n\n```ignore\n"
)]
#[cfg_attr(feature = "macros", doc = "\n```\n")]
/// # use xso::convert_via_fromstr_and_display;
/// # use core::fmt::{self, Display};
/// # use core::str::FromStr;
/// struct Foo;
///
/// impl FromStr for Foo {
/// #    type Err = core::convert::Infallible;
/// #
/// #    fn from_str(s: &str) -> Result<Self, Self::Err> { todo!() }
///     /* ... */
/// }
///
/// impl Display for Foo {
/// #    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { todo!() }
///     /* ... */
/// }
///
/// convert_via_fromstr_and_display!(
///     Foo,
/// );
/// ```
#[macro_export]
macro_rules! convert_via_fromstr_and_display {
    ($($(#[cfg $cfg:tt])?$t:ty),+ $(,)?) => {
        $(
            $(
                #[cfg $cfg]
            )?
            impl $crate::FromXmlText for $t {
                #[doc = concat!("Parse [`", stringify!($t), "`] from XML text via [`FromStr`][`core::str::FromStr`].")]
                fn from_xml_text(s: String) -> Result<Self, $crate::error::Error> {
                    s.parse().map_err($crate::error::Error::text_parse_error)
                }
            }

            $(
                #[cfg $cfg]
            )?
            impl $crate::AsXmlText for $t {
                #[doc = concat!("Convert [`", stringify!($t), "`] to XML text via [`Display`][`core::fmt::Display`].\n\nThis implementation never fails.")]
                fn as_xml_text(&self) -> Result<$crate::exports::alloc::borrow::Cow<'_, str>, $crate::error::Error> {
                    Ok($crate::exports::alloc::borrow::Cow::Owned(self.to_string()))
                }
            }
        )+
    }
}

/// This provides an implementation compliant with xsd::bool.
impl FromXmlText for bool {
    /// Parse a boolean from XML text.
    ///
    /// The values `"1"` and `"true"` are considered true. The values `"0"`
    /// and `"false"` are considered `false`. Any other value is invalid and
    /// will return an error.
    fn from_xml_text(s: String) -> Result<Self, Error> {
        match s.as_str() {
            "1" => "true",
            "0" => "false",
            other => other,
        }
        .parse()
        .map_err(Error::text_parse_error)
    }
}

/// This provides an implementation compliant with xsd::bool.
impl AsXmlText for bool {
    /// Convert a boolean to XML text.
    ///
    /// `true` is converted to `"true"` and `false` is converted to `"false"`.
    /// This implementation never fails.
    fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
        match self {
            true => Ok(Cow::Borrowed("true")),
            false => Ok(Cow::Borrowed("false")),
        }
    }
}

convert_via_fromstr_and_display! {
    u8,
    u16,
    u32,
    u64,
    u128,
    usize,
    i8,
    i16,
    i32,
    i64,
    i128,
    isize,
    f32,
    f64,
    char,
    core::net::IpAddr,
    core::net::Ipv4Addr,
    core::net::Ipv6Addr,
    core::net::SocketAddr,
    core::net::SocketAddrV4,
    core::net::SocketAddrV6,
    core::num::NonZeroU8,
    core::num::NonZeroU16,
    core::num::NonZeroU32,
    core::num::NonZeroU64,
    core::num::NonZeroU128,
    core::num::NonZeroUsize,
    core::num::NonZeroI8,
    core::num::NonZeroI16,
    core::num::NonZeroI32,
    core::num::NonZeroI64,
    core::num::NonZeroI128,
    core::num::NonZeroIsize,

    #[cfg(feature = "uuid")]
    uuid::Uuid,

    #[cfg(feature = "jid")]
    jid::Jid,
    #[cfg(feature = "jid")]
    jid::FullJid,
    #[cfg(feature = "jid")]
    jid::BareJid,
    #[cfg(feature = "jid")]
    jid::NodePart,
    #[cfg(feature = "jid")]
    jid::DomainPart,
    #[cfg(feature = "jid")]
    jid::ResourcePart,

    #[cfg(feature = "serde_json")]
    serde_json::Value,
}

impl FromXmlText for String {
    /// Return the string unchanged.
    fn from_xml_text(data: String) -> Result<Self, Error> {
        Ok(data)
    }
}

impl<T: FromXmlText, B: ToOwned<Owned = T>> FromXmlText for Cow<'_, B> {
    /// Return a [`Cow::Owned`] containing the parsed value.
    fn from_xml_text(data: String) -> Result<Self, Error> {
        Ok(Cow::Owned(T::from_xml_text(data)?))
    }
}

impl<T: FromXmlText> FromXmlText for Option<T> {
    /// Return a [`Some`] containing the parsed value.
    fn from_xml_text(data: String) -> Result<Self, Error> {
        Ok(Some(T::from_xml_text(data)?))
    }
}

impl<T: FromXmlText> FromXmlText for Box<T> {
    /// Return a [`Box`] containing the parsed value.
    fn from_xml_text(data: String) -> Result<Self, Error> {
        Ok(Box::new(T::from_xml_text(data)?))
    }
}

impl AsXmlText for String {
    /// Return the borrowed string contents.
    fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
        Ok(Cow::Borrowed(self))
    }
}

impl AsXmlText for str {
    /// Return the borrowed string contents.
    fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
        Ok(Cow::Borrowed(self))
    }
}

impl AsXmlText for &str {
    /// Return the borrowed string contents.
    fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
        Ok(Cow::Borrowed(self))
    }
}

impl<T: AsXmlText> AsXmlText for Box<T> {
    /// Return the borrowed [`Box`] contents.
    fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
        T::as_xml_text(self)
    }
}

impl<B: AsXmlText + ToOwned> AsXmlText for Cow<'_, B> {
    /// Return the borrowed [`Cow`] contents.
    fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
        B::as_xml_text(self)
    }
}

impl<T: AsXmlText> AsXmlText for &T {
    /// Delegate to the `AsXmlText` implementation on `T`.
    fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
        T::as_xml_text(*self)
    }
}

impl<T: AsXmlText> AsOptionalXmlText for T {
    fn as_optional_xml_text(&self) -> Result<Option<Cow<'_, str>>, Error> {
        <Self as AsXmlText>::as_optional_xml_text(self)
    }
}

impl<T: AsXmlText> AsOptionalXmlText for Option<T> {
    fn as_optional_xml_text(&self) -> Result<Option<Cow<'_, str>>, Error> {
        self.as_ref()
            .map(T::as_optional_xml_text)
            .transpose()
            .map(Option::flatten)
    }
}

/// Represent a way to encode/decode text data into a Rust type.
///
/// This trait can be used in scenarios where implementing [`FromXmlText`]
/// and/or [`AsXmlText`] on a type is not feasible or sensible, such as the
/// following:
///
/// 1. The type originates in a foreign crate, preventing the implementation
///    of foreign traits.
///
/// 2. There is more than one way to convert a value to/from XML.
///
/// The codec to use for a text can be specified in the attributes understood
/// by `FromXml` and `AsXml` derive macros. See the documentation of the
/// [`FromXml`][`macro@crate::FromXml`] derive macro for details.
#[diagnostic::on_unimplemented(
    message = "`{Self}` cannot be used as XML text codec for values of type `{T}`."
)]
pub trait TextCodec<T> {
    /// Decode a string value into the type.
    fn decode(&self, s: String) -> Result<T, Error>;

    /// Encode the type as string value.
    ///
    /// If this returns `None`, the string value is not emitted at all.
    fn encode<'x>(&self, value: &'x T) -> Result<Option<Cow<'x, str>>, Error>;

    /// Apply a filter to this codec.
    ///
    /// Filters preprocess strings before they are handed to the codec for
    /// parsing, allowing to, for example, make the codec ignore irrelevant
    /// content by stripping it.
    // NOTE: The bound on T is needed because any given type A may implement
    // TextCodec for any number of types. If we pass T down to the `Filtered`
    // struct, rustc can do type inference on which `TextCodec`
    // implementation the `filtered` method is supposed to have been called
    // on.
    fn filtered<F: TextFilter>(self, filter: F) -> Filtered<F, Self, T>
    where
        // placing the bound here (instead of on the `TextCodec<T>` trait
        // itself) preserves object-safety of TextCodec<T>.
        Self: Sized,
    {
        Filtered {
            filter,
            codec: self,
            bound: PhantomData,
        }
    }
}

/// Wrapper struct to apply a filter to a codec.
///
/// You can construct a value of this type via [`TextCodec::filtered`].
// NOTE: see the note on TextCodec::filtered for why we bind `T` here, too.
pub struct Filtered<F, C, T> {
    filter: F,
    codec: C,
    bound: PhantomData<T>,
}

impl<T, F: TextFilter, C: TextCodec<T>> TextCodec<T> for Filtered<F, C, T> {
    fn decode(&self, s: String) -> Result<T, Error> {
        let s = self.filter.preprocess(s);
        self.codec.decode(s)
    }

    fn encode<'x>(&self, value: &'x T) -> Result<Option<Cow<'x, str>>, Error> {
        self.codec.encode(value)
    }
}

/// Text codec which does no transform.
pub struct Plain;

impl TextCodec<String> for Plain {
    fn decode(&self, s: String) -> Result<String, Error> {
        Ok(s)
    }

    fn encode<'x>(&self, value: &'x String) -> Result<Option<Cow<'x, str>>, Error> {
        Ok(Some(Cow::Borrowed(value.as_str())))
    }
}

/// Text codec which returns `None` if the input to decode is the empty string, instead of
/// attempting to decode it.
///
/// Particularly useful when parsing `Option<T>` on `#[xml(text)]`, which does not support
/// `Option<_>` otherwise.
pub struct EmptyAsNone;

impl<T> TextCodec<Option<T>> for EmptyAsNone
where
    T: FromXmlText + AsXmlText,
{
    fn decode(&self, s: String) -> Result<Option<T>, Error> {
        if s.is_empty() {
            Ok(None)
        } else {
            Some(T::from_xml_text(s)).transpose()
        }
    }

    fn encode<'x>(&self, value: &'x Option<T>) -> Result<Option<Cow<'x, str>>, Error> {
        Ok(value
            .as_ref()
            .map(AsXmlText::as_xml_text)
            .transpose()?
            .and_then(|v| (!v.is_empty()).then_some(v)))
    }
}

/// Text codec which returns None instead of the empty string.
pub struct EmptyAsError;

impl TextCodec<String> for EmptyAsError {
    fn decode(&self, s: String) -> Result<String, Error> {
        if s.is_empty() {
            Err(Error::Other("Empty text node."))
        } else {
            Ok(s)
        }
    }

    fn encode<'x>(&self, value: &'x String) -> Result<Option<Cow<'x, str>>, Error> {
        if value.is_empty() {
            Err(Error::Other("Empty text node."))
        } else {
            Ok(Some(Cow::Borrowed(value.as_str())))
        }
    }
}

/// Trait for preprocessing text data from XML.
///
/// This may be used by codecs to allow to customize some of their behaviour.
pub trait TextFilter {
    /// Process the incoming string and return the result of the processing.
    fn preprocess(&self, s: String) -> String;
}

/// Text preprocessor which returns the input unchanged.
pub struct NoFilter;

impl TextFilter for NoFilter {
    fn preprocess(&self, s: String) -> String {
        s
    }
}

/// Text preprocessor to remove all whitespace.
pub struct StripWhitespace;

impl TextFilter for StripWhitespace {
    fn preprocess(&self, s: String) -> String {
        let s: String = s
            .chars()
            .filter(|ch| *ch != ' ' && *ch != '\n' && *ch != '\t')
            .collect();
        s
    }
}

/// Text codec transforming text to binary using standard `base64`.
///
/// `Base64` uses the [`base64::engine::general_purpose::STANDARD`] engine.
/// [`TextCodec`] is also automatically implemented for any value which
/// implements [`base64::engine::Engine`], allowing you to choose different
/// alphabets easily.
#[cfg(feature = "base64")]
pub struct Base64;

#[cfg(feature = "base64")]
impl TextCodec<Vec<u8>> for Base64 {
    fn decode(&self, s: String) -> Result<Vec<u8>, Error> {
        base64::engine::Engine::decode(&StandardBase64Engine, s.as_bytes())
            .map_err(Error::text_parse_error)
    }

    fn encode<'x>(&self, value: &'x Vec<u8>) -> Result<Option<Cow<'x, str>>, Error> {
        Ok(Some(Cow::Owned(base64::engine::Engine::encode(
            &StandardBase64Engine,
            value,
        ))))
    }
}

#[cfg(feature = "base64")]
impl<'x> TextCodec<Cow<'x, [u8]>> for Base64 {
    fn decode(&self, s: String) -> Result<Cow<'x, [u8]>, Error> {
        base64::engine::Engine::decode(&StandardBase64Engine, s.as_bytes())
            .map_err(Error::text_parse_error)
            .map(Cow::Owned)
    }

    fn encode<'a>(&self, value: &'a Cow<'x, [u8]>) -> Result<Option<Cow<'a, str>>, Error> {
        Ok(Some(Cow::Owned(base64::engine::Engine::encode(
            &StandardBase64Engine,
            value,
        ))))
    }
}

#[cfg(feature = "base64")]
impl<T> TextCodec<Option<T>> for Base64
where
    Base64: TextCodec<T>,
{
    fn decode(&self, s: String) -> Result<Option<T>, Error> {
        if s.is_empty() {
            return Ok(None);
        }
        Ok(Some(self.decode(s)?))
    }

    fn encode<'x>(&self, decoded: &'x Option<T>) -> Result<Option<Cow<'x, str>>, Error> {
        decoded
            .as_ref()
            .map(|x| self.encode(x))
            .transpose()
            .map(Option::flatten)
    }
}

#[cfg(feature = "base64")]
impl<T: base64::engine::Engine> TextCodec<Vec<u8>> for T {
    fn decode(&self, s: String) -> Result<Vec<u8>, Error> {
        base64::engine::Engine::decode(self, s.as_bytes()).map_err(Error::text_parse_error)
    }

    fn encode<'x>(&self, value: &'x Vec<u8>) -> Result<Option<Cow<'x, str>>, Error> {
        Ok(Some(Cow::Owned(base64::engine::Engine::encode(
            self, value,
        ))))
    }
}

#[cfg(feature = "base64")]
impl<T: base64::engine::Engine, U> TextCodec<Option<U>> for T
where
    T: TextCodec<U>,
{
    fn decode(&self, s: String) -> Result<Option<U>, Error> {
        if s.is_empty() {
            return Ok(None);
        }
        Ok(Some(TextCodec::decode(self, s)?))
    }

    fn encode<'x>(&self, decoded: &'x Option<U>) -> Result<Option<Cow<'x, str>>, Error> {
        decoded
            .as_ref()
            .map(|x| TextCodec::encode(self, x))
            .transpose()
            .map(Option::flatten)
    }
}

/// Text codec transforming text to binary using hexadecimal nibbles.
///
/// The length must be known at compile-time.
pub struct FixedHex<const N: usize>;

impl<const N: usize> TextCodec<[u8; N]> for FixedHex<N> {
    fn decode(&self, s: String) -> Result<[u8; N], Error> {
        if s.len() != 2 * N {
            return Err(Error::Other("Invalid length"));
        }

        let mut bytes = [0u8; N];
        for i in 0..N {
            bytes[i] =
                u8::from_str_radix(&s[2 * i..2 * i + 2], 16).map_err(Error::text_parse_error)?;
        }

        Ok(bytes)
    }

    fn encode<'x>(&self, value: &'x [u8; N]) -> Result<Option<Cow<'x, str>>, Error> {
        let mut bytes = String::with_capacity(N * 2);
        for byte in value {
            bytes.extend(format!("{:02x}", byte).chars());
        }
        Ok(Some(Cow::Owned(bytes)))
    }
}

impl<T, const N: usize> TextCodec<Option<T>> for FixedHex<N>
where
    FixedHex<N>: TextCodec<T>,
{
    fn decode(&self, s: String) -> Result<Option<T>, Error> {
        if s.is_empty() {
            return Ok(None);
        }
        Ok(Some(self.decode(s)?))
    }

    fn encode<'x>(&self, decoded: &'x Option<T>) -> Result<Option<Cow<'x, str>>, Error> {
        decoded
            .as_ref()
            .map(|x| self.encode(x))
            .transpose()
            .map(Option::flatten)
    }
}

/// Text codec for colon-separated bytes of uppercase hexadecimal.
pub struct ColonSeparatedHex;

impl TextCodec<Vec<u8>> for ColonSeparatedHex {
    fn decode(&self, s: String) -> Result<Vec<u8>, Error> {
        assert_eq!((s.len() + 1) % 3, 0);
        let mut bytes = Vec::with_capacity((s.len() + 1) / 3);
        for i in 0..(1 + s.len()) / 3 {
            let byte =
                u8::from_str_radix(&s[3 * i..3 * i + 2], 16).map_err(Error::text_parse_error)?;
            if 3 * i + 2 < s.len() {
                assert_eq!(&s[3 * i + 2..3 * i + 3], ":");
            }
            bytes.push(byte);
        }
        Ok(bytes)
    }

    fn encode<'x>(&self, decoded: &'x Vec<u8>) -> Result<Option<Cow<'x, str>>, Error> {
        // TODO: Super inefficient!
        let mut bytes = Vec::with_capacity(decoded.len());
        for byte in decoded {
            bytes.push(format!("{:02X}", byte));
        }
        Ok(Some(Cow::Owned(bytes.join(":"))))
    }
}
