// Copyright (c) 2024 Jonas Schäfer <jonas@zombofant.net>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

//! Handling of enums

use std::collections::HashMap;

use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::*;

use crate::common::{AsXmlParts, FromXmlParts, ItemDef, XmlNameMatcher};
use crate::compound::Compound;
use crate::error_message::ParentRef;
use crate::meta::{reject_key, Flag, NameRef, NamespaceRef, QNameRef, XmlCompoundMeta};
use crate::state::{AsItemsStateMachine, FromEventsMatchMode, FromEventsStateMachine};
use crate::structs::StructInner;
use crate::types::{ref_ty, ty_from_ident};

/// The definition of an enum variant, switched on the XML element's name,
/// inside a [`NameSwitchedEnum`].
struct NameVariant {
    /// The XML name of the element to map the enum variant to.
    name: NameRef,

    /// The name of the variant
    ident: Ident,

    /// The field(s) of this struct.
    inner: Compound,
}

impl NameVariant {
    /// Construct a new name-selected variant from its declaration.
    fn new(decl: &Variant, enum_namespace: &NamespaceRef) -> Result<Self> {
        // We destructure here so that we get informed when new fields are
        // added and can handle them, either by processing them or raising
        // an error if they are present.
        let XmlCompoundMeta {
            span: meta_span,
            qname: QNameRef { namespace, name },
            exhaustive,
            debug,
            builder,
            iterator,
            on_unknown_attribute,
            on_unknown_child,
            transparent,
            discard,
            deserialize_callback,
            attribute,
            value,
        } = XmlCompoundMeta::parse_from_attributes(&decl.attrs)?;

        reject_key!(debug flag not on "enum variants" only on "enums and structs");
        reject_key!(exhaustive flag not on "enum variants" only on "enums");
        reject_key!(namespace not on "enum variants" only on "enums and structs");
        reject_key!(builder not on "enum variants" only on "enums and structs");
        reject_key!(iterator not on "enum variants" only on "enums and structs");
        reject_key!(transparent flag not on "named enum variants" only on "structs");
        reject_key!(deserialize_callback not on "enum variants" only on "enums and structs");
        reject_key!(attribute not on "enum variants" only on "attribute-switched enums");
        reject_key!(value not on "name-switched enum variants" only on "attribute-switched enum variants");

        let Some(name) = name else {
            return Err(Error::new(
                meta_span,
                "`name` is required on name-switched enum variants",
            ));
        };

        Ok(Self {
            name,
            ident: decl.ident.clone(),
            inner: Compound::from_fields(
                &decl.fields,
                enum_namespace,
                on_unknown_attribute,
                on_unknown_child,
                discard,
            )?,
        })
    }

