use core::str;
use core::str::Utf8Error;

use le_stream::{FromLeStream, ToLeStream};

use crate::constants::U8_CAPACITY;
use crate::types::{OctStr, Type};

/// A string type, which can be up to [`OctStr::MAX_SIZE`] bytes long.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, FromLeStream, ToLeStream)]
#[repr(transparent)]
pub struct String<const CAPACITY: usize = U8_CAPACITY>(OctStr<CAPACITY>);

impl<const CAPACITY: usize> String<CAPACITY> {
    /// Return the length in bytes.
    #[must_use]
    pub fn len(&self) -> usize {
        self.0.len()
    }

    /// Determine whether the string is empty.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    /// Try to parse the underlying bytes as a UTF-8 string.
    ///
    /// # Errors
    ///
    /// If the bytes are not valid UTF-8, this will return an [`Utf8Error`].
    pub fn try_as_str(&self) -> Result<&str, Utf8Error> {
        str::from_utf8(self.0.as_ref())
    }

    /// Attempt to truncate the string to a new size.
    ///
    /// # Errors
    ///
    /// Returns the original string if the new capacity is smaller than the current size.
    pub fn truncate<const NEW_CAPACITY: usize>(self) -> Result<String<NEW_CAPACITY>, Self> {
        self.0.truncate().map_err(Self).map(String::<NEW_CAPACITY>)
    }

    /// Attempt to widen a string to a new, larger capacity.
    #[must_use]
    pub fn widen<const NEW_CAPACITY: usize>(self) -> String<NEW_CAPACITY> {
        String(self.0.widen())
    }
}

impl<const CAPACITY: usize> AsRef<[u8]> for String<CAPACITY> {
    fn as_ref(&self) -> &[u8] {
        self.0.as_ref()
    }
}

impl<const CAPACITY: usize> From<heapless::String<CAPACITY, u8>> for String<CAPACITY> {
    fn from(value: heapless::String<CAPACITY, u8>) -> Self {
        Self(OctStr::from(value.into_bytes()))
    }
}

impl<const CAPACITY: usize> From<String<CAPACITY>> for Type {
    fn from(value: String<CAPACITY>) -> Self {
        Self::String(value.widen())
    }
}

impl<const CAPACITY: usize> TryFrom<&str> for String<CAPACITY> {
    type Error = ();

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        OctStr::try_from(value.as_bytes()).map(Self)
    }
}

impl<const CAPACITY: usize> TryFrom<Type> for String<CAPACITY> {
    type Error = Type;

    fn try_from(typ: Type) -> Result<Self, Self::Error> {
        if let Type::String(string) = typ {
            string.truncate().map_err(Type::String)
        } else {
            Err(typ)
        }
    }
}
