from sys import version_info

import gdb

if version_info[0] >= 3:
    xrange = range

ZERO_FIELD = "__0"
FIRST_FIELD = "__1"


def unwrap_unique_or_non_null(unique_or_nonnull):
    # BACKCOMPAT: rust 1.32
    # https://github.com/rust-lang/rust/commit/7a0911528058e87d22ea305695f4047572c5e067
    # BACKCOMPAT: rust 1.60
    # https://github.com/rust-lang/rust/commit/2a91eeac1a2d27dd3de1bf55515d765da20fd86f
    ptr = unique_or_nonnull["pointer"]
    return ptr if ptr.type.code == gdb.TYPE_CODE_PTR else ptr[ptr.type.fields()[0]]


def unwrap_scalar_wrappers(wrapper):
    while not wrapper.type.is_scalar:
        wrapper = wrapper[wrapper.type.fields()[0]]
    return wrapper


# GDB 14 has a tag class that indicates that extension methods are ok
# to call.  Use of this tag only requires that printers hide local
# attributes and methods by prefixing them with "_".
if hasattr(gdb, "ValuePrinter"):
    printer_base = gdb.ValuePrinter
else:
    printer_base = object


class EnumProvider(printer_base):
    def __init__(self, valobj):
        content = valobj[valobj.type.fields()[0]]
        fields = content.type.fields()
        self._empty = len(fields) == 0
        if not self._empty:
            if len(fields) == 1:
                discriminant = 0
            else:
                discriminant = int(content[fields[0]]) + 1
            self._active_variant = content[fields[discriminant]]
            self._name = fields[discriminant].name
            self._full_name = "{}::{}".format(valobj.type.name, self._name)
        else:
            self._full_name = valobj.type.name

    def to_string(self):
        return self._full_name

    def children(self):
        if not self._empty:
            yield self._name, self._active_variant


class StdStringProvider(printer_base):
    def __init__(self, valobj):
        self._valobj = valobj
        vec = valobj["vec"]
        self._length = int(vec["len"])
        self._data_ptr = unwrap_unique_or_non_null(vec["buf"]["inner"]["ptr"])

    def to_string(self):
        return self._data_ptr.lazy_string(encoding="utf-8", length=self._length)

    @staticmethod
    def display_hint():
        return "string"


class StdOsStringProvider(printer_base):
    def __init__(self, valobj):
        self._valobj = valobj
        buf = self._valobj["inner"]["inner"]
        is_windows = "Wtf8Buf" in buf.type.name
        vec = buf["bytes"] if is_windows else buf

        self._length = int(vec["len"])
        self._data_ptr = unwrap_unique_or_non_null(vec["buf"]["inner"]["ptr"])

    def to_string(self):
        return self._data_ptr.lazy_string(encoding="utf-8", length=self._length)

    def display_hint(self):
        return "string"


class StdStrProvider(printer_base):
    def __init__(self, valobj):
        self._valobj = valobj
        self._length = int(valobj["length"])
        self._data_ptr = valobj["data_ptr"]

    def to_string(self):
        return self._data_ptr.lazy_string(encoding="utf-8", length=self._length)

    @staticmethod
    def display_hint():
        return "string"


def _enumerate_array_elements(element_ptrs):
    for i, element_ptr in enumerate(element_ptrs):
        key = "[{}]".format(i)
        element = element_ptr.dereference()

        try:
            # rust-lang/rust#64343: passing deref expr to `str` allows
            # catching exception on garbage pointer
            str(element)
        except RuntimeError:
            yield key, "inaccessible"

            break

        yield key, element


class StdSliceProvider(printer_base):
    def __init__(self, valobj):
        self._valobj = valobj
        self._length = int(valobj["length"])
        self._data_ptr = valobj["data_ptr"]

    def to_string(self):
        return "{}(size={})".format(self._valobj.type, self._length)

    def children(self):
        return _enumerate_array_elements(
            self._data_ptr + index for index in xrange(self._length)
        )

    def num_children(self):
        return self._length

    @staticmethod
    def display_hint():
        return "array"


