# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/09_meta.ipynb.

# %% auto 0
__all__ = ['ABCSpecies', 'ABCDuck', 'DuckProtocolMeta', 'DuckModuleMeta']

# %% ../nbs/09_meta.ipynb 6
import sys
from abc import ABCMeta, abstractmethod
from functools import wraps, partial
from inspect import isclass
from importlib import import_module
from importlib.util import (module_from_spec)
from importlib.machinery import ModuleSpec

# %% ../nbs/09_meta.ipynb 8
from types import ModuleType, NoneType, FunctionType
from typing import (Any, Self, Callable, TypeGuard, ClassVar, _ProtocolMeta)

# %% ../nbs/09_meta.ipynb 10
#| export

# %% ../nbs/09_meta.ipynb 12
#| export

# %% ../nbs/09_meta.ipynb 14
from quac.cons import (
    ANNOYING_ATTRS, MODULE_TYPE_ATTRS,
    _DUCK, ALL_ATTRS,
    __BASES__, __PROPS__, __MODULE__, __QUALNAME__, __ANNOTATIONS__,  
    __HUSH__, __DUCK__, __ALTS__, __DUCKMODULE__, __DUCKTYPES__
)
from .type import T, O, P, Ts
from .prot import NotProtocol, OptProtocol, DuckTypeProtocol, DuckSpecProtocol
from .enum import Species, LoaderState
from .errs import DuckException, DuckNameError, DuckImportError, DuckAttributeError, DuckModuleNotFoundError
from .grds import isalias, hasduck, hasload, isclstype, isduck, isduckspec, _istype
from .misc import find_last_spec, prepare_types_for_instancecheck, prepare_ducktypes, prepare_isinstance_types
from .solo import EmptyModule
from .spec import DuckSpec

# %% ../nbs/09_meta.ipynb 17
class ABCParam(ABCMeta):
    __args__: tuple
    _parameterized: bool
    def __getitem__(cls, t: T) -> T: 
        cls.__args__ = tuple(t, )
        cls.__qualname__ = str(cls)
        return cls
    
    def __str__(cls):
        return f'{cls.__name__}[{cls.__args__[0]}]'

# %% ../nbs/09_meta.ipynb 19
class ABCSpecies(ABCMeta):
    __species__: ClassVar[Species] = Species.REG
    '''The specific "species" of the protocol. Defaults to `REG` (regular).'''
        
    def __reg__(cls) -> Self:
        '''Set the "species" to `REG` (regular).'''
        cls.__species__ = Species.REG
        return cls
    
    def __not__(cls) -> Self:
        '''Set the "species" to `NOT` (negated).'''
        cls.__species__ = Species.NOT
        return cls
    
    def __opt__(cls) -> Self:
        '''Set the "species" to `OPT` (optional).'''
        cls.__species__ = Species.OPT
        return cls
    
    def __migrate__(cls, species: Species) -> Self:
        '''Migrate the protocol to a different "species".'''
        match species:
            case Species.REG: return cls.__reg__()
            case Species.NOT: return cls.__not__()
            case Species.OPT: return cls.__opt__()
            case _: return cls # NOTE: fail silently
        return cls
    
    def __hash__(self) -> int:
        return super().__hash__()

