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

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

# %% ../nbs/03_meta.ipynb 6
import os, inspect
from abc import abstractmethod
from functools import wraps, singledispatch

# %% ../nbs/03_meta.ipynb 8
from types import NoneType 
from typing import (Type, TypeGuard, Protocol, _ProtocolMeta, runtime_checkable, overload, )

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


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


# %% ../nbs/03_meta.ipynb 14
from .type import (T, Types, Guard, StrBool)
from .util import (classname, qualname, settypes)
from .grds import (aliascheck)

# %% ../nbs/03_meta.ipynb 17
class ProtocolBaseMeta(_ProtocolMeta):
    '''Base metaclass for creating protocol classes with custom type checking.

    This metaclass provides mechanisms to define custom type checks for classes 
    and to dynamically create classes with these checks.

    Attributes
    ----------
    types : Types
        A tuple of types that the class will check against.
    attrs : dict
        A dictionary of attributes that the class will check against.

    Methods
    -------
    getbtype()
        Abstract method to get the base type of the protocol.
    check(obj)
        Checks if an object conforms to the protocol.
    makeguard()
        Creates a guard method for the protocol.
    makebases(cls)
        Determines the base classes for a new class instance.
    default_kwds(**kwargs)
        Provides default keyword arguments for class creation.
    classname(cls, clsname)
        Gets the class name.
    qualname(cls, *types, clsname, prefix, suffix, dropnone)
        Generates a qualified name for the class.
    settypes(cls, *types, clsname, prefix, suffix, dropnone)
        Sets the types for the class.
    setattrs(cls, attrs)
        Sets the attributes for the class.
    setbtype(cls, btype)
        Sets the base type for the class.
    __call__(cls, *args, **kwargs)
        A callable method that enables the dynamic creation of classes.
    '''
    
    types: Types = tuple()
    attrs: dict = dict()

    @classmethod
    @abstractmethod
    def getbtype(cls):
        '''Abstract class method to get the base type of the protocol.
        
        Returns
        -------
        None
            Placeholder return type for the abstract method.
        '''
        return None
    
    def check(cls, obj) -> Guard:
        '''Class method to check if an object conforms to the protocol.

        Parameters
        ----------
        obj : Any
            The object to be checked against the protocol.

        Returns
        -------
        Guard
            The result of the type checking.
        '''
        return aliascheck(obj, cls.types, cls.attrs)    
    
    @classmethod
    def makeguard(mcls) -> Guard:
        '''Class method to create a guard method for the protocol.

        Returns
        -------
        Guard
            A class method acting as a guard for the protocol.
        '''
        return classmethod(lambda cls, obj: mcls.check(cls, obj))
        # return classmethod(lambda cls, obj: aliascheck(obj, cls.types, cls.attrs))
        return classmethod(lambda cls, obj: cls.check(obj))
    
    @classmethod
    def makebases(mcls, cls):
        '''Class method to determine the base classes for a new class instance.

        Parameters
        ----------
        cls : type
            The class for which the base classes are to be determined.

        Returns
        -------
        tuple
            A tuple of base classes.
        '''
        btype = mcls.getbtype()
        bases = (cls, btype) if btype not in cls.__bases__ else (cls,)
        return bases

    @classmethod
    def default_kwds(cls, **kwargs) -> dict:
        '''Class method to provide default keyword arguments for class creation.

        Parameters
        ----------
        **kwargs : dict
            Keyword arguments for class creation.

        Returns
        -------
        dict
            A dictionary of default keyword arguments.
        '''
        kwds = dict(
            clsname  = kwargs.get('clsname', True),
            prefix   = kwargs.get('prefix', None),
            suffix   = kwargs.get('suffix', None),
            dropnone = kwargs.get('dropnone', True),
        )
        return kwds

    def __new__(mcls, name, bases, dct, **kwargs):
        '''Class method to create a new class instance.

        Parameters
        ----------
        name : str
            The name of the new class.
        bases : tuple
            A tuple of base classes.
        dct : dict
            A dictionary of class attributes.
        **kwargs : dict
            Additional keyword arguments.

        Returns
        -------
        type
            A new class instance.
        '''
        kwds = mcls.default_kwds(**kwargs)
        
        dct['types'] = (types := kwargs.pop('types', getattr(mcls, 'types', tuple())))
        dct['attrs'] = (attrs := kwargs.pop('attrs', getattr(mcls, 'attrs', dict())))        
        # dct['btype'] = (btype := kwargs.pop('btype', getattr(mcls, 'btype', None)))
        # dct['check'] = classmethod(lambda cls, obj: aliascheck(obj, types, attrs))
        dct['check'] = mcls.makeguard()        
        new = super().__new__(mcls, name, bases, dct)
        new.__qualname__ = new.qualname(*types, **kwds)
        new.settypes(*types, **kwds)
        new.setattrs(attrs)
        # new.setbtype(attrs)
        
        return new
    
    def __instancecheck__(self, ins) -> Guard:
        '''Instance method to check if an instance conforms to the protocol.

        Parameters
        ----------
        ins : Any
            The instance to be checked.

        Returns
        -------
        Guard
            The result of the instance checking.
        '''
        return self.check(ins)
    
    def classname(cls, clsname: StrBool = True) -> StrBool:
        '''Class method to get the class name.

        Parameters
        ----------
        clsname : StrBool
            The class name or a boolean indicating if the class name should be used.

        Returns
        -------
        StrBool
            The class name or a boolean value.
        '''
        return classname(cls, clsname)
        
    def qualname(cls, *types: Types, clsname: StrBool = True, prefix: str = None, suffix: str = None, dropnone: bool = True) -> str:
        '''Class method to generate a qualified name for the class.

        Parameters
        ----------
        *types : Types
            A tuple of types.
        clsname : StrBool
            The class name or a boolean indicating if the class name should be used.
        prefix : str, optional
            A prefix for the qualified name.
        suffix : str, optional
            A suffix for the qualified name.
        dropnone : bool, optional
            A flag to indicate whether None types should be dropped.

        Returns
        -------
        str
            The qualified name of the class.
        '''
        return qualname(cls, *types, clsname=clsname, prefix=prefix, suffix=suffix, dropnone=dropnone)
    
    def settypes(cls, *types: Types, clsname: StrBool = True, prefix: str = None, suffix: str = None, dropnone: bool = True):
        '''Class method to set the types for the class.

        Parameters
        ----------
        *types : Types
            A tuple of types.
        clsname : StrBool
            The class name or a boolean indicating if the class name should be used.
        prefix : str, optional
            A prefix for the qualified name.
        suffix : str, optional
            A suffix for the qualified name.
        dropnone : bool, optional
            A flag to indicate whether None types should be dropped.
        '''
        settypes(cls, *types, clsname=clsname, prefix=prefix, suffix=suffix, dropnone=dropnone)
        
    def setattrs(cls, attrs: dict = dict()):
        '''Class method to set the attributes for the class.

        Parameters
        ----------
        attrs : dict, optional
            A dictionary of attributes to be set for the class.
        '''
        setattr(cls, 'attrs', attrs)

    def setbtype(cls, btype: Type = None):
        '''Class method to set the base type for the class.

        Parameters
        ----------
        btype : Type, optional
            The base type to be set for the class.
        '''
        setattr(cls, 'btype', btype)

    @overload
    def __call__(cls: 'ProtocolBaseMeta', base: type, *dtypes: Types, **kwargs) -> TypeGuard[T]: ...
    @overload
    def __call__(cls: type, instance: object) -> TypeGuard[T]: ...
    @overload
    def __call__(cls: type, *instances: tuple[object, ...]) -> TypeGuard[T]: ...
    @overload
    def __call__(cls: type, *args, **kwargs) -> TypeGuard[T]: ...    
    def __call__(cls: type, *args, **kwargs) -> TypeGuard[T]:
        '''Overloaded callable method that enables dynamic creation and type checking of classes.

        Parameters
        ----------
        *args : variable
            Variable arguments for different use cases.
        **kwargs : dict
            Keyword arguments for different use cases.

        Returns
        -------
        TypeGuard[T]
            Depending on the arguments, either performs type checking or creates a new class instance.
        '''
        if len(args) > 0 and not isinstance(args[0], type) or args[0] is NoneType:
            if len(args) == 1: return cls.check(args[0])
            if all(not inspect.isclass(i) for i in args):
                return tuple(cls.check(i) for i in args)
            return cls.check(args[0])
        
        dtypes = args
        def decorator(kls):
            '''Decorator to apply negation to a class.'''
            clsname = kwargs.get('clsname', kls.__name__)
            bases = cls.makebases(kls)
            dct = dict()
            new = cls.__class__(clsname, bases, dct, types=dtypes, **kwargs)
            return new
        return decorator