class StdVecProvider(printer_base):
    def __init__(self, valobj):
        self._valobj = valobj
        self._length = int(valobj["len"])
        self._data_ptr = unwrap_unique_or_non_null(valobj["buf"]["inner"]["ptr"])
        ptr_ty = gdb.Type.pointer(valobj.type.template_argument(0))
        self._data_ptr = self._data_ptr.reinterpret_cast(ptr_ty)

    def to_string(self):
        return "Vec(size={})".format(self._length)

    def children(self):
        return _enumerate_array_elements(
            self._data_ptr + index for index in xrange(self._length)
        )

    def num_children(self):
        return self._length

    @staticmethod
    def display_hint():
        return "array"


class StdVecDequeProvider(printer_base):
    def __init__(self, valobj):
        self._valobj = valobj

        head = valobj["head"]

        # BACKCOMPAT: rust 1.95
        if head.type.code != gdb.TYPE_CODE_INT:
            head = head[ZERO_FIELD]
        self._head = int(head)
        self._size = int(valobj["len"])
        # BACKCOMPAT: rust 1.75
        cap = valobj["buf"]["inner"]["cap"]
        if cap.type.code != gdb.TYPE_CODE_INT:
            cap = cap[ZERO_FIELD]
        self._cap = int(cap)
        self._data_ptr = unwrap_unique_or_non_null(valobj["buf"]["inner"]["ptr"])
        ptr_ty = gdb.Type.pointer(valobj.type.template_argument(0))
        self._data_ptr = self._data_ptr.reinterpret_cast(ptr_ty)

    def to_string(self):
        return "VecDeque(size={})".format(self._size)

    def children(self):
        return _enumerate_array_elements(
            (self._data_ptr + ((self._head + index) % self._cap))
            for index in xrange(self._size)
        )

    def num_children(self):
        return self._size

    @staticmethod
    def display_hint():
        return "array"


class StdRcProvider(printer_base):
    def __init__(self, valobj, is_atomic=False):
        self._valobj = valobj
        self._is_atomic = is_atomic
        self._ptr = unwrap_unique_or_non_null(valobj["ptr"])
        self._value = self._ptr["data" if is_atomic else "value"]
        self._strong = unwrap_scalar_wrappers(self._ptr["strong"])
        self._weak = unwrap_scalar_wrappers(self._ptr["weak"]) - 1

    def to_string(self):
        if self._is_atomic:
            return "Arc(strong={}, weak={})".format(int(self._strong), int(self._weak))
        else:
            return "Rc(strong={}, weak={})".format(int(self._strong), int(self._weak))

    def children(self):
        yield "value", self._value
        yield "strong", self._strong
        yield "weak", self._weak


class StdCellProvider(printer_base):
    def __init__(self, valobj):
        self._value = valobj["value"]["value"]

    def to_string(self):
        return "Cell"

    def children(self):
        yield "value", self._value


class StdRefProvider(printer_base):
    def __init__(self, valobj):
        self._value = valobj["value"].dereference()
        self._borrow = valobj["borrow"]["borrow"]["value"]["value"]

    def to_string(self):
        borrow = int(self._borrow)
        if borrow >= 0:
            return "Ref(borrow={})".format(borrow)
        else:
            return "Ref(borrow_mut={})".format(-borrow)

    def children(self):
        yield "*value", self._value
        yield "borrow", self._borrow


class StdRefCellProvider(printer_base):
    def __init__(self, valobj):
        self._value = valobj["value"]["value"]
        self._borrow = valobj["borrow"]["value"]["value"]

    def to_string(self):
        borrow = int(self._borrow)
        if borrow >= 0:
            return "RefCell(borrow={})".format(borrow)
        else:
            return "RefCell(borrow_mut={})".format(-borrow)

    def children(self):
        yield "value", self._value
        yield "borrow", self._borrow