# %% ../nbs/09_meta.ipynb 21
class ABCDuck(_ProtocolMeta, ABCSpecies, type):
    __ducktype__: ClassVar[tuple[type[T], ...]] = tuple()
    '''The specific types that the protocol is duck-typed to.''';
    
    __expected__: ClassVar[dict[str, Any]] = dict()
    '''Dictionary of attributes and their expected values to check in the object.''';
    
    __required__: ClassVar[tuple[str, ...]] = tuple()
    '''Iterable of attribute names that the object must have.''';
    
    __allequal__: ClassVar[tuple[str, ...]] = tuple(ALL_ATTRS)
    '''Iterable of attributes to check if their values are all the same in an iterable object.''';

    __extrafns__: ClassVar[dict[str, Callable[[O, T], TypeGuard[T]]]] = dict()
    '''Mapping of attribute names to guard functions for additional checks.''';
    
    __usealias__: ClassVar[bool] = True
    '''Whether or not to use the `isalias` function for instance checking or `__guard__`.''';
    
    
    def __types__(cls) -> tuple[type, ...]:
        '''Return the types that the protocol is duck-typed to.'''
        curtypes = list(cls.__ducktype__)
        unducked = prepare_ducktypes(cls)
        cls.__ducktype__ = unducked
        return unducked
    
    def __typesq__(cls) -> tuple[type, ...]:
        '''Return the types that the protocol is duck-typed to, including NoneType.'''
        return cls.__types__() + (NoneType, )
    
    def __usetypes__(cls) -> tuple[type, ...]:
        '''Return the types that the protocol is duck-typed and to use.'''
        if cls.__species__ == Species.OPT: return cls.__typesq__()
        return cls.__types__()
    
    def __guard__(cls, inst: Any) -> TypeGuard[type[T]]: 
        '''Return a type guard for the protocol.'''
        return isinstance(inst, prepare_isinstance_types(cls.__types__()))
    
    def __guardq__(cls, inst: Any) -> TypeGuard[type[T]]: 
        '''Return a type guard for the protocol, including NoneType.'''
        return isinstance(inst, prepare_isinstance_types(cls.__typesq__()))
    
    def __alias__(cls, inst: Any) -> TypeGuard[type[T]]:
        isfn: FunctionType = partial(isalias, 
            attrs = cls.__expected__, 
            hasattrs = cls.__required__, 
            allattrs = cls.__allequal__, 
            mapattrs = cls.__extrafns__
        )
        match cls.__species__:
            case Species.REG: return isfn(inst, types=cls.__types__())
            case Species.NOT: return not isfn(inst, types=cls.__types__())
            case Species.OPT: return isfn(inst, types=cls.__typesq__())
        return isalias(
            inst, 
            types = cls.__types__(), 
            guards = (cls.__guard__, ),
            attrs = cls.__expected__, 
            hasattrs = cls.__required__, 
            allattrs = cls.__allequal__, 
            mapattrs = cls.__extrafns__
        )
    
    def __instancecheck__(cls, inst: Any) -> TypeGuard[type[T]]: 
        '''Instance check for the protocol using custom `__guard__` method
        which enables duck-typing and inheritance.'''
        if cls.__usealias__:  
            return cls.__alias__(inst)
        match cls.__species__:
            case Species.REG: return cls.__guard__(inst)
            case Species.NOT: return cls.__notinstance__(inst)
            case Species.OPT: return cls.__optinstance__(inst)
            case _: return cls.__guard__(inst)
        return cls.__guard__(inst)
    
    def __subclasscheck__(cls, inst: Any) -> TypeGuard[type[T]]: 
        '''Instance check for the protocol using custom `__guard__` method
        which enables duck-typing and inheritance.'''
        # print('subclass check', cls, type(cls), inst, type(inst), )
        match cls.__species__:
            case Species.REG: return issubclass(inst, cls.__types__())
            case Species.NOT: return not issubclass(inst, cls.__types__())
            case Species.OPT: return issubclass(inst, cls.__typesq__())
        return issubclass(inst, cls.__types__())
    
    def __notinstance__(cls, inst: Any) -> TypeGuard[NotProtocol[T]]:
        '''Return a type guard for NOT the protocol.'''
        return not cls.__instancecheck__(inst)

    def __optinstance__(cls, inst: Any) -> TypeGuard[OptProtocol[T]]:
        '''Return a type guard for the protocol or NoneType.'''
        return cls.__guardq__(inst)

    def __invert__(cls) -> NotProtocol[T]:
        '''Return the NOT type guard for the protocol.'''
        return cls.__notinstance__

    def __memberguard__(cls, inst: Any) -> TypeGuard[type[T]]: 
        '''Member check for the protocol using custom `__types__` method
        which enables duck-typing and inheritance.'''
        types = cls.__types__()
        if cls.__species__ == Species.OPT: types = cls.__typesq__()
        def checkmember(inst: Any) -> TypeGuard[type[T]]:
            return bool((inst in types) ^ (cls.__species__ == Species.NOT))
        return checkmember

    def __membercheck__(cls, inst: Any) -> TypeGuard[type[T]]: 
        '''Member check for the protocol using custom `__types__` method
        which enables duck-typing and inheritance.'''
        match cls.__species__:
            case Species.REG: return inst in cls.__types__()
            case Species.NOT: return not inst in cls.__types__()
            case Species.OPT: return inst in cls.__typesq__()
            case _: return inst in cls.__types__()

    def __contains__(cls, inst: Any) -> TypeGuard[type[T]]: 
        '''Instance check for the protocol using custom `__guard__` method
        which enables duck-typing and inheritance.'''
        return cls.__membercheck__(inst)
    
    def __eq__(cls, inst: Any) -> TypeGuard[type[T]]: 
        '''Equality check for the protocol using custom `__guard__` method'''
        try: 
            if cls.__instancecheck__(inst): 
                return True
        except TypeError as inserror: ...
        try: 
            if cls.__subclasscheck__(inst):
                return True
        except TypeError as suberror: ...
        try: 
            if inst in cls.__types__():
                return True
        except TypeError as inerror: ...
        try: 
            if inst is cls:
                return True
        except TypeError as iserror: ...
        return False
    
    def __repr__(cls) -> TypeGuard[type[T]]:
        name = cls.__name__
        match cls.__species__:
            case Species.REG: return f'<duck {name}>'
            case Species.NOT: return f'<!duck {name}>'
            case Species.OPT: return f'<?duck {name}>'
            
    def __call__(cls, *args, **kwargs) -> TypeGuard[type[T]]:
        '''Return the protocol class itself.'''

        fn = cls.__instancecheck__
            
        
        # print(cls, args, fn, fn.__name__)
        results = [fn(arg) for arg in args]
        if len(results) == 1: return results[0]
        return tuple(results)

    def __hash__(self) -> int:
        return super().__hash__()
    
    def __new__(mcls, name, bases, namespace, **kwargs):
        '''Create a new protocol class.'''
        # print('meta.__new__\t', mcls, name, bases, namespace, kwargs)
        new = super().__new__(mcls, name, bases, namespace)
        new = mcls.__metahook__(new, name, bases, namespace, **kwargs)
        return new
        
    @classmethod
    def __metahook__(mcls, cls, name, bases, namespace, **kwargs):
        '''Hook for the protocol metaclass.'''
        # print('meta.__metahook__\t', mcls, cls, name, bases, namespace, kwargs)
        hook = getattr(cls, '__metahook__', None)
        if callable(hook): 
            adj = hook(mcls, name, bases, namespace, **kwargs)
        return cls
    
    def __init__(cls, *args, **kargs):
        ...
        # print('__init__\t', cls, args, kargs)
    
    def __init_subclass__(cls, *args, **kwargs):
        super().__init_subclass__()
        # print('meta.__init_subclass__\t', cls, args, kwargs)
        return 