    fn make_from_events_statemachine(
        &self,
        enum_ident: &Ident,
        state_ty_ident: &Ident,
    ) -> Result<FromEventsStateMachine> {
        let xml_name = &self.name;

        Ok(self
            .inner
            .make_from_events_statemachine(
                state_ty_ident,
                &ParentRef::Named(Path {
                    leading_colon: None,
                    segments: [
                        PathSegment::from(enum_ident.clone()),
                        self.ident.clone().into(),
                    ]
                    .into_iter()
                    .collect(),
                }),
                &self.ident.to_string(),
            )?
            .with_augmented_init(|init| {
                quote! {
                    if name.1 != #xml_name {
                        ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch {
                            name,
                            attrs,
                        })
                    } else {
                        #init
                    }
                }
            })
            .compile())
    }

    fn make_as_item_iter_statemachine(
        &self,
        xml_namespace: &NamespaceRef,
        enum_ident: &Ident,
        state_ty_ident: &Ident,
        item_iter_ty_lifetime: &Lifetime,
    ) -> Result<AsItemsStateMachine> {
        let xml_name = &self.name;

        Ok(self
            .inner
            .make_as_item_iter_statemachine(
                &ParentRef::Named(Path {
                    leading_colon: None,
                    segments: [
                        PathSegment::from(enum_ident.clone()),
                        self.ident.clone().into(),
                    ]
                    .into_iter()
                    .collect(),
                }),
                state_ty_ident,
                &self.ident.to_string(),
                item_iter_ty_lifetime,
            )?
            .with_augmented_init(|init| {
                quote! {
                    let name = (
                        ::xso::exports::rxml::Namespace::from(#xml_namespace),
                        ::xso::exports::alloc::borrow::Cow::Borrowed(#xml_name),
                    );
                    #init
                }
            })
            .compile())
    }
}

/// The definition of a enum which switches based on the XML element name,
/// with the XML namespace fixed.
struct NameSwitchedEnum {
    /// The XML namespace of the element to map the enum to.
    namespace: NamespaceRef,

    /// The variants of the enum.
    variants: Vec<NameVariant>,

    /// Flag indicating whether the enum is exhaustive.
    exhaustive: bool,
}

impl NameSwitchedEnum {
    fn new<'x, I: IntoIterator<Item = &'x Variant>>(
        namespace: NamespaceRef,
        exhaustive: Flag,
        variant_iter: I,
    ) -> Result<Self> {
        let mut variants = Vec::new();
        let mut seen_names = HashMap::new();
        for variant in variant_iter {
            let variant = NameVariant::new(variant, &namespace)?;
            if let Some(other) = seen_names.get(&variant.name) {
                return Err(Error::new_spanned(
                    variant.name,
                    format!(
                        "duplicate `name` in enum: variants {} and {} have the same XML name",
                        other, variant.ident
                    ),
                ));
            }
            seen_names.insert(variant.name.clone(), variant.ident.clone());
            variants.push(variant);
        }

        Ok(Self {
            namespace,
            variants,
            exhaustive: exhaustive.is_set(),
        })
    }

    /// Provide the `XmlNameMatcher` template for this enum.
    ///
    /// Name-switched enums always return a matcher in the namespace of the
    /// elements they match against.
    fn xml_name_matcher(&self) -> Result<XmlNameMatcher> {
        Ok(XmlNameMatcher::InNamespace(
            self.namespace.to_token_stream(),
        ))
    }

    /// Build the deserialisation statemachine for the name-switched enum.
    fn make_from_events_statemachine(
        &self,
        target_ty_ident: &Ident,
        state_ty_ident: &Ident,
    ) -> Result<FromEventsStateMachine> {
        let xml_namespace = &self.namespace;

        let mut statemachine = FromEventsStateMachine::new();
        for variant in self.variants.iter() {
            statemachine
                .merge(variant.make_from_events_statemachine(target_ty_ident, state_ty_ident)?);
        }

        statemachine.set_pre_init(quote! {
            if name.0 != #xml_namespace {
                return ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch {
                    name,
                    attrs,
                })
            }
        });

        if self.exhaustive {
            let mismatch_err = format!("This is not a {} element.", target_ty_ident);
            statemachine.set_fallback(quote! {
                ::core::result::Result::Err(::xso::error::FromEventsError::Invalid(
                    ::xso::error::Error::Other(#mismatch_err),
                ))
            })
        }

        Ok(statemachine)
    }

    /// Build the serialisation statemachine for the name-switched enum.
    fn make_as_item_iter_statemachine(
        &self,
        target_ty_ident: &Ident,
        state_ty_ident: &Ident,
        item_iter_ty_lifetime: &Lifetime,
    ) -> Result<AsItemsStateMachine> {
        let mut statemachine = AsItemsStateMachine::new();
        for variant in self.variants.iter() {
            statemachine.merge(variant.make_as_item_iter_statemachine(
                &self.namespace,
                target_ty_ident,
                state_ty_ident,
                item_iter_ty_lifetime,
            )?);
        }

        Ok(statemachine)
    }
}

/// The definition of an enum variant, switched on the XML element's
/// attribute's value, inside a [`AttributeSwitchedEnum`].
struct ValueVariant {
    /// The verbatim value to match.
    value: String,

