//! Native enum metadata, parsing, and flag operations.

#![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the public compatibility map.
#![allow(clippy::must_use_candidate)] // Attributes are not part of the mapped C# surface.

use crate::compat::EnumValue;

/// Metadata needed to reproduce the reflection-based C# enum helpers.
pub trait EnumValueMetadata: Copy + Sized {
    /// Parses a named value using the C# helper's case-insensitive comparison.
    fn parse_name(name: &str) -> Option<Self>;

    /// Returns the C# identifier for this value, when it is named.
    fn variant_name(self) -> Option<&'static str>;

    /// Returns the value's `EnumInfoAttribute.Text`, when present.
    fn enum_text(self) -> Option<&'static str> {
        None
    }
}

/// C# enum presentation metadata.
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct EnumInfoAttribute {
    /// Text used when presenting the enum to a user.
    pub text: String,
}

impl EnumInfoAttribute {
    /// Creates an attribute with empty presentation text.
    pub fn new_with_constructor() -> Result<Self, crate::Error> {
        Ok(Self::default())
    }

    /// Creates an attribute with the supplied presentation text.
    pub fn new_with_string(text: String) -> Result<Self, crate::Error> {
        Ok(Self { text })
    }
}

/// Reflection-free equivalent of the C# enum metadata extension.
pub struct EnumInfoExtensions;

impl EnumInfoExtensions {
    /// Returns attached presentation text, or an empty string when absent.
    pub fn get_text(value: EnumValue) -> Result<String, crate::Error> {
        Ok(value.text.unwrap_or_default().to_owned())
    }
}

macro_rules! impl_enum_metadata {
    ($ty:ty; $($variant:ident $(=> $text:literal)?),+ $(,)?) => {
        impl EnumValueMetadata for $ty {
            fn parse_name(name: &str) -> Option<Self> {
                $(if name.eq_ignore_ascii_case(stringify!($variant)) {
                    return Some(Self::$variant);
                })+
                None
            }

            fn variant_name(self) -> Option<&'static str> {
                Some(match self { $(Self::$variant => stringify!($variant),)+ })
            }

            #[allow(unreachable_patterns)]
            fn enum_text(self) -> Option<&'static str> {
                match self {
                    $($(Self::$variant => Some($text),)?)+
                    _ => None,
                }
            }
        }

        impl From<$ty> for EnumValue {
            fn from(value: $ty) -> Self {
                Self {
                    name: value.variant_name().unwrap_or_default(),
                    text: value.enum_text(),
                }
            }
        }
    };
}

impl_enum_metadata!(crate::AssetType;
    Animation => "animatn", Bodypart => "bodypart", CallingCard => "callcard",
    Clothing => "clothing", Folder => "category", Gesture => "gesture",
    ImageJPEG => "jpeg", ImageTGA => "img_tga", LSLBytecode => "lslbyte",
    LSLText => "lsltext", Landmark => "landmark", Link => "link",
    LinkFolder => "link_f", Material => "material", Mesh => "mesh",
    Notecard => "notecard", Object => "object", Person => "person",
    Script => "script", Settings => "settings", Simstate => "simstate",
    Sound => "sound", SoundWAV => "snd_wav", Texture => "texture",
    TextureTGA => "txtr_tga", Unknown => "invalid", Widget => "widget");

impl_enum_metadata!(crate::FolderType;
    Animation => "Animations", BasicRoot => "Basic Root", BodyPart => "Body Parts",
    CallingCard => "Calling Cards", Clothing => "Clothing",
    CurrentOutfit => "Current Outfit", EnsembleEnd, EnsembleStart,
    Favorites => "Favorites", Gesture => "Gestures", Inbox => "Received Items",
    LSLText => "Scripts", Landmark => "Landmarks", LostAndFound => "Lost And Found",
    MarketplaceListings => "Marketplace Listings", MarketplaceStock => "New Stock",
    MarketplaceVersion => "Marketplace Version", Material => "Material", Mesh => "Meshes",
    MyOutfits => "My Outfits", None, Notecard => "Notecards", Object => "Objects",
    OldRoot, Outbox => "Merchant Outbox", Outfit => "New Outfit",
    Root => "My Inventory", Settings => "Settings", Snapshot => "Photo Album",
    Sound => "Sounds", Suitcase => "Suitcase", Texture => "Textures", Trash => "Trash");

