# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/07_misc.ipynb.

# %% auto 0
__all__ = ['get_option', 'find_spec_or_none', 'find_last_spec', 'getretann', 'prep_kwargs', 'asset_attrs', 'asset_sub_pkg',
           'asset_last', 'asset_name', 'parse_asset', 'insys', 'find_asset_module', 'load_asset', 'try_loaded_attr',
           'try_ducked_attr', 'unduck', 'hatchegg', 'putinrow', 'prepare_ducktypes', 'prepare_isinstance_types',
           'prepare_types_for_instancecheck', 'tryattr']

# %% ../nbs/07_misc.ipynb 6
import io, sys
from inspect import get_annotations
from functools import wraps

from contextlib import redirect_stderr, redirect_stdout

from importlib.abc import Loader
from importlib.util import (module_from_spec, find_spec)
from importlib.machinery import ModuleSpec

# %% ../nbs/07_misc.ipynb 8
from types import ModuleType
from typing import Any, Union, Iterable, Optional, get_args

# %% ../nbs/07_misc.ipynb 10
#| export

# %% ../nbs/07_misc.ipynb 12
#| export

# %% ../nbs/07_misc.ipynb 14
from quac.cons import (
    DOT, NIL, LOAD, LOADER, RETURN, DUCK_SPEC_ATTRS, 
    __DOC__, __NEW__, __CALL__, __LOAD__, __INIT__, __NAME__, __MODULE__, __SIGNATURE__, __ANNOTATIONS__,
    __HATCHED__, __ALLCHICS__, __AREINROW__, __DUCKTYPE__
)
from .type import T, P
from .prot import NotSpecial, DuckSpecProtocol, DuckModuleProtocol, DuckSpecProtocol, DuckTypeProtocol
from .grds import iscls, isnotspecial, isnotiterstr, isduck, isduckspec, isduckmodule, hasduck, hasload, isclstype, _istype, isoptional, hasducktype
from .errs import (DuckException, DuckNameError, DuckImportError, DuckAttributeError, DuckModuleNotFoundError)

# %% ../nbs/07_misc.ipynb 17
def get_option(x, i: int = 0):
    "Check if `x` is an optional type"
    if not isoptional(x): return None
    args = get_args(x)
    if len(args) == 0: return None
    return args[i if i < len(args) else -1] 

# %% ../nbs/07_misc.ipynb 19
@wraps(find_spec)
def find_spec_or_none(name: str, package: str | None = None) -> ModuleSpec | None:
    # NOTE: needed as find_spec will fail if stub is an attribute e.g. 
    # `pandas.core.arrays.categorical.CategoricalAccessor`
    try: return find_spec(name, package)
    except ModuleNotFoundError: return None

def find_last_spec(fullpath: str) -> tuple[ModuleSpec | None, str | None]:
    stubs = fullpath.split(DOT)
    while len(stubs) > 0:
        stub = DOT.join(stubs)
        spec = find_spec_or_none(stub)
        if spec: break
        stubs.pop()
    return spec, stub

# %% ../nbs/07_misc.ipynb 20
def getretann(obj) -> Any:
    if hasattr(obj, __ANNOTATIONS__): return get_annotations(obj).get(RETURN, None)
    else: return None

# %% ../nbs/07_misc.ipynb 22
def prep_kwargs(obj: object, *args: P.args, __attrs: tuple[str, ...] = DUCK_SPEC_ATTRS, **kwargs: P.kwargs) -> dict:
    objkws, argkws = dict(),  dict(zip(__attrs, args))
    for i, attr in enumerate(__attrs):
        objkws.setdefault(attr, getattr(obj, attr, None))
        argkws.setdefault(attr, objkws.get(attr))
        kwargs.setdefault(attr, argkws.get(attr))
    return kwargs

# %% ../nbs/07_misc.ipynb 24
def asset_attrs(asset) -> tuple[str, ...]:
    *_, rest = asset.removesuffix(DOT).partition(DOT)
    return rest.split(DOT)

def asset_sub_pkg(asset) -> tuple[str, str]:
    name, _, attr = asset.removesuffix(DOT).rpartition(DOT)
    return name, attr

def asset_last(asset: str) -> str:
    *_, name = str(asset).removesuffix(DOT).rpartition(DOT)
    return name

def asset_name(asset, is_module: bool = False) -> str:
    if is_module or DOT not in asset: return asset
    name, _ = asset_sub_pkg(asset)
    return name

def parse_asset(asset: str, is_module: bool = False) -> tuple[str, str | None]:
    if is_module or DOT not in asset: return asset, None
    name, attr = asset_sub_pkg(asset)
    return name, attr

# %% ../nbs/07_misc.ipynb 29
def insys(asset: str, is_module: bool = False) -> bool:
    return asset_name(asset, is_module) in sys.modules

