"""
Dereference and format pointer chains.
"""

from __future__ import annotations

from typing import List

import pwndbg.aglib.arch
import pwndbg.aglib.memory
import pwndbg.aglib.typeinfo
import pwndbg.aglib.vmmap
import pwndbg.color.memory as M
import pwndbg.enhance
import pwndbg.integration
from pwndbg.color import ColorConfig
from pwndbg.color import ColorParamSpec
from pwndbg.color import theme

LIMIT = pwndbg.config.add_param(
    "dereference-limit", 5, "max number of pointers to dereference in a chain"
)

c = ColorConfig(
    "chain",
    [
        ColorParamSpec("arrow", "normal", "color of chain formatting (arrow)"),
        ColorParamSpec(
            "contiguous-marker", "normal", "color of chain formatting (contiguous marker)"
        ),
    ],
)


def get(
    address: int | None,
    limit: int = LIMIT,
    offset: int = 0,
    hard_stop: int | None = None,
    hard_end: int = 0,
    include_start: bool = True,
    safe_linking: bool = False,
) -> List[int] | None:
    """
    Recursively dereferences an address. For bare metal, it will stop when the address is not in any of vmmap pages to avoid redundant dereference.

    Arguments:
        address: the first address to begin dereferencing
        limit: number of valid pointers
        offset: offset into the address to get the next pointer
        hard_stop: address to stop at
        hard_end: value to append when hard_stop is reached
        include_start: whether to include starting address or not
        safe_linking: whether this chain use safe-linking

    Returns:
        A list representing pointers of each ```address``` and reference
    """
    if address is None:
        return None
    assert address >= 0, "address must be positive"

    limit = int(limit)

    result = [address] if include_start else []

    is_pagefault_supported = pwndbg.aglib.memory.is_pagefault_supported()

    for _ in range(limit):
        # Don't follow cycles, except to stop at the second occurrence.
        if result.count(address) >= 2:
            break

        if hard_stop is not None and address == hard_stop:
            result.append(hard_end)
            break

        try:
            address = address + offset

            # On embedded systems, it's non uncommon for MMIO regions to exist where memory reads might mutate the hardware/process state.
            # This check prevents the memory dereferences to protect against this case.
            # See discussion here: https://github.com/pwndbg/pwndbg/pull/385
            if not is_pagefault_supported and not pwndbg.aglib.vmmap.find(address):
                break

            next_address = pwndbg.aglib.memory.read_pointer_width(address)
            address = next_address ^ ((address >> 12) if safe_linking else 0)
            address &= pwndbg.aglib.arch.ptrmask
            result.append(address)
        except pwndbg.dbg_mod.Error:
            break

    return result


config_arrow_left = theme.add_param("chain-arrow-left", "◂—", "left arrow of chain formatting")
config_arrow_right = theme.add_param("chain-arrow-right", "—▸", "right arrow of chain formatting")
config_contiguous = theme.add_param(
    "chain-contiguous-marker", "...", "contiguous marker of chain formatting"
)


def format(
    value: int | List[int] | None,
    limit: int = LIMIT,
    code: bool = True,
    offset: int = 0,
    hard_stop: int | None = None,
    hard_end: int = 0,
    safe_linking: bool = False,
    enhance_string_len: int | None = None,
) -> str:
    """
    Recursively dereferences an address into string representation, or convert the list representation
    of address dereferences into string representation.

    Arguments:
        value: Either the starting address to be sent to get, or the result of get (a list)
        limit: Number of valid pointers
        code: Hint that indicates the value may be an instruction
        offset: Offset into the address to get the next pointer
        hard_stop: Value to stop on
        hard_end: Value to append when hard_stop is reached: null, value of hard stop, a string.
        safe_linking: whether this chain use safe-linking
        enhance_string_len: The length of string to display for enhancement of the last pointer
    Returns:
        A string representing pointers of each address and reference
        Strings format: 0x0804a10 —▸ 0x08061000 ◂— 0x41414141
    """
    if value is None:
        return "<unavailable>"

    limit = int(limit)

    # Allow results from get function to be passed to format
    if isinstance(value, list):
        chain = value
    else:
        chain = get(value, limit, offset, hard_stop, hard_end, safe_linking=safe_linking) or []

    arrow_left = c.arrow(f" {config_arrow_left} ")
    arrow_right = c.arrow(f" {config_arrow_right} ")

    # Colorize the chain
    rest = [M.get_address_and_symbol(addr) if addr >= 0 else "" for addr in chain]

    # If the dereference limit is zero, skip any enhancements.
    if limit == 0:
        return rest[0]
    # Otherwise replace last element with the enhanced information.
    rest = rest[:-1]

    # Enhance the last entry
    # If there are no pointers (e.g. eax = 0x41414141), then enhance
    # the only element there is.
    if len(chain) == 1:
        # Note the "attempt_dereference" argument, which is set to False.
        # In general, this function assumes that the caller has manually fully dereferenced the input list of pointers.
        # If the only value in the list is a pointer, the function assumes this is purposeful and that that pointer cannot be dereferenced.
        # This is because the code that generated the list determined that we cannot safely reason about the dereferenced value at the current program state.
        # This case only applies to lists of length one, because if the list has more than one value, we already know
        # that the second to last value, chain[-2], can be safely dereferenced - how else would chain[-1] exist?
        # In other case where chain[-1] is not a pointer, the argument has no effect.
        enhanced = pwndbg.enhance.enhance(
            chain[-1],
            code=code,
            attempt_dereference=False,
            enhance_string_len=enhance_string_len,
        )
    # We want to enhance the last pointer value. If an offset was used
    # chain failed at that offset, so display that offset.
    elif len(chain) < limit + 1:
        enhanced = pwndbg.enhance.enhance(
            chain[-2] + offset,
            code=code,
            safe_linking=safe_linking,
            enhance_string_len=enhance_string_len,
        )

    else:
        enhanced = c.contiguous_marker(f"{config_contiguous}")

    if len(chain) == 1:
        return enhanced

    return arrow_right.join(rest) + arrow_left + enhanced