impl_enum_metadata!(crate::WearableType;
    Alpha => "Alpha", Eyes => "Eyes", Gloves => "Gloves", Hair => "Hair",
    Invalid => "Invalid", Jacket => "Jacket", Pants => "Pants", Physics => "Physics",
    Shape => "Shape", Shirt => "Shirt", Shoes => "Shoes", Skin => "Skin",
    Skirt => "Skirt", Socks => "Socks", Tattoo => "Tattoo",
    Underpants => "Underpants", Undershirt => "Undershirt", Universal => "Universal");

impl_enum_metadata!(crate::AttachmentPoint;
    AltLeftEar => "Alt Left Ear", AltLeftEye => "Alt Left Eye",
    AltRightEar => "Alt Right Ear", AltRightEye => "Alt Right Eye", Chest => "Chest",
    Chin => "Chin", Default => "Default", Groin => "Groin", HUDBottom => "HUD Bottom",
    HUDBottomLeft => "HUD Bottom Left", HUDBottomRight => "HUD Bottom Right",
    HUDCenter => "HUD Center 1", HUDCenter2 => "HUD Center 2", HUDTop => "HUD Top Center",
    HUDTopLeft => "HUD Top Left", HUDTopRight => "HUD Top Right", Jaw => "Jaw",
    LeftEar => "Left Ear", LeftEyeball => "Left Eye", LeftFoot => "Left Foot",
    LeftForearm => "Left Lower Arm", LeftHand => "Left Hand",
    LeftHandRing => "Left Ring Finger", LeftHindFoot => "Left Hind Foot",
    LeftHip => "Left Hip", LeftLowerLeg => "Left Lower Leg", LeftPec => "Left Pec",
    LeftShoulder => "Left Shoulder", LeftUpperArm => "Left Upper Arm",
    LeftUpperLeg => "Left Upper Leg", LeftWing => "Left Wing", Mouth => "Mouth",
    Neck => "Neck", Nose => "Nose", Pelvis => "Pelvis", RightEar => "Right Ear",
    RightEyeball => "Right Eye", RightFoot => "Right Foot",
    RightForearm => "Right Lower Arm", RightHand => "Right Hand",
    RightHandRing => "Right Ring Finger", RightHindFoot => "Right Hind Foot",
    RightHip => "Right Hip", RightLowerLeg => "Right Lower Leg", RightPec => "Right Pec",
    RightShoulder => "Right Shoulder", RightUpperArm => "Right Upper Arm",
    RightUpperLeg => "Right Upper Leg", RightWing => "Right Wing",
    Root => "Avatar Center", Skull => "Head", Spine => "Back", Stomach => "Belly",
    TailBase => "Tail Base", TailTip => "Tail Tip", Tongue => "Tongue");

impl_enum_metadata!(crate::ClickAction; Buy, OpenMedia, OpenTask, Pay, PlayMedia, Sit, Touch);
impl_enum_metadata!(crate::FaceType; InnerSide, OuterSide0, OuterSide1, OuterSide2, OuterSide3, PathBegin, PathEnd, ProfileBegin, ProfileEnd);
impl_enum_metadata!(crate::JointType; Hinge, Invalid, Point);
impl_enum_metadata!(crate::Material; Flesh, Glass, Light, Metal, Plastic, Rubber, Stone, Wood);
impl_enum_metadata!(crate::ObjectCategory; Group, Invalid, None, Other, Owner, Selected, Temporary);
impl_enum_metadata!(crate::PCode; Avatar, Grass, NewTree, None, ParticleSystem, Prim, Tree);
impl_enum_metadata!(crate::PathCurve; Circle, Circle2, Flexible, Line, Test);
impl_enum_metadata!(crate::PhysicsShapeType; ConvexHull, None, Prim);
impl_enum_metadata!(crate::PrimType; Box, Cylinder, Mesh, Prism, Ring, Sculpt, Sphere, Torus, Tube, Unknown);
impl_enum_metadata!(crate::ProfileCurve; Circle, EqualTriangle, HalfCircle, IsoTriangle, RightTriangle, Square);
impl_enum_metadata!(crate::SaleType; Contents, Copy, Not, Original);
impl_enum_metadata!(crate::SculptType; Cylinder, Invert, Mesh, Mirror, None, Plane, Sphere, Torus);
impl_enum_metadata!(crate::UtilsPlatform; Linux, OSX, Unknown, Windows, WindowsCE);

