use std::borrow::Cow;
use std::fmt::Debug;

use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote, quote_spanned, ToTokens};
use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{parse_quote, parse_quote_spanned, spanned::Spanned, ImplItemFn, Result, Token};

use crate::attributes::kw::frozen;
use crate::attributes::{
    self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute,
    ModuleAttribute, NameAttribute, NameLitStr, NewImplTypeAttribute, NewImplTypeAttributeValue,
    RenameAllAttribute, StrFormatterAttribute,
};
use crate::combine_errors::CombineErrors;
#[cfg(feature = "experimental-inspect")]
use crate::introspection::{
    attribute_introspection_code, class_introspection_code, function_introspection_code,
    introspection_id_const,
};
use crate::konst::{ConstAttributes, ConstSpec};
use crate::method::{FnArg, FnSpec, PyArg, RegularArg};
#[cfg(feature = "experimental-inspect")]
use crate::py_expr::PyExpr;
use crate::pyfunction::{ConstructorAttribute, FunctionSignature};
#[cfg(feature = "experimental-inspect")]
use crate::pyimpl::method_introspection_code;
use crate::pyimpl::{gen_py_const, get_cfg_attributes, PyClassMethodsType};
#[cfg(feature = "experimental-inspect")]
use crate::pymethod::field_python_name;
use crate::pymethod::{
    impl_py_class_attribute, impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef,
    MethodAndSlotDef, PropertyType, SlotDef, __GETITEM__, __HASH__, __INT__, __LEN__, __NEW__,
    __REPR__, __RICHCMP__, __STR__,
};
use crate::utils::{self, apply_renaming_rule, get_doc, locate_tokens_at, Ctx, PythonDoc};
use crate::PyFunctionOptions;

/// If the class is derived from a Rust `struct` or `enum`.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PyClassKind {
    Struct,
    Enum,
}

/// The parsed arguments of the pyclass macro
#[derive(Clone)]
pub struct PyClassArgs {
    pub class_kind: PyClassKind,
    pub options: PyClassPyO3Options,
}

impl PyClassArgs {
    fn parse(input: ParseStream<'_>, kind: PyClassKind) -> Result<Self> {
        Ok(PyClassArgs {
            class_kind: kind,
            options: PyClassPyO3Options::parse(input)?,
        })
    }

    pub fn parse_struct_args(input: ParseStream<'_>) -> syn::Result<Self> {
        Self::parse(input, PyClassKind::Struct)
    }

    pub fn parse_enum_args(input: ParseStream<'_>) -> syn::Result<Self> {
        Self::parse(input, PyClassKind::Enum)
    }
}

#[derive(Clone, Default)]
pub struct PyClassPyO3Options {
    pub krate: Option<CrateAttribute>,
    pub dict: Option<kw::dict>,
    pub eq: Option<kw::eq>,
    pub eq_int: Option<kw::eq_int>,
    pub extends: Option<ExtendsAttribute>,
    pub get_all: Option<kw::get_all>,
    pub freelist: Option<FreelistAttribute>,
    pub frozen: Option<kw::frozen>,
    pub hash: Option<kw::hash>,
    pub immutable_type: Option<kw::immutable_type>,
    pub mapping: Option<kw::mapping>,
    pub module: Option<ModuleAttribute>,
    pub name: Option<NameAttribute>,
    pub ord: Option<kw::ord>,
    pub rename_all: Option<RenameAllAttribute>,
    pub sequence: Option<kw::sequence>,
    pub set_all: Option<kw::set_all>,
    pub new: Option<NewImplTypeAttribute>,
    pub str: Option<StrFormatterAttribute>,
    pub subclass: Option<kw::subclass>,
    pub unsendable: Option<kw::unsendable>,
    pub weakref: Option<kw::weakref>,
    pub generic: Option<kw::generic>,
    pub from_py_object: Option<kw::from_py_object>,
    pub skip_from_py_object: Option<kw::skip_from_py_object>,
}

pub enum PyClassPyO3Option {
    Crate(CrateAttribute),
    Dict(kw::dict),
    Eq(kw::eq),
    EqInt(kw::eq_int),
    Extends(ExtendsAttribute),
    Freelist(FreelistAttribute),
    Frozen(kw::frozen),
    GetAll(kw::get_all),
    Hash(kw::hash),
    ImmutableType(kw::immutable_type),
    Mapping(kw::mapping),
    Module(ModuleAttribute),
    Name(NameAttribute),
    Ord(kw::ord),
    RenameAll(RenameAllAttribute),
    Sequence(kw::sequence),
    SetAll(kw::set_all),
    New(NewImplTypeAttribute),
    Str(StrFormatterAttribute),
    Subclass(kw::subclass),
    Unsendable(kw::unsendable),
    Weakref(kw::weakref),
    Generic(kw::generic),
    FromPyObject(kw::from_py_object),
    SkipFromPyObject(kw::skip_from_py_object),
}

impl Parse for PyClassPyO3Option {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        let lookahead = input.lookahead1();
        if lookahead.peek(Token![crate]) {
            input.parse().map(PyClassPyO3Option::Crate)
        } else if lookahead.peek(kw::dict) {
            input.parse().map(PyClassPyO3Option::Dict)
        } else if lookahead.peek(kw::eq) {
            input.parse().map(PyClassPyO3Option::Eq)
        } else if lookahead.peek(kw::eq_int) {
            input.parse().map(PyClassPyO3Option::EqInt)
        } else if lookahead.peek(kw::extends) {
            input.parse().map(PyClassPyO3Option::Extends)
        } else if lookahead.peek(attributes::kw::freelist) {
            input.parse().map(PyClassPyO3Option::Freelist)
        } else if lookahead.peek(attributes::kw::frozen) {
            input.parse().map(PyClassPyO3Option::Frozen)
        } else if lookahead.peek(attributes::kw::get_all) {
            input.parse().map(PyClassPyO3Option::GetAll)
        } else if lookahead.peek(attributes::kw::hash) {
            input.parse().map(PyClassPyO3Option::Hash)
        } else if lookahead.peek(attributes::kw::immutable_type) {
            input.parse().map(PyClassPyO3Option::ImmutableType)
        } else if lookahead.peek(attributes::kw::mapping) {
            input.parse().map(PyClassPyO3Option::Mapping)
        } else if lookahead.peek(attributes::kw::module) {
            input.parse().map(PyClassPyO3Option::Module)
        } else if lookahead.peek(kw::name) {
            input.parse().map(PyClassPyO3Option::Name)
        } else if lookahead.peek(attributes::kw::ord) {
            input.parse().map(PyClassPyO3Option::Ord)
        } else if lookahead.peek(kw::rename_all) {
            input.parse().map(PyClassPyO3Option::RenameAll)
        } else if lookahead.peek(attributes::kw::sequence) {
            input.parse().map(PyClassPyO3Option::Sequence)
        } else if lookahead.peek(attributes::kw::set_all) {
            input.parse().map(PyClassPyO3Option::SetAll)
        } else if lookahead.peek(attributes::kw::new) {
            input.parse().map(PyClassPyO3Option::New)
        } else if lookahead.peek(attributes::kw::str) {
            input.parse().map(PyClassPyO3Option::Str)
        } else if lookahead.peek(attributes::kw::subclass) {
            input.parse().map(PyClassPyO3Option::Subclass)
        } else if lookahead.peek(attributes::kw::unsendable) {
            input.parse().map(PyClassPyO3Option::Unsendable)
        } else if lookahead.peek(attributes::kw::weakref) {
            input.parse().map(PyClassPyO3Option::Weakref)
        } else if lookahead.peek(attributes::kw::generic) {
            input.parse().map(PyClassPyO3Option::Generic)
        } else if lookahead.peek(attributes::kw::from_py_object) {
            input.parse().map(PyClassPyO3Option::FromPyObject)
        } else if lookahead.peek(attributes::kw::skip_from_py_object) {
            input.parse().map(PyClassPyO3Option::SkipFromPyObject)
        } else {
            Err(lookahead.error())
        }
    }
}

impl Parse for PyClassPyO3Options {
    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
        let mut options: PyClassPyO3Options = Default::default();

        for option in Punctuated::<PyClassPyO3Option, syn::Token![,]>::parse_terminated(input)? {
            options.set_option(option)?;
        }

        Ok(options)
    }
}

impl PyClassPyO3Options {
    pub fn take_pyo3_options(&mut self, attrs: &mut Vec<syn::Attribute>) -> syn::Result<()> {
        take_pyo3_options(attrs)?
            .into_iter()
            .try_for_each(|option| self.set_option(option))
    }

    fn set_option(&mut self, option: PyClassPyO3Option) -> syn::Result<()> {
        macro_rules! set_option {
            ($key:ident) => {
                {
                    ensure_spanned!(
                        self.$key.is_none(),
                        $key.span() => concat!("`", stringify!($key), "` may only be specified once")
                    );
                    self.$key = Some($key);
                }
            };
        }

        match option {
            PyClassPyO3Option::Crate(krate) => set_option!(krate),
            PyClassPyO3Option::Dict(dict) => set_option!(dict),
            PyClassPyO3Option::Eq(eq) => set_option!(eq),
            PyClassPyO3Option::EqInt(eq_int) => set_option!(eq_int),
            PyClassPyO3Option::Extends(extends) => set_option!(extends),
            PyClassPyO3Option::Freelist(freelist) => set_option!(freelist),
            PyClassPyO3Option::Frozen(frozen) => set_option!(frozen),
            PyClassPyO3Option::GetAll(get_all) => set_option!(get_all),
            PyClassPyO3Option::ImmutableType(immutable_type) => {
                set_option!(immutable_type)
            }
            PyClassPyO3Option::Hash(hash) => set_option!(hash),
            PyClassPyO3Option::Mapping(mapping) => set_option!(mapping),
            PyClassPyO3Option::Module(module) => set_option!(module),
            PyClassPyO3Option::Name(name) => set_option!(name),
            PyClassPyO3Option::Ord(ord) => set_option!(ord),
            PyClassPyO3Option::RenameAll(rename_all) => set_option!(rename_all),
            PyClassPyO3Option::Sequence(sequence) => set_option!(sequence),
            PyClassPyO3Option::SetAll(set_all) => set_option!(set_all),
            PyClassPyO3Option::New(new) => set_option!(new),
            PyClassPyO3Option::Str(str) => set_option!(str),
            PyClassPyO3Option::Subclass(subclass) => set_option!(subclass),
            PyClassPyO3Option::Unsendable(unsendable) => set_option!(unsendable),
            PyClassPyO3Option::Weakref(weakref) => set_option!(weakref),
            PyClassPyO3Option::Generic(generic) => set_option!(generic),
            PyClassPyO3Option::SkipFromPyObject(skip_from_py_object) => {
                ensure_spanned!(
                    self.from_py_object.is_none(),
                    skip_from_py_object.span() => "`skip_from_py_object` and `from_py_object` are mutually exclusive"
                );
                set_option!(skip_from_py_object)
            }
            PyClassPyO3Option::FromPyObject(from_py_object) => {
                ensure_spanned!(
                    self.skip_from_py_object.is_none(),
                    from_py_object.span() => "`skip_from_py_object` and `from_py_object` are mutually exclusive"
                );
                set_option!(from_py_object)
            }
        }
        Ok(())
    }
}

pub fn build_py_class(
    class: &mut syn::ItemStruct,
    mut args: PyClassArgs,
    methods_type: PyClassMethodsType,
) -> syn::Result<TokenStream> {
    args.options.take_pyo3_options(&mut class.attrs)?;

    let ctx = &Ctx::new(&args.options.krate, None);
    let doc = utils::get_doc(&class.attrs, None);

    if let Some(lt) = class.generics.lifetimes().next() {
        bail_spanned!(
            lt.span() => concat!(
                "#[pyclass] cannot have lifetime parameters. For an explanation, see \
                https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html#no-lifetime-parameters"
            )
        );
    }

    ensure_spanned!(
        class.generics.params.is_empty(),
        class.generics.span() => concat!(
            "#[pyclass] cannot have generic parameters. For an explanation, see \
            https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html#no-generic-parameters"
        )
    );

    let mut field_options: Vec<(&syn::Field, FieldPyO3Options)> = match &mut class.fields {
        syn::Fields::Named(fields) => fields
            .named
            .iter_mut()
            .map(
                |field| match FieldPyO3Options::take_pyo3_options(&mut field.attrs) {
                    Ok(options) => Ok((&*field, options)),
                    Err(e) => Err(e),
                },
            )
            .collect::<Vec<_>>(),
        syn::Fields::Unnamed(fields) => fields
            .unnamed
            .iter_mut()
            .map(
                |field| match FieldPyO3Options::take_pyo3_options(&mut field.attrs) {
                    Ok(options) => Ok((&*field, options)),
                    Err(e) => Err(e),
                },
            )
            .collect::<Vec<_>>(),
        syn::Fields::Unit => {
            let mut results = Vec::new();

            if let Some(attr) = args.options.set_all {
                results.push(Err(syn::Error::new_spanned(attr, UNIT_SET)));
            };
            if let Some(attr) = args.options.get_all {
                results.push(Err(syn::Error::new_spanned(attr, UNIT_GET)));
            };

            results
        }
    }
    .into_iter()
    .try_combine_syn_errors()?;

    if let Some(attr) = args.options.get_all {
        for (_, FieldPyO3Options { get, .. }) in &mut field_options {
            if let Some(old_get) = get.replace(Annotated::Struct(attr)) {
                return Err(syn::Error::new(old_get.span(), DUPE_GET));
            }
        }
    }

    if let Some(attr) = args.options.set_all {
        for (_, FieldPyO3Options { set, .. }) in &mut field_options {
            if let Some(old_set) = set.replace(Annotated::Struct(attr)) {
                return Err(syn::Error::new(old_set.span(), DUPE_SET));
            }
        }
    }

    impl_class(&class.ident, &args, doc, field_options, methods_type, ctx)
}

enum Annotated<X, Y> {
    Field(X),
    Struct(Y),
}

impl<X: Spanned, Y: Spanned> Annotated<X, Y> {
    fn span(&self) -> Span {
        match self {
            Self::Field(x) => x.span(),
            Self::Struct(y) => y.span(),
        }
    }
}

