//! Types and traits used for registering classes with PHP.

use std::{
    marker::PhantomData,
    sync::atomic::{AtomicPtr, Ordering},
};

use once_cell::sync::OnceCell;

use crate::{
    builders::{ClassBuilder, FunctionBuilder},
    convert::IntoZvalDyn,
    describe::DocComments,
    exception::PhpException,
    flags::{ClassFlags, MethodFlags, PropertyFlags},
    internal::property::PropertyDescriptor,
    zend::{ClassEntry, ExecuteData, ZendObjectHandlers},
};

/// A type alias for a tuple containing a function pointer to a class entry
/// and a string representing the class name used in stubs.
pub type ClassEntryInfo = (fn() -> &'static ClassEntry, &'static str);

/// Implemented on Rust types which are exported to PHP. Allows users to get and
/// set PHP properties on the object.
pub trait RegisteredClass: Sized + 'static {
    /// PHP class name of the registered class.
    const CLASS_NAME: &'static str;

    /// Function to be called when building the class. Allows user to modify the
    /// class at runtime (add runtime constants etc).
    const BUILDER_MODIFIER: Option<fn(ClassBuilder) -> ClassBuilder>;

    /// Parent class entry. Optional.
    const EXTENDS: Option<ClassEntryInfo>;

    /// Interfaces implemented by the class.
    const IMPLEMENTS: &'static [ClassEntryInfo];

    /// PHP flags applied to the class.
    const FLAGS: ClassFlags = ClassFlags::empty();

    /// Doc comments for the class.
    const DOC_COMMENTS: DocComments = &[];

    /// Returns a reference to the class metadata, which stores the class entry,
    /// handlers, and property descriptors.
    ///
    /// This must be statically allocated, and is usually done through the
    /// [`macro@php_class`] macro.
    ///
    /// [`macro@php_class`]: crate::php_class
    fn get_metadata() -> &'static ClassMetadata<Self>;

    /// Returns method properties (from `#[php(getter)]`/`#[php(setter)]`).
    /// Resolved via autoref specialization in the macro-generated code.
    #[must_use]
    fn method_properties() -> &'static [PropertyDescriptor<Self>] {
        &[]
    }

    /// Returns the method builders required to build the class.
    fn method_builders() -> Vec<(FunctionBuilder<'static>, MethodFlags)>;

    /// Returns the class constructor (if any).
    fn constructor() -> Option<ConstructorMeta<Self>>;

    /// Returns the constants provided by the class.
    fn constants() -> &'static [(&'static str, &'static dyn IntoZvalDyn, DocComments)];

    /// Returns the static properties provided by the class.
    ///
    /// Static properties are declared at the class level and managed by PHP,
    /// not by Rust handlers. Each tuple contains (name, flags, default, docs).
    /// The default value is optional - `None` means null default.
    #[must_use]
    fn static_properties() -> &'static [(
        &'static str,
        PropertyFlags,
        Option<&'static (dyn IntoZvalDyn + Sync)>,
        DocComments,
    )] {
        &[]
    }

    /// Returns interfaces from `#[php_impl_interface]` trait implementations.
    ///
    /// This method is generated by the `#[php_class]` macro and uses the
    /// `inventory` crate to collect interface registrations across crate
    /// boundaries at link time.
    ///
    /// The default implementation returns an empty vector. The macro overrides
    /// this to iterate over `InterfaceRegistration` entries matching this type.
    #[must_use]
    fn interface_implementations() -> Vec<ClassEntryInfo> {
        Vec::new()
    }

    /// Returns methods from `#[php_impl_interface]` trait implementations.
    ///
    /// This method is generated by the `#[php_class]` macro and uses the
    /// `inventory` crate to collect method registrations across crate
    /// boundaries at link time.
    ///
    /// The default implementation returns an empty vector. The macro overrides
    /// this to iterate over `MethodRegistration` entries matching this type.
    #[must_use]
    fn interface_method_implementations() -> Vec<(FunctionBuilder<'static>, MethodFlags)> {
        Vec::new()
    }

    /// Returns a default instance of the class for immediate initialization.
    ///
    /// This is used when PHP creates an object without calling the constructor,
    /// such as when throwing exceptions via `zend_throw_exception_ex`. For types
    /// that derive `Default`, this will return `Some(Self::default())`, allowing
    /// the object to be properly initialized even without a constructor call.
    #[must_use]
    fn default_init() -> Option<Self> {
        None
    }

    /// Returns a clone of this object for PHP's `clone` operator.
    ///
    /// When PHP executes `clone $obj`, this method is called to produce a copy
    /// of the Rust data. For types that derive `Clone`, the `#[php_class]` macro
    /// generates an implementation returning `Some(self.clone())`. Types that
    /// don't implement `Clone` use the default (returns `None`), which causes
    /// PHP to throw an error when attempting to clone.
    #[must_use]
    fn clone_obj(&self) -> Option<Self> {
        None
    }
}

/// Stores metadata about a classes Rust constructor, including the function
/// pointer and the arguments of the function.
pub struct ConstructorMeta<T> {
    /// Constructor function.
    pub constructor: fn(&mut ExecuteData) -> ConstructorResult<T>,
    /// Function called to build the constructor function. Usually adds
    /// arguments.
    pub build_fn: fn(FunctionBuilder) -> FunctionBuilder,
    /// Add constructor modification
    pub flags: Option<MethodFlags>,
}

/// Result returned from a constructor of a class.
pub enum ConstructorResult<T> {
    /// Successfully constructed the class, contains the new class object.
    Ok(T),
    /// An exception occurred while constructing the class.
    Exception(PhpException),
    /// Invalid arguments were given to the constructor.
    ArgError,
}