macro_rules! impl_raw_enum_metadata {
    ($ty:ty; $($name:literal => $constant:ident),+ $(,)?) => {
        impl EnumValueMetadata for $ty {
            fn parse_name(name: &str) -> Option<Self> {
                $(if name.eq_ignore_ascii_case($name) { return Some(Self::$constant); })+
                None
            }

            fn variant_name(self) -> Option<&'static str> {
                $(if self == Self::$constant { return Some($name); })+
                None
            }
        }

        impl From<$ty> for EnumValue {
            fn from(value: $ty) -> Self {
                Self { name: value.variant_name().unwrap_or_default(), text: None }
            }
        }
    };
}

impl_raw_enum_metadata!(crate::HoleType;
    "Circle" => Circle, "Same" => Same, "Square" => Square, "Triangle" => Triangle);
impl_raw_enum_metadata!(crate::InventoryType;
    "Animation" => ANIMATION, "Attachment" => ATTACHMENT, "CallingCard" => CALLING_CARD,
    "Category" => CATEGORY, "Gesture" => GESTURE, "LSL" => LSL,
    "Landmark" => LANDMARK, "Material" => MATERIAL, "Mesh" => MESH,
    "Notecard" => NOTECARD, "Object" => OBJECT, "RootCategory" => ROOT_CATEGORY,
    "Settings" => SETTINGS, "Snapshot" => SNAPSHOT, "Sound" => SOUND,
    "Texture" => TEXTURE, "Unknown" => UNKNOWN, "Wearable" => WEARABLE);
impl_raw_enum_metadata!(crate::ExtraParamType;
    "ExtendedMesh" => EXTENDED_MESH, "Flexible" => FLEXIBLE, "Light" => LIGHT,
    "LightImage" => LIGHT_IMAGE, "Mesh" => MESH, "ReflectionProbe" => REFLECTION_PROBE,
    "RenderMaterial" => RENDER_MATERIAL, "Sculpt" => SCULPT);
impl_raw_enum_metadata!(crate::PrimFlags;
    "AllowInventoryDrop" => ALLOW_INVENTORY_DROP, "AnimSource" => ANIM_SOURCE,
    "CameraDecoupled" => CAMERA_DECOUPLED, "CameraSource" => CAMERA_SOURCE,
    "CastShadows" => CAST_SHADOWS, "CreateSelected" => CREATE_SELECTED,
    "DieAtEdge" => DIE_AT_EDGE, "Flying" => FLYING, "InventoryEmpty" => INVENTORY_EMPTY,
    "JointHinge" => JOINT_HINGE, "JointLP2P" => JOINT_LP2_P, "JointP2P" => JOINT_P2_P,
    "JointWheel" => JOINT_WHEEL, "Money" => MONEY, "None" => NONE,
    "ObjectAnyOwner" => OBJECT_ANY_OWNER, "ObjectCopy" => OBJECT_COPY,
    "ObjectGroupOwned" => OBJECT_GROUP_OWNED, "ObjectModify" => OBJECT_MODIFY,
    "ObjectMove" => OBJECT_MOVE, "ObjectOwnerModify" => OBJECT_OWNER_MODIFY,
    "ObjectTransfer" => OBJECT_TRANSFER, "ObjectYouOfficer" => OBJECT_YOU_OFFICER,
    "ObjectYouOwner" => OBJECT_YOU_OWNER, "Phantom" => PHANTOM, "Physics" => PHYSICS,
    "ReturnAtEdge" => RETURN_AT_EDGE, "Sandbox" => SANDBOX, "Scripted" => SCRIPTED,
    "Temporary" => TEMPORARY, "TemporaryOnRez" => TEMPORARY_ON_REZ,
    "Touch" => TOUCH, "ZlibCompressed" => ZLIB_COMPRESSED);
impl_raw_enum_metadata!(crate::SoundFlags;
    "Loop" => LOOP_, "None" => NONE, "Queue" => QUEUE, "Stop" => STOP,
    "SyncMaster" => SYNC_MASTER, "SyncPending" => SYNC_PENDING, "SyncSlave" => SYNC_SLAVE);

