//! Implements the [`InputString`] type storing the parsed input.

use core::fmt;

use storage::Storage;

/// Conditionally stores the input string in parse errors.
///
/// This type stores the input string of a parse function depending on whether `alloc` feature is
/// enabled. When it is enabled, the string is stored inside as `String`. When disabled this is a
/// zero-sized type and attempt to store a string does nothing.
///
/// This provides two methods to format the error strings depending on the context.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct InputString(Storage);

impl InputString {
    /// Displays a message saying `failed to parse <self> as <what>`.
    ///
    /// This is normally used with the `write_err!` macro.
    ///
    /// # Examples
    ///
    /// ```
    /// use core::fmt;
    /// use bitcoin_internals::error::InputString;
    /// use bitcoin_internals::write_err;
    ///
    /// /// An example parsing error including the parse error from core.
    /// #[derive(Debug, Clone, PartialEq, Eq)]
    /// pub struct ParseError {
    ///     input: InputString,
    ///     error: core::num::ParseIntError,
    /// }
    ///
    /// impl fmt::Display for ParseError {
    ///     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    ///         // Outputs "failed to parse '<input string>' as foo"
    ///         write_err!(f, "{}", self.input.display_cannot_parse("foo"); self.error)
    ///     }
    /// }
    /// ```
    pub fn display_cannot_parse<'a, T>(&'a self, what: &'a T) -> CannotParse<'a, T>
    where
        T: fmt::Display + ?Sized,
    {
        CannotParse { input: self, what }
    }

    /// Formats a message saying `<self> is not a known <what>`.
    ///
    /// This is normally used in leaf parse errors (with no source) when parsing an enum.
    ///
    /// # Examples
    ///
    /// ```
    /// use core::fmt;
    /// use bitcoin_internals::error::InputString;
    ///
    /// /// An example parsing error.
    /// #[derive(Debug, Clone, PartialEq, Eq)]
    /// pub struct ParseError(InputString);
    ///
    /// impl fmt::Display for ParseError {
    ///     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    ///         // Outputs "'<input string>' is not a known foo"
    ///         self.0.unknown_variant("foo", f)
    ///     }
    /// }
    /// ```
    pub fn unknown_variant<T>(&self, what: &T, f: &mut fmt::Formatter) -> fmt::Result
    where
        T: fmt::Display + ?Sized,
    {
        storage::unknown_variant(&self.0, what, f)
    }
}

macro_rules! impl_from {
    ($($type:ty),+ $(,)?) => {
        $(
            impl From<$type> for InputString {
                fn from(input: $type) -> Self {
                    #[allow(clippy::useless_conversion)]
                    InputString(input.into())
                }
            }
        )+
    }
}

impl_from!(&str);

/// Displays message saying `failed to parse <input> as <what>`.
///
/// This is created by `display_cannot_parse` method and should be used as
/// `write_err!("{}", self.input.display_cannot_parse("what is parsed"); self.source)` in parse
/// error [`Display`](fmt::Display) imlementation if the error has source. If the error doesn't
/// have a source just use regular `write!` with same formatting arguments.
pub struct CannotParse<'a, T: fmt::Display + ?Sized> {
    input: &'a InputString,
    what: &'a T,
}

impl<'a, T: fmt::Display + ?Sized> fmt::Display for CannotParse<'a, T> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        storage::cannot_parse(&self.input.0, &self.what, f)
    }
}

#[cfg(not(feature = "alloc"))]
mod storage {
    use core::fmt;

    #[derive(Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
    pub(super) struct Storage;

    impl fmt::Debug for Storage {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            f.write_str("<unknown input string - compiled without the `alloc` feature>")
        }
    }

    impl From<&str> for Storage {
        fn from(_value: &str) -> Self { Storage }
    }

    pub(super) fn cannot_parse<W>(_: &Storage, what: &W, f: &mut fmt::Formatter) -> fmt::Result
    where
        W: fmt::Display + ?Sized,
    {
        write!(f, "failed to parse {}", what)
    }

    pub(super) fn unknown_variant<W>(_: &Storage, what: &W, f: &mut fmt::Formatter) -> fmt::Result
    where
        W: fmt::Display + ?Sized,
    {
        write!(f, "unknown {}", what)
    }
}

#[cfg(feature = "alloc")]
mod storage {
    use core::fmt;

    use super::InputString;

    pub(super) type Storage = alloc::string::String;

    pub(super) fn cannot_parse<W>(input: &Storage, what: &W, f: &mut fmt::Formatter) -> fmt::Result
    where
        W: fmt::Display + ?Sized,
    {
        write!(f, "failed to parse '{}' as {}", input, what)
    }

    pub(super) fn unknown_variant<W>(inp: &Storage, what: &W, f: &mut fmt::Formatter) -> fmt::Result
    where
        W: fmt::Display + ?Sized,
    {
        write!(f, "'{}' is not a known {}", inp, what)
    }

    impl_from!(alloc::string::String, alloc::boxed::Box<str>, alloc::borrow::Cow<'_, str>);
}