/// `#[pyo3()]` options for pyclass fields
struct FieldPyO3Options {
    get: Option<Annotated<kw::get, kw::get_all>>,
    set: Option<Annotated<kw::set, kw::set_all>>,
    name: Option<NameAttribute>,
}

enum FieldPyO3Option {
    Get(attributes::kw::get),
    Set(attributes::kw::set),
    Name(NameAttribute),
}

impl Parse for FieldPyO3Option {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        let lookahead = input.lookahead1();
        if lookahead.peek(attributes::kw::get) {
            input.parse().map(FieldPyO3Option::Get)
        } else if lookahead.peek(attributes::kw::set) {
            input.parse().map(FieldPyO3Option::Set)
        } else if lookahead.peek(attributes::kw::name) {
            input.parse().map(FieldPyO3Option::Name)
        } else {
            Err(lookahead.error())
        }
    }
}

impl FieldPyO3Options {
    fn take_pyo3_options(attrs: &mut Vec<syn::Attribute>) -> Result<Self> {
        let mut options = FieldPyO3Options {
            get: None,
            set: None,
            name: None,
        };

        for option in take_pyo3_options(attrs)? {
            match option {
                FieldPyO3Option::Get(kw) => {
                    if options.get.replace(Annotated::Field(kw)).is_some() {
                        return Err(syn::Error::new(kw.span(), UNIQUE_GET));
                    }
                }
                FieldPyO3Option::Set(kw) => {
                    if options.set.replace(Annotated::Field(kw)).is_some() {
                        return Err(syn::Error::new(kw.span(), UNIQUE_SET));
                    }
                }
                FieldPyO3Option::Name(name) => {
                    if options.name.replace(name).is_some() {
                        return Err(syn::Error::new(options.name.span(), UNIQUE_NAME));
                    }
                }
            }
        }

        Ok(options)
    }
}

fn get_class_python_name<'a>(cls: &'a Ident, args: &'a PyClassArgs) -> Cow<'a, Ident> {
    args.options
        .name
        .as_ref()
        .map(|name_attr| Cow::Borrowed(&name_attr.value.0))
        .unwrap_or_else(|| Cow::Owned(cls.unraw()))
}

#[cfg(feature = "experimental-inspect")]
fn get_class_type_hint(cls: &Ident, args: &PyClassArgs, ctx: &Ctx) -> TokenStream {
    let pyo3_path = &ctx.pyo3_path;
    let name = get_class_python_name(cls, args).to_string();
    if let Some(module) = &args.options.module {
        let module = module.value.value();
        quote! {
            #pyo3_path::inspect::PyStaticExpr::PyClass(#pyo3_path::inspect::PyClassNameStaticExpr::new(
                &#pyo3_path::inspect::PyStaticExpr::Attribute {
                    value: &#pyo3_path::inspect::PyStaticExpr::Name {
                        id: #module
                    },
                    attr: #name
                },
                Self::_PYO3_INTROSPECTION_ID
            ))
        }
    } else {
        quote! {
            #pyo3_path::inspect::PyStaticExpr::PyClass(#pyo3_path::inspect::PyClassNameStaticExpr::new(
                &#pyo3_path::inspect::PyStaticExpr::Name {
                    id: #name
                },
                Self::_PYO3_INTROSPECTION_ID
            ))
        }
    }
}

fn impl_class(
    cls: &Ident,
    args: &PyClassArgs,
    doc: Option<PythonDoc>,
    field_options: Vec<(&syn::Field, FieldPyO3Options)>,
    methods_type: PyClassMethodsType,
    ctx: &Ctx,
) -> Result<TokenStream> {
    let Ctx { pyo3_path, .. } = ctx;
    let pytypeinfo_impl = impl_pytypeinfo(cls, args, ctx);

    if let Some(str) = &args.options.str {
        if str.value.is_some() {
            // check if any renaming is present
            let no_naming_conflict = field_options.iter().all(|x| x.1.name.is_none())
                & args.options.name.is_none()
                & args.options.rename_all.is_none();
            ensure_spanned!(no_naming_conflict, str.value.span() => "The format string syntax is incompatible with any renaming via `name` or `rename_all`");
        }
    }

    let (default_new, default_new_slot) = pyclass_new_impl(
        &args.options,
        &syn::parse_quote!(#cls),
        field_options.iter().map(|(f, _)| f),
        ctx,
    )?;

    let mut default_methods = descriptors_to_items(
        cls,
        args.options.rename_all.as_ref(),
        args.options.frozen,
        field_options,
        ctx,
    )?;

    let (default_class_getitem, default_class_getitem_method) =
        pyclass_class_getitem(&args.options, &syn::parse_quote!(#cls), ctx)?;

    if let Some(default_class_getitem_method) = default_class_getitem_method {
        default_methods.push(default_class_getitem_method);
    }

    let (default_str, default_str_slot) =
        implement_pyclass_str(&args.options, &syn::parse_quote!(#cls), ctx);

    let (default_richcmp, default_richcmp_slot) =
        pyclass_richcmp(&args.options, &syn::parse_quote!(#cls), ctx)?;

    let (default_hash, default_hash_slot) =
        pyclass_hash(&args.options, &syn::parse_quote!(#cls), ctx)?;

    let mut slots = Vec::new();
    slots.extend(default_richcmp_slot);
    slots.extend(default_hash_slot);
    slots.extend(default_str_slot);
    slots.extend(default_new_slot);

    let mut impl_builder =
        PyClassImplsBuilder::new(cls, cls, args, methods_type, default_methods, slots);
    if let Some(doc) = doc {
        impl_builder = impl_builder.doc(doc);
    }
    let py_class_impl: TokenStream = [
        impl_builder.impl_pyclass(ctx),
        impl_builder.impl_into_py(ctx),
        impl_builder.impl_pyclassimpl(ctx)?,
        impl_builder.impl_add_to_module(ctx),
        impl_builder.impl_freelist(ctx),
        impl_builder.impl_introspection(ctx, None),
    ]
    .into_iter()
    .collect();

    Ok(quote! {
        impl #pyo3_path::types::DerefToPyAny for #cls {}

        #pytypeinfo_impl

        #py_class_impl

        #[doc(hidden)]
        #[allow(non_snake_case)]
        impl #cls {
            #default_richcmp
            #default_hash
            #default_str
            #default_new
            #default_class_getitem
        }
    })
}

enum PyClassEnum<'a> {
    Simple(PyClassSimpleEnum<'a>),
    Complex(PyClassComplexEnum<'a>),
}

impl<'a> PyClassEnum<'a> {
    fn new(enum_: &'a mut syn::ItemEnum) -> syn::Result<Self> {
        let has_only_unit_variants = enum_
            .variants
            .iter()
            .all(|variant| matches!(variant.fields, syn::Fields::Unit));

        Ok(if has_only_unit_variants {
            let simple_enum = PyClassSimpleEnum::new(enum_)?;
            Self::Simple(simple_enum)
        } else {
            let complex_enum = PyClassComplexEnum::new(enum_)?;
            Self::Complex(complex_enum)
        })
    }
}

pub fn build_py_enum(
    enum_: &mut syn::ItemEnum,
    mut args: PyClassArgs,
    method_type: PyClassMethodsType,
) -> syn::Result<TokenStream> {
    args.options.take_pyo3_options(&mut enum_.attrs)?;

    let ctx = &Ctx::new(&args.options.krate, None);
    if let Some(extends) = &args.options.extends {
        bail_spanned!(extends.span() => "enums can't extend from other classes");
    } else if let Some(subclass) = &args.options.subclass {
        bail_spanned!(subclass.span() => "enums can't be inherited by other classes");
    } else if enum_.variants.is_empty() {
        bail_spanned!(enum_.brace_token.span.join() => "#[pyclass] can't be used on enums without any variants");
    }

    if let Some(generic) = &args.options.generic {
        bail_spanned!(generic.span() => "enums do not support #[pyclass(generic)]");
    }

    let doc = utils::get_doc(&enum_.attrs, None);
    let enum_ = PyClassEnum::new(enum_)?;
    impl_enum(enum_, &args, doc, method_type, ctx)
}

struct PyClassSimpleEnum<'a> {
    ident: &'a syn::Ident,
    // The underlying #[repr] of the enum, used to implement __int__ and __richcmp__.
    // This matters when the underlying representation may not fit in `isize`.
    repr_type: syn::Ident,
    variants: Vec<PyClassEnumUnitVariant<'a>>,
}

impl<'a> PyClassSimpleEnum<'a> {
    fn new(enum_: &'a mut syn::ItemEnum) -> syn::Result<Self> {
        fn is_numeric_type(t: &syn::Ident) -> bool {
            [
                "u8", "i8", "u16", "i16", "u32", "i32", "u64", "i64", "u128", "i128", "usize",
                "isize",
            ]
            .iter()
            .any(|&s| t == s)
        }

        fn extract_unit_variant_data(
            variant: &mut syn::Variant,
        ) -> syn::Result<PyClassEnumUnitVariant<'_>> {
            use syn::Fields;
            let ident = match &variant.fields {
                Fields::Unit => &variant.ident,
                _ => bail_spanned!(variant.span() => "Must be a unit variant."),
            };
            let options = EnumVariantPyO3Options::take_pyo3_options(&mut variant.attrs)?;
            Ok(PyClassEnumUnitVariant {
                ident,
                options,
                attrs: variant.attrs.clone(),
            })
        }

        let ident = &enum_.ident;

        // According to the [reference](https://doc.rust-lang.org/reference/items/enumerations.html),
        // "Under the default representation, the specified discriminant is interpreted as an isize
        // value", so `isize` should be enough by default.
        let mut repr_type = syn::Ident::new("isize", proc_macro2::Span::call_site());
        if let Some(attr) = enum_.attrs.iter().find(|attr| attr.path().is_ident("repr")) {
            let args =
                attr.parse_args_with(Punctuated::<TokenStream, Token![!]>::parse_terminated)?;
            if let Some(ident) = args
                .into_iter()
                .filter_map(|ts| syn::parse2::<syn::Ident>(ts).ok())
                .find(is_numeric_type)
            {
                repr_type = ident;
            }
        }

        let variants: Vec<_> = enum_
            .variants
            .iter_mut()
            .map(extract_unit_variant_data)
            .collect::<syn::Result<_>>()?;
        Ok(Self {
            ident,
            repr_type,
            variants,
        })
    }
}

struct PyClassComplexEnum<'a> {
    ident: &'a syn::Ident,
    variants: Vec<PyClassEnumVariant<'a>>,
}

impl<'a> PyClassComplexEnum<'a> {
    fn new(enum_: &'a mut syn::ItemEnum) -> syn::Result<Self> {
        let witness = enum_
            .variants
            .iter()
            .find(|variant| !matches!(variant.fields, syn::Fields::Unit))
            .expect("complex enum has a non-unit variant")
            .ident
            .to_owned();

        let extract_variant_data =
            |variant: &'a mut syn::Variant| -> syn::Result<PyClassEnumVariant<'a>> {
                use syn::Fields;
                let ident = &variant.ident;
                let options = EnumVariantPyO3Options::take_pyo3_options(&mut variant.attrs)?;

                let variant = match &variant.fields {
                    Fields::Unit => {
                        bail_spanned!(variant.span() => format!(
                            "Unit variant `{ident}` is not yet supported in a complex enum\n\
                            = help: change to an empty tuple variant instead: `{ident}()`\n\
                            = note: the enum is complex because of non-unit variant `{witness}`",
                            ident=ident, witness=witness))
                    }
                    Fields::Named(fields) => {
                        let fields = fields
                            .named
                            .iter()
                            .map(|field| PyClassEnumVariantNamedField {
                                ident: field.ident.as_ref().expect("named field has an identifier"),
                                ty: &field.ty,
                                attrs: &field.attrs,
                                span: field.span(),
                            })
                            .collect();

                        PyClassEnumVariant::Struct(PyClassEnumStructVariant {
                            ident,
                            fields,
                            options,
                            attrs: variant.attrs.clone(),
                        })
                    }
                    Fields::Unnamed(types) => {
                        let fields = types
                            .unnamed
                            .iter()
                            .map(|field| PyClassEnumVariantUnnamedField {
                                ty: &field.ty,
                                span: field.span(),
                                attrs: &field.attrs,
                            })
                            .collect();

                        PyClassEnumVariant::Tuple(PyClassEnumTupleVariant {
                            ident,
                            fields,
                            options,
                            attrs: variant.attrs.clone(),
                        })
                    }
                };

                Ok(variant)
            };

        let ident = &enum_.ident;

        let variants: Vec<_> = enum_
            .variants
            .iter_mut()
            .map(extract_variant_data)
            .collect::<syn::Result<_>>()?;

        Ok(Self { ident, variants })
    }
}

enum PyClassEnumVariant<'a> {
    // TODO(mkovaxx): Unit(PyClassEnumUnitVariant<'a>),
    Struct(PyClassEnumStructVariant<'a>),
    Tuple(PyClassEnumTupleVariant<'a>),
}

trait EnumVariant {
    fn get_ident(&self) -> &syn::Ident;
    fn get_options(&self) -> &EnumVariantPyO3Options;
    fn get_attrs(&self) -> &[syn::Attribute];

    fn get_python_name(&self, args: &PyClassArgs) -> Cow<'_, syn::Ident> {
        self.get_options()
            .name
            .as_ref()
            .map(|name_attr| Cow::Borrowed(&name_attr.value.0))
            .unwrap_or_else(|| {
                let name = self.get_ident().unraw();
                if let Some(attr) = &args.options.rename_all {
                    let new_name = apply_renaming_rule(attr.value.rule, &name.to_string());
                    Cow::Owned(Ident::new(&new_name, Span::call_site()))
                } else {
                    Cow::Owned(name)
                }
            })
    }
}

impl EnumVariant for PyClassEnumVariant<'_> {
    fn get_ident(&self) -> &syn::Ident {
        match self {
            Self::Struct(struct_variant) => struct_variant.ident,
            Self::Tuple(tuple_variant) => tuple_variant.ident,
        }
    }

    fn get_options(&self) -> &EnumVariantPyO3Options {
        match self {
            Self::Struct(struct_variant) => &struct_variant.options,
            Self::Tuple(tuple_variant) => &tuple_variant.options,
        }
    }

    fn get_attrs(&self) -> &[syn::Attribute] {
        match self {
            Self::Struct(struct_variant) => &struct_variant.attrs,
            Self::Tuple(tuple_variant) => &tuple_variant.attrs,
        }
    }
}

/// A unit variant has no fields
struct PyClassEnumUnitVariant<'a> {
    ident: &'a syn::Ident,
    options: EnumVariantPyO3Options,
    attrs: Vec<syn::Attribute>,
}

impl EnumVariant for PyClassEnumUnitVariant<'_> {
    fn get_ident(&self) -> &syn::Ident {
        self.ident
    }

    fn get_options(&self) -> &EnumVariantPyO3Options {
        &self.options
    }

    fn get_attrs(&self) -> &[syn::Attribute] {
        &self.attrs
    }
}

/// A struct variant has named fields
struct PyClassEnumStructVariant<'a> {
    ident: &'a syn::Ident,
    fields: Vec<PyClassEnumVariantNamedField<'a>>,
    options: EnumVariantPyO3Options,
    attrs: Vec<syn::Attribute>,
}

impl PyClassEnumStructVariant<'_> {
    fn python_name(&self) -> Cow<'_, syn::Ident> {
        self.options
            .name
            .as_ref()
            .map(|name_attr| Cow::Borrowed(&name_attr.value.0))
            .unwrap_or_else(|| Cow::Owned(self.ident.unraw()))
    }
}