# %% ../nbs/03_meta.ipynb 20
class ProtocolMeta(ProtocolBaseMeta):
    '''Metaclass for creating protocol classes with a specific base type.

    Inherits from ProtocolBaseMeta and provides a specific base type for the 
    protocol classes created using this metaclass.

    Example
    -------
    >>> @Alias(str, bytes)
    ... class StrBytes: ...
    >>> StrBytes('hello')  # Checks if 'hello' is an instance of str or bytes.
    '''
    @classmethod
    def getbtype(cls): return ProtoType

@runtime_checkable
class ProtoType(Protocol):
    '''Base protocol class with custom type checking capabilities.

    This class provides mechanisms for custom type checking and can be used as a 
    base class for creating protocol types.

    Examples
    --------
    >>> @Not(str, bytes)
    ... class NotStrBytes: ...
    >>> NotStrBytes(123)  # Checks if 123 is not an instance of str or bytes.

    ### Subclassing Example
    >>> class MyType(ProtoType, types=(int, float)):
    ...     \'\'\'Custom type that checks against int and float\'\'\'
    >>> MyType(10)  # Checks if 10 is an instance of int or float.
    '''
    @classmethod
    def __init_subclass__(cls, *args, types: Types = tuple(), attrs: dict = dict(), **kwargs) -> None:
        super().__init_subclass__()
        kwds = cls.default_kwds(**attrs)
        cls.settypes(*types, **kwds)
        cls.setattrs(attrs)
        cls.setbtype(cls)

