# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/06_prot.ipynb.

# %% auto 0
__all__ = ['defprops', 'ProtocolBaseMeta', 'ProtocolMeta', 'AliasMeta', 'Alias', 'NotMeta', 'Not', 'OptionalMeta', 'Opt',
           'PrototypeMeta', 'Prototype']

# %% ../nbs/06_prot.ipynb 6
from abc import abstractmethod
from functools import wraps

# %% ../nbs/06_prot.ipynb 8
from types import (NoneType,)
from typing import (
    Self, Type, Union, TypeGuard, ClassVar, Callable, 
    Iterable, Optional, Protocol, _ProtocolMeta, runtime_checkable, overload, 
)

# %% ../nbs/06_prot.ipynb 10
#| export


# %% ../nbs/06_prot.ipynb 12
from nchr import NIL, TILDE
from nlit import (
    ALT,  TYPES, PREFIX, SUFFIX, CLSNAME, DROPNONE, 
    __QUALNAME__, __PROPS__, __MODULE__, __ANNOTATIONS__, __BASES__, 
)
from pdec import Property, slotprops
from matr import matr

# %% ../nbs/06_prot.ipynb 14
from .cons import (ALLATTRS, CALLFNCS, ProtocolMethod)
from .atyp import T, Types, IterT, IterFunc, IterStr, Guard, AttrGuards
from .grds import isalias
from .util import opttypes, filtkwds, qualname, getname

# %% ../nbs/06_prot.ipynb 16
defprops = dict(
    btype    = Property('btype', type, None, 'basetype for the protocol'),
    types    = Property('types', IterT, tuple(), 'types that the protocol represents'),
    guards   = Property('guards', IterFunc, tuple(), 'guard functions used to validate the protocol'),
    attrs    = Property('attrs', dict, dict(), 'attributes and their expected values for instances of the protocol'),
    hasattrs = Property('hasattrs', IterStr, tuple(), 'attributes that instances of the protocol must have'),
    allattrs = Property('allattrs', IterStr, ALLATTRS, 'attrs of which all values in the iterable are to be checked against the type guards.'),
    mapattrs = Property('mapattrs', AttrGuards, dict(), 'attributes and custom functions to be used to validate the protocol'),
    priority = Property('priority', IterStr, CALLFNCS, 'priority of the overloaded __call__ method'),
    inverted = Property('inverted', bool, False, 'whether the protocol is inverted'),
    optional = Property('optional', bool, False, 'whether the protocol is optional'),
)
'''default properties for protocols''';

