from io import BytesIO
from typing import Any
import cbor2
from cbor2 import CBORTag, CBORDecoder

from . import bindings


def tag_hook(decoder, tag, shareable_index=None):
    if tag.tag == 37:
        return bindings.UUID(bytes=tag.value)
    if tag.tag == 32:
        return bindings.URL(tag.value)
    return tag


def default_encoder(encoder, value):
    if isinstance(value, bindings.UUID):
        encoder.encode(CBORTag(37, value.bytes))

    if isinstance(value, bindings.URL):
        encoder.encode(CBORTag(32, value.bytes))


def dumps(obj, **kwargs):
    return cbor2.dumps(obj, default=default_encoder, **kwargs)


def loads(payload, **kwargs):
    # return cbor2.loads(payload,tag_hook=tag_hook,**kwargs)
    return _loads(payload, tag_hook=tag_hook, **kwargs)


def _loads(s, **kwargs):
    with BytesIO(s) as fp:
        # use a custom decoder in pure Py. C version doesn't support
        # subclassing, and enforce usage of builtin semantic tags
        dec = CustomDecoder(fp, **kwargs)
        return dec.decode()


class CustomDecoder(CBORDecoder):
    """Custom CBOR decoder, this version simply drop the builtin semantic tags"""

    def decode_semantic(self, subtype: int) -> Any:
        tagnum = self._decode_length(subtype)

        tag = CBORTag(tagnum, None)
        self.set_shareable(tag)
        tag.value = self._decode(unshared=True)
        if self._tag_hook:
            tag = self._tag_hook(self, tag)


def cleanup(obj):
    """
    recursive walk a object to search for un-wanted CBOR tags.
    Transform this tag in string format, this can be UUID, URL..
    Should be Ok, with list, dicts..
    Warning: This operate in-place changes.
    Warning: This won't work for tags in dict keys.
    """
    if isinstance(obj, list):
        for i in range(0, len(obj)):
            obj[i] = cleanup(obj[i])
        return obj

    if isinstance(obj, dict):
        for k in obj.keys():
            obj.update({k: cleanup(obj[k])})
        return obj

    if type(obj) in bindings.classes:
        return str(obj)
    else:
        return obj