class StdNonZeroNumberProvider(printer_base):
    def __init__(self, valobj):
        fields = valobj.type.fields()
        assert len(fields) == 1
        field = fields[0]

        inner_valobj = valobj[field]

        inner_fields = inner_valobj.type.fields()
        assert len(inner_fields) == 1
        inner_field = inner_fields[0]

        self._value = inner_valobj[inner_field]

    def to_string(self):
        return self._value


# Yields children (in a provider's sense of the word) for a BTreeMap.
def children_of_btree_map(map):
    # Yields each key/value pair in the node and in any child nodes.
    def children_of_node(node_ptr, height):
        def cast_to_internal(node):
            internal_type_name = node.type.target().name.replace(
                "LeafNode", "InternalNode", 1
            )
            internal_type = gdb.lookup_type(internal_type_name)
            return node.cast(internal_type.pointer())

        if node_ptr.type.name.startswith("alloc::collections::btree::node::BoxedNode<"):
            # BACKCOMPAT: rust 1.49
            node_ptr = node_ptr["ptr"]
        node_ptr = unwrap_unique_or_non_null(node_ptr)
        leaf = node_ptr.dereference()
        keys = leaf["keys"]
        vals = leaf["vals"]
        edges = cast_to_internal(node_ptr)["edges"] if height > 0 else None
        length = leaf["len"]

        for i in xrange(0, length + 1):
            if height > 0:
                child_ptr = edges[i]["value"]["value"][ZERO_FIELD]
                for child in children_of_node(child_ptr, height - 1):
                    yield child
            if i < length:
                # Avoid "Cannot perform pointer math on incomplete type" on zero-sized arrays.
                key_type_size = keys.type.sizeof
                val_type_size = vals.type.sizeof
                key = (
                    keys[i]["value"]["value"][ZERO_FIELD]
                    if key_type_size > 0
                    else gdb.parse_and_eval("()")
                )
                val = (
                    vals[i]["value"]["value"][ZERO_FIELD]
                    if val_type_size > 0
                    else gdb.parse_and_eval("()")
                )
                yield key, val

    if map["length"] > 0:
        root = map["root"]
        if root.type.name.startswith("core::option::Option<"):
            root = root.cast(gdb.lookup_type(root.type.name[21:-1]))
        node_ptr = root["node"]
        height = root["height"]
        for child in children_of_node(node_ptr, height):
            yield child


class StdBTreeSetProvider(printer_base):
    def __init__(self, valobj):
        self._valobj = valobj

    def to_string(self):
        return "BTreeSet(size={})".format(self._valobj["map"]["length"])

    def children(self):
        inner_map = self._valobj["map"]
        for i, (child, _) in enumerate(children_of_btree_map(inner_map)):
            yield "[{}]".format(i), child

    @staticmethod
    def display_hint():
        return "array"


class StdBTreeMapProvider(printer_base):
    def __init__(self, valobj):
        self._valobj = valobj

    def to_string(self):
        return "BTreeMap(size={})".format(self._valobj["length"])

    def children(self):
        for i, (key, val) in enumerate(children_of_btree_map(self._valobj)):
            yield "key{}".format(i), key
            yield "val{}".format(i), val

    @staticmethod
    def display_hint():
        return "map"