struct PyClassEnumTupleVariant<'a> {
    ident: &'a syn::Ident,
    fields: Vec<PyClassEnumVariantUnnamedField<'a>>,
    options: EnumVariantPyO3Options,
    attrs: Vec<syn::Attribute>,
}

impl PyClassEnumTupleVariant<'_> {
    fn python_name(&self) -> Cow<'_, syn::Ident> {
        self.options
            .name
            .as_ref()
            .map(|name_attr| Cow::Borrowed(&name_attr.value.0))
            .unwrap_or_else(|| Cow::Owned(self.ident.unraw()))
    }
}

struct PyClassEnumVariantNamedField<'a> {
    ident: &'a syn::Ident,
    ty: &'a syn::Type,
    attrs: &'a [syn::Attribute],
    span: Span,
}

struct PyClassEnumVariantUnnamedField<'a> {
    ty: &'a syn::Type,
    attrs: &'a [syn::Attribute],
    span: Span,
}

/// `#[pyo3()]` options for pyclass enum variants
#[derive(Clone, Default)]
struct EnumVariantPyO3Options {
    name: Option<NameAttribute>,
    constructor: Option<ConstructorAttribute>,
}

enum EnumVariantPyO3Option {
    Name(NameAttribute),
    Constructor(ConstructorAttribute),
}

impl Parse for EnumVariantPyO3Option {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        let lookahead = input.lookahead1();
        if lookahead.peek(attributes::kw::name) {
            input.parse().map(EnumVariantPyO3Option::Name)
        } else if lookahead.peek(attributes::kw::constructor) {
            input.parse().map(EnumVariantPyO3Option::Constructor)
        } else {
            Err(lookahead.error())
        }
    }
}

impl EnumVariantPyO3Options {
    fn take_pyo3_options(attrs: &mut Vec<syn::Attribute>) -> Result<Self> {
        let mut options = EnumVariantPyO3Options::default();

        take_pyo3_options(attrs)?
            .into_iter()
            .try_for_each(|option| options.set_option(option))?;

        Ok(options)
    }

    fn set_option(&mut self, option: EnumVariantPyO3Option) -> syn::Result<()> {
        macro_rules! set_option {
            ($key:ident) => {
                {
                    ensure_spanned!(
                        self.$key.is_none(),
                        $key.span() => concat!("`", stringify!($key), "` may only be specified once")
                    );
                    self.$key = Some($key);
                }
            };
        }

        match option {
            EnumVariantPyO3Option::Constructor(constructor) => set_option!(constructor),
            EnumVariantPyO3Option::Name(name) => set_option!(name),
        }
        Ok(())
    }
}

// todo(remove this dead code allowance once __repr__ is implemented
#[allow(dead_code)]
pub enum PyFmtName {
    Str,
    Repr,
}