# %% ../nbs/06_prot.ipynb 17
@slotprops(defprops)
class ProtocolBaseMeta(_ProtocolMeta):
    '''A metaclass for creating protocol base classes with custom attributes and guards.

    This metaclass allows for the definition of custom protocol classes with specific type checking,
    attribute requirements, and guard functions.

    Attributes
    ----------
    types : IterT
        Types to check against in the protocol.
        
    guards : IterFunc
        Additional guard functions for type checking.
        
    attrs : dict
        Attributes to compare values against.
        
    hasattrs : IterStr
        Attributes that must exist in the instance.
        
    allattrs : IterStr
        Attributes to check that values are all the same if the object is iterable.
        
    mapattrs : AttrGuards
        Attributes to check with custom functions.
        
    priority : IterStr
        Priority of the overloaded `__call__` method.
        
    inverted : bool
        Whether the protocol is inverted.
        
    optional : bool
        Whether the protocol is optional.
        
    _slots : ClassVar[dict[str, Property]]
        Defined in `slotprops` decorator.

    Methods
    -------
    __init__(self, name, bases, dct, **kwargs)
        Initialize the ProtocolBaseMeta instance.
        
    __new__(mcls, name, bases, dct, **kwargs)
        Create a new class instance.
        
    __instanceguard__(self, ins)
        Check if an instance conforms to the protocol.
        
    guard(self, obj)
        The actual guard function for the protocol.
        
    __instancecheck__(self, ins)
        Check if an instance conforms to the protocol.
        
    __invopt__(self)
        Invert optional.
        
    __invert__(self)
        Invert the protocol.
        
    __not__(self)
        Negate the protocol.
        
    default_kwds(cls, **kwargs)
        Return default keyword arguments.
        
    setqualname(cls, *types, name, **kwargs)
        Set the qualified name of the class.
        
    getbtype(cls)
        Base type (subclass of protocol) that the metaclass should reference.
        
    makebases(cls, kls)
        Determine the base classes for a new class instance.
        
    check(self, *vals)
        Map `cls.guard` to a tuple of values.
        
    new(cls, *types, **kwargs)
        Use class as a decorator.
        
    construct(cls, *args, **kwargs)
        Construct a class from `cls.types`.
        
    typeguard(cls, *args, **kwargs)
        Construct a typeguard for `cls.types`.
        
    __redirect__(cls, *args, __generator, __decorator, __inschecks, **kwargs)
        Redirect to a specific method based on keyword arguments.
        
    __call__(cls, *args, **kwargs)
        Overloaded call method for various functionalities.

    Examples
    --------    
    >>> class ProtocolMeta(ProtocolBaseMeta):
    ...     \'\'\'A metaclass for creating protocol classes with custom type checking.
    ...
    ...     Methods
    ...     -------
    ...     getbtype(cls)
    ...         Returns the base type for the protocol class.
    ...    \'\'\'
    >>>    @classmethod
    >>>    def getbtype(cls): return ProtoType
    ...
    >>> @runtime_checkable
    >>> class ProtoType(Protocol):
    >>>    \'\'\'Base protocol class with custom type checking capabilities.\'\'\'
    '''
    types: IterT = tuple()        # types to check
    guards: IterFunc = tuple()    # additional guards to check
    attrs: dict = dict()          # attributes compare values against
    hasattrs: IterStr = tuple()   # attributes to ensure exist
    allattrs: IterStr = ALLATTRS  # attributes to check that values are all the same if object is iterable
    mapattrs: AttrGuards = dict() # attributes to check with custom functions
    priority: IterStr = CALLFNCS  # priority of the overloaded __call__ method
    inverted: bool = False        # whether the protocol is inverted
    optional: bool = False        # whether the protocol is optional
    __PROPS__: ClassVar[dict[str, Property]] # defined in `slotprops` decorator (was formally ._slots)
    # clsattrs: ClassVar[tuple[str]] = ('types', 'guards', 'attrs', 'hasattrs', 'allattrs', 'mapattrs', 'priority')
    
    def __init__(self: Self, name: str, bases: tuple = tuple(), dct: dict = dict(), **kwargs):
        # super().__init__(name, bases, dct)
        for attr, prop in self.__props__.items():
            aval = kwargs.get(attr, prop.val)
            setattr(self, prop.prv, aval)
            
        self.types = matr.ib(self.types)
        
    def __new__(mcls: Type[Self], name: str, bases: tuple = tuple(), dct: dict = dict(), **kwargs):
        dct.update(**mcls.getprops(**kwargs))
        # dct.update(**mcls.getprops(mcls, **kwargs))
        new = super().__new__(mcls, name, bases, dct)
        for attr, value in kwargs.items():
            setattr(new, attr, value)
        
        kwargs.update(mcls.default_kwds(**kwargs))
        new.types = matr.ib(new.types)
        new.setqualname(*new.types, **kwargs)
        return new

    # @classmethod
    def __init_subclass__(cls: Type[Self], *args, **kwargs):
        super().__init_subclass__(*args, **kwargs)
        for attr, aval in cls.getprops(*args, **kwargs).items():
        # for attr, aval in cls.getprops(cls, *args, **kwargs).items():
            if attr == TYPES:
                setattr(cls, TYPES, opttypes(aval, kwargs.get(DROPNONE, True)))
            else: 
                setattr(cls, attr, aval)
                
    def __invert__(self: Self) -> Self:
        '''Invert the protocol.'''
        self.inverted = not self.inverted
        return self
    
    def __not__(self: Self) -> Self:
        '''Negate the protocol.'''
        self.inverted = not self.inverted
        return self
    
    def __invopt__(self: Self) -> Self:
        '''Invert optional.'''
        self.optional = not self.optional
        return self
                        
    def __instanceguard__(self: Self, ins: object) -> Guard:
        '''Instance method to check if an instance conforms to the protocol.'''
        self.types = matr.ib(self.types)
        res = isalias(ins, self.types, self.guards, self.attrs, self.hasattrs, self.allattrs, self.mapattrs)
        if self.optional: 
            res = res or ins is None
        if self.inverted: 
            res = not res
        return res
    
    def guard(self, obj: object) -> Guard:
        '''The actual guard function for the protocol.'''
        return self.__instanceguard__(obj)

    def __instancecheck__(self: Self, ins: object) -> Guard:
        '''Instance method to check if an instance conforms to the protocol.'''
        return self.guard(ins)
    
    def check(self: Self, *vals) -> Iterable[Guard]:
        '''Map `cls.guard` to a tuple of values.'''
        if len(vals) == 1:
            return self.guard(vals[0])
        return tuple(map(self.guard, vals))

    @classmethod
    def default_kwds(cls: Type[Self], **kwargs) -> dict:
        return {k: kwargs.get(k, v) for k, v in zip((ALT, PREFIX, SUFFIX, DROPNONE), (None, NIL, NIL, True))}

    # @classmethod
    def setqualname(cls: Type[Self], *types, name: str = None, **kwargs) -> Type[Self]: 
        if name is None or name == NIL:
            kwds = filtkwds(qualname, **kwargs)
            kwds.pop(TYPES, None)
            name = qualname(cls, *types, **kwds)
        setattr(cls, __QUALNAME__, name)
        return cls
    
    @classmethod
    @abstractmethod
    def getbtype(cls) -> type:
        '''base type (subclass of protocol) that the metaclass should reference.'''
        return None
    
    @classmethod
    def makebases(cls: Type[Self], kls: Optional[T] = None) -> Types:
        '''Class method to determine the base classes for a new class instance.'''
        btype = cls.getbtype()
        
        nobases = not hasattr(kls, __BASES__) and not isinstance(kls, type)
        if kls is None or nobases: return (btype, )
        bases = (kls, btype) if btype not in kls.__bases__ else (kls, )
        return bases
    
    @classmethod
    def new(cls: Type[Self], *types: Types, **kwargs) -> Type['ProtocolBaseMeta']:
        def decorator(kls: Optional[T] = None):
            clsname = kwargs.get(CLSNAME, getname(kls))
            bases = cls.makebases(kls)
            cdict = dict()
            new = cls.__class__(clsname, bases, cdict, types=types, **kwargs)
            return new
        return decorator
    
    # @classmethod
    def construct(cls: Type[Self], *args, **kwargs) -> Type[Self]:
        '''Try to construct a class from `cls.types`.'''
        for typ in cls.types:
            if callable(typ):
                try: return typ(*args, **kwargs)
                except: continue
        return None
    
    def typeguard(cls: Self) -> Callable[[T], TypeGuard[Self]]:
        f'''Generate the `is{cls.__name__}` type guard method for the protocol.'''
        cname = cls.__name__
        fname = f'is{cname}'
        @wraps(cls.guard, assigned=(__MODULE__, __ANNOTATIONS__))
        def isins(x: object) -> TypeGuard[Self]:
            f'''Check if `x` is an instance of `{cname}`.'''
            return isinstance(x, cls)
        
        isins.__name__ = fname
        isins.__qualname__ = fname
        return isins
    
    def __redirect__(
        cls: Type[Self], *args,
        __generator: bool = False, 
        __decorator: bool = False, 
        __inschecks: bool = False,
        __typeguard: bool = False,
        **kwargs
    ) -> Union[Iterable[Guard], Type[Self], Type['ProtocolBaseMeta']]:
        '''Call a one of the methods invoked by the overloaded `__call__` method by a specific
        keyword argument.
        
        Parameters
        ----------
        args : tuple
            Arguments to pass to the method.
            
        kwargs : dict
            Keyword arguments to pass to the method. 
        
        Other Parameters
        ----------------
        __generator : bool
            Whether to try and construct a class from `cls.types`.
            If True, call `cls.con` method.
            
        __decorator : bool
            Whether to use class as a decorator.
            If True, call `cls.new` method.
            
        __inschecks : bool
            Whether to invoke `__instancecheck__`s. If True, calls 
            `cls.check` method.
            
        __typeguard : bool
            Whether to generate a type guard method. If True, calls
            `cls.typeguard` method.
            
        __gen : bool
            Shorthand alias for `__generator`
            
        __dec : bool
            Shorthand alias for `__decorator`
            
        __ins : bool
            Shorthand alias for `__inschecks`
            
        __tfn : bool
            Shorthand alias for `__typeguard`
        '''
        __generator = __generator or kwargs.pop('__gen', __generator)
        __decorator = __decorator or kwargs.pop('__dec', __decorator)
        __inschecks = __inschecks or kwargs.pop('__ins', __inschecks)
        __inschecks = __inschecks or kwargs.pop('__tfn', __inschecks)
        for opt in cls.priority:
            match opt:
                case ProtocolMethod.ins:
                    if __inschecks: return cls.check(*args)
                case ProtocolMethod.dec:
                    if __decorator: return cls.new(*args, **kwargs)
                case ProtocolMethod.gen:
                    if __generator: return cls.construct(*args, **kwargs)
                case ProtocolMethod.tfn:
                    if __typeguard: return cls.typeguard()
                case _: continue
        return None
    
    @overload
    def __call__(cls: 'ProtocolBaseMeta', base: type, *dtypes: Types, **kwargs) -> Guard: ...
    @overload
    def __call__(cls: type, instance: object) -> Guard: ...
    @overload
    def __call__(cls: type, *instances: tuple[object, ...]) -> Guard: ...
    @overload
    def __call__(cls: type, *args, **kwargs) -> Guard: ...    
    def __call__(cls: Self, *args, **kwargs) -> Guard:
        res = cls.__redirect__(*args, **kwargs)
        if res is not None: return res
        
        # NOTE: if we get here, we will have to guess which method to use
        hasargs = lambda: len(cls.types) > 0 and len(args) > 0
        arg0cls = lambda: isinstance(args[0], type) or args[0] is NoneType
        
        # option one, use the `check` instances
        if hasargs() and not arg0cls():
            gidx = cls.priority.index('gen')
            iidx = cls.priority.index('ins')
            if gidx < iidx: return cls.construct(*args, **kwargs)
            return cls.check(*args)
        
        # option two: create a new class instance via decorator
        deco = cls.new(*args, **kwargs)
        return deco