impl<T, E> From<std::result::Result<T, E>> for ConstructorResult<T>
where
    E: Into<PhpException>,
{
    fn from(result: std::result::Result<T, E>) -> Self {
        match result {
            Ok(x) => Self::Ok(x),
            Err(e) => Self::Exception(e.into()),
        }
    }
}

impl<T> From<T> for ConstructorResult<T> {
    fn from(result: T) -> Self {
        Self::Ok(result)
    }
}

/// Stores the class entry, handlers, and property descriptors for a Rust type
/// which has been exported to PHP. Usually allocated statically.
pub struct ClassMetadata<T: 'static> {
    handlers: OnceCell<ZendObjectHandlers>,
    field_properties: &'static [PropertyDescriptor<T>],
    method_properties: OnceCell<&'static [PropertyDescriptor<T>]>,
    method_mangled_names: OnceCell<Box<[Box<str>]>>,
    ce: AtomicPtr<ClassEntry>,

    // `AtomicPtr` is used here because it is `Send + Sync`.
    // fn() -> T could have been used but that is incompatible with const fns at
    // the moment.
    phantom: PhantomData<AtomicPtr<T>>,
}

impl<T: 'static> ClassMetadata<T> {
    /// Creates a new class metadata instance with the given field properties.
    #[must_use]
    pub const fn new(field_properties: &'static [PropertyDescriptor<T>]) -> Self {
        Self {
            handlers: OnceCell::new(),
            field_properties,
            method_properties: OnceCell::new(),
            method_mangled_names: OnceCell::new(),
            ce: AtomicPtr::new(std::ptr::null_mut()),
            phantom: PhantomData,
        }
    }
}

impl<T: 'static> Default for ClassMetadata<T> {
    fn default() -> Self {
        Self::new(&[])
    }
}

impl<T: RegisteredClass> ClassMetadata<T> {
    /// Returns an immutable reference to the object handlers contained inside
    /// the class metadata.
    pub fn handlers(&self) -> &ZendObjectHandlers {
        self.handlers.get_or_init(ZendObjectHandlers::new::<T>)
    }

    /// Checks if the class entry has been stored, returning a boolean.
    pub fn has_ce(&self) -> bool {
        !self.ce.load(Ordering::SeqCst).is_null()
    }

    /// Retrieves a reference to the stored class entry.
    ///
    /// # Panics
    ///
    /// Panics if there is no class entry stored inside the class metadata.
    pub fn ce(&self) -> &'static ClassEntry {
        // SAFETY: There are only two values that can be stored in the atomic
        // ptr: null or a static reference to a class entry. On the null case,
        // `as_ref()` will return `None` and the function will panic.
        unsafe { self.ce.load(Ordering::SeqCst).as_ref() }
            .expect("Attempted to retrieve class entry before it has been stored.")
    }

    /// Stores a reference to a class entry inside the class metadata.
    ///
    /// # Panics
    ///
    /// Panics if the class entry has already been set in the class metadata.
    /// This function should only be called once.
    pub fn set_ce(&self, ce: &'static mut ClassEntry) {
        self.ce
            .compare_exchange(
                std::ptr::null_mut(),
                ce,
                Ordering::SeqCst,
                Ordering::Relaxed,
            )
            .expect("Class entry has already been set");
    }

    /// Finds a property descriptor by name.
    ///
    /// Checks field properties first, then method properties. Linear scan is
    /// used since typical PHP classes have few properties and cache locality
    /// beats hashing at small N.
    #[must_use]
    #[inline]
    pub fn find_property(&self, name: &str) -> Option<&PropertyDescriptor<T>> {
        self.field_properties
            .iter()
            .find(|p| p.name == name)
            .or_else(|| self.method_properties().iter().find(|p| p.name == name))
    }

    /// Returns the field properties (from `#[php(prop)]` struct fields).
    #[must_use]
    #[inline]
    pub fn field_properties(&self) -> &'static [PropertyDescriptor<T>] {
        self.field_properties
    }

    /// Returns the method properties (from `#[php(getter)]`/`#[php(setter)]`).
    /// Lazily initialized on first access from `RegisteredClass::method_properties()`.
    #[must_use]
    #[inline]
    pub fn method_properties(&self) -> &'static [PropertyDescriptor<T>] {
        self.method_properties.get_or_init(T::method_properties)
    }

    /// Iterates over all properties (field + method).
    pub fn all_properties(&self) -> impl Iterator<Item = &PropertyDescriptor<T>> {
        self.field_properties
            .iter()
            .chain(self.method_properties().iter())
    }

    /// Returns pre-computed PHP-convention mangled names for method properties.
    ///
    /// Lazily initialized on first access. One allocation per class for the
    /// entire process lifetime. Field properties already carry compile-time
    /// mangled names in their [`PropertyDescriptor::mangled_name`] field.
    #[must_use]
    #[inline]
    pub fn method_mangled_names(&self) -> &[Box<str>] {
        self.method_mangled_names.get_or_init(|| {
            self.method_properties()
                .iter()
                .map(|desc| {
                    if desc.flags.contains(PropertyFlags::Private) {
                        format!("\0{}\0{}", T::CLASS_NAME, desc.name).into_boxed_str()
                    } else if desc.flags.contains(PropertyFlags::Protected) {
                        format!("\0*\0{}", desc.name).into_boxed_str()
                    } else {
                        desc.name.into()
                    }
                })
                .collect()
        })
    }
}