fn implement_py_formatting(
    ty: &syn::Type,
    ctx: &Ctx,
    option: &StrFormatterAttribute,
) -> (ImplItemFn, MethodAndSlotDef) {
    let mut fmt_impl = match &option.value {
        Some(opt) => {
            let fmt = &opt.fmt;
            let args = &opt
                .args
                .iter()
                .map(|member| quote! {self.#member})
                .collect::<Vec<TokenStream>>();
            let fmt_impl: ImplItemFn = syn::parse_quote! {
                fn __pyo3__generated____str__(&self) -> ::std::string::String {
                    ::std::format!(#fmt, #(#args, )*)
                }
            };
            fmt_impl
        }
        None => {
            let fmt_impl: syn::ImplItemFn = syn::parse_quote! {
                fn __pyo3__generated____str__(&self) -> ::std::string::String {
                    ::std::format!("{}", &self)
                }
            };
            fmt_impl
        }
    };
    let fmt_slot = generate_protocol_slot(
        ty,
        &mut fmt_impl,
        &__STR__,
        "__str__",
        #[cfg(feature = "experimental-inspect")]
        FunctionIntrospectionData {
            names: &["__str__"],
            arguments: Vec::new(),
            returns: parse_quote! { ::std::string::String },
            is_returning_not_implemented_on_extraction_error: false,
        },
        ctx,
    )
    .unwrap();
    (fmt_impl, fmt_slot)
}

fn implement_pyclass_str(
    options: &PyClassPyO3Options,
    ty: &syn::Type,
    ctx: &Ctx,
) -> (Option<ImplItemFn>, Option<MethodAndSlotDef>) {
    match &options.str {
        Some(option) => {
            let (default_str, default_str_slot) = implement_py_formatting(ty, ctx, option);
            (Some(default_str), Some(default_str_slot))
        }
        _ => (None, None),
    }
}

fn impl_enum(
    enum_: PyClassEnum<'_>,
    args: &PyClassArgs,
    doc: Option<PythonDoc>,
    methods_type: PyClassMethodsType,
    ctx: &Ctx,
) -> Result<TokenStream> {
    if let Some(str_fmt) = &args.options.str {
        ensure_spanned!(str_fmt.value.is_none(), str_fmt.value.span() => "The format string syntax cannot be used with enums")
    }

    match enum_ {
        PyClassEnum::Simple(simple_enum) => {
            impl_simple_enum(simple_enum, args, doc, methods_type, ctx)
        }
        PyClassEnum::Complex(complex_enum) => {
            impl_complex_enum(complex_enum, args, doc, methods_type, ctx)
        }
    }
}

fn impl_simple_enum(
    simple_enum: PyClassSimpleEnum<'_>,
    args: &PyClassArgs,
    doc: Option<PythonDoc>,
    methods_type: PyClassMethodsType,
    ctx: &Ctx,
) -> Result<TokenStream> {
    let Ctx { pyo3_path, .. } = ctx;
    let cls = simple_enum.ident;
    let ty: syn::Type = syn::parse_quote!(#cls);
    let variants = simple_enum.variants;
    let pytypeinfo = impl_pytypeinfo(cls, args, ctx);

    for variant in &variants {
        ensure_spanned!(variant.options.constructor.is_none(), variant.options.constructor.span() => "`constructor` can't be used on a simple enum variant");
    }

    let variant_cfg_check = generate_cfg_check(&variants, cls);

    let (default_repr, default_repr_slot) = {
        let variants_repr = variants.iter().map(|variant| {
            let variant_name = variant.ident;
            let cfg_attrs = get_cfg_attributes(&variant.attrs);
            // Assuming all variants are unit variants because they are the only type we support.
            let repr = format!(
                "{}.{}",
                get_class_python_name(cls, args),
                variant.get_python_name(args),
            );
            quote! { #(#cfg_attrs)* #cls::#variant_name => #repr, }
        });
        let mut repr_impl: syn::ImplItemFn = syn::parse_quote! {
            fn __pyo3__repr__(&self) -> &'static str {
                match *self {
                    #(#variants_repr)*
                }
            }
        };
        let repr_slot = generate_default_protocol_slot(
            &ty,
            &mut repr_impl,
            &__REPR__,
            #[cfg(feature = "experimental-inspect")]
            FunctionIntrospectionData {
                names: &["__repr__"],
                arguments: Vec::new(),
                returns: parse_quote! { &'static str },
                is_returning_not_implemented_on_extraction_error: false,
            },
            ctx,
        )?;
        (repr_impl, repr_slot)
    };

    let (default_str, default_str_slot) = implement_pyclass_str(&args.options, &ty, ctx);

    let repr_type = &simple_enum.repr_type;

    let (default_int, default_int_slot) = {
        // This implementation allows us to convert &T to #repr_type without implementing `Copy`
        let variants_to_int = variants.iter().map(|variant| {
            let variant_name = variant.ident;
            let cfg_attrs = get_cfg_attributes(&variant.attrs);
            quote! { #(#cfg_attrs)* #cls::#variant_name => #cls::#variant_name as #repr_type, }
        });
        let mut int_impl: syn::ImplItemFn = syn::parse_quote! {
            fn __pyo3__int__(&self) -> #repr_type {
                match *self {
                    #(#variants_to_int)*
                }
            }
        };
        let int_slot = generate_default_protocol_slot(
            &ty,
            &mut int_impl,
            &__INT__,
            #[cfg(feature = "experimental-inspect")]
            FunctionIntrospectionData {
                names: &["__int__"],
                arguments: Vec::new(),
                returns: parse_quote!(#repr_type),
                is_returning_not_implemented_on_extraction_error: false,
            },
            ctx,
        )?;
        (int_impl, int_slot)
    };

    let (default_richcmp, default_richcmp_slot) =
        pyclass_richcmp_simple_enum(&args.options, &ty, repr_type, ctx)?;
    let (default_hash, default_hash_slot) = pyclass_hash(&args.options, &ty, ctx)?;

    let mut default_slots = vec![default_repr_slot, default_int_slot];
    default_slots.extend(default_richcmp_slot);
    default_slots.extend(default_hash_slot);
    default_slots.extend(default_str_slot);

    let mut impl_builder = PyClassImplsBuilder::new(
        cls,
        cls,
        args,
        methods_type,
        simple_enum_default_methods(
            cls,
            variants
                .iter()
                .map(|v| (v.ident, v.get_python_name(args), v.attrs.as_slice())),
            ctx,
        ),
        default_slots,
    );
    if let Some(doc) = doc {
        impl_builder = impl_builder.doc(doc);
    }

    let enum_into_pyobject_impl = {
        let output_type = get_conversion_type_hint(ctx, &format_ident!("OUTPUT_TYPE"), cls);

        let num = variants.len();
        let i = (0..num).map(proc_macro2::Literal::usize_unsuffixed);
        let variant_idents = variants.iter().map(|v| v.ident);
        let cfgs = variants.iter().map(|v| get_cfg_attributes(&v.attrs));
        quote! {
            impl<'py> #pyo3_path::conversion::IntoPyObject<'py> for #cls {
                type Target = Self;
                type Output = #pyo3_path::Bound<'py, Self>;
                type Error = #pyo3_path::PyErr;
                #output_type

                fn into_pyobject(self, py: #pyo3_path::Python<'py>) -> #pyo3_path::PyResult<#pyo3_path::Bound<'py, Self>> {
                    // TODO(icxolu): switch this to lookup the variants on the type object, once that is immutable
                    static SINGLETON: [#pyo3_path::sync::PyOnceLock<#pyo3_path::Py<#cls>>; #num] =
                        [const { #pyo3_path::sync::PyOnceLock::<#pyo3_path::Py<#cls>>::new() }; #num];
                    let idx: usize = match self {
                        #(
                            #(#cfgs)*
                            Self::#variant_idents => #i,
                        )*
                    };
                    #[allow(unreachable_code)] // May happen if all variants are disabled by cfg attributes
                    {
                        // Ensure that the type object is initialized before we attempt to initialize any of the singleton instances,
                        // to avoid deadlocking.
                        <Self as #pyo3_path::impl_::pyclass::PyClassImpl>::lazy_type_object().get_or_try_init(py)?;
                        SINGLETON[idx].get_or_try_init(py, || {
                            #pyo3_path::Py::new(py, self)
                        }).map(|obj| ::std::clone::Clone::clone(obj.bind(py)))
                    }
                }
            }
        }
    };

    let pyclass_impls: TokenStream = [
        impl_builder.impl_pyclass(ctx),
        enum_into_pyobject_impl,
        impl_builder.impl_pyclassimpl(ctx)?,
        impl_builder.impl_add_to_module(ctx),
        impl_builder.impl_freelist(ctx),
        impl_builder.impl_introspection(ctx, None),
    ]
    .into_iter()
    .collect();

    Ok(quote! {
        #variant_cfg_check

        #pytypeinfo

        #pyclass_impls

        #[doc(hidden)]
        #[allow(non_snake_case)]
        impl #cls {
            #default_repr
            #default_int
            #default_richcmp
            #default_hash
            #default_str
        }
    })
}

fn impl_complex_enum(
    complex_enum: PyClassComplexEnum<'_>,
    args: &PyClassArgs,
    doc: Option<PythonDoc>,
    methods_type: PyClassMethodsType,
    ctx: &Ctx,
) -> Result<TokenStream> {
    let Ctx { pyo3_path, .. } = ctx;
    let cls = complex_enum.ident;
    let ty: syn::Type = syn::parse_quote!(#cls);

    // Need to rig the enum PyClass options
    let args = {
        let mut rigged_args = args.clone();
        // Needs to be frozen to disallow `&mut self` methods, which could break a runtime invariant
        rigged_args.options.frozen = parse_quote!(frozen);
        // Needs to be subclassable by the variant PyClasses
        rigged_args.options.subclass = parse_quote!(subclass);
        rigged_args
    };

    let ctx = &Ctx::new(&args.options.krate, None);
    let cls = complex_enum.ident;
    let variants = complex_enum.variants;
    let pytypeinfo = impl_pytypeinfo(cls, &args, ctx);

    let (default_richcmp, default_richcmp_slot) = pyclass_richcmp(&args.options, &ty, ctx)?;
    let (default_hash, default_hash_slot) = pyclass_hash(&args.options, &ty, ctx)?;

    let (default_str, default_str_slot) = implement_pyclass_str(&args.options, &ty, ctx);

    let mut default_slots = vec![];
    default_slots.extend(default_richcmp_slot);
    default_slots.extend(default_hash_slot);
    default_slots.extend(default_str_slot);

    let mut impl_builder = PyClassImplsBuilder::new(
        cls,
        cls,
        &args,
        methods_type,
        complex_enum_default_methods(
            cls,
            variants
                .iter()
                .map(|v| (v.get_ident(), v.get_python_name(&args), v.get_attrs())),
            ctx,
        ),
        default_slots,
    );
    if let Some(doc) = doc {
        impl_builder = impl_builder.doc(doc);
    }

    let enum_into_pyobject_impl = {
        let match_arms = variants
            .iter()
            .map(|variant| {
                let variant_ident = variant.get_ident();
                let variant_cls = gen_complex_enum_variant_class_ident(cls, variant.get_ident());
                quote! {
                    #cls::#variant_ident { .. } => {
                        let pyclass_init = <#pyo3_path::PyClassInitializer<Self> as ::std::convert::From<Self>>::from(self).add_subclass(#variant_cls);
                        unsafe { #pyo3_path::Bound::new(py, pyclass_init).map(|b| b.cast_into_unchecked()) }
                    }
                }
            });
        let output_type = get_conversion_type_hint(ctx, &format_ident!("OUTPUT_TYPE"), cls);
        quote! {
            impl<'py> #pyo3_path::conversion::IntoPyObject<'py> for #cls {
                type Target = Self;
                type Output = #pyo3_path::Bound<'py, Self>;
                type Error = #pyo3_path::PyErr;
                #output_type

                fn into_pyobject(self, py: #pyo3_path::Python<'py>) -> #pyo3_path::PyResult<#pyo3_path::Bound<'py, Self>> {
                    match self {
                        #(#match_arms)*
                    }
                }
            }
        }
    };

    let pyclass_impls: TokenStream = [
        impl_builder.impl_pyclass(ctx),
        enum_into_pyobject_impl,
        impl_builder.impl_pyclassimpl(ctx)?,
        impl_builder.impl_add_to_module(ctx),
        impl_builder.impl_freelist(ctx),
        impl_builder.impl_introspection(ctx, None),
    ]
    .into_iter()
    .collect();

    let mut variant_cls_zsts = vec![];
    let mut variant_cls_pytypeinfos = vec![];
    let mut variant_cls_pyclass_impls = vec![];
    let mut variant_cls_impls = vec![];
    for variant in variants {
        let variant_name = variant.get_ident().clone();
        let variant_cls = gen_complex_enum_variant_class_ident(cls, &variant_name);

        let variant_cls_zst = quote! {
            #[doc(hidden)]
            #[allow(non_camel_case_types)]
            struct #variant_cls;
        };
        variant_cls_zsts.push(variant_cls_zst);

        let variant_args = PyClassArgs {
            class_kind: PyClassKind::Struct,
            // TODO(mkovaxx): propagate variant.options
            options: {
                let mut rigged_options: PyClassPyO3Options = parse_quote!(extends = #cls, frozen);
                // If a specific module was given to the base class, use it for all variants.
                rigged_options.module.clone_from(&args.options.module);
                rigged_options
            },
        };

        let variant_cls_pytypeinfo = impl_pytypeinfo(&variant_cls, &variant_args, ctx);
        variant_cls_pytypeinfos.push(variant_cls_pytypeinfo);

        let (variant_cls_impl, field_getters, mut slots) =
            impl_complex_enum_variant_cls(cls, &args, &variant, ctx)?;
        variant_cls_impls.push(variant_cls_impl);

        let variant_doc = get_doc(variant.get_attrs(), None);
        let variant_new = complex_enum_variant_new(cls, variant, ctx)?;
        slots.push(variant_new);

        let mut impl_builder = PyClassImplsBuilder::new(
            &variant_cls,
            &variant_name,
            &variant_args,
            methods_type,
            field_getters,
            slots,
        );
        if let Some(doc) = variant_doc {
            impl_builder = impl_builder.doc(doc);
        }

        let pyclass_impl: TokenStream = [
            impl_builder.impl_pyclass(ctx),
            impl_builder.impl_into_py(ctx),
            impl_builder.impl_pyclassimpl(ctx)?,
            impl_builder.impl_add_to_module(ctx),
            impl_builder.impl_freelist(ctx),
            impl_builder.impl_introspection(ctx, Some(cls)),
        ]
        .into_iter()
        .collect();

        variant_cls_pyclass_impls.push(pyclass_impl);
    }

    Ok(quote! {
        #pytypeinfo

        #pyclass_impls

        #[doc(hidden)]
        #[allow(non_snake_case)]
        impl #cls {
            #default_richcmp
            #default_hash
            #default_str
        }

        #(#variant_cls_zsts)*

        #(#variant_cls_pytypeinfos)*

        #(#variant_cls_pyclass_impls)*

        #(#variant_cls_impls)*
    })
}

fn impl_complex_enum_variant_cls(
    enum_name: &syn::Ident,
    args: &PyClassArgs,
    variant: &PyClassEnumVariant<'_>,
    ctx: &Ctx,
) -> Result<(TokenStream, Vec<MethodAndMethodDef>, Vec<MethodAndSlotDef>)> {
    match variant {
        PyClassEnumVariant::Struct(struct_variant) => {
            impl_complex_enum_struct_variant_cls(enum_name, args, struct_variant, ctx)
        }
        PyClassEnumVariant::Tuple(tuple_variant) => {
            impl_complex_enum_tuple_variant_cls(enum_name, args, tuple_variant, ctx)
        }
    }
}

fn impl_complex_enum_variant_match_args(
    ctx @ Ctx { pyo3_path, .. }: &Ctx,
    variant_cls_type: &syn::Type,
    field_names: &[Ident],
) -> syn::Result<(MethodAndMethodDef, syn::ImplItemFn)> {
    let ident = format_ident!("__match_args__");
    let field_names_unraw = field_names.iter().map(|name| name.unraw());
    let mut match_args_impl: syn::ImplItemFn = {
        parse_quote! {
            #[classattr]
            fn #ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::Bound<'_, #pyo3_path::types::PyTuple>> {
                #pyo3_path::types::PyTuple::new::<&str, _>(py, [
                    #(stringify!(#field_names_unraw),)*
                ])
            }
        }
    };

    let spec = FnSpec::parse(
        &mut match_args_impl.sig,
        &mut match_args_impl.attrs,
        Default::default(),
    )?;
    #[cfg_attr(not(feature = "experimental-inspect"), allow(unused_mut))]
    let mut variant_match_args = impl_py_class_attribute(variant_cls_type, &spec, ctx)?;
    #[cfg(feature = "experimental-inspect")]
    variant_match_args.add_introspection(attribute_introspection_code(
        pyo3_path,
        Some(variant_cls_type),
        "__match_args__".into(),
        PyExpr::tuple(
            field_names
                .iter()
                .map(|name| PyExpr::str_constant(name.unraw().to_string())),
        ),
        syn::Type::Tuple(syn::TypeTuple {
            attrs: Vec::new(),
            paren_token: syn::token::Paren::default(),
            elems: field_names
                .iter()
                .map(|_| {
                    let t: syn::Type = parse_quote!(&'static str);
                    t
                })
                .collect(),
        }),
        None,
        true,
    ));
    Ok((variant_match_args, match_args_impl))
}

fn impl_complex_enum_struct_variant_cls(
    enum_name: &syn::Ident,
    args: &PyClassArgs,
    variant: &PyClassEnumStructVariant<'_>,
    ctx: &Ctx,
) -> Result<(TokenStream, Vec<MethodAndMethodDef>, Vec<MethodAndSlotDef>)> {
    let Ctx { pyo3_path, .. } = ctx;
    let variant_ident = &variant.ident;
    let variant_cls = gen_complex_enum_variant_class_ident(enum_name, variant.ident);
    let variant_cls_type = parse_quote!(#variant_cls);

    let mut field_names: Vec<Ident> = vec![];
    let mut fields_with_types: Vec<TokenStream> = vec![];
    let mut field_getters = vec![];
    let mut field_getter_impls: Vec<TokenStream> = vec![];
    for field in &variant.fields {
        let field_name = field.ident;
        let field_type = field.ty;
        let field_with_type = quote! { #field_name: #field_type };

        let field_getter = complex_enum_variant_field_getter(
            &variant_cls_type,
            field_name,
            field_type,
            field.attrs,
            field.span,
            ctx,
        )?;

        let field_getter_impl = quote! {
            fn #field_name(slf: #pyo3_path::PyClassGuard<'_, Self>, py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::Py<#pyo3_path::PyAny>> {
                #[allow(unused_imports)]
                use #pyo3_path::impl_::pyclass::Probe as _;
                match &*slf.into_super() {
                    #enum_name::#variant_ident { #field_name, .. } =>
                        #pyo3_path::impl_::pyclass::ConvertField::<
                            { #pyo3_path::impl_::pyclass::IsIntoPyObjectRef::<#field_type>::VALUE }
                        >::convert_field::<#field_type>(#field_name, py),
                    _ => ::core::unreachable!("Wrong complex enum variant found in variant wrapper PyClass"),
                }
            }
        };

        field_names.push(field_name.clone());
        fields_with_types.push(field_with_type);
        field_getters.push(field_getter);
        field_getter_impls.push(field_getter_impl);
    }

    let (qualname, qualname_impl) = impl_complex_enum_variant_qualname(
        &get_class_python_name(enum_name, args),
        &variant.python_name(),
        &variant_cls_type,
        ctx,
    )?;

    field_getters.push(qualname);

    let (variant_match_args, match_args_const_impl) =
        impl_complex_enum_variant_match_args(ctx, &variant_cls_type, &field_names)?;

    field_getters.push(variant_match_args);

    let cls_impl = quote! {
        #[doc(hidden)]
        #[allow(non_snake_case)]
        impl #variant_cls {
            #[allow(clippy::too_many_arguments)]
            fn __pymethod_constructor__(py: #pyo3_path::Python<'_>, #(#fields_with_types,)*) -> #pyo3_path::PyClassInitializer<#variant_cls> {
                let base_value = #enum_name::#variant_ident { #(#field_names,)* };
                <#pyo3_path::PyClassInitializer<#enum_name> as ::std::convert::From<#enum_name>>::from(base_value).add_subclass(#variant_cls)
            }

            #match_args_const_impl

            #qualname_impl

            #(#field_getter_impls)*
        }
    };

    Ok((cls_impl, field_getters, Vec::new()))
}

fn impl_complex_enum_tuple_variant_field_getters(
    ctx: &Ctx,
    variant: &PyClassEnumTupleVariant<'_>,
    enum_name: &syn::Ident,
    variant_cls_type: &syn::Type,
    variant_ident: &&Ident,
    field_names: &mut Vec<Ident>,
    fields_types: &mut Vec<syn::Type>,
) -> Result<(Vec<MethodAndMethodDef>, Vec<syn::ImplItemFn>)> {
    let Ctx { pyo3_path, .. } = ctx;

    let mut field_getters = vec![];
    let mut field_getter_impls = vec![];

    for (index, field) in variant.fields.iter().enumerate() {
        let field_name = format_ident!("_{}", index);
        let field_type = field.ty;

        let field_getter = complex_enum_variant_field_getter(
            variant_cls_type,
            &field_name,
            field_type,
            field.attrs,
            field.span,
            ctx,
        )?;

        // Generate the match arms needed to destructure the tuple and access the specific field
        let field_access_tokens: Vec<_> = (0..variant.fields.len())
            .map(|i| {
                if i == index {
                    quote! { val }
                } else {
                    quote! { _ }
                }
            })
            .collect();
        let field_getter_impl: syn::ImplItemFn = parse_quote! {
            fn #field_name(slf: #pyo3_path::PyClassGuard<'_, Self>, py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::Py<#pyo3_path::PyAny>> {
                #[allow(unused_imports)]
                use #pyo3_path::impl_::pyclass::Probe as _;
                match &*slf.into_super() {
                    #enum_name::#variant_ident ( #(#field_access_tokens), *) =>
                        #pyo3_path::impl_::pyclass::ConvertField::<
                            { #pyo3_path::impl_::pyclass::IsIntoPyObjectRef::<#field_type>::VALUE }
                        >::convert_field::<#field_type>(val, py),
                    _ => ::core::unreachable!("Wrong complex enum variant found in variant wrapper PyClass"),
                }
            }
        };

        field_names.push(field_name);
        fields_types.push(field_type.clone());
        field_getters.push(field_getter);
        field_getter_impls.push(field_getter_impl);
    }

    Ok((field_getters, field_getter_impls))
}

fn impl_complex_enum_tuple_variant_len(
    ctx: &Ctx,

    variant_cls_type: &syn::Type,
    num_fields: usize,
) -> Result<(MethodAndSlotDef, syn::ImplItemFn)> {
    let Ctx { pyo3_path, .. } = ctx;

    let mut len_method_impl: syn::ImplItemFn = parse_quote! {
        fn __len__(slf: #pyo3_path::PyClassGuard<'_, Self>) -> #pyo3_path::PyResult<usize> {
            ::std::result::Result::Ok(#num_fields)
        }
    };

    let variant_len = generate_default_protocol_slot(
        variant_cls_type,
        &mut len_method_impl,
        &__LEN__,
        #[cfg(feature = "experimental-inspect")]
        FunctionIntrospectionData {
            names: &["__len__"],
            arguments: Vec::new(),
            returns: parse_quote! { ::std::primitive::usize },
            is_returning_not_implemented_on_extraction_error: false,
        },
        ctx,
    )?;

    Ok((variant_len, len_method_impl))
}

fn impl_complex_enum_tuple_variant_getitem(
    ctx: &Ctx,
    variant_cls: &syn::Ident,
    variant_cls_type: &syn::Type,
    num_fields: usize,
) -> Result<(MethodAndSlotDef, syn::ImplItemFn)> {
    let Ctx { pyo3_path, .. } = ctx;

    let match_arms: Vec<_> = (0..num_fields)
        .map(|i| {
            let field_access = format_ident!("_{}", i);
            quote! { #i =>
                #pyo3_path::IntoPyObjectExt::into_py_any(#variant_cls::#field_access(slf, py)?, py)
            }
        })
        .collect();

    let mut get_item_method_impl: syn::ImplItemFn = parse_quote! {
        fn __getitem__(slf: #pyo3_path::PyClassGuard<'_, Self>, py: #pyo3_path::Python<'_>, idx: usize) -> #pyo3_path::PyResult< #pyo3_path::Py<#pyo3_path::PyAny>> {
            match idx {
                #( #match_arms, )*
                _ => ::std::result::Result::Err(#pyo3_path::exceptions::PyIndexError::new_err("tuple index out of range")),
            }
        }
    };

    let variant_getitem = generate_default_protocol_slot(
        variant_cls_type,
        &mut get_item_method_impl,
        &__GETITEM__,
        #[cfg(feature = "experimental-inspect")]
        FunctionIntrospectionData {
            names: &["__getitem__"],
            arguments: vec![FnArg::Regular(RegularArg {
                name: Cow::Owned(Ident::new("key", variant_cls.span())),
                ty: &parse_quote! { ::std::primitive::usize },
                from_py_with: None,
                default_value: None,
                option_wrapped_type: None,
                annotation: None,
            })],
            returns: parse_quote! { #pyo3_path::Py<#pyo3_path::PyAny> }, // TODO: figure out correct type
            is_returning_not_implemented_on_extraction_error: false,
        },
        ctx,
    )?;

    Ok((variant_getitem, get_item_method_impl))
}

fn impl_complex_enum_tuple_variant_cls(
    enum_name: &syn::Ident,
    args: &PyClassArgs,
    variant: &PyClassEnumTupleVariant<'_>,
    ctx: &Ctx,
) -> Result<(TokenStream, Vec<MethodAndMethodDef>, Vec<MethodAndSlotDef>)> {
    let Ctx { pyo3_path, .. } = ctx;
    let variant_ident = &variant.ident;
    let variant_cls = gen_complex_enum_variant_class_ident(enum_name, variant.ident);
    let variant_cls_type = parse_quote!(#variant_cls);

    let mut slots = vec![];

    // represents the index of the field
    let mut field_names: Vec<Ident> = vec![];
    let mut field_types: Vec<syn::Type> = vec![];

    let (mut field_getters, field_getter_impls) = impl_complex_enum_tuple_variant_field_getters(
        ctx,
        variant,
        enum_name,
        &variant_cls_type,
        variant_ident,
        &mut field_names,
        &mut field_types,
    )?;

    let (qualname, qualname_impl) = impl_complex_enum_variant_qualname(
        &get_class_python_name(enum_name, args),
        &variant.python_name(),
        &variant_cls_type,
        ctx,
    )?;

    field_getters.push(qualname);

    let num_fields = variant.fields.len();

    let (variant_len, len_method_impl) =
        impl_complex_enum_tuple_variant_len(ctx, &variant_cls_type, num_fields)?;

    slots.push(variant_len);

    let (variant_getitem, getitem_method_impl) =
        impl_complex_enum_tuple_variant_getitem(ctx, &variant_cls, &variant_cls_type, num_fields)?;

    slots.push(variant_getitem);

    let (variant_match_args, match_args_method_impl) =
        impl_complex_enum_variant_match_args(ctx, &variant_cls_type, &field_names)?;

    field_getters.push(variant_match_args);

    let cls_impl = quote! {
        #[doc(hidden)]
        #[allow(non_snake_case)]
        impl #variant_cls {
            #[allow(clippy::too_many_arguments)]
            fn __pymethod_constructor__(py: #pyo3_path::Python<'_>, #(#field_names : #field_types,)*) -> #pyo3_path::PyClassInitializer<#variant_cls> {
                let base_value = #enum_name::#variant_ident ( #(#field_names,)* );
                <#pyo3_path::PyClassInitializer<#enum_name> as ::std::convert::From<#enum_name>>::from(base_value).add_subclass(#variant_cls)
            }

            #len_method_impl

            #getitem_method_impl

            #match_args_method_impl

            #qualname_impl

            #(#field_getter_impls)*
        }
    };

    Ok((cls_impl, field_getters, slots))
}

fn gen_complex_enum_variant_class_ident(enum_: &Ident, variant: &Ident) -> Ident {
    format_ident!("{}_{}", enum_, variant)
}

fn impl_complex_enum_variant_qualname(
    enum_name: &syn::Ident,
    variant_ident: &syn::Ident,
    variant_cls_type: &syn::Type,
    ctx: &Ctx,
) -> syn::Result<(MethodAndMethodDef, syn::ImplItemFn)> {
    let Ctx { pyo3_path, .. } = ctx;
    let qualname = format!("{}.{}", enum_name, variant_ident);
    let mut qualname_impl: syn::ImplItemFn = {
        parse_quote! {
            #[classattr]
            fn __qualname__(py: #pyo3_path::Python<'_>) -> &'static str {
                #qualname
            }
        }
    };

    let spec = FnSpec::parse(
        &mut qualname_impl.sig,
        &mut qualname_impl.attrs,
        Default::default(),
    )?;

    // NB: Deliberately add no introspection here, this is __qualname__
    let qualname = impl_py_class_attribute(variant_cls_type, &spec, ctx)?;
    Ok((qualname, qualname_impl))
}

#[cfg(feature = "experimental-inspect")]
struct FunctionIntrospectionData<'a> {
    names: &'a [&'a str],
    arguments: Vec<FnArg<'a>>,
    is_returning_not_implemented_on_extraction_error: bool,
    returns: syn::Type,
}

#[cfg(feature = "experimental-inspect")]
impl FunctionIntrospectionData<'_> {
    fn generate(self, ctx: &Ctx, cls: &syn::Type, slot: &SlotDef) -> TokenStream {
        let mut signature = FunctionSignature::from_arguments(self.arguments);
        if !slot.takes_args_and_kwargs() {
            signature
                .python_signature
                .make_all_parameters_positional_only();
        }
        self.names
            .iter()
            .flat_map(|name| {
                function_introspection_code(
                    &ctx.pyo3_path,
                    None,
                    name,
                    &signature,
                    Some("self"),
                    PyExpr::from_return_type(self.returns.clone(), Some(cls)),
                    [],
                    false,
                    self.is_returning_not_implemented_on_extraction_error,
                    None,
                    Some(cls),
                )
            })
            .collect()
    }
}

fn generate_protocol_slot(
    cls: &syn::Type,
    method: &mut syn::ImplItemFn,
    slot: &SlotDef,
    name: &str,
    #[cfg(feature = "experimental-inspect")] introspection_data: FunctionIntrospectionData<'_>,
    ctx: &Ctx,
) -> syn::Result<MethodAndSlotDef> {
    let spec = FnSpec::parse(
        &mut method.sig,
        &mut method.attrs,
        PyFunctionOptions::default(),
    )?;
    #[cfg_attr(not(feature = "experimental-inspect"), allow(unused_mut))]
    let mut def = slot.generate_type_slot(cls, &spec, name, ctx)?;
    #[cfg(feature = "experimental-inspect")]
    def.add_introspection(introspection_data.generate(ctx, cls, slot));
    Ok(def)
}

fn generate_default_protocol_slot(
    cls: &syn::Type,
    method: &mut syn::ImplItemFn,
    slot: &SlotDef,
    #[cfg(feature = "experimental-inspect")] introspection_data: FunctionIntrospectionData<'_>,
    ctx: &Ctx,
) -> syn::Result<MethodAndSlotDef> {
    let spec = FnSpec::parse(
        &mut method.sig,
        &mut Vec::new(),
        PyFunctionOptions::default(),
    )?;
    let name = spec.name.to_string();
    #[cfg_attr(not(feature = "experimental-inspect"), allow(unused_mut))]
    let mut def = slot.generate_type_slot(
        &syn::parse_quote!(#cls),
        &spec,
        &format!("__default_{name}__"),
        ctx,
    )?;
    #[cfg(feature = "experimental-inspect")]
    def.add_introspection(introspection_data.generate(ctx, cls, slot));
    Ok(def)
}

fn simple_enum_default_methods<'a>(
    cls: &'a syn::Ident,
    unit_variant_names: impl IntoIterator<
        Item = (&'a syn::Ident, Cow<'a, syn::Ident>, &'a [syn::Attribute]),
    >,
    ctx: &Ctx,
) -> Vec<MethodAndMethodDef> {
    let cls_type: syn::Type = syn::parse_quote!(#cls);
    unit_variant_names
        .into_iter()
        .map(|(var, py_name, attrs)| {
            let spec = ConstSpec {
                rust_ident: var.clone(),
                attributes: ConstAttributes {
                    is_class_attr: true,
                    name: Some(NameAttribute {
                        kw: syn::parse_quote! { name },
                        value: NameLitStr(py_name.into_owned()),
                    }),
                },
                #[cfg(feature = "experimental-inspect")]
                expr: None,
                #[cfg(feature = "experimental-inspect")]
                ty: cls_type.clone(),
                #[cfg(feature = "experimental-inspect")]
                doc: get_doc(attrs, None),
            };
            let method = gen_py_const(&cls_type, &spec, ctx);
            let associated_method_tokens = method.associated_method;
            let method_def_tokens = method.method_def;
            let cfg_attrs = get_cfg_attributes(attrs);

            let associated_method = quote! {
                #(#cfg_attrs)*
                #associated_method_tokens
            };
            let method_def = quote! {
                #(#cfg_attrs)*
                #method_def_tokens
            };

            MethodAndMethodDef {
                associated_method,
                method_def,
            }
        })
        .collect()
}

#[cfg_attr(not(feature = "experimental-inspect"), expect(unused_variables))]
fn complex_enum_default_methods<'a>(
    cls: &'a Ident,
    variant_names: impl IntoIterator<Item = (&'a Ident, Cow<'a, Ident>, &'a [syn::Attribute])>,
    ctx: &Ctx,
) -> Vec<MethodAndMethodDef> {
    let cls_type = syn::parse_quote!(#cls);
    variant_names
        .into_iter()
        .map(|(var, py_name, attrs)| {
            #[cfg(feature = "experimental-inspect")]
            let variant_cls = gen_complex_enum_variant_class_ident(cls, &py_name);
            let spec = ConstSpec {
                rust_ident: var.clone(),
                attributes: ConstAttributes {
                    is_class_attr: true,
                    name: Some(NameAttribute {
                        kw: syn::parse_quote! { name },
                        value: NameLitStr(py_name.into_owned()),
                    }),
                },
                #[cfg(feature = "experimental-inspect")]
                expr: None,
                #[cfg(feature = "experimental-inspect")]
                ty: parse_quote!(#variant_cls),
                #[cfg(feature = "experimental-inspect")]
                doc: get_doc(attrs, None),
            };
            gen_complex_enum_variant_attr(cls, &cls_type, &spec, ctx)
        })
        .collect()
}

pub fn gen_complex_enum_variant_attr(
    cls: &syn::Ident,
    cls_type: &syn::Type,
    spec: &ConstSpec,
    ctx: &Ctx,
) -> MethodAndMethodDef {
    let Ctx { pyo3_path, .. } = ctx;
    let member = &spec.rust_ident;
    let wrapper_ident = format_ident!("__pymethod_variant_cls_{}__", member);
    let python_name = spec.null_terminated_python_name();

    let variant_cls = gen_complex_enum_variant_class_ident(cls, member);
    let associated_method = quote! {
        fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::Py<#pyo3_path::PyAny>> {
            ::std::result::Result::Ok(py.get_type::<#variant_cls>().into_any().unbind())
        }
    };

    let method_def = quote! {
        #pyo3_path::impl_::pymethods::PyMethodDefType::ClassAttribute({
            #pyo3_path::impl_::pymethods::PyClassAttributeDef::new(
                #python_name,
                #cls_type::#wrapper_ident
            )
        })
    };

    MethodAndMethodDef {
        associated_method,
        method_def,
    }
}

fn complex_enum_variant_new<'a>(
    cls: &'a syn::Ident,
    variant: PyClassEnumVariant<'a>,
    ctx: &Ctx,
) -> Result<MethodAndSlotDef> {
    match variant {
        PyClassEnumVariant::Struct(struct_variant) => {
            complex_enum_struct_variant_new(cls, struct_variant, ctx)
        }
        PyClassEnumVariant::Tuple(tuple_variant) => {
            complex_enum_tuple_variant_new(cls, tuple_variant, ctx)
        }
    }
}

fn complex_enum_struct_variant_new<'a>(
    cls: &'a syn::Ident,
    variant: PyClassEnumStructVariant<'a>,
    ctx: &Ctx,
) -> Result<MethodAndSlotDef> {
    let Ctx { pyo3_path, .. } = ctx;
    let variant_cls = gen_complex_enum_variant_class_ident(cls, variant.ident);
    let variant_cls_type: syn::Type = parse_quote!(#variant_cls);

    let arg_py_ident: syn::Ident = parse_quote!(py);
    let arg_py_type: syn::Type = parse_quote!(#pyo3_path::Python<'_>);

    let args = {
        let mut args = vec![
            // py: Python<'_>
            FnArg::Py(PyArg {
                name: &arg_py_ident,
                ty: &arg_py_type,
            }),
        ];

        for field in &variant.fields {
            args.push(FnArg::Regular(RegularArg {
                name: Cow::Borrowed(field.ident),
                ty: field.ty,
                from_py_with: None,
                default_value: None,
                option_wrapped_type: None,
                #[cfg(feature = "experimental-inspect")]
                annotation: None,
            }));
        }
        args
    };

    let signature = if let Some(constructor) = variant.options.constructor {
        FunctionSignature::from_arguments_and_attribute(args, constructor.into_signature())?
    } else {
        FunctionSignature::from_arguments(args)
    };

    let spec = FnSpec {
        tp: crate::method::FnType::FnStatic,
        name: &format_ident!("__pymethod_constructor__"),
        python_name: format_ident!("__new__"),
        signature,
        text_signature: None,
        asyncness: None,
        unsafety: None,
        warnings: vec![],
        output: syn::ReturnType::Default,
    };

    #[cfg_attr(not(feature = "experimental-inspect"), allow(unused_mut))]
    let mut def =
        __NEW__.generate_type_slot(&variant_cls_type, &spec, "__default___new____", ctx)?;
    #[cfg(feature = "experimental-inspect")]
    def.add_introspection(method_introspection_code(
        &spec,
        &[],
        &variant_cls_type,
        false,
        ctx,
    ));
    Ok(def)
}

fn complex_enum_tuple_variant_new<'a>(
    cls: &'a syn::Ident,
    variant: PyClassEnumTupleVariant<'a>,
    ctx: &Ctx,
) -> Result<MethodAndSlotDef> {
    let Ctx { pyo3_path, .. } = ctx;

    let variant_cls = gen_complex_enum_variant_class_ident(cls, variant.ident);
    let variant_cls_type: syn::Type = parse_quote!(#variant_cls);

    let arg_py_ident: syn::Ident = parse_quote!(py);
    let arg_py_type: syn::Type = parse_quote!(#pyo3_path::Python<'_>);

    let args = {
        let mut args = vec![FnArg::Py(PyArg {
            name: &arg_py_ident,
            ty: &arg_py_type,
        })];

        for (i, field) in variant.fields.iter().enumerate() {
            args.push(FnArg::Regular(RegularArg {
                name: std::borrow::Cow::Owned(format_ident!("_{}", i)),
                ty: field.ty,
                from_py_with: None,
                default_value: None,
                option_wrapped_type: None,
                #[cfg(feature = "experimental-inspect")]
                annotation: None,
            }));
        }
        args
    };

    let signature = if let Some(constructor) = variant.options.constructor {
        FunctionSignature::from_arguments_and_attribute(args, constructor.into_signature())?
    } else {
        FunctionSignature::from_arguments(args)
    };

    let spec = FnSpec {
        tp: crate::method::FnType::FnStatic,
        name: &format_ident!("__pymethod_constructor__"),
        python_name: format_ident!("__new__"),
        signature,
        text_signature: None,
        asyncness: None,
        unsafety: None,
        warnings: vec![],
        output: syn::ReturnType::Default,
    };

    #[cfg_attr(not(feature = "experimental-inspect"), allow(unused_mut))]
    let mut def =
        __NEW__.generate_type_slot(&variant_cls_type, &spec, "__default___new____", ctx)?;
    #[cfg(feature = "experimental-inspect")]
    def.add_introspection(method_introspection_code(
        &spec,
        &[],
        &variant_cls_type,
        false,
        ctx,
    ));
    Ok(def)
}

fn complex_enum_variant_field_getter(
    variant_cls_type: &syn::Type,
    field_name: &Ident,
    field_type: &syn::Type,
    field_attrs: &[syn::Attribute],
    field_span: Span,
    ctx: &Ctx,
) -> Result<MethodAndMethodDef> {
    let mut arg = parse_quote!(py: Python<'_>);
    let py = FnArg::parse(&mut arg)?;
    let signature = FunctionSignature::from_arguments(vec![py]);

    let self_type = crate::method::SelfType::TryFromBoundRef {
        span: field_span,
        non_null: true,
    };

    let spec = FnSpec {
        tp: crate::method::FnType::Getter(self_type.clone()),
        name: field_name,
        python_name: field_name.unraw(),
        signature,
        text_signature: None,
        asyncness: None,
        unsafety: None,
        warnings: vec![],
        output: parse_quote!(-> #field_type),
    };

    let property_type = PropertyType::Function {
        self_type: &self_type,
        spec: &spec,
        doc: get_doc(field_attrs, None),
    };

    #[cfg_attr(not(feature = "experimental-inspect"), allow(unused_mut))]
    let mut getter = impl_py_getter_def(variant_cls_type, property_type, ctx)?;
    #[cfg(feature = "experimental-inspect")]
    getter.add_introspection(method_introspection_code(
        &spec,
        field_attrs,
        variant_cls_type,
        false,
        ctx,
    ));
    Ok(getter)
}

fn descriptors_to_items(
    cls: &Ident,
    rename_all: Option<&RenameAllAttribute>,
    frozen: Option<frozen>,
    field_options: Vec<(&syn::Field, FieldPyO3Options)>,
    ctx: &Ctx,
) -> Result<Vec<MethodAndMethodDef>> {
    let ty = syn::parse_quote!(#cls);
    let mut items = Vec::new();
    for (field_index, (field, options)) in field_options.into_iter().enumerate() {
        if let FieldPyO3Options {
            name: Some(name),
            get: None,
            set: None,
        } = options
        {
            return Err(syn::Error::new_spanned(name, USELESS_NAME));
        }

        if options.get.is_some() {
            let renaming_rule = rename_all.map(|rename_all| rename_all.value.rule);
            #[cfg_attr(not(feature = "experimental-inspect"), allow(unused_mut))]
            let mut getter = impl_py_getter_def(
                &ty,
                PropertyType::Descriptor {
                    field_index,
                    field,
                    python_name: options.name.as_ref(),
                    renaming_rule,
                },
                ctx,
            )?;
            #[cfg(feature = "experimental-inspect")]
            {
                // We generate introspection data
                let parent = parse_quote!(#cls);
                let return_type = field.ty.clone();
                getter.add_introspection(function_introspection_code(
                    &ctx.pyo3_path,
                    None,
                    &field_python_name(field, options.name.as_ref(), renaming_rule)?,
                    &FunctionSignature::from_arguments(vec![]),
                    Some("self"),
                    PyExpr::from_into_py_object_maybe_ref(return_type, Some(&parent)),
                    vec![PyExpr::builtin("property")],
                    false,
                    false,
                    get_doc(&field.attrs, None).as_ref(),
                    Some(&parent),
                ));
            }
            items.push(getter);
        }

        if let Some(set) = options.set {
            ensure_spanned!(frozen.is_none(), set.span() => "cannot use `#[pyo3(set)]` on a `frozen` class");
            let renaming_rule = rename_all.map(|rename_all| rename_all.value.rule);
            #[cfg_attr(not(feature = "experimental-inspect"), allow(unused_mut))]
            let mut setter = impl_py_setter_def(
                &ty,
                PropertyType::Descriptor {
                    field_index,
                    field,
                    python_name: options.name.as_ref(),
                    renaming_rule,
                },
                ctx,
            )?;
            #[cfg(feature = "experimental-inspect")]
            {
                // We generate introspection data
                let name = field_python_name(field, options.name.as_ref(), renaming_rule)?;
                setter.add_introspection(function_introspection_code(
                    &ctx.pyo3_path,
                    None,
                    &name,
                    &FunctionSignature::from_arguments(vec![FnArg::Regular(RegularArg {
                        name: Cow::Owned(format_ident!("value")),
                        ty: &field.ty,
                        from_py_with: None,
                        default_value: None,
                        option_wrapped_type: None,
                        annotation: None,
                    })]),
                    Some("self"),
                    PyExpr::none(),
                    vec![PyExpr::attribute(
                        PyExpr::attribute(
                            PyExpr::from_type(
                                syn::Type::Path(syn::TypePath {
                                    attrs: Vec::new(),
                                    qself: None,
                                    path: cls.clone().into(),
                                }),
                                None,
                            ),
                            name.clone(),
                        ),
                        "setter",
                    )],
                    false,
                    false,
                    get_doc(&field.attrs, None).as_ref(),
                    Some(&parse_quote!(#cls)),
                ));
            }
            items.push(setter);
        };
    }
    Ok(items)
}

fn impl_pytypeinfo(cls: &Ident, attr: &PyClassArgs, ctx: &Ctx) -> TokenStream {
    let Ctx { pyo3_path, .. } = ctx;

    #[cfg(feature = "experimental-inspect")]
    let type_hint = {
        let type_hint = get_class_type_hint(cls, attr, ctx);
        quote! { const TYPE_HINT: #pyo3_path::inspect::PyStaticExpr = #type_hint; }
    };
    #[cfg(not(feature = "experimental-inspect"))]
    let type_hint = quote! {};
    #[cfg(not(feature = "experimental-inspect"))]
    let _ = attr;

    quote! {
        unsafe impl #pyo3_path::type_object::PyTypeInfo for #cls {

            const NAME: &str = <Self as #pyo3_path::PyClass>::NAME;
            const MODULE: ::std::option::Option<&str> = <Self as #pyo3_path::impl_::pyclass::PyClassImpl>::MODULE;

            #type_hint

            #[inline]
            fn type_object_raw(py: #pyo3_path::Python<'_>) -> *mut #pyo3_path::ffi::PyTypeObject {
                #pyo3_path::impl_::pyclass::pyclass_type_object_raw::<Self>(py)
            }
        }
    }
}

fn pyclass_richcmp_arms(
    options: &PyClassPyO3Options,
    ctx: &Ctx,
) -> std::result::Result<TokenStream, syn::Error> {
    let Ctx { pyo3_path, .. } = ctx;

    let eq_arms = options
        .eq
        .map(|eq| eq.span)
        .or(options.eq_int.map(|eq_int| eq_int.span))
        .map(|span| {
            quote_spanned! { span =>
                #pyo3_path::pyclass::CompareOp::Eq => {
                    #pyo3_path::IntoPyObjectExt::into_py_any(self_val == other, py)
                },
                #pyo3_path::pyclass::CompareOp::Ne => {
                    #pyo3_path::IntoPyObjectExt::into_py_any(self_val != other, py)
                },
            }
        })
        .unwrap_or_default();

    if let Some(ord) = options.ord {
        ensure_spanned!(options.eq.is_some(), ord.span() => "The `ord` option requires the `eq` option.");
    }

    let ord_arms = options
        .ord
        .map(|ord| {
            quote_spanned! { ord.span() =>
                #pyo3_path::pyclass::CompareOp::Gt => {
                    #pyo3_path::IntoPyObjectExt::into_py_any(self_val > other, py)
                },
                #pyo3_path::pyclass::CompareOp::Lt => {
                    #pyo3_path::IntoPyObjectExt::into_py_any(self_val < other, py)
                 },
                #pyo3_path::pyclass::CompareOp::Le => {
                    #pyo3_path::IntoPyObjectExt::into_py_any(self_val <= other, py)
                 },
                #pyo3_path::pyclass::CompareOp::Ge => {
                    #pyo3_path::IntoPyObjectExt::into_py_any(self_val >= other, py)
                 },
            }
        })
        .unwrap_or_else(|| quote! { _ => ::std::result::Result::Ok(py.NotImplemented()) });

    Ok(quote! {
        #eq_arms
        #ord_arms
    })
}

fn pyclass_richcmp_simple_enum(
    options: &PyClassPyO3Options,
    cls: &syn::Type,
    repr_type: &syn::Ident,
    ctx: &Ctx,
) -> Result<(Option<syn::ImplItemFn>, Option<MethodAndSlotDef>)> {
    let Ctx { pyo3_path, .. } = ctx;
    if let Some(eq_int) = options.eq_int {
        ensure_spanned!(options.eq.is_some(), eq_int.span() => "The `eq_int` option requires the `eq` option.");
    }

    if options.eq.is_none() && options.eq_int.is_none() {
        return Ok((None, None));
    }

    let arms = pyclass_richcmp_arms(options, ctx)?;

    let eq = options.eq.map(|eq| {
        quote_spanned! { eq.span() =>
            let self_val = self;
            if let ::std::result::Result::Ok(other) = #pyo3_path::types::PyAnyMethods::extract::<#pyo3_path::PyClassGuard<'_, Self>>(other) {
                let other = &*other;
                return match op {
                    #arms
                }
            }
        }
    });

    let eq_int = options.eq_int.map(|eq_int| {
        quote_spanned! { eq_int.span() =>
            let self_val = self.__pyo3__int__();
            if let ::std::result::Result::Ok(other) = #pyo3_path::types::PyAnyMethods::extract::<#repr_type>(other).or_else(|_| {
                #pyo3_path::types::PyAnyMethods::extract::<#pyo3_path::PyClassGuard<'_, Self>>(other).map(|o| o.__pyo3__int__())
            }) {
                return match op {
                    #arms
                }
            }
        }
    });

    let mut richcmp_impl = parse_quote! {
        fn __pyo3__generated____richcmp__(
            &self,
            py: #pyo3_path::Python,
            other: &#pyo3_path::Bound<'_, #pyo3_path::PyAny>,
            op: #pyo3_path::pyclass::CompareOp
        ) -> #pyo3_path::PyResult<#pyo3_path::Py<#pyo3_path::PyAny>> {
            #eq

            #eq_int

            ::std::result::Result::Ok(py.NotImplemented())
        }
    };
    #[cfg(feature = "experimental-inspect")]
    let any = parse_quote!(#pyo3_path::Py<#pyo3_path::PyAny>);
    #[cfg(feature = "experimental-inspect")]
    let introspection = FunctionIntrospectionData {
        names: &["__eq__", "__ne__"],
        // `value` is the parameter name CPython uses in the `tp_richcompare` slot wrappers
        arguments: vec![FnArg::Regular(RegularArg {
            name: Cow::Owned(format_ident!("value")),
            ty: &any,
            from_py_with: None,
            default_value: None,
            option_wrapped_type: None,
            annotation: None,
        })],
        returns: parse_quote!(::std::primitive::bool),
        is_returning_not_implemented_on_extraction_error: true,
    };
    let richcmp_slot = if options.eq.is_some() {
        generate_protocol_slot(
            cls,
            &mut richcmp_impl,
            &__RICHCMP__,
            "__richcmp__",
            #[cfg(feature = "experimental-inspect")]
            introspection,
            ctx,
        )?
    } else {
        generate_default_protocol_slot(
            cls,
            &mut richcmp_impl,
            &__RICHCMP__,
            #[cfg(feature = "experimental-inspect")]
            introspection,
            ctx,
        )?
    };
    Ok((Some(richcmp_impl), Some(richcmp_slot)))
}

fn pyclass_richcmp(
    options: &PyClassPyO3Options,
    cls: &syn::Type,
    ctx: &Ctx,
) -> Result<(Option<syn::ImplItemFn>, Option<MethodAndSlotDef>)> {
    let Ctx { pyo3_path, .. } = ctx;
    if let Some(eq_int) = options.eq_int {
        bail_spanned!(eq_int.span() => "`eq_int` can only be used on simple enums.")
    }

    let arms = pyclass_richcmp_arms(options, ctx)?;
    if options.eq.is_some() {
        let mut richcmp_impl = parse_quote! {
            fn __pyo3__generated____richcmp__(
                &self,
                py: #pyo3_path::Python,
                other: &#pyo3_path::Bound<'_, #pyo3_path::PyAny>,
                op: #pyo3_path::pyclass::CompareOp
            ) -> #pyo3_path::PyResult<#pyo3_path::Py<#pyo3_path::PyAny>> {
                let self_val = self;
                if let ::std::result::Result::Ok(other) = #pyo3_path::types::PyAnyMethods::extract::<#pyo3_path::PyClassGuard<'_, Self>>(other) {
                    let other = &*other;
                    match op {
                        #arms
                    }
                } else {
                    ::std::result::Result::Ok(py.NotImplemented())
                }
            }
        };
        let richcmp_slot = generate_protocol_slot(
            cls,
            &mut richcmp_impl,
            &__RICHCMP__,
            "__richcmp__",
            #[cfg(feature = "experimental-inspect")]
            FunctionIntrospectionData {
                names: if options.ord.is_some() {
                    &["__eq__", "__ne__", "__lt__", "__le__", "__gt__", "__ge__"]
                } else {
                    &["__eq__", "__ne__"]
                },
                // `value` is the parameter name CPython uses in the `tp_richcompare` slot wrappers
                arguments: vec![FnArg::Regular(RegularArg {
                    name: Cow::Owned(format_ident!("value")),
                    ty: &parse_quote!(&#cls),
                    from_py_with: None,
                    default_value: None,
                    option_wrapped_type: None,
                    annotation: None,
                })],
                returns: parse_quote! { ::std::primitive::bool },
                is_returning_not_implemented_on_extraction_error: true,
            },
            ctx,
        )?;
        Ok((Some(richcmp_impl), Some(richcmp_slot)))
    } else {
        Ok((None, None))
    }
}

fn pyclass_hash(
    options: &PyClassPyO3Options,
    cls: &syn::Type,
    ctx: &Ctx,
) -> Result<(Option<syn::ImplItemFn>, Option<MethodAndSlotDef>)> {
    if options.hash.is_some() {
        ensure_spanned!(
            options.frozen.is_some(), options.hash.span() => "The `hash` option requires the `frozen` option.";
            options.eq.is_some(), options.hash.span() => "The `hash` option requires the `eq` option.";
        );
    }
    match options.hash {
        Some(opt) => {
            let mut hash_impl = parse_quote_spanned! { opt.span() =>
                fn __pyo3__generated____hash__(&self) -> u64 {
                    let mut s = ::std::collections::hash_map::DefaultHasher::new();
                    ::std::hash::Hash::hash(self, &mut s);
                    ::std::hash::Hasher::finish(&s)
                }
            };
            let hash_slot = generate_protocol_slot(
                cls,
                &mut hash_impl,
                &__HASH__,
                "__hash__",
                #[cfg(feature = "experimental-inspect")]
                FunctionIntrospectionData {
                    names: &["__hash__"],
                    arguments: Vec::new(),
                    returns: parse_quote! { ::std::primitive::u64 },
                    is_returning_not_implemented_on_extraction_error: false,
                },
                ctx,
            )?;
            Ok((Some(hash_impl), Some(hash_slot)))
        }
        None => Ok((None, None)),
    }
}

fn pyclass_new_impl<'a>(
    options: &PyClassPyO3Options,
    ty: &syn::Type,
    fields: impl Iterator<Item = &'a &'a syn::Field>,
    ctx: &Ctx,
) -> Result<(Option<ImplItemFn>, Option<MethodAndSlotDef>)> {
    if options
        .new
        .as_ref()
        .is_some_and(|o| matches!(o.value, NewImplTypeAttributeValue::FromFields))
    {
        ensure_spanned!(
            options.extends.is_none(), options.new.span() => "The `new=\"from_fields\"` option cannot be used with `extends`.";
        );
    }

    let mut tuple_struct: bool = false;

    match &options.new {
        Some(opt) => {
            let mut field_idents = vec![];
            let mut field_types = vec![];
            for (idx, field) in fields.enumerate() {
                tuple_struct = field.ident.is_none();

                field_idents.push(
                    field
                        .ident
                        .clone()
                        .unwrap_or_else(|| format_ident!("_{}", idx)),
                );
                field_types.push(&field.ty);
            }

            let mut new_impl = if tuple_struct {
                parse_quote_spanned! { opt.span() =>
                    #[new]
                    fn __pyo3_generated____new__( #( #field_idents : #field_types ),* ) -> Self {
                        Self (
                            #( #field_idents, )*
                        )
                    }
                }
            } else {
                parse_quote_spanned! { opt.span() =>
                    #[new]
                    fn __pyo3_generated____new__( #( #field_idents : #field_types ),* ) -> Self {
                        Self {
                            #( #field_idents, )*
                        }
                    }
                }
            };

            let new_slot = generate_protocol_slot(
                ty,
                &mut new_impl,
                &__NEW__,
                "__new__",
                #[cfg(feature = "experimental-inspect")]
                FunctionIntrospectionData {
                    names: &["__new__"],
                    arguments: field_idents
                        .iter()
                        .zip(field_types.iter())
                        .map(|(ident, ty)| {
                            FnArg::Regular(RegularArg {
                                name: Cow::Owned(ident.clone()),
                                ty,
                                from_py_with: None,
                                default_value: None,
                                option_wrapped_type: None,
                                annotation: None,
                            })
                        })
                        .collect(),
                    returns: ty.clone(),
                    is_returning_not_implemented_on_extraction_error: false,
                },
                ctx,
            )
            .unwrap();

            Ok((Some(new_impl), Some(new_slot)))
        }
        None => Ok((None, None)),
    }
}

fn pyclass_class_getitem(
    options: &PyClassPyO3Options,
    cls: &syn::Type,
    ctx: &Ctx,
) -> Result<(Option<syn::ImplItemFn>, Option<MethodAndMethodDef>)> {
    let Ctx { pyo3_path, .. } = ctx;
    match options.generic {
        Some(_) => {
            let ident = format_ident!("__class_getitem__");
            let mut class_getitem_impl: syn::ImplItemFn = {
                parse_quote! {
                    #[classmethod]
                    fn #ident<'py>(
                        cls: &#pyo3_path::Bound<'py, #pyo3_path::types::PyType>,
                        key: &#pyo3_path::Bound<'py, #pyo3_path::types::PyAny>
                    ) -> #pyo3_path::PyResult<#pyo3_path::Bound<'py, #pyo3_path::types::PyGenericAlias>> {
                        #pyo3_path::types::PyGenericAlias::new(cls.py(), cls.as_any(), key)
                    }
                }
            };

            let spec = FnSpec::parse(
                &mut class_getitem_impl.sig,
                &mut class_getitem_impl.attrs,
                Default::default(),
            )?;

            let class_getitem_method = crate::pymethod::impl_py_method_def(
                cls,
                &spec,
                spec.get_doc(&class_getitem_impl.attrs).as_ref(),
                ctx,
            )?;
            Ok((Some(class_getitem_impl), Some(class_getitem_method)))
        }
        None => Ok((None, None)),
    }
}

