# coding: utf-8


import logging
import zlib
import operator
from typing import Union

from django.utils.encoding import force_bytes, force_text
from idm.utils import json

log = logging.getLogger(__name__)


def iterate_over_comparable(inst):
    """
    Fast internal iteration over the attr descriptors.

    Using fields to iterate is slow because it involves deepcopy.
    """
    return (attr for attr in inst.__class__.__attrs_attrs__ if attr.cmp)


def hash_pair(internal_hasher, key, value):
    from idm.users.models import User

    internal_hasher.update(force_bytes(key))
    if value is None:
        internal_hasher.update(b'')
    elif isinstance(value, dict):
        value = sorted(value.items(), key=operator.itemgetter(0))
        for _, item in value:
            for attr in iterate_over_comparable(item):
                attrvalue = getattr(item, attr.name)
                if isinstance(attrvalue, dict):
                    attrvalue = json.dumps(attrvalue, sort_keys=True)
                hash_pair(internal_hasher, attr.name, attrvalue)
    elif isinstance(value, (bool, int)):
        internal_hasher.update(force_bytes(int(value)))
    elif isinstance(value, User):  # hack, remove it
        internal_hasher.update(force_bytes(value.username))
    else:
        assert isinstance(value, str)
        internal_hasher.update(force_bytes(value))


def build_hash(canonical, children_hashes=(), debug=False):
    if debug:
        internal_hasher = DebugHasher()
    else:
        internal_hasher = CRC32Hasher()
    for attr in iterate_over_comparable(canonical):
        value = getattr(canonical, attr.name)
        hash_pair(internal_hasher, attr.name, value)
    children_hashes = list(children_hashes)
    children_hashes.sort()
    for child_hash in children_hashes:
        internal_hasher.update(child_hash)
    digest = force_text(internal_hasher.hexdigest())
    return digest


class DebugHasher(object):
    def __init__(self):
        self.digest = []

    def update(self, value):
        self.digest.append(value)

    def hexdigest(self):
        result = b'|'.join(map(force_bytes, self.digest))
        return result


class CRC32Hasher(object):
    def __init__(self):
        self.digest = []

    def update(self, value: Union[str, bytes]):
        self.digest.append(value)

    def hexdigest(self):
        prepared = b'|'.join(map(force_bytes, self.digest))
        checksum = zlib.crc32(prepared) & 0xffffffff  # Для консистентности при использовании различных версий Python
        return hex(checksum)


class Hasher(object):
    def __init__(self, debug=False):
        self.debug = debug

    def hash_object(self, obj, children_hashes=()):
        canonical = obj.as_canonical()
        return build_hash(canonical, children_hashes, self.debug)

    def hash_canonical(self, canonical):
        canonical.hash = build_hash(canonical, self.get_child_hashes(canonical), debug=self.debug)
        return canonical

    def get_child_hashes(self, canonical):
        for child in canonical.children:
            self.hash_canonical(child)
            yield child.hash