# %% ../nbs/06_prot.ipynb 18
class ProtocolMeta(ProtocolBaseMeta):
    '''A metaclass for creating protocol classes with custom type checking.

    This metaclass extends `ProtocolBaseMeta` to provide additional functionalities 
    specific to protocol classes.

    Methods
    -------
    getbtype(cls)
        Returns the base type for the protocol class.

    Examples
    --------
    >>> @runtime_checkable
    >>> class ProtoType(Protocol):
    >>>    \'\'\'Base protocol class with custom type checking capabilities.\'\'\'
    '''
    @classmethod
    def getbtype(cls): return Protocol

# %% ../nbs/06_prot.ipynb 20
class AliasMeta(ProtocolMeta):
    '''Metaclass for creating alias classes with custom type checking.'''
    @classmethod
    def getbtype(cls): return Alias

@runtime_checkable
class Alias(Protocol, metaclass=AliasMeta):
    '''Class representing an alias with custom type checking.
    
    Examples
    --------
    >>> @Alias(str, bytes)
    >>> class StrBytes: ...

    >>> @Alias(int, str)
    >>> class IntStr: ...

    >>> IntStr, type(IntStr), IntStr.types
    (abc.IntStr({int, str}), __main__.AliasMeta, (int, str))

    >>> IntStr(1, '2', None, [], __ins=True)
    (True, True, False, False)
    '''