def find_asset_module(asset: str, is_module: bool = False) -> ModuleType:
    name, attr = parse_asset(asset, is_module)
    if insys(asset, is_module): return sys.modules[name]
    
    spec: ModuleSpec | None = find_spec(name)
    module: ModuleType = module_from_spec(spec)
    loader: Loader = getattr(spec, LOADER, None)
    
    try: loader.exec_module(module)
    except NameError as e: ...
    return module

def load_asset(asset: str, is_module: bool = False):
    name, attr = parse_asset(asset, is_module)
    module = find_asset_module(asset, is_module)
    if is_module or attr is None: return module
    return getattr(module, attr)

def try_loaded_attr(asset: str, is_module: bool = False, hush: bool = False):
    if not hush: return load_asset(asset, is_module)
    with redirect_stderr(io.StringIO()), redirect_stdout(io.StringIO()):
        return load_asset(asset, is_module)

# %% ../nbs/07_misc.ipynb 33
def try_ducked_attr(asset: str, ducked: Optional[DuckSpecProtocol] = None):
    if ducked is None: raise DuckImportError(asset)
    try:
        item = ducked
        for attr in asset_attrs(asset):
            item = getattr(item, attr)
        return item
    except NameError as e: 
        raise DuckNameError(*e.args)
    
    except ModuleNotFoundError as e: 
        raise DuckModuleNotFoundError(*e.args, e.name, e.path)
    
    except ImportError as e: 
        raise DuckImportError(*e.args, e.name, e.path)
    
    except AttributeError as e: 
        raise DuckAttributeError(*e.args, e.name, e.obj)
    
    except Exception as e: 
        raise DuckException(*e.args)
    
    return item

# %% ../nbs/07_misc.ipynb 35
def unduck(item: Any):
    try:
        if isduckspec(item): return item.duck
        elif callable(getattr(item, LOAD, None)):
            return item.load()
        elif callable(getattr(item, __LOAD__, None)):
            return item.__load__()
    except Exception: ...
    return item

def _fromducks(var: Iterable):
    iterstr = isnotiterstr(var)
    if not iterstr: return var
    cls = type(var)
    var = list(var)
    for i, e in enumerate(var): 
        var[i] = unduck(e)
    return cls(var)

def _fromcall(var: Iterable):
    if not callable(var): 
        return var
    try: return var()
    except: ...
    return var

# %% ../nbs/07_misc.ipynb 36
def hatchegg(cls: DuckSpecProtocol) -> tuple[type, bool, bool]:
    '''Try to load the asset stored in the duck specification.
    
    Returns
    -------
        chic : type, optional
            the loaded asset from a `DuckSpec`
        
        eggq : bool
            Whether the input `cls` was a duckspec
        
        isok : bool
            Whether the attempt to load the asset succeeded or not.
    '''
    if not (isduckspec(cls) or hasduck(cls)): 
        return None, False, True
    try:
        if not getattr(cls, __HATCHED__, False):
            chic = cls.duck
            setattr(cls, __HATCHED__, True)
            return chic, True, True
        else:
            return cls.duck, True, True
    except Exception as e: ...
    return None, True, False
    


# %% ../nbs/07_misc.ipynb 38
def putinrow(cls: DuckTypeProtocol, force: bool = False):
    '''
    Returns
    -------
    types: tuple[type, ...]
        The resolved types of the given ducktype. Note that if `cls`
        is not a duck, then returns `(cls, )`
    
    inrow: bool
        Whether all of the types in `cls.__ducktype__` are resolved.
        If `cls` is not a duck, then defaults to True
        
    duckq: bool
        Whether or not the input class `cls` was a ducktype to begin with.
    '''
    if not (isduck(cls) or hasducktype(cls)):
        return (cls, ), True, False
    
    # print('putinrow, cls=', cls.__name__)
    curtypes = list(cls.__ducktype__)
    
    if getattr(cls, __AREINROW__, False) and not force: 
        return tuple(curtypes), True, True
    
    gathered = dict() # collection of found types, to prevent inf recursion through instance checking
    areinrow = [] # whether each type of __ducktype__ is "in row"
    foundrow = [] # order in which types were resolved
    
    # for each type in Duck.__ducktype__
    for t in curtypes:
        # if t is a DuckSpec (`egg`) load the item (`chic`)
        chic, eggq, isok = hatchegg(t)
        if eggq:
            # Add the resolved `DuckSpec`'s item to gathered and put in the foundrow
            # if isok otherwise keep the original `DuckSpec` in the ducktype
            use = chic if isok else t
            if not gathered.get(use, False):
                gathered[use] = True
                foundrow.append(use)
            
            # keep track of if this type was resolved or not
            areinrow.append(isok)
            continue             
        
        # if t is also a Duck, we need to recurse
        if (isduck(t) or hasducktype(t)):
            inrow = getattr(t, __AREINROW__, False) and not force
            if not inrow:
                # attempt to put `t.__ducktype__` in row.
                try:
                    types, inrow, duckq = putinrow(t, force)
                    setattr(t, __AREINROW__, inrow)
                    areinrow.append(inrow)
                    setattr(t, __DUCKTYPE__, tuple(types))
                except:
                    # failed to get them in row
                    setattr(t, __AREINROW__, False)
                    areinrow.append(False)
            else:
                # they are already in row
                areinrow.append(True)
            
            # add each or `t.__ducktype__`'s to gathered and 
            # put in the foundrow if missing
            subducks = list(t.__ducktype__)
            for d in subducks:
                if not gathered.get(d, False):
                    gathered[d] = True
                    foundrow.append(d)
                    continue
                
        else:
            # not a duck or a duckspec, so they are fine as is
            if not gathered.get(t, False):
                gathered[t] = True
                foundrow.append(t)
            areinrow.append([True])
    
    # determine if this operation completed successfully
    areinrow = all(areinrow)
    setattr(cls, __AREINROW__, areinrow)
    
    foundrow = tuple(foundrow) # __ducktype__ is a tuple
    setattr(cls, __DUCKTYPE__, foundrow)
    return foundrow, areinrow, True

