"""Utilities for working with JSON and json-like structures - deeply nested Python dicts and lists

This lets us iterate over child nodes and access elements with a dot-notation.
"""
__all__ = (
            'wrapJSON',
        )
__version__ = '0.2.0'

strTypes = (str,bytes)

class WrapJSONSet(set):
    def __new__(cls,S,defaults={}):
        self = set.__new__(cls,S)
        self.defaults = {}
        self.trackend = None
        return self

def __xname__(self,x,kind='[]'):
    '''extend the current name by the access method and value'''
    if isinstance(x,strTypes):
        if isinstance(x,bytes): x = x.decode('utf8')
        x = repr(x) if ' ' in x or kind=='[]' else x
    x = f'{self.__name__}.{x}' if kind=='.' else f'{self.__name__}[{x}]'
    return 'None' if x is None else str(x)

class _WrapJSONM:
    '''functionality for wrapped float, int, str, bytes classes'''
    def __new__(cls, v, name, safe):
        self = super(_WrapJSONM,cls).__new__(cls,v)
        self.__name__ = name
        self.__safe__ = safe
        return self

    def __getattr__(self,a):
        name = __xname__(self, a,'.')
        if self.__safe__:
            self.__safe__(name)
            return self
        else:
            raise ValueError(f"No attribute '{a}' in {name}")

    def __getitem__(self,a):
        name = __xname__(self, a)
        try:
            return wrapJSON(self.__class__.__bases__[1].__getitem__(self,a),name,self.__safe__)
        except Exception as e:
            if self.__safe__:
                self.__safe__(name)
                return self
            else:
                raise IndexError(f"No item '{a}' in {name}") from e

class WrapJSONStr(_WrapJSONM,str):
    pass

class WrapJSONBytes(_WrapJSONM,bytes):
    pass

class WrapJSONFloat(_WrapJSONM,float):
    pass

class WrapJSONInt(_WrapJSONM,int):
    pass

def wrapJSON(v,name='',safe=False, defaults={}):
    '''
    wrapJSON(obj, name='', safe=False, defaults)

    returns a wrapped object for types dict, list, int, float, str, bytes

    at the top level you should pass ina meaningful name

    if safe is True then a name tree is constructed as the wrapped object
    is traversed. A report of missing paths will be available as
    attribute __missing_report__

    the defaulst dict can be used to define a default value for missing paths.

    if safe is False then error tracebacks may still give useful information
    about the path through the wrapped object
    '''
    if safe is True:
        safe = WrapJSONSet(())
        safe.defaults = defaults
        safe = safe.add
    return WrapJSONDict(v,name=name,safe=safe) if isinstance(v,dict) \
            else WrapJSONList(v,name,safe) if isinstance(v,list) \
            else WrapJSONFloat(v,name,safe) if isinstance(v,float) \
            else WrapJSONInt(v,name,safe) if isinstance(v,int) \
            else WrapJSONStr(v,name,safe) if isinstance(v,str) \
            else WrapJSONBytes(v,name,safe) if isinstance(v,bytes) \
            else v

class _WrapJSONC:
    @property
    def __missing_report__(self):
        '''create a missing report from the missing set'''
        missing = self.__safe__.__self__ if self.__safe__ else None
        if missing:
            import textwrap
            missing = textwrap.wrap(' '.join(sorted(missing)))
            _ = '\n\x20\x20'
            return f'Missing names or items{_}{_.join(missing)}'
        elif missing is None:
            return 'Missing names/items report is not available'
        else:
            return 'No missing names or items'

    def __set_defaults__(self,defaults={}):
        if self.__safe__:
            self.__safe__.__self__.defaults.update(defaults)

class WrapJSONList(list,_WrapJSONC):
    def __init__(self,v,name='',safe=False):
        list.__init__(self,v)
        self.__name__ = name
        self.__safe__ = safe
    def __getitem__(self,x):
        name = __xname__(self,x)
        safe = self.__safe__
        try:
            return wrapJSON(list.__getitem__(self,x),name,safe)
        except (TypeError,IndexError) as e:
            if safe:
                safe(name)
                return wrapJSON(safe.__self__.defaults.get(name,''),name,safe)
            else:
                raise IndexError(f"No item '{x}' in {name}" ) from e

    def __getattr__(self,a):
        '''should only be called for bad attributes'''
        name = __xname__(self, a,'.')
        if self.__safe__:
            self.__safe__(name)
            return wrapJSON(self.__safe__.__self__.defaults.get(name,''),name,self.__safe__)
        else:
            raise ValueError(f"No attribute '{a}' in {name}")

    def __iter__(self):
        return map((lambda i: wrapJSON(i[1],__xname__(self,i[0]),self.__safe__)), enumerate(super().__iter__()))

class WrapJSONDict(dict,_WrapJSONC):
    "Allows dotted access"
    def __new__(cls,*args,**kwds):
        name = kwds.pop('name')
        safe = kwds.pop('safe')
        self = dict.__new__(cls,*args,**kwds)
        self.__name__ = name
        self.__safe__ = safe
        return self

    def __init__(self,*args,**kwds):
        kwds.pop('name','')
        kwds.pop('safe','')
        dict.__init__(self,*args,**kwds)

    def __getattr__(self, attr, default=None):
        name = __xname__(self,attr,'.')
        safe = self.__safe__
        if attr in self:
            return wrapJSON(self[attr],name,safe)
        elif isinstance(attr,bytes) and attr.decode('utf8') in self:
            return wrapJSON(self[attr.decode('utf8')],name,safe)
        else:
            try:
                return dict.__getattr__(self,attr)
            except AttributeError:
                if safe:
                    safe(name)
                    return wrapJSON(safe.__self__.defaults.get(name,''),name,safe)
                else:
                    raise AttributeError(f"No attribute or key '{attr}' in {name}" )

    def __getitem__(self,x):
        name = __xname__(self,x)
        safe = self.__safe__
        try:
            return wrapJSON(dict.__getitem__(self,x),name,safe)
        except KeyError as e:
            if safe:
                safe(name)
                return wrapJSON(safe.__self__.defaults.get(name,''),name,safe)
            else:
                raise IndexError(f"No key '{x}' in {name}" ) from e

    def values(self):
        safe = self.__safe__
        return map((lambda i: (wrapJSON(i[1],__xname__(self,i[0],'.'),safe))), super().items())

    def items(self):
        safe = self.__safe__
        return map((lambda i: (i[0],wrapJSON(i[1],__xname__(self,i[0],'.'),safe))), super().items())