# %% ../nbs/09_meta.ipynb 36
class DuckProtocolMeta(_ProtocolMeta, ABCSpecies, ABCMeta, type):
    __ducktype__: ClassVar[tuple[type[T], ...]] = tuple()
    '''The specific types that the protocol is duck-typed to.''';
    
    _duck: ClassVar[tuple[type[T], ...]]
    @property
    def __duck__(cls) -> tuple[type[T], ...]:
        if getattr(cls, _DUCK, None) is None:
            unducked = prepare_types_for_instancecheck(cls.__ducktype__)
            setattr(cls, _DUCK, unducked)
        return cls._duck
    
    @abstractmethod
    def __types__(cls) -> tuple[type, ...]:
        '''Return the types that the protocol is duck-typed to.'''
        return prepare_types_for_instancecheck(cls.__duck__)
        duck = list(cls._duck)
        for i, d in enumerate(duck):
            if not (isinstance(d, type) or isclass(d)):
                duck[i] = type(d)
        return tuple(duck)

    @abstractmethod
    def __typesq__(cls) -> tuple[type, ...]: 
        '''Return the types that the protocol is duck-typed to, including NoneType.'''
        return cls.__types__() + (NoneType, )
        
    @abstractmethod
    def __guard__(cls, inst: Any) -> TypeGuard[type[T]]: 
        '''Return a type guard for the protocol.'''
        return isinstance(inst, cls.__types__())
    
    @abstractmethod
    def __guardq__(cls, inst: Any) -> TypeGuard[type[T]]: 
        '''Return a type guard for the protocol, including NoneType.'''
        return isinstance(inst, cls.__typesq__())
        
    def __instancecheck__(cls, inst: Any) -> TypeGuard[type[T]]: 
        '''Instance check for the protocol using custom `__guard__` method
        which enables duck-typing and inheritance.'''
        match cls.__species__:
            case Species.REG: return cls.__guard__(inst)
            case Species.NOT: return cls.__notinstance__(inst)
            case Species.OPT: return cls.__optinstance__(inst)
        return cls.__guard__(inst)
    
    def __subclasscheck__(cls, inst: Any) -> TypeGuard[type[T]]: 
        '''Instance check for the protocol using custom `__guard__` method
        which enables duck-typing and inheritance.'''
        match cls.__species__:
            case Species.REG: return issubclass(inst, cls.__types__())
            case Species.NOT: return not issubclass(inst, cls.__types__())
            case Species.OPT: return issubclass(inst, cls.__typesq__())
        return issubclass(inst, cls.__types__())
    
    @abstractmethod
    def __notinstance__(cls, inst: Any) -> TypeGuard[NotProtocol[T]]:
        '''Return a type guard for NOT the protocol.'''
        return not cls.__instancecheck__(inst)
    
    @abstractmethod
    def __optinstance__(cls, inst: Any) -> TypeGuard[OptProtocol[T]]:
        '''Return a type guard for the protocol or NoneType.'''
        return cls.__guardq__(inst)
    
    @abstractmethod
    def __invert__(cls) -> NotProtocol[T]:
        '''Return the NOT type guard for the protocol.'''
        return cls.__notinstance__
    
    @abstractmethod
    def __membercheck__(cls, inst: Any) -> TypeGuard[type[T]]: 
        '''Member check for the protocol using custom `__types__` method
        which enables duck-typing and inheritance.'''
        match cls.__species__:
            case Species.REG: return inst in cls.__types__()
            case Species.NOT: return not inst in cls.__types__()
            case Species.OPT: return inst in cls.__typesq__()
        return inst in cls.__types__()
    
    def __contains__(cls, inst: Any) -> TypeGuard[type[T]]: 
        '''Instance check for the protocol using custom `__guard__` method
        which enables duck-typing and inheritance.'''
        return cls.__membercheck__(inst)
    
    @abstractmethod
    def __eq__(cls, inst: Any) -> TypeGuard[type[T]]: 
        '''Equality check for the protocol using custom `__guard__` method'''
        try: 
            if cls.__instancecheck__(inst): 
                return True
        except TypeError as inserror: ...
        try: 
            if cls.__subclasscheck__(inst):
                return True
        except TypeError as suberror: ...
        try: 
            if inst in cls.__types__():
                return True
        except TypeError as inerror: ...
        try: 
            if inst is cls:
                return True
        except TypeError as iserror: ...
        return False
    
    def __hash__(self) -> int:
        return super().__hash__()