# %% ../nbs/06_prot.ipynb 22
class NotMeta(ProtocolMeta):
    '''Metaclass for creating negation classes with custom type checking.'''
    @classmethod
    def getbtype(cls): return Not

    @classmethod
    def default_kwds(cls, **kwargs) -> dict:
        kwds = super().default_kwds(**kwargs)
        kwds[PREFIX] = TILDE
        return kwds
    
    def guard(self, obj) -> Guard:
        return not super().guard(obj)
    
@runtime_checkable
class Not(Protocol, metaclass=NotMeta):
    '''Class representing a negation with custom type checking.
    
    Examples
    --------
    >>> @Not(str, bytes)
    >>> class NotStrBytes: ...

    >>> @Not(int)
    >>> class NotInt: ...

    >>> vals = (10, None, 'hello', 42.30, tuple())
    >>> NotInt(*vals,  __ins=True)
    (False, True, True, True, True)
    '''

# %% ../nbs/06_prot.ipynb 24
class OptionalMeta(ProtocolMeta):
    '''Metaclass for creating optional type classes with custom type checking.'''
    @classmethod
    def getbtype(cls): return Opt

    @classmethod
    def default_kwds(cls, **kwargs) -> dict:
        kwds = super().default_kwds(**kwargs)
        kwds[PREFIX] = '?'        
        return kwds
    
    def __new__(mcls, name, bases, dct, **kwargs):
        '''Create a new class instance.'''
        types = kwargs.pop(TYPES, tuple())
        for ntype in (None, ):
            if ntype not in types:
                types += (ntype, )
        dct[TYPES] = types
        new = super().__new__(mcls, name, bases, dct, types = types, **kwargs)
        return new
    