# %% ../nbs/07_misc.ipynb 42
def prepare_ducktypes(cls, force: bool = False) -> tuple[type, ...]:
    unducked, *_ = putinrow(cls, force=force)
    return unducked
    
    curtypes = list(cls.__ducktype__)
    unducked = []
    found = dict()
    for t in curtypes:
        if isduckspec(t) or hasduck(t):
            # print('isduckspec')
            try:
                t = t.duck
                if not found.get(t, False):
                    found[t] = True
                    unducked.append(t)
                    continue
            except Exception as e: ...
                
        if isduck(t):    
            # print('isduck')
            resolved = list(t.__ducktype__)
            for d in resolved:
                if not found.get(d, False):
                    found[d] = True
                    unducked.append(d)
                    continue
            # unducked.extend(list(t.__types__()))
            continue
        
        if not found.get(t, False):
            found[t] = True
            unducked.append(t)
            continue
        # unducked.append(t)
            
            
    # unducked = list(set(unducked))
    
    # unducked = list(filter(lambda e: unducked.count(e) == 1, unducked))    
    # unducked = sorted(
    #     unducked, 
    #     key = lambda t: (
    #         iscls(t), 
    #         getattr(t, '__name__', ''),
    #         curtypes.index(t) if t in curtypes else -1,
    #     ),
    #     reverse=True
    # )
    
    unducked = tuple(unducked)
    return unducked

# %% ../nbs/07_misc.ipynb 43
def prepare_isinstance_types(types: tuple[type | Any, ...]) -> tuple[type, ...]:
    types = tuple([t if _istype(t) else type(t) for t in types])
    return types

# %% ../nbs/07_misc.ipynb 44
def prepare_types_for_instancecheck(types: tuple[type, ...]) -> tuple[type, ...]:
    results = []
    for t in types:
        if hasduck(t): 
            try:
                results.append(t.duck)
                continue
            except Exception: ...
            
        if hasload(t): 
            try:
                results.append(t.load())
                continue
            except Exception: ...
            
        if isclstype(t): 
            results.append(t)
            
        else: 
            results.append(type(t))
    
    return tuple(results)

# %% ../nbs/07_misc.ipynb 46
def tryattr(
    asset: str, 
    is_module: bool = False, 
    ducked: Optional[DuckModuleProtocol] = None,
    default: Any = None,
    bases: tuple = (),
    attrs: dict = {},
    hush: bool = True,
    delayed: dict[str, bool] = dict(),
    **kwargs
) -> type:
    item, ierr, derr = None, False, False
    try: item = try_loaded_attr(asset, is_module, hush=hush)         
    except (NameError, ImportError, AttributeError): ierr = True    
    
    if isinstance(item, ModuleType) and is_module: return item
    elif not ierr: return item
    
    try: item = try_ducked_attr(asset, ducked)
    except (DuckException, DuckNameError, DuckImportError, DuckAttributeError, DuckModuleNotFoundError): derr = True
            
    attr = asset_last(asset)
    
    isducked = isduckmodule(item)


    itemname = getattr(item, __NAME__, None)
    goodname, notfound = attr == itemname, attr != itemname
    maketype: bool = (isducked and notfound) or (ierr or derr) or default in (None, type)
    
    found = not isducked and goodname
    if found: return item
    
    
    for k, v in delayed.items():
        if not v and k in locals(): continue
        var = locals().get(k)
        var = _fromducks(var)
        var = _fromcall(var)
        try:
            match k:
                case 'attrs': attrs = var()
                case 'bases': bases = var()
                case 'default': default = var()
                case _: ...
        except: ...
    
    try:
        if maketype: 
            # moddict = dict(__module__=globals().get(__MODULE__, 'quac'))
            # attrs.update(moddict)
            item = type(attr, bases, attrs)
            
        else: item = default
    
    except Exception: 
        item = default
    
    item = unduck(item)
    return item