# %% ../nbs/09_meta.ipynb 38
class DuckModuleMeta(ABCMeta, type):
    __duckmodule__: ClassVar[str]
    '''The name of the module to duck.''';
    
    __hush__: ClassVar[bool] = True
    '''Whether or not to raise AttributeError when an attribute is not found.''';
    
    __alts__: ClassVar[dict[str, type]] = {}
    '''Alternatives to use when an attribute is not found.''';
    
    def __instancecheck__(cls: type[T], obj: Any) -> bool:
        '''Check if the object is an instance of the class.'''
        issubbed = isinstance(obj, (ModuleType, ))
        samename = object.__getattribute__(cls, __DUCKMODULE__) == obj.__name__
        hasattrs = all(hasattr(obj, attr) for attr in MODULE_TYPE_ATTRS)
        hasprivs = all(hasattr(obj, attr) for attr in (__DUCKMODULE__, __HUSH__, __ALTS__))
        if issubbed and samename and (hasattrs or hasprivs): return True
        return False
     
    
    def __getattr__(cls: type[T], name: str) -> Any:
        hush = object.__getattribute__(cls, __HUSH__)
        try:
            if name in ANNOYING_ATTRS: 
                try:
                    # return super().__getattribute__(name)
                    raise AttributeError(f"Module '{cls.__name__}' has no attribute '{name}'.")
                except:
                    if object.__getattribute__(cls, __HUSH__): return EmptyModule
                    raise AttributeError(f'''Module '{cls.__name__}' has no attribute '{name}'.''')
            
            # base module name
            base = object.__getattribute__(cls, __DUCKMODULE__)
            # full module name
            full = f'{base}.{name}' if cls.__name__ != 'DuckModule' else name
            
            mkey = full # key to use for sys.modules
            spec: ModuleSpec | None = None # module spec
            stub: str | None = None
            
            # <base.name> not in sys modules
            if full not in sys.modules:            
                # try using importlib to load the module
                try:
                    module = import_module(full)
                    sys.modules[full] = module
                    mkey = full
                except ModuleNotFoundError: ...
                    
                # try using the last found module spec
                try:
                    spec, stub = find_last_spec(full)
                    module: ModuleType = module_from_spec(spec)
                    sys.modules[stub] = module
                    mkey = stub
                    
                except:
                    if object.__getattribute__(cls, __HUSH__): return EmptyModule
                    raise AttributeError(f'''Module '{cls.__name__}' has no attribute '{name}'.''')
                
            # <base.name> or <stub> in sys.modules
            module = sys.modules[mkey]
            
            # create a DuckModule subclass for the submodule if needed            
            # @wraps(cls, assigned=('__name__', '__qualname__', '__doc__', '__annotations__'), updated=())
            # class ducked(cls):
            #     __duckmodule__ = mkey
                
            ducked = type(mkey, (cls, ), dict(__duckmodule__=mkey, __alts__=cls.__alts__, __hush__=cls.__hush__,))
            # ducked = ducked(module.__name__, module.__doc__)
            # ducked.__dict__ = {**module.__dict__, **ducked.__dict__}
            # ducked.__dict__.update({ **module.__dict__, **ducked.__dict__,**dict(_duck=mkey, __alts__=cls.__alts__, __hush__=cls.__hush__,)})

            # Try to get the attribute from the module if it exists
            try: 
                attr = object.__getattribute__(module, name)
                if LoaderState.get(module) not in [LoaderState.NONE, LoaderState.DUCKED]:
                    sys.modules[mkey] = LoaderState.set(module, LoaderState.EXECED)
                return attr
            except AttributeError: ...
            
            # Try actually loading the module if it hasn't been loaded yet
            try: 
                sys.modules[mkey] = LoaderState.exec(module)
                return object.__getattribute__(module, name)
            except AttributeError: ...
            
            # Handle fallbacks if defined
            if hasattr(cls, __ALTS__) and name in cls.__alts__: return cls.__alts__[name]
            
            return ducked
        
        except NameError as e:
            if not hush: raise DuckNameError(*e.args)
        
        except ModuleNotFoundError as e:
            if not hush: raise DuckModuleNotFoundError(e.args, e.name, e.path)
        
        except ImportError as e:
            if not hush: raise DuckImportError(e.args, e.name, e.path)
        
        except AttributeError as e:
            if not hush: raise DuckAttributeError(e.args, e.name, e.obj)
        
        except Exception as e:
            if not hush: raise DuckException(f'NameError: {e}')
        
        return EmptyModule
    
    def __hash__(self) -> int:
        return super().__hash__()