    /// The identifier of the variant
    ident: Ident,

    /// The field(s) of the variant.
    inner: Compound,
}

impl ValueVariant {
    /// Construct a new value-selected variant from its declaration.
    fn new(decl: &Variant, enum_namespace: &NamespaceRef) -> Result<Self> {
        // We destructure here so that we get informed when new fields are
        // added and can handle them, either by processing them or raising
        // an error if they are present.
        let XmlCompoundMeta {
            span: meta_span,
            qname: QNameRef { namespace, name },
            exhaustive,
            debug,
            builder,
            iterator,
            on_unknown_attribute,
            on_unknown_child,
            transparent,
            discard,
            deserialize_callback,
            attribute,
            value,
        } = XmlCompoundMeta::parse_from_attributes(&decl.attrs)?;

        reject_key!(debug flag not on "enum variants" only on "enums and structs");
        reject_key!(exhaustive flag not on "enum variants" only on "enums");
        reject_key!(namespace not on "enum variants" only on "enums and structs");
        reject_key!(name not on "attribute-switched enum variants" only on "enums, structs and name-switched enum variants");
        reject_key!(builder not on "enum variants" only on "enums and structs");
        reject_key!(iterator not on "enum variants" only on "enums and structs");
        reject_key!(transparent flag not on "named enum variants" only on "structs");
        reject_key!(deserialize_callback not on "enum variants" only on "enums and structs");
        reject_key!(attribute not on "enum variants" only on "attribute-switched enums");

        let Some(value) = value else {
            return Err(Error::new(
                meta_span,
                "`value` is required on attribute-switched enum variants",
            ));
        };

        Ok(Self {
            value: value.value(),
            ident: decl.ident.clone(),
            inner: Compound::from_fields(
                &decl.fields,
                enum_namespace,
                on_unknown_attribute,
                on_unknown_child,
                discard,
            )?,
        })
    }

    fn make_from_events_statemachine(
        &self,
        enum_ident: &Ident,
        state_ty_ident: &Ident,
    ) -> Result<FromEventsStateMachine> {
        let value = &self.value;

        Ok(self
            .inner
            .make_from_events_statemachine(
                state_ty_ident,
                &ParentRef::Named(Path {
                    leading_colon: None,
                    segments: [
                        PathSegment::from(enum_ident.clone()),
                        self.ident.clone().into(),
                    ]
                    .into_iter()
                    .collect(),
                }),
                &self.ident.to_string(),
            )?
            .with_augmented_init(|init| {
                quote! {
                    #value => { #init },
                }
            })
            .compile())
    }