# %% ../nbs/03_meta.ipynb 22
class AliasMeta(ProtocolBaseMeta):
    '''Metaclass for creating alias classes with custom type checking.

    Inherits from ProtocolBaseMeta and provides a specific base type for the 
    alias classes created using this metaclass.

    Example
    -------
    >>> @Alias(str, bytes)
    ... class StrBytes: ...
    >>> StrBytes('example')  # Checks if 'example' is an instance of str or bytes.
    ''' 
    @classmethod
    def getbtype(cls): return Alias
    

@runtime_checkable
class Alias(ProtoType, Protocol, metaclass=AliasMeta):
    '''Class representing an alias with custom type checking.

    Inherits from ProtoType and Protocol, and is created using the AliasMeta 
    metaclass. It represents an alias with specific type checking behavior.

    Example
    -------
    >>> @Alias(str, bytes)
    ... class StrBytes: ...
    >>> StrBytes(b'bytes')  # Checks if b'bytes' is an instance of str or bytes.
    '''
    ...

# %% ../nbs/03_meta.ipynb 24
class NotMeta(ProtocolBaseMeta):
    '''Metaclass for creating negation classes with custom type checking.

    Inherits from ProtocolBaseMeta and overrides certain methods to provide 
    negation logic in type checking for the classes created using this metaclass.

    Example
    -------
    >>> @Not(str, bytes)
    ... class NotStrBytes: ...
    >>> NotStrBytes(['not', 'str', 'or', 'bytes'])  # Checks if list is not an instance of str or bytes.
    '''
    @classmethod
    def getbtype(cls): return Not

    @classmethod
    def default_kwds(cls, **kwargs) -> dict:
        kwds = super().default_kwds(**kwargs)
        kwds['prefix'] = '~'        
        return kwds
    
    def check(self, obj) -> Guard:     
        return not aliascheck(obj, self.types, self.attrs)
    
@runtime_checkable
class Not(ProtoType, Protocol, metaclass=NotMeta):
    '''Class representing a negation with custom type checking.

    Inherits from ProtoType and Protocol, and is created using the NotMeta 
    metaclass. It represents a negation type with specific type checking behavior.

    Example
    -------
    >>> @Not(str, bytes)
    ... class NotStrBytes: ...
    >>> NotStrBytes(100)  # Checks if 100 is not an instance of str or bytes.
    '''
    ...

# %% ../nbs/03_meta.ipynb 26
class OptionalMeta(ProtocolBaseMeta):
    '''Metaclass for creating optional type classes with custom type checking.

    Inherits from ProtocolBaseMeta and modifies the creation process to include 
    NoneType in the types for the classes created using this metaclass.

    Example
    -------
    >>> @Opt(str)
    ... class StrQ: ...
    >>> StrQ(None)  # Checks if None is an instance of str or NoneType.
    >>> StrQ('optional')  # Checks if 'optional' is an instance of str or NoneType.
    '''
    @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(ProtoType, Protocol, metaclass=OptionalMeta):
    '''Class representing an optional type with custom type checking.

    Inherits from ProtoType and Protocol, and is created using the OptionalMeta 
    metaclass. It represents an optional type with specific type checking behavior.

    Example
    -------
    >>> @Opt(int)
    ... class IntQ: ...
    >>> IntQ(None)  # Checks if None is an instance of int or NoneType.
    >>> IntQ(42)  # Checks if 42 is an instance of int or NoneType.
    '''
    ...