@runtime_checkable
class Opt(Protocol, metaclass=OptionalMeta):
    '''Class representing an optional type with custom type checking.
    
    Examples
    --------
    >>> @Opt(str)
    >>> class StrQ: ...

    >>> @Opt(int)
    >>> class IntQ: ...

    >>> vals = (10, None, 'hello', 42.30, tuple())
    >>> IntQ(*vals,  __ins=True)
    (True, True, False, False, False)

    >>> @Alias(IntQ, str)
    >>> class IntQStr: ...

    >>> IntQStr(*vals,  __ins=True)
    (True, True, True, False, False)
    '''

# %% ../nbs/06_prot.ipynb 26
class PrototypeMeta(ProtocolMeta):
    '''Metaclass for creating optional type classes with custom type checking.'''  
    q: Opt # optional version of self (i.e. with `None` added to `types`)
    n: Not # negated version of self (i.e. `not self.guad()`)
    
    @classmethod
    def getbtype(cls): return Prototype
    def __new__(mcls: Type[Self], name: str, bases: tuple = tuple(), dct: dict = dict(), **kwargs):
        types = kwargs.pop(TYPES, ())
        new = super().__new__(mcls, name, bases, dct, types=types, **kwargs)
        
        @Not(new, **kwargs)
        class notcls: ...
        
        @Opt(new, **kwargs)
        class optcls: ...
        
        notcls.__qualname__ = qualname(new, *new.types, **Not.default_kwds(**kwargs))
        optcls.__qualname__ = qualname(new, *new.types, **Opt.default_kwds(**kwargs))
        notcls.__name__ = f'{TILDE}{new.__name__}'
        optcls.__name__ = f'?{new.__name__}'
        
        setattr(new, 'n', notcls)
        setattr(new, 'q', optcls)
        return new      
    
@runtime_checkable
class Prototype(Protocol, metaclass=PrototypeMeta):
    '''Class representing an alias with custom type checking.
    
    Examples
    --------
    >>> @Alias(str, bytes)
    >>> class StrBytes: ...
    ...
    >>> @Alias(int, str)
    >>> class IntStr: ...
    ...
    >>> @Not(str, bytes)
    >>> class NotStrBytes: ...
    ...
    >>> @Not(int)
    >>> class NotInt: ...
    ...
    >>> @Opt(str)
    >>> class StrQ: ...
    ...
    >>> @Opt(int)
    >>> class IntQ: ...
    ...
    >>> @Alias(IntQ, str)
    >>> class IntQStr: ...
    ...
    >>> vals = (10, None, 'hello', 42.30, tuple())
    >>> tuple(map(lambda c: c(*vals, __ins=True), (IntStr, NotInt, IntQ, IntQStr, )))
    ( # (10,    None, 'hello', 42.30,  tuple())
        (True, False,    True, False,  False), # IntStr
        (False, True,    True,  True,   True), # NotInt
        (True,  True,   False, False,  False), # OptInt
        (True,  True,    True, False,  False)  # IntQStr
    )
    
    ...
    ...
    >>> @Typed(int)
    >>> class tint: ...
    ...
    >>> tuple(((k.__name__, k.__qualname__) for k in (tint, tint.n, tint.q)))
    (('tint', 'tint({int})'), ('~tint', 'tint(~{int})'), ('?tint', 'tint(?{int})'))
    ...
    >>> tint(None, 1, 's', __ins=True), tint.q(None, 1, 's', __ins=True), tint.n(None, 1, 's', __ins=True)
    ((False, True, False), (True, True, False), (True, False, True))
    ...
    >>> vals = (10, None, 'hello', 42.30, tuple())
    >>> tuple(map(lambda x: (tint(x, __ins=True), tint.q(x, __ins=True), tint.n(x, __ins=True)), vals))
    # tint, tint.q, tint.n)
    ((True,  True,  False),
    (False, True,   True),
    (False, False,  True),
    (False, False,  True),
    (False, False,  True))
    ...
    ...
    # prioritize the order of the __call__ methods to `('ins', 'gen', 'dec')` 
    # i.e. (`check`, `con`, `new`). Note that the default order is `('gen', 'ins', 'dec')`
    # NOTE: default order of the __call__ methods (`check`, `con`, `new`)
    >>> @Typed(int, priority=('ins', 'gen', 'dec'))
    >>> class tint: ...
    ...
    >>> tint(None, 1, 's'), tint.q(None, 1, 's'), tint.n(None, 1, 's')
    ((False, True, False), (True, True, False), (True, False, True))
    '''
    q: Opt # optional version of self (i.e. with `None` added to `types`)
    n: Not # negated version of self (i.e. `not self.guad()`)
