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

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

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

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


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

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

# %% ../nbs/08_meta.ipynb 14
from .cons import ANNOYING_ATTRS, MODULE_TYPE_ATTRS
from .type import T, O
from .prot import NotProtocol, OptProtocol
from .enum import Species, LoaderState
from .errs import DuckException, DuckNameError, DuckImportError, DuckAttributeError, DuckModuleNotFoundError
from .solo import Module
from .util import find_last_spec
from .grds import isclstype, isalias, hasduck, hasload

# %% ../nbs/08_meta.ipynb 16
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, DuckModuleMeta))
        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 Module
                    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 Module
                    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 Module

# %% ../nbs/08_meta.ipynb 18
class ABCSpecies(ABCMeta):
    __species__: ClassVar[Species] = Species.REG
    '''The specific "species" of the protocol. Defaults to `REG` (regular).'''
        
    def __reg__(cls) -> type[T]:
        '''Set the "species" to `REG` (regular).'''
        cls.__species__ = Species.REG
        return cls
    
    def __not__(cls) -> Species:
        '''Set the "species" to `NOT` (negated).'''
        cls.__species__ = Species.NOT
        return cls
    
    def __opt__(cls) -> Species:
        '''Set the "species" to `OPT` (optional).'''
        cls.__species__ = Species.OPT
        return cls
    
    def __migrate__(cls, species: Species) -> type[T]:
        '''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

# %% ../nbs/08_meta.ipynb 20
# class DuckMeta(_ProtocolMeta, ABCSpecies, type):
class ABCDuck(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(('type', 'dtype', 'types'))
    '''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__`.''';
    
    @property
    def __types__(cls) -> tuple[type, ...]:
        '''Return the types that the protocol is duck-typed to.'''
        types = []
        for t in cls.__ducktype__:
            if hasduck(t): 
                try:
                    types.append(t.duck)
                    continue
                except Exception: ...
            if hasload(t): 
                try:
                    types.append(t.load())
                except Exception: ...
                
            if isclstype(t): 
                types.append(t)
            else: 
                types.append(type(t))
        
        return tuple(types)
    
    @property
    def __typesq__(cls) -> tuple[type, ...]:
        '''Return the types that the protocol is duck-typed to, including NoneType.'''
        return cls.__types__ + (NoneType, )
    
    @property
    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, cls.__types__)
    
    def __guardq__(cls, inst: Any) -> TypeGuard[type[T]]: 
        '''Return a type guard for the protocol, including NoneType.'''
        return isinstance(inst, cls.__typesq__)
    
    def __alias__(cls: type[T], inst: Any) -> TypeGuard[type[T]]:
        return isalias(
            inst, 
            types = cls.__ducktype__, 
            # 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.'''
        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__
        base = f'<duck {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.'''
        species = kwargs.get('species', cls.__species__)
        match species:
            case Species.REG: fn = cls.__guard__
            case Species.NOT: fn = cls.__notinstance__
            case Species.OPT: fn = cls.__optinstance__
            case _: fn = cls.__guard__
        results = [fn(arg) for arg in args]
        if len(results) == 1: return results[0]
        return tuple(results)
            
        