/// Implements most traits used by `#[pyclass]`.
///
/// Specifically, it implements traits that only depend on class name,
/// and attributes of `#[pyclass]`, and docstrings.
/// Therefore it doesn't implement traits that depends on struct fields and enum variants.
struct PyClassImplsBuilder<'a> {
    /// Identifier of the class Rust struct
    cls_ident: &'a Ident,
    /// Name of the class in Python
    cls_name: &'a Ident,
    attr: &'a PyClassArgs,
    methods_type: PyClassMethodsType,
    default_methods: Vec<MethodAndMethodDef>,
    default_slots: Vec<MethodAndSlotDef>,
    doc: Option<PythonDoc>,
}

impl<'a> PyClassImplsBuilder<'a> {
    fn new(
        cls_ident: &'a Ident,
        cls_name: &'a Ident,
        attr: &'a PyClassArgs,
        methods_type: PyClassMethodsType,
        default_methods: Vec<MethodAndMethodDef>,
        default_slots: Vec<MethodAndSlotDef>,
    ) -> Self {
        Self {
            cls_ident,
            cls_name,
            attr,
            methods_type,
            default_methods,
            default_slots,
            doc: None,
        }
    }

    fn doc(self, doc: PythonDoc) -> Self {
        Self {
            doc: Some(doc),
            ..self
        }
    }

