use alloc::string::{String, ToString};
use core::{fmt, str::FromStr};

#[cfg(all(feature = "arbitrary", feature = "serde", test))]
use miden_core::serde::{Deserializable, Serializable};

/// This represents the way in which a dependent will link against a dependency during assembly.
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "arbitrary", derive(proptest_derive::Arbitrary))]
#[cfg_attr(
    all(feature = "arbitrary", feature = "serde", test),
    miden_test_serde_macros::serde_test(binary_serde(true))
)]
#[repr(u8)]
pub enum Linkage {
    /// Link against the target package dynamically, i.e. it is expected that the package will be
    /// provided to the VM at runtime so that it is available for the dependent.
    #[default]
    Dynamic = 0,
    /// Link the contents of the target package into the dependent package, as if they were defined
    /// as part of the dependent.
    ///
    /// This linkage mode ensures that the dependency does not have to be provided to the VM
    /// separately in order to execute code from the dependent.
    Static,
}

impl Linkage {
    /// Get the string representation of this linkage
    pub const fn as_str(&self) -> &'static str {
        match self {
            Self::Dynamic => "dynamic",
            Self::Static => "static",
        }
    }
}

impl fmt::Display for Linkage {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

/// The error produced when parsing [Linkage] from a string value
#[derive(Debug, thiserror::Error)]
#[error("unknown linkage '{0}': expected either 'dynamic' or 'static'")]
pub struct UnknownLinkageError(String);

impl FromStr for Linkage {
    type Err = UnknownLinkageError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "default" | "dynamic" => Ok(Self::Dynamic),
            "static" => Ok(Self::Static),
            other => Err(UnknownLinkageError(other.to_string())),
        }
    }
}

#[cfg(feature = "serde")]
mod serialization {
    use alloc::format;

    use miden_core::serde::{
        ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
    };

    use super::Linkage;

    impl serde::Serialize for Linkage {
        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
        where
            S: serde::Serializer,
        {
            if serializer.is_human_readable() {
                self.as_str().serialize(serializer)
            } else {
                (*self as u8).serialize(serializer)
            }
        }
    }

    impl<'de> serde::Deserialize<'de> for Linkage {
        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
        where
            D: serde::Deserializer<'de>,
        {
            if deserializer.is_human_readable() {
                <&'de str>::deserialize(deserializer)?
                    .parse::<Linkage>()
                    .map_err(serde::de::Error::custom)
            } else {
                match u8::deserialize(deserializer)? {
                    0 => Ok(Self::Dynamic),
                    1 => Ok(Self::Static),
                    other => {
                        Err(serde::de::Error::custom(format!("invalid Linkage tag '{other}'")))
                    },
                }
            }
        }
    }

    impl Serializable for Linkage {
        fn write_into<W: ByteWriter>(&self, target: &mut W) {
            target.write_u8(*self as u8);
        }
    }

    impl Deserializable for Linkage {
        fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
            match source.read_u8()? {
                0 => Ok(Self::Dynamic),
                1 => Ok(Self::Static),
                other => Err(DeserializationError::InvalidValue(format!(
                    "unknown Linkage tag '{other}'"
                ))),
            }
        }
    }
}