macro_rules! impl_flag_ops {
    ($ty:ty, $raw:ty) => {
        impl $ty {
            /// Returns every known and unknown protocol bit.
            #[must_use]
            pub const fn bits(self) -> $raw {
                self.0
            }

            /// Constructs flags without discarding future protocol bits.
            #[must_use]
            pub const fn from_bits_retain(bits: $raw) -> Self {
                Self(bits)
            }

            /// Returns whether all supplied bits are present.
            #[must_use]
            pub const fn contains(self, other: Self) -> bool {
                self.0 & other.0 == other.0
            }

            /// Returns whether any supplied bit is present.
            #[must_use]
            pub const fn intersects(self, other: Self) -> bool {
                self.0 & other.0 != 0
            }

            /// Returns whether no bits are set.
            #[must_use]
            pub const fn is_empty(self) -> bool {
                self.0 == 0
            }
        }

        impl std::ops::BitOr for $ty {
            type Output = Self;
            fn bitor(self, rhs: Self) -> Self {
                Self(self.0 | rhs.0)
            }
        }
        impl std::ops::BitAnd for $ty {
            type Output = Self;
            fn bitand(self, rhs: Self) -> Self {
                Self(self.0 & rhs.0)
            }
        }
        impl std::ops::BitXor for $ty {
            type Output = Self;
            fn bitxor(self, rhs: Self) -> Self {
                Self(self.0 ^ rhs.0)
            }
        }
        impl std::ops::Not for $ty {
            type Output = Self;
            fn not(self) -> Self {
                Self(!self.0)
            }
        }
        impl std::ops::BitOrAssign for $ty {
            fn bitor_assign(&mut self, rhs: Self) {
                self.0 |= rhs.0;
            }
        }
        impl std::ops::BitAndAssign for $ty {
            fn bitand_assign(&mut self, rhs: Self) {
                self.0 &= rhs.0;
            }
        }
        impl std::ops::BitXorAssign for $ty {
            fn bitxor_assign(&mut self, rhs: Self) {
                self.0 ^= rhs.0;
            }
        }
    };
}

impl_flag_ops!(crate::ExtraParamType, u16);
impl_flag_ops!(crate::PrimFlags, u32);
impl_flag_ops!(crate::SoundFlags, u8);

impl crate::HoleType {
    /// Returns the raw protocol value.
    #[must_use]
    pub const fn raw(self) -> u8 {
        self.0
    }

    /// Retains a raw value, including extensions unknown to this version.
    #[must_use]
    pub const fn from_raw(value: u8) -> Self {
        Self(value)
    }
}

impl crate::InventoryType {
    /// Returns the raw protocol value.
    #[must_use]
    pub const fn raw(self) -> i8 {
        self.0
    }

    /// Retains a raw value, including extensions unknown to this version.
    #[must_use]
    pub const fn from_raw(value: i8) -> Self {
        Self(value)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        AssetType, AttachmentPoint, ClickAction, ExtraParamType, FaceType, FolderType, HoleType,
        InventoryType, JointType, Material, ObjectCategory, PCode, PathCurve, PhysicsShapeType,
        PrimFlags, PrimType, ProfileCurve, SaleType, SculptType, SoundFlags, UtilsPlatform,
        WearableType,
    };

    #[test]
    fn discriminants_and_unknown_flag_bits_round_trip() {
        assert_eq!(AssetType::Material as i8, 57);
        assert_eq!(AttachmentPoint::RightHindFoot as u8, 55);
        assert_eq!(std::mem::size_of::<PrimFlags>(), 4);
        let future = PrimFlags::from_bits_retain(0x0000_0001 | 0x0000_0800);
        assert!(future.contains(PrimFlags::PHYSICS));
        assert_eq!(future.bits(), 0x0000_0801);
        assert_eq!((SoundFlags::LOOP_ | SoundFlags::QUEUE).bits(), 0x11);
        assert_eq!(SoundFlags::from_bits_retain(0x80).bits(), 0x80);
        assert_eq!(ExtraParamType::from_bits_retain(0x8000).bits(), 0x8000);
        assert_eq!(HoleType::from_raw(0x40).raw(), 0x40);
        assert_eq!(InventoryType::from_raw(120).raw(), 120);
    }

