use crate::*;

/// Discriminant arithmetic glue to make `#[derive(Enum)]` function.
pub trait UsizeTag: Sized {
    /// Used by [`EnumTag::from_const`].
    ///
    /// ## Panics
    ///
    /// Panics on out-of-bounds.
    fn from_usize(n: usize) -> Self;
    /// Convert a trusted value to [`usize`].
    fn to_usize(&self) -> usize;
    /// Convert an untrusted value to [`usize`].
    fn try_to_usize(&self) -> Option<usize>;
}

impl UsizeTag for bool {
    fn from_usize(n: usize) -> Self {
        match n {
            0 => false,
            1 => true,
            _ => panic!("out of bounds"),
        }
    }

    fn to_usize(&self) -> usize {
        *self as _
    }

    fn try_to_usize(&self) -> Option<usize> {
        Some(self.to_usize())
    }
}

/// [`EnumKind::Tag`].
#[derive(
    ToOutput, InlineOutput, ListHashes, Topological, Tagged, ParseAsInline, Size, MaybeHasNiche,
)]
pub struct EnumTag<T, const MAX: usize>(T);

impl<T: UsizeTag, const MAX: usize> UsizeTag for EnumTag<T, MAX> {
    fn from_usize(n: usize) -> Self {
        assert!(n < MAX);
        Self(UsizeTag::from_usize(n))
    }

    fn to_usize(&self) -> usize {
        self.0.to_usize()
    }

    fn try_to_usize(&self) -> Option<usize> {
        self.0.try_to_usize()
    }
}

impl<T: UsizeTag, const MAX: usize> EnumTag<T, MAX> {
    /// Inherent alias of [`UsizeTag::to_usize`].
    pub fn to_usize(&self) -> usize {
        self.0.to_usize()
    }

    /// Generate the tag from a statically known index.
    pub fn from_const<const N: usize>() -> Self {
        assert!(N < MAX);
        Self(UsizeTag::from_usize(N))
    }
}

impl<T: ParseInline<I> + UsizeTag, I: ParseInput, const MAX: usize> ParseInline<I>
    for EnumTag<T, MAX>
{
    fn parse_inline(input: &mut I) -> crate::Result<Self> {
        let n_raw = T::parse_inline(input)?;
        let n: Option<usize> = n_raw.try_to_usize();
        if let Some(n) = n {
            if n < MAX {
                return Ok(Self(n_raw));
            }
        }
        Err(Error::DiscriminantOverflow)
    }
}

/// An [`Inline`] identifying variants of an [`Enum`].
pub trait EnumKind: Copy {
    /// Underlying [`Inline`]. Typically [`EnumTag`].
    type Tag;
    /// Get the underlying [`Inline`].
    fn to_tag(self) -> Self::Tag;
    /// Convert from an [`EnumTag`] assuming the value is within bounds, such that otherwise we have
    /// caught their violation during parsing of that tag.
    ///
    /// Panics on out-of-bounds.
    fn from_tag(tag: Self::Tag) -> Self;
}

/// `enum`.
pub trait Enum {
    /// Discriminant uniquely identifying this `enum`'s variants.
    type Kind: EnumKind;
    /// Get the [`EnumKind`] of this variant.
    fn kind(&self) -> Self::Kind;
}

/// [`Enum`]-specific [`Parse`].
pub trait EnumParse<I: ParseInput>: Enum + Parse<I> {
    /// Given an already-parsed [`EnumKind`], parse the rest of a [`Parse`] [`Enum`].
    fn enum_parse(kind: Self::Kind, input: I) -> crate::Result<Self>;
    /// For implementing [`Parse::parse`].
    fn parse_as_enum(mut input: I) -> crate::Result<Self>
    where
        <Self::Kind as EnumKind>::Tag: ParseInline<I>,
    {
        Self::enum_parse(Self::Kind::from_tag(input.parse_inline()?), input)
    }
}

/// [`Enum`]-specific [`ParseInline`].
pub trait EnumParseInline<I: ParseInput>: Enum + ParseInline<I> {
    /// Given an already-parsed [`EnumKind`], parse the rest of a [`ParseInline`] [`Enum`].
    fn enum_parse_inline(kind: Self::Kind, input: &mut I) -> crate::Result<Self>;
    /// For implementing [`ParseInline::parse_inline`].
    fn parse_as_inline_enum(input: &mut I) -> crate::Result<Self>
    where
        <Self::Kind as EnumKind>::Tag: ParseInline<I>,
    {
        Self::enum_parse_inline(Self::Kind::from_tag(input.parse_inline()?), input)
    }
}
