"""
Prints structures in a manner similar to Windbg's "dt" command.
"""

from __future__ import annotations

import re
from typing import List

import gdb

import pwndbg.gdblib.memory
import pwndbg.gdblib.typeinfo


def get_type(v: gdb.Value) -> str:
    t = v.type
    while not t.name:
        if t.code == gdb.TYPE_CODE_PTR:
            t = t.target()
    return t.name


def get_typename(t: gdb.Type) -> str:
    return str(t)


def get_arrsize(f: gdb.Value) -> int:
    t = f.type
    if t.code != gdb.TYPE_CODE_ARRAY:
        return 0
    t2 = t.target()
    return int(t.sizeof / t2.sizeof)


def get_field_by_name(obj: gdb.Value, field: str) -> gdb.Value:
    # Dereference once
    if obj.type.code == gdb.TYPE_CODE_PTR:
        obj = obj.dereference()
    for f in re.split(r"(->|\.|\[\d+\])", field):
        if not f:
            continue
        if f == "->":
            obj = obj.dereference()
        elif f == ".":
            pass
        elif f.startswith("["):
            n = int(f.strip("[]"))
            obj = obj.cast(obj.dereference().type.pointer())
            obj += n
            obj = obj.dereference()
        else:
            obj = obj[f]
    return obj


def happy(typename: str) -> str:
    prefix = ""
    if "unsigned" in typename:
        prefix = "u"
        typename = typename.replace("unsigned ", "")
    return (
        prefix
        + {
            "char": "char",
            "short int": "short",
            "long int": "long",
            "int": "int",
            "long long": "longlong",
            "float": "float",
            "double": "double",
        }[typename]
    )


def dt(name: str = "", addr: int | gdb.Value | None = None, obj: gdb.Value | None = None) -> str:
    """
    Dump out a structure type Windbg style.
    """
    # Return value is a list of strings.of
    # We concatenate at the end.
    rv: List[str] = []

    if obj and not name:
        t = obj.type
        while t.code == (gdb.TYPE_CODE_PTR):
            t = t.target()
            obj = obj.dereference()
        name = str(t)

    # Lookup the type name specified by the user
    else:
        t = pwndbg.gdblib.typeinfo.load(name)

    if not t:
        return ""

    # If it's not a struct (e.g. int or char*), bail
    if t.code not in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_TYPEDEF, gdb.TYPE_CODE_UNION):
        raise Exception(f"Not a structure: {t}")

    # If an address was specified, create a Value of the
    # specified type at that address.
    if addr is not None:
        obj = pwndbg.gdblib.memory.get_typed_pointer_value(t, addr)

    # Header, optionally include the name
    header = name
    if obj:
        header = f"{header} @ {hex(int(obj.address))}"
    rv.append(header)

    if t.strip_typedefs().code == gdb.TYPE_CODE_ARRAY:
        return "Arrays not supported yet"
    if t.strip_typedefs().code not in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION):
        t = {name: obj or gdb.Value(0).cast(t)}

    for name, field in t.items():
        # Offset into the parent structure
        o = getattr(field, "bitpos", 0) // 8
        b = getattr(field, "bitpos", 0) % 8
        extra = str(field.type)
        ftype = field.type.strip_typedefs()

        if obj and obj.type.strip_typedefs().code in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION):
            v = obj[name]

            if ftype.code == gdb.TYPE_CODE_INT:
                v = hex(int(v))
            if (
                ftype.code in (gdb.TYPE_CODE_PTR, gdb.TYPE_CODE_ARRAY)
                and ftype.target() == pwndbg.gdblib.typeinfo.uchar
            ):
                data = pwndbg.gdblib.memory.read(int(v.address), ftype.sizeof)
                v = " ".join("%02x" % b for b in data)

            extra = v

        # Adjust trailing lines in 'extra' to line up
        # This is necessary when there are nested structures.
        # Ideally we'd expand recursively if the type is complex.
        extra_lines: List[str] = []
        for i, line in enumerate(str(extra).splitlines()):
            if i == 0:
                extra_lines.append(line)
            else:
                extra_lines.append(35 * " " + line)
        extra = "\n".join(extra_lines)

        bitpos = "" if not b else (".%i" % b)

        if obj:
            line = "    0x%016x +0x%04x%s %-20s : %s" % (
                int(obj.address) + o,
                o,
                bitpos,
                name,
                extra,
            )
        else:
            line = "    +0x%04x%s %-20s : %s" % (o, bitpos, name, extra)
        rv.append(line)

    return "\n".join(rv)