    #[test]
    fn every_enum_uses_the_golden_underlying_width() {
        assert_eq!(std::mem::size_of::<AssetType>(), 1);
        assert_eq!(std::mem::size_of::<AttachmentPoint>(), 1);
        assert_eq!(std::mem::size_of::<ClickAction>(), 1);
        assert_eq!(std::mem::size_of::<ExtraParamType>(), 2);
        assert_eq!(std::mem::size_of::<FaceType>(), 2);
        assert_eq!(std::mem::size_of::<FolderType>(), 1);
        assert_eq!(std::mem::size_of::<HoleType>(), 1);
        assert_eq!(std::mem::size_of::<InventoryType>(), 1);
        assert_eq!(std::mem::size_of::<JointType>(), 1);
        assert_eq!(std::mem::size_of::<Material>(), 1);
        assert_eq!(std::mem::size_of::<ObjectCategory>(), 4);
        assert_eq!(std::mem::size_of::<PCode>(), 1);
        assert_eq!(std::mem::size_of::<PathCurve>(), 1);
        assert_eq!(std::mem::size_of::<PhysicsShapeType>(), 1);
        assert_eq!(std::mem::size_of::<PrimFlags>(), 4);
        assert_eq!(std::mem::size_of::<PrimType>(), 4);
        assert_eq!(std::mem::size_of::<ProfileCurve>(), 1);
        assert_eq!(std::mem::size_of::<SaleType>(), 1);
        assert_eq!(std::mem::size_of::<SculptType>(), 1);
        assert_eq!(std::mem::size_of::<SoundFlags>(), 1);
        assert_eq!(std::mem::size_of::<UtilsPlatform>(), 4);
        assert_eq!(std::mem::size_of::<WearableType>(), 1);

        let all_prim_flags = PrimFlags::PHYSICS
            | PrimFlags::CREATE_SELECTED
            | PrimFlags::OBJECT_MODIFY
            | PrimFlags::OBJECT_COPY
            | PrimFlags::OBJECT_ANY_OWNER
            | PrimFlags::OBJECT_YOU_OWNER
            | PrimFlags::SCRIPTED
            | PrimFlags::TOUCH
            | PrimFlags::OBJECT_MOVE
            | PrimFlags::MONEY
            | PrimFlags::PHANTOM
            | PrimFlags::INVENTORY_EMPTY
            | PrimFlags::JOINT_HINGE
            | PrimFlags::JOINT_P2_P
            | PrimFlags::JOINT_LP2_P
            | PrimFlags::JOINT_WHEEL
            | PrimFlags::ALLOW_INVENTORY_DROP
            | PrimFlags::OBJECT_TRANSFER
            | PrimFlags::OBJECT_GROUP_OWNED
            | PrimFlags::OBJECT_YOU_OFFICER
            | PrimFlags::CAMERA_DECOUPLED
            | PrimFlags::ANIM_SOURCE
            | PrimFlags::CAMERA_SOURCE
            | PrimFlags::CAST_SHADOWS
            | PrimFlags::DIE_AT_EDGE
            | PrimFlags::RETURN_AT_EDGE
            | PrimFlags::SANDBOX
            | PrimFlags::FLYING
            | PrimFlags::OBJECT_OWNER_MODIFY
            | PrimFlags::TEMPORARY_ON_REZ
            | PrimFlags::TEMPORARY
            | PrimFlags::ZLIB_COMPRESSED;
        assert_eq!(all_prim_flags.bits(), u32::MAX);
        let all_sound_flags = SoundFlags::LOOP_
            | SoundFlags::SYNC_MASTER
            | SoundFlags::SYNC_SLAVE
            | SoundFlags::SYNC_PENDING
            | SoundFlags::QUEUE
            | SoundFlags::STOP;
        assert_eq!(all_sound_flags.bits(), 0x3f);
    }

    #[test]
    fn metadata_and_case_insensitive_parsing_match_csharp() {
        assert_eq!(AssetType::parse_name("lsltEXt"), Some(AssetType::LSLText));
        let value: EnumValue = AssetType::LSLText.into();
        assert_eq!(
            EnumInfoExtensions::get_text(value),
            Ok("lsltext".to_owned())
        );
        let value: EnumValue = AttachmentPoint::Skull.into();
        assert_eq!(EnumInfoExtensions::get_text(value), Ok("Head".to_owned()));
    }
}