# BACKCOMPAT: rust 1.35
class StdOldHashMapProvider(printer_base):
    def __init__(self, valobj, show_values=True):
        self._valobj = valobj
        self._show_values = show_values

        self._table = self._valobj["table"]
        self._size = int(self._table["size"])
        self._hashes = self._table["hashes"]
        self._hash_uint_type = self._hashes.type
        self._hash_uint_size = self._hashes.type.sizeof
        self._modulo = 2**self._hash_uint_size
        self._data_ptr = self._hashes[ZERO_FIELD]["pointer"]

        self._capacity_mask = int(self._table["capacity_mask"])
        self._capacity = (self._capacity_mask + 1) % self._modulo

        marker = self._table["marker"].type
        self._pair_type = marker.template_argument(0)
        self._pair_type_size = self._pair_type.sizeof

        self._valid_indices = []
        for idx in range(self._capacity):
            data_ptr = self._data_ptr.cast(self._hash_uint_type.pointer())
            address = data_ptr + idx
            hash_uint = address.dereference()
            hash_ptr = hash_uint[ZERO_FIELD]["pointer"]
            if int(hash_ptr) != 0:
                self._valid_indices.append(idx)

    def to_string(self):
        if self._show_values:
            return "HashMap(size={})".format(self._size)
        else:
            return "HashSet(size={})".format(self._size)

    def children(self):
        start = int(self._data_ptr) & ~1

        hashes = self._hash_uint_size * self._capacity
        align = self._pair_type_size
        len_rounded_up = (
            (
                (((hashes + align) % self._modulo - 1) % self._modulo)
                & ~((align - 1) % self._modulo)
            )
            % self._modulo
            - hashes
        ) % self._modulo

        pairs_offset = hashes + len_rounded_up
        pairs_start = gdb.Value(start + pairs_offset).cast(self._pair_type.pointer())

        for index in range(self._size):
            table_index = self._valid_indices[index]
            idx = table_index & self._capacity_mask
            element = (pairs_start + idx).dereference()
            if self._show_values:
                yield "key{}".format(index), element[ZERO_FIELD]
                yield "val{}".format(index), element[FIRST_FIELD]
            else:
                yield "[{}]".format(index), element[ZERO_FIELD]

    def display_hint(self):
        return "map" if self._show_values else "array"


class StdHashMapProvider(printer_base):
    def __init__(self, valobj, show_values=True):
        self._valobj = valobj
        self._show_values = show_values

        table = self._table()
        table_inner = table["table"]
        capacity = int(table_inner["bucket_mask"]) + 1
        ctrl = table_inner["ctrl"]["pointer"]

        self._size = int(table_inner["items"])
        self._pair_type = table.type.template_argument(0).strip_typedefs()

        self._new_layout = not table_inner.type.has_key("data")
        if self._new_layout:
            self._data_ptr = ctrl.cast(self._pair_type.pointer())
        else:
            self._data_ptr = table_inner["data"]["pointer"]

        self._valid_indices = []
        for idx in range(capacity):
            address = ctrl + idx
            value = address.dereference()
            is_presented = value & 128 == 0
            if is_presented:
                self._valid_indices.append(idx)

    def _table(self):
        if self._show_values:
            hashbrown_hashmap = self._valobj["base"]
        elif self._valobj.type.fields()[0].name == "map":
            # BACKCOMPAT: rust 1.47
            # HashSet wraps std::collections::HashMap, which wraps hashbrown::HashMap
            hashbrown_hashmap = self._valobj["map"]["base"]
        else:
            # HashSet wraps hashbrown::HashSet, which wraps hashbrown::HashMap
            hashbrown_hashmap = self._valobj["base"]["map"]
        return hashbrown_hashmap["table"]

    def to_string(self):
        if self._show_values:
            return "HashMap(size={})".format(self._size)
        else:
            return "HashSet(size={})".format(self._size)

    def children(self):
        pairs_start = self._data_ptr

        for index in range(self._size):
            idx = self._valid_indices[index]
            if self._new_layout:
                idx = -(idx + 1)
            element = (pairs_start + idx).dereference()
            if self._show_values:
                yield "key{}".format(index), element[ZERO_FIELD]
                yield "val{}".format(index), element[FIRST_FIELD]
            else:
                yield "[{}]".format(index), element[ZERO_FIELD]

    def num_children(self):
        result = self._size
        if self._show_values:
            result *= 2
        return result

    def display_hint(self):
        return "map" if self._show_values else "array"