    fn impl_pyclass(&self, ctx: &Ctx) -> TokenStream {
        let Ctx { pyo3_path, .. } = ctx;
        let cls = self.cls_ident;

        let cls_name = get_class_python_name(self.cls_name, self.attr).to_string();

        let frozen = if self.attr.options.frozen.is_some() {
            quote! { #pyo3_path::pyclass::boolean_struct::True }
        } else {
            quote! { #pyo3_path::pyclass::boolean_struct::False }
        };

        quote! {
            impl #pyo3_path::PyClass for #cls {
                const NAME: &str = #cls_name;
                type Frozen = #frozen;
            }
        }
    }

    fn impl_into_py(&self, ctx: &Ctx) -> TokenStream {
        let Ctx { pyo3_path, .. } = ctx;
        let cls = self.cls_ident;
        let attr = self.attr;
        // If #cls is not extended type, we allow Self->PyObject conversion
        if attr.options.extends.is_none() {
            let output_type = get_conversion_type_hint(ctx, &format_ident!("OUTPUT_TYPE"), cls);
            quote! {
                impl<'py> #pyo3_path::conversion::IntoPyObject<'py> for #cls {
                    type Target = Self;
                    type Output = #pyo3_path::Bound<'py, Self>;
                    type Error = #pyo3_path::PyErr;
                    #output_type

                    fn into_pyobject(self, py: #pyo3_path::Python<'py>) -> #pyo3_path::PyResult<#pyo3_path::Bound<'py, Self>> {
                        #pyo3_path::Bound::new(py, self)
                    }
                }
            }
        } else {
            quote! {}
        }
    }
    fn impl_pyclassimpl(&self, ctx: &Ctx) -> Result<TokenStream> {
        let Ctx { pyo3_path, .. } = ctx;
        let cls = self.cls_ident;
        // `RAW_DOC` defaults to `c""` in the trait definition; only emit it when there is a
        // docstring.
        let raw_doc = match &self.doc {
            Some(doc) => {
                let doc = doc.to_cstr_stream(ctx)?;
                Some(quote! { const RAW_DOC: &'static ::std::ffi::CStr = #doc; })
            }
            None => None,
        };

        // All of these consts have defaults in the `PyClassImpl` trait definition; only emit
        // them when they differ from the default, to keep the generated code small.
        let module = self
            .attr
            .options
            .module
            .as_ref()
            .map(|ModuleAttribute { value, .. }| {
                quote! { const MODULE: ::std::option::Option<&str> = ::core::option::Option::Some(#value); }
            });

        let is_basetype = self.attr.options.subclass.is_some();
        let base = match &self.attr.options.extends {
            Some(extends_attr) => extends_attr.value.clone(),
            None => parse_quote! { #pyo3_path::PyAny },
        };
        let is_subclass = self.attr.options.extends.is_some();
        let is_mapping: bool = self.attr.options.mapping.is_some();
        let is_sequence: bool = self.attr.options.sequence.is_some();
        let is_immutable_type = self.attr.options.immutable_type.is_some();

        ensure_spanned!(
            !(is_mapping && is_sequence),
            cls.span() => "a `#[pyclass]` cannot be both a `mapping` and a `sequence`"
        );

        let is_basetype = is_basetype.then(|| quote! { const IS_BASETYPE: bool = true; });
        let is_subclass_const = is_subclass.then(|| quote! { const IS_SUBCLASS: bool = true; });
        let is_mapping = is_mapping.then(|| quote! { const IS_MAPPING: bool = true; });
        let is_sequence = is_sequence.then(|| quote! { const IS_SEQUENCE: bool = true; });
        let is_immutable_type =
            is_immutable_type.then(|| quote! { const IS_IMMUTABLE_TYPE: bool = true; });

        let dict_offset = if self.attr.options.dict.is_some() {
            quote! {
                fn dict_offset() -> ::std::option::Option<#pyo3_path::impl_::pyclass::PyObjectOffset> {
                    ::std::option::Option::Some(#pyo3_path::impl_::pyclass::dict_offset::<Self>())
                }
            }
        } else {
            TokenStream::new()
        };

        let weaklist_offset = if self.attr.options.weakref.is_some() {
            quote! {
                fn weaklist_offset() -> ::std::option::Option<#pyo3_path::impl_::pyclass::PyObjectOffset> {
                    ::std::option::Option::Some(#pyo3_path::impl_::pyclass::weaklist_offset::<Self>())
                }
            }
        } else {
            TokenStream::new()
        };

        let thread_checker = if self.attr.options.unsendable.is_some() {
            quote! { #pyo3_path::impl_::pyclass::ThreadCheckerImpl }
        } else {
            quote! { #pyo3_path::impl_::pyclass::NoopThreadChecker }
        };

        let (pymethods_items, inventory, inventory_class) = match self.methods_type {
            PyClassMethodsType::Specialization => (
                quote! {{
                    use #pyo3_path::impl_::pyclass::PyMethods as _;
                    #pyo3_path::impl_::pyclass::PyClassImplCollector::<Self>::new().py_methods()
                }},
                None,
                None,
            ),
            PyClassMethodsType::Inventory => {
                // To allow multiple #[pymethods] block, we define inventory types.
                let inventory_class_name = syn::Ident::new(
                    &format!("Pyo3MethodsInventoryFor{}", cls.unraw()),
                    Span::call_site(),
                );
                (
                    quote! {
                        ::std::boxed::Box::new(
                            ::std::iter::Iterator::map(
                                #pyo3_path::inventory::iter::<<Self as #pyo3_path::impl_::pyclass::PyClassImpl>::Inventory>(),
                                #pyo3_path::impl_::pyclass::PyClassInventory::items
                            )
                        )
                    },
                    Some(quote! { type Inventory = #inventory_class_name; }),
                    Some(define_inventory_class(&inventory_class_name, ctx)),
                )
            }
        };

        let default_methods = self
            .default_methods
            .iter()
            .map(|meth| &meth.associated_method)
            .chain(
                self.default_slots
                    .iter()
                    .map(|meth| &meth.associated_method),
            )
            .collect::<Vec<_>>();
        // Skip emitting the associated-methods impl block entirely when it would be empty.
        let default_methods_impl = (!default_methods.is_empty()).then(|| {
            quote! {
                #[doc(hidden)]
                #[allow(non_snake_case)]
                impl #cls {
                    #(#default_methods)*
                }
            }
        });

        let default_method_defs = self
            .default_methods
            .iter()
            .map(|meth| &meth.method_def)
            .collect::<Vec<_>>();
        let default_slot_defs = self
            .default_slots
            .iter()
            .map(|slot| &slot.slot_def)
            .collect::<Vec<_>>();
        let freelist_slots = self.freelist_slots(ctx);

        // When there are no intrinsic items, point at a shared empty `PyClassItems` to keep the
        // generated code small.
        let items_iter = if default_method_defs.is_empty()
            && default_slot_defs.is_empty()
            && freelist_slots.is_empty()
        {
            quote! {
                fn items_iter() -> #pyo3_path::impl_::pyclass::PyClassItemsIter {
                    #pyo3_path::impl_::pyclass::PyClassItemsIter::new(
                        &#pyo3_path::impl_::pyclass::NO_PY_CLASS_ITEMS,
                        #pymethods_items,
                    )
                }
            }
        } else {
            quote! {
                fn items_iter() -> #pyo3_path::impl_::pyclass::PyClassItemsIter {
                    static INTRINSIC_ITEMS: #pyo3_path::impl_::pyclass::PyClassItems = #pyo3_path::impl_::pyclass::PyClassItems {
                        methods: &[#(#default_method_defs),*],
                        slots: &[#(#default_slot_defs),* #(#freelist_slots),*],
                    };
                    #pyo3_path::impl_::pyclass::PyClassItemsIter::new(&INTRINSIC_ITEMS, #pymethods_items)
                }
            }
        };

        let class_mutability = if self.attr.options.frozen.is_some() {
            quote! {
                ImmutableChild
            }
        } else {
            quote! {
                MutableChild
            }
        };

        let attr = self.attr;
        let dict = if attr.options.dict.is_some() {
            quote! { #pyo3_path::impl_::pyclass::PyClassDictSlot }
        } else {
            quote! { #pyo3_path::impl_::pyclass::PyClassDummySlot }
        };

        // insert space for weak ref
        let weakref = if attr.options.weakref.is_some() {
            quote! { #pyo3_path::impl_::pyclass::PyClassWeakRefSlot }
        } else {
            quote! { #pyo3_path::impl_::pyclass::PyClassDummySlot }
        };

        let base_nativetype = if attr.options.extends.is_some() {
            quote! { <Self::BaseType as #pyo3_path::impl_::pyclass::PyClassBaseType>::BaseNativeType }
        } else {
            quote! { #pyo3_path::PyAny }
        };

        let pyclass_base_type_impl = attr.options.subclass.map(|subclass| {
            quote_spanned! { subclass.span() =>
                impl #pyo3_path::impl_::pyclass::PyClassBaseType for #cls {
                    type LayoutAsBase = <Self as #pyo3_path::impl_::pyclass::PyClassImpl>::Layout;
                    type BaseNativeType = <Self as #pyo3_path::impl_::pyclass::PyClassImpl>::BaseNativeType;
                    type Initializer = #pyo3_path::pyclass_init::PyClassInitializer<Self>;
                    type PyClassMutability = <Self as #pyo3_path::impl_::pyclass::PyClassImpl>::PyClassMutability;
                    type Layout<T: #pyo3_path::impl_::pyclass::PyClassImpl> = <Self::BaseNativeType as #pyo3_path::impl_::pyclass::PyClassBaseType>::Layout<T>;
                }
            }
        });

        let mut assertions = TokenStream::new();

        // Classes must implement send / sync, unless `#[pyclass(unsendable)]` is used
        if attr.options.unsendable.is_none() {
            let pyo3_path = locate_tokens_at(pyo3_path.to_token_stream(), cls.span());
            assertions.extend(quote_spanned! { cls.span() => #pyo3_path::impl_::pyclass::assert_pyclass_send_sync::<#cls>(); });
        };

        if let Some(kw) = &attr.options.immutable_type {
            let pyo3_path = locate_tokens_at(pyo3_path.to_token_stream(), kw.span());
            assertions.extend(quote_spanned! {
                kw.span() => {
                    const ASSERT_IMMUTABLE_SUPPORTED: () = #pyo3_path::impl_::pyclass::assert_immutable_type_supported();
                };
            });
        }

        let deprecation = if self.attr.options.skip_from_py_object.is_some() {
            quote_spanned! { self.attr.options.skip_from_py_object.span() =>
                const _: () = #pyo3_path::impl_::deprecated::SKIP_FROM_PY_OBJECT_DEPRECATED;
            }
        } else {
            TokenStream::new()
        };

        let extract_pyclass_with_clone = if let Some(from_py_object) =
            self.attr.options.from_py_object
        {
            let input_type = get_conversion_type_hint(ctx, &format_ident!("INPUT_TYPE"), cls);
            quote_spanned! { from_py_object.span() =>
                impl<'a, 'py> #pyo3_path::FromPyObject<'a, 'py> for #cls
                where
                    Self: ::std::clone::Clone,
                {
                    type Error = #pyo3_path::pyclass::PyClassGuardError<'a, 'py>;

                    #input_type

                    fn extract(obj: #pyo3_path::Borrowed<'a, 'py, #pyo3_path::PyAny>) -> ::std::result::Result<Self,  <Self as #pyo3_path::FromPyObject<'a, 'py>>::Error> {
                        #pyo3_path::impl_::pyclass::extract_pyclass_with_clone(obj)
                    }
                }
            }
        } else {
            TokenStream::new()
        };

        let assertions = (!assertions.is_empty()).then(|| {
            quote! {
                #[allow(dead_code)]
                const _: () = {
                    #assertions
                };
            }
        });

        Ok(quote! {
            #deprecation

            #extract_pyclass_with_clone

            #assertions

            #pyclass_base_type_impl

            impl #pyo3_path::impl_::pyclass::PyClassImpl for #cls {
                #module
                #is_basetype
                #is_subclass_const
                #is_mapping
                #is_sequence
                #is_immutable_type

                type Layout = <Self::BaseNativeType as #pyo3_path::impl_::pyclass::PyClassBaseType>::Layout<Self>;
                type BaseType = #base;
                type ThreadChecker = #thread_checker;
                #inventory
                type PyClassMutability = <<#base as #pyo3_path::impl_::pyclass::PyClassBaseType>::PyClassMutability as #pyo3_path::impl_::pycell::PyClassMutability>::#class_mutability;
                type Dict = #dict;
                type WeakRef = #weakref;
                type BaseNativeType = #base_nativetype;

                #items_iter

                #raw_doc

                const DOC: &'static ::std::ffi::CStr = {
                    use #pyo3_path::impl_ as impl_;
                    use impl_::pyclass::Probe as _;
                    const DOC_PIECES: &'static [&'static [u8]] = impl_::pyclass::doc::PyClassDocGenerator::<
                        #cls,
                        { impl_::pyclass::HasNewTextSignature::<#cls>::VALUE }
                    >::DOC_PIECES;
                    const LEN: usize = impl_::concat::combined_len(DOC_PIECES);
                    const DOC: &'static [u8] = &impl_::concat::combine_to_array::<LEN>(DOC_PIECES);
                    impl_::pyclass::doc::doc_bytes_as_cstr(DOC)
                };

                #dict_offset

                #weaklist_offset

                fn lazy_type_object() -> &'static #pyo3_path::impl_::pyclass::LazyTypeObject<Self> {
                    use #pyo3_path::impl_::pyclass::LazyTypeObject;
                    static TYPE_OBJECT: LazyTypeObject<#cls> = LazyTypeObject::new();
                    &TYPE_OBJECT
                }
            }

            #default_methods_impl

            #inventory_class
        })
    }

    fn impl_add_to_module(&self, ctx: &Ctx) -> TokenStream {
        let Ctx { pyo3_path, .. } = ctx;
        let cls = self.cls_ident;
        quote! {
            impl #cls {
                #[doc(hidden)]
                pub const _PYO3_DEF: #pyo3_path::impl_::pymodule::AddClassToModule<Self> = #pyo3_path::impl_::pymodule::AddClassToModule::new();
            }
        }
    }

    fn impl_freelist(&self, ctx: &Ctx) -> TokenStream {
        let cls = self.cls_ident;
        let Ctx { pyo3_path, .. } = ctx;

        self.attr.options.freelist.as_ref().map_or(quote! {}, |freelist| {
            let freelist = &freelist.value;
            quote! {
                impl #pyo3_path::impl_::pyclass::PyClassWithFreeList for #cls {
                    #[inline]
                    fn get_free_list(py: #pyo3_path::Python<'_>) -> &'static ::std::sync::Mutex<#pyo3_path::impl_::freelist::PyObjectFreeList> {
                        static FREELIST: #pyo3_path::sync::PyOnceLock<::std::sync::Mutex<#pyo3_path::impl_::freelist::PyObjectFreeList>> = #pyo3_path::sync::PyOnceLock::new();
                        &FREELIST.get_or_init(py, || ::std::sync::Mutex::new(#pyo3_path::impl_::freelist::PyObjectFreeList::with_capacity(#freelist)))
                    }
                }
            }
        })
    }

    fn freelist_slots(&self, ctx: &Ctx) -> Vec<TokenStream> {
        let Ctx { pyo3_path, .. } = ctx;
        let cls = self.cls_ident;

        if self.attr.options.freelist.is_some() {
            vec![
                quote! {
                    #pyo3_path::ffi::PyType_Slot {
                        slot: #pyo3_path::ffi::Py_tp_alloc,
                        pfunc: #pyo3_path::impl_::pyclass::alloc_with_freelist::<#cls> as *mut _,
                    }
                },
                quote! {
                    #pyo3_path::ffi::PyType_Slot {
                        slot: #pyo3_path::ffi::Py_tp_free,
                        pfunc: #pyo3_path::impl_::pyclass::free_with_freelist::<#cls> as *mut _,
                    }
                },
            ]
        } else {
            Vec::new()
        }
    }

    #[cfg(feature = "experimental-inspect")]
    fn impl_introspection(&self, ctx: &Ctx, parent: Option<&Ident>) -> TokenStream {
        let Ctx { pyo3_path, .. } = ctx;
        let name = get_class_python_name(self.cls_name, self.attr).to_string();
        let ident = self.cls_ident;
        let static_introspection = class_introspection_code(
            pyo3_path,
            ident,
            &name,
            self.attr.options.extends.as_ref().map(|attr| {
                PyExpr::from_type(
                    syn::Type::Path(syn::TypePath {
                        attrs: Vec::new(),
                        qself: None,
                        path: attr.value.clone(),
                    }),
                    None,
                )
            }),
            self.attr.options.subclass.is_none(),
            parent.map(|p| parse_quote!(#p)).as_ref(),
            self.doc.as_ref(),
        );
        let introspection_id = introspection_id_const();
        quote! {
            #static_introspection
            impl #ident {
                #introspection_id
            }
        }
    }

    #[cfg(not(feature = "experimental-inspect"))]
    fn impl_introspection(&self, _ctx: &Ctx, _parent: Option<&Ident>) -> TokenStream {
        quote! {}
    }
}

fn define_inventory_class(inventory_class_name: &syn::Ident, ctx: &Ctx) -> TokenStream {
    let Ctx { pyo3_path, .. } = ctx;
    quote! {
        #[doc(hidden)]
        pub struct #inventory_class_name {
            items: #pyo3_path::impl_::pyclass::PyClassItems,
        }
        impl #inventory_class_name {
            pub const fn new(items: #pyo3_path::impl_::pyclass::PyClassItems) -> Self {
                Self { items }
            }
        }

        impl #pyo3_path::impl_::pyclass::PyClassInventory for #inventory_class_name {
            fn items(&self) -> &#pyo3_path::impl_::pyclass::PyClassItems {
                &self.items
            }
        }

        #pyo3_path::inventory::collect!(#inventory_class_name);
    }
}

fn generate_cfg_check(variants: &[PyClassEnumUnitVariant<'_>], cls: &syn::Ident) -> TokenStream {
    if variants.is_empty() {
        return quote! {};
    }

    let mut conditions = Vec::new();

    for variant in variants {
        let cfg_attrs = get_cfg_attributes(&variant.attrs);

        if cfg_attrs.is_empty() {
            // There's at least one variant of the enum without cfg attributes,
            // so the check is not necessary
            return quote! {};
        }

        for attr in cfg_attrs {
            if let syn::Meta::List(meta) = &attr.meta {
                let cfg_tokens = &meta.tokens;
                conditions.push(quote! { not(#cfg_tokens) });
            }
        }
    }

    quote_spanned! {
        cls.span() =>
        #[cfg(all(#(#conditions),*))]
        ::core::compile_error!(concat!("#[pyclass] can't be used on enums without any variants - all variants of enum `", stringify!(#cls), "` have been configured out by cfg attributes"));
    }
}

fn get_conversion_type_hint(
    Ctx { pyo3_path, .. }: &Ctx,
    konst: &Ident,
    cls: &Ident,
) -> TokenStream {
    if cfg!(feature = "experimental-inspect") {
        quote!(const #konst: #pyo3_path::inspect::PyStaticExpr = <#cls as #pyo3_path::PyTypeInfo>::TYPE_HINT;)
    } else {
        TokenStream::new()
    }
}

const UNIQUE_GET: &str = "`get` may only be specified once";
const UNIQUE_SET: &str = "`set` may only be specified once";
const UNIQUE_NAME: &str = "`name` may only be specified once";

const DUPE_SET: &str = "useless `set` - the struct is already annotated with `set_all`";
const DUPE_GET: &str = "useless `get` - the struct is already annotated with `get_all`";
const UNIT_GET: &str =
    "`get_all` on an unit struct does nothing, because unit structs have no fields";
const UNIT_SET: &str =
    "`set_all` on an unit struct does nothing, because unit structs have no fields";

const USELESS_NAME: &str = "`name` is useless without `get` or `set`";