    fn make_as_item_iter_statemachine(
        &self,
        elem_namespace: &NamespaceRef,
        elem_name: &NameRef,
        attr_namespace: &TokenStream,
        attr_name: &NameRef,
        enum_ident: &Ident,
        state_ty_ident: &Ident,
        item_iter_ty_lifetime: &Lifetime,
    ) -> Result<AsItemsStateMachine> {
        let attr_value = &self.value;

        Ok(self
            .inner
            .make_as_item_iter_statemachine(
                &ParentRef::Named(Path {
                    leading_colon: None,
                    segments: [
                        PathSegment::from(enum_ident.clone()),
                        self.ident.clone().into(),
                    ]
                    .into_iter()
                    .collect(),
                }),
                state_ty_ident,
                &self.ident.to_string(),
                item_iter_ty_lifetime,
            )?
            .with_augmented_init(|init| {
                quote! {
                    let name = (
                        ::xso::exports::rxml::Namespace::from(#elem_namespace),
                        ::xso::exports::alloc::borrow::Cow::Borrowed(#elem_name),
                    );
                    #init
                }
            })
            .with_extra_header_state(
                // Note: we convert the identifier to a string here to prevent
                // its Span from leaking into the new identifier.
                quote::format_ident!("{}Discriminator", &self.ident.to_string()),
                quote! {
                    ::xso::Item::Attribute(
                        #attr_namespace,
                        ::xso::exports::alloc::borrow::Cow::Borrowed(#attr_name),
                        ::xso::exports::alloc::borrow::Cow::Borrowed(#attr_value),
                    )
                },
            )
            .compile())
    }
}

/// The definition of an enum where each variant represents a different value
/// of a fixed attribute.
struct AttributeSwitchedEnum {
    /// The XML namespace of the element.
    elem_namespace: NamespaceRef,

    /// The XML name of the element.
    elem_name: NameRef,

    /// The XML namespace of the attribute, or None if the attribute isn't
    /// namespaced.'
    attr_namespace: Option<NamespaceRef>,

    /// The XML name of the attribute.
    attr_name: NameRef,

    /// Enum variant definitions
    variants: Vec<ValueVariant>,
}

impl AttributeSwitchedEnum {
    fn new<'x, I: IntoIterator<Item = &'x Variant>>(
        elem_namespace: NamespaceRef,
        elem_name: NameRef,
        attr_namespace: Option<NamespaceRef>,
        attr_name: NameRef,
        variant_iter: I,
    ) -> Result<Self> {
        let mut variants = Vec::new();
        let mut seen_values = HashMap::new();
        for variant in variant_iter {
            let variant = ValueVariant::new(variant, &elem_namespace)?;
            if let Some(other) = seen_values.get(&variant.value) {
                return Err(Error::new_spanned(
                    variant.value,
                    format!(
                        "duplicate `value` in enum: variants {} and {} have the same attribute value",
                        other, variant.ident
                    ),
                ));
            }
            seen_values.insert(variant.value.clone(), variant.ident.clone());
            variants.push(variant);
        }

        Ok(Self {
            elem_namespace,
            elem_name,
            attr_namespace,
            attr_name,
            variants,
        })
    }

    /// Provide the `XmlNameMatcher` template for this enum.
    ///
    /// Attribute-switched enums always return a matcher specific to the
    /// element they operate on.
    fn xml_name_matcher(&self) -> Result<XmlNameMatcher> {
        Ok(XmlNameMatcher::Specific(
            self.elem_namespace.to_token_stream(),
            self.elem_name.to_token_stream(),
        ))
    }

    /// Build the deserialisation statemachine for the attribute-switched enum.
    fn make_from_events_statemachine(
        &self,
        target_ty_ident: &Ident,
        state_ty_ident: &Ident,
    ) -> Result<FromEventsStateMachine> {
        let elem_namespace = &self.elem_namespace;
        let elem_name = &self.elem_name;
        let attr_namespace = match self.attr_namespace.as_ref() {
            Some(v) => v.to_token_stream(),
            None => quote! {
                ::xso::exports::rxml::Namespace::none()
            },
        };
        let attr_name = &self.attr_name;

        let mut statemachine = FromEventsStateMachine::new();
        for variant in self.variants.iter() {
            statemachine
                .merge(variant.make_from_events_statemachine(target_ty_ident, state_ty_ident)?);
        }

        statemachine.set_pre_init(quote! {
            let attr = {
                if name.0 != #elem_namespace || name.1 != #elem_name {
                    return ::core::result::Result::Err(
                        ::xso::error::FromEventsError::Mismatch {
                            name,
                            attrs,
                        },
                    );
                }

                let ::core::option::Option::Some(attr) = attrs.remove(#attr_namespace, #attr_name) else {
                    return ::core::result::Result::Err(
                        ::xso::error::FromEventsError::Invalid(
                            ::xso::error::Error::Other("Missing discriminator attribute.")
                        ),
                    );
                };

                attr
            };
        });
        statemachine.set_fallback(quote! {
            ::core::result::Result::Err(
                ::xso::error::FromEventsError::Invalid(
                    ::xso::error::Error::Other("Unknown value for discriminator attribute.")
                ),
            )
        });
        statemachine.set_match_mode(FromEventsMatchMode::Matched {
            expr: quote! { attr.as_str() },
        });

        Ok(statemachine)
    }

    /// Build the serialisation statemachine for the attribute-switched enum.
    fn make_as_item_iter_statemachine(
        &self,
        target_ty_ident: &Ident,
        state_ty_ident: &Ident,
        item_iter_ty_lifetime: &Lifetime,
    ) -> Result<AsItemsStateMachine> {
        let attr_namespace = match self.attr_namespace.as_ref() {
            Some(v) => v.to_token_stream(),
            None => quote! {
                ::xso::exports::rxml::Namespace::NONE
            },
        };

        let mut statemachine = AsItemsStateMachine::new();
        for variant in self.variants.iter() {
            statemachine.merge(variant.make_as_item_iter_statemachine(
                &self.elem_namespace,
                &self.elem_name,
                &attr_namespace,
                &self.attr_name,
                target_ty_ident,
                state_ty_ident,
                item_iter_ty_lifetime,
            )?);
        }

        Ok(statemachine)
    }
}

/// The definition of an enum variant in a [`DynamicEnum`].
struct DynamicVariant {
    /// The identifier of the enum variant.
    ident: Ident,

    /// The definition of the struct-like which resembles the enum variant.
    inner: StructInner,
}

impl DynamicVariant {
    fn new(variant: &Variant) -> Result<Self> {
        let ident = variant.ident.clone();
        let meta = XmlCompoundMeta::parse_from_attributes(&variant.attrs)?;

        // We destructure here so that we get informed when new fields are
        // added and can handle them, either by processing them or raising
        // an error if they are present.
        let XmlCompoundMeta {
            span: _,
            qname: _, // used by StructInner
            ref exhaustive,
            ref debug,
            ref builder,
            ref iterator,
            on_unknown_attribute: _, // used by StructInner
            on_unknown_child: _,     // used by StructInner
            transparent: _,          // used by StructInner
            discard: _,              // used by StructInner
            ref deserialize_callback,
            ref attribute,
            ref value,
        } = meta;

        reject_key!(debug flag not on "enum variants" only on "enums and structs");
        reject_key!(exhaustive flag not on "enum variants" only on "enums");
        reject_key!(builder not on "enum variants" only on "enums and structs");
        reject_key!(iterator not on "enum variants" only on "enums and structs");
        reject_key!(deserialize_callback not on "enum variants" only on "enums and structs");
        reject_key!(attribute not on "enum variants" only on "attribute-switched enums");
        reject_key!(value not on "dynamic enum variants" only on "attribute-switched enum variants");

        let inner = StructInner::new(meta, &variant.fields)?;
        Ok(Self { ident, inner })
    }
}

/// The definition of an enum where each variant is a completely unrelated
/// possible XML subtree.
struct DynamicEnum {
    /// The enum variants.
    variants: Vec<DynamicVariant>,
}

impl DynamicEnum {
    fn new<'x, I: IntoIterator<Item = &'x Variant>>(variant_iter: I) -> Result<Self> {
        let mut variants = Vec::new();
        for variant in variant_iter {
            variants.push(DynamicVariant::new(variant)?);
        }

        Ok(Self { variants })
    }

    /// Provide the `XmlNameMatcher` template for this dynamic enum.
    ///
    /// Dynamic enums return the superset of the matchers of their variants,
    /// which in many cases will be XmlNameMatcher::Any.
    fn xml_name_matcher(&self) -> Result<XmlNameMatcher> {
        let mut iter = self.variants.iter();
        let Some(first) = iter.next() else {
            // Since the enum has no variants, it cannot match anything. We
            // use an empty namespace and an empty name, which will never
            // match.
            return Ok(XmlNameMatcher::Custom(quote! {
                ::xso::fromxml::XmlNameMatcher::<'static>::Specific("", "")
            }));
        };

        let first_matcher = first.inner.xml_name_matcher()?;
        let mut composition = quote! { #first_matcher };
        for variant in iter {
            let next_matcher = variant.inner.xml_name_matcher()?;
            composition.extend(quote! { .superset(#next_matcher) });
        }

        Ok(XmlNameMatcher::Custom(composition))
    }

    /// Build the deserialisation statemachine for the dynamic enum.
    fn make_from_events_statemachine(
        &self,
        target_ty_ident: &Ident,
        state_ty_ident: &Ident,
    ) -> Result<FromEventsStateMachine> {
        let mut statemachine = FromEventsStateMachine::new();
        for variant in self.variants.iter() {
            let submachine = variant.inner.make_from_events_statemachine(
                state_ty_ident,
                &ParentRef::Named(Path {
                    leading_colon: None,
                    segments: [
                        PathSegment::from(target_ty_ident.clone()),
                        variant.ident.clone().into(),
                    ]
                    .into_iter()
                    .collect(),
                }),
                &variant.ident.to_string(),
            )?;

            statemachine.merge(submachine.compile());
        }

        Ok(statemachine)
    }

    /// Build the serialisation statemachine for the dynamic enum.
    fn make_as_item_iter_statemachine(
        &self,
        target_ty_ident: &Ident,
        state_ty_ident: &Ident,
        item_iter_ty_lifetime: &Lifetime,
    ) -> Result<AsItemsStateMachine> {
        let mut statemachine = AsItemsStateMachine::new();
        for variant in self.variants.iter() {
            let submachine = variant.inner.make_as_item_iter_statemachine(
                &ParentRef::Named(Path {
                    leading_colon: None,
                    segments: [
                        PathSegment::from(target_ty_ident.clone()),
                        variant.ident.clone().into(),
                    ]
                    .into_iter()
                    .collect(),
                }),
                state_ty_ident,
                &variant.ident.to_string(),
                item_iter_ty_lifetime,
            )?;

            statemachine.merge(submachine.compile());
        }

        Ok(statemachine)
    }
}

/// The definition of an enum.
enum EnumInner {
    /// The enum switches based on the XML name of the element, with the XML
    /// namespace fixed.
    NameSwitched(NameSwitchedEnum),

    /// The enum switches based on the value of an attribute of the XML
    /// element.
    AttributeSwitched(AttributeSwitchedEnum),

    /// The enum consists of variants with entirely unrelated XML structures.
    Dynamic(DynamicEnum),
}

impl EnumInner {
    fn new<'x, I: IntoIterator<Item = &'x Variant>>(
        meta: XmlCompoundMeta,
        variant_iter: I,
    ) -> Result<Self> {
        // We destructure here so that we get informed when new fields are
        // added and can handle them, either by processing them or raising
        // an error if they are present.
        let XmlCompoundMeta {
            span,
            qname: QNameRef { namespace, name },
            exhaustive,
            debug,
            builder,
            iterator,
            on_unknown_attribute,
            on_unknown_child,
            transparent,
            discard,
            deserialize_callback,
            attribute,
            value,
        } = meta;

        // These must've been cleared by the caller. Because these being set
        // is a programming error (in xso-proc) and not a usage error, we
        // assert here instead of using reject_key!.
        assert!(builder.is_none());
        assert!(iterator.is_none());
        assert!(!debug.is_set());
        assert!(deserialize_callback.is_none());

        reject_key!(transparent flag not on "enums" only on "structs");
        reject_key!(on_unknown_attribute not on "enums" only on "enum variants and structs");
        reject_key!(on_unknown_child not on "enums" only on "enum variants and structs");
        reject_key!(discard vec not on "enums" only on "enum variants and structs");
        reject_key!(value not on "enums" only on "attribute-switched enum variants");

        if let Some(attribute) = attribute {
            let Some(attr_name) = attribute.qname.name else {
                return Err(Error::new(
                    attribute.span,
                    "missing `name` for `attribute` key",
                ));
            };

            let attr_namespace = attribute.qname.namespace;

            let Some(elem_namespace) = namespace else {
                let mut error =
                    Error::new(span, "missing `namespace` key on attribute-switched enum");
                error.combine(Error::new(
                    attribute.span,
                    "enum is attribute-switched because of the `attribute` key here",
                ));
                return Err(error);
            };

            let Some(elem_name) = name else {
                let mut error = Error::new(span, "missing `name` key on attribute-switched enum");
                error.combine(Error::new(
                    attribute.span,
                    "enum is attribute-switched because of the `attribute` key here",
                ));
                return Err(error);
            };

            if !exhaustive.is_set() {
                return Err(Error::new(
                    span,
                    "attribute-switched enums must be marked as `exhaustive`. non-exhaustive attribute-switched enums are not supported."
                ));
            }

            Ok(Self::AttributeSwitched(AttributeSwitchedEnum::new(
                elem_namespace,
                elem_name,
                attr_namespace,
                attr_name,
                variant_iter,
            )?))
        } else if let Some(namespace) = namespace {
            reject_key!(name not on "name-switched enums" only on "their variants");
            Ok(Self::NameSwitched(NameSwitchedEnum::new(
                namespace,
                exhaustive,
                variant_iter,
            )?))
        } else {
            reject_key!(name not on "dynamic enums" only on "their variants");
            reject_key!(exhaustive flag not on "dynamic enums" only on "name-switched enums");
            Ok(Self::Dynamic(DynamicEnum::new(variant_iter)?))
        }
    }

    /// Provide the `XmlNameMatcher` template for this enum.
    fn xml_name_matcher(&self) -> Result<XmlNameMatcher> {
        match self {
            Self::NameSwitched(ref inner) => inner.xml_name_matcher(),
            Self::AttributeSwitched(ref inner) => inner.xml_name_matcher(),
            Self::Dynamic(ref inner) => inner.xml_name_matcher(),
        }
    }

    /// Build the deserialisation statemachine for the enum.
    fn make_from_events_statemachine(
        &self,
        target_ty_ident: &Ident,
        state_ty_ident: &Ident,
    ) -> Result<FromEventsStateMachine> {
        match self {
            Self::NameSwitched(ref inner) => {
                inner.make_from_events_statemachine(target_ty_ident, state_ty_ident)
            }
            Self::AttributeSwitched(ref inner) => {
                inner.make_from_events_statemachine(target_ty_ident, state_ty_ident)
            }
            Self::Dynamic(ref inner) => {
                inner.make_from_events_statemachine(target_ty_ident, state_ty_ident)
            }
        }
    }

    /// Build the serialisation statemachine for the enum.
    fn make_as_item_iter_statemachine(
        &self,
        target_ty_ident: &Ident,
        state_ty_ident: &Ident,
        item_iter_ty_lifetime: &Lifetime,
    ) -> Result<AsItemsStateMachine> {
        match self {
            Self::NameSwitched(ref inner) => inner.make_as_item_iter_statemachine(
                target_ty_ident,
                state_ty_ident,
                item_iter_ty_lifetime,
            ),
            Self::AttributeSwitched(ref inner) => inner.make_as_item_iter_statemachine(
                target_ty_ident,
                state_ty_ident,
                item_iter_ty_lifetime,
            ),
            Self::Dynamic(ref inner) => inner.make_as_item_iter_statemachine(
                target_ty_ident,
                state_ty_ident,
                item_iter_ty_lifetime,
            ),
        }
    }
}

/// Definition of an enum and how to parse it.
pub(crate) struct EnumDef {
    /// Implementation of the enum itself
    inner: EnumInner,

    /// Name of the target type.
    target_ty_ident: Ident,

    /// Name of the builder type.
    builder_ty_ident: Ident,

    /// Name of the iterator type.
    item_iter_ty_ident: Ident,

    /// Flag whether debug mode is enabled.
    debug: bool,

    /// Optional validator function to call.
    deserialize_callback: Option<Path>,
}

impl EnumDef {
    /// Create a new enum from its name, meta, and variants.
    pub(crate) fn new<'x, I: IntoIterator<Item = &'x Variant>>(
        ident: &Ident,
        mut meta: XmlCompoundMeta,
        variant_iter: I,
    ) -> Result<Self> {
        let builder_ty_ident = match meta.builder.take() {
            Some(v) => v,
            None => quote::format_ident!("{}FromXmlBuilder", ident.to_string()),
        };

        let item_iter_ty_ident = match meta.iterator.take() {
            Some(v) => v,
            None => quote::format_ident!("{}AsXmlIterator", ident.to_string()),
        };

        let debug = meta.debug.take().is_set();
        let deserialize_callback = meta.deserialize_callback.take();

        Ok(Self {
            inner: EnumInner::new(meta, variant_iter)?,
            target_ty_ident: ident.clone(),
            builder_ty_ident,
            item_iter_ty_ident,
            debug,
            deserialize_callback,
        })
    }
}

impl ItemDef for EnumDef {
    fn make_from_events_builder(
        &self,
        vis: &Visibility,
        name_ident: &Ident,
        attrs_ident: &Ident,
    ) -> Result<FromXmlParts> {
        let target_ty_ident = &self.target_ty_ident;
        let builder_ty_ident = &self.builder_ty_ident;
        let state_ty_ident = quote::format_ident!("{}State", builder_ty_ident);

        let defs = self
            .inner
            .make_from_events_statemachine(target_ty_ident, &state_ty_ident)?
            .render(
                vis,
                builder_ty_ident,
                &state_ty_ident,
                &TypePath {
                    qself: None,
                    path: target_ty_ident.clone().into(),
                }
                .into(),
                self.deserialize_callback.as_ref(),
            )?;

        Ok(FromXmlParts {
            defs,
            from_events_body: quote! {
                #builder_ty_ident::new(#name_ident, #attrs_ident, ctx)
            },
            builder_ty_ident: builder_ty_ident.clone(),
            name_matcher: self.inner.xml_name_matcher()?,
        })
    }

    fn make_as_xml_iter(&self, vis: &Visibility) -> Result<AsXmlParts> {
        let target_ty_ident = &self.target_ty_ident;
        let item_iter_ty_ident = &self.item_iter_ty_ident;
        let item_iter_ty_lifetime = Lifetime {
            apostrophe: Span::call_site(),
            ident: Ident::new("xso_proc_as_xml_iter_lifetime", Span::call_site()),
        };
        let item_iter_ty = Type::Path(TypePath {
            qself: None,
            path: Path {
                leading_colon: None,
                segments: [PathSegment {
                    ident: item_iter_ty_ident.clone(),
                    arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments {
                        colon2_token: None,
                        lt_token: token::Lt {
                            spans: [Span::call_site()],
                        },
                        args: [GenericArgument::Lifetime(item_iter_ty_lifetime.clone())]
                            .into_iter()
                            .collect(),
                        gt_token: token::Gt {
                            spans: [Span::call_site()],
                        },
                    }),
                }]
                .into_iter()
                .collect(),
            },
        });
        let state_ty_ident = quote::format_ident!("{}State", item_iter_ty_ident);

        let defs = self
            .inner
            .make_as_item_iter_statemachine(
                target_ty_ident,
                &state_ty_ident,
                &item_iter_ty_lifetime,
            )?
            .render(
                vis,
                &ref_ty(
                    ty_from_ident(target_ty_ident.clone()).into(),
                    item_iter_ty_lifetime.clone(),
                ),
                &state_ty_ident,
                &item_iter_ty_lifetime,
                &item_iter_ty,
            )?;

        Ok(AsXmlParts {
            defs,
            as_xml_iter_body: quote! {
                #item_iter_ty_ident::new(self)
            },
            item_iter_ty,
            item_iter_ty_lifetime,
        })
    }

    fn debug(&self) -> bool {
        self.debug
    }
}
