# Copyright 2008-2021 pydicom authors. See LICENSE file for details.
"""Utility functions used in debugging writing and reading"""

from io import BytesIO
import os
import sys
from typing import BinaryIO, TYPE_CHECKING

from pydicom.valuerep import VR

if TYPE_CHECKING:  # pragma: no cover
    from pydicom.dataset import Dataset


def print_character(ordchr: int) -> str:
    """Return a printable character, or '.' for non-printable ones."""
    if 31 < ordchr < 126 and ordchr != 92:
        return chr(ordchr)

    return "."


def filedump(
    filename: str | bytes | os.PathLike,
    start_address: int = 0,
    stop_address: int | None = None,
) -> str:
    """Dump out the contents of a file to a standard hex dump 16 bytes wide"""

    with open(filename, "rb") as f:
        return hexdump(f, start_address, stop_address)


def datadump(
    data: bytes, start_address: int = 0, stop_address: int | None = None
) -> str:
    """Return a hex string representation of `data`."""
    return hexdump(BytesIO(data), start_address, stop_address)


def hexdump(
    f: BinaryIO,
    start_address: int = 0,
    stop_address: int | None = None,
    show_address: bool = True,
) -> str:
    """Return a formatted string of hex bytes and characters in data.

    This is a utility function for debugging file writing.

    Parameters
    ----------
    f : BinaryIO
        The file-like to dump.
    start_address : int, optional
        The offset where the dump should start (default ``0``)
    stop_address : int, optional
        The offset where the dump should end, by default the entire file will
        be dumped.
    show_address : bool, optional
        If ``True`` (default) then include the offset of each line of output.

    Returns
    -------
    str
    """

    s = []

    # Determine the maximum number of characters for the offset
    max_offset_len = len(f"{f.seek(0, 2):X}")
    if stop_address:
        max_offset_len = len(f"{stop_address:X}")

    f.seek(start_address)
    while True:
        offset = f.tell()
        if stop_address and offset > stop_address:
            break

        data = f.read(16)
        if not data:
            break

        current = []

        if show_address:
            # Offset at the start of the current line
            current.append(f"{offset:0{max_offset_len}X}  ")

        # Add hex version of the current line
        b = " ".join([f"{x:02X}" for x in data])
        current.append(f"{b:<49}")  # if fewer than 16 bytes, pad out to length

        # Append the ASCII version of the current line (or . if not ASCII)
        current.append("".join([print_character(x) for x in data]))

        s.append("".join(current))

    return "\n".join(s)


def pretty_print(
    ds: "Dataset", indent_level: int = 0, indent_chars: str = "  "
) -> None:
    """Print a dataset directly, with indented levels.

    This is just like Dataset._pretty_str, but more useful for debugging as it
    prints each item immediately rather than composing a string, making it
    easier to immediately see where an error in processing a dataset starts.

    """

    indent = indent_chars * indent_level
    next_indent = indent_chars * (indent_level + 1)
    for elem in ds:
        if elem.VR == VR.SQ:  # a sequence
            print(f"{indent}{elem.tag} {elem.name} -- {len(elem.value)} item(s)")
            for dataset in elem.value:
                pretty_print(dataset, indent_level + 1)
                print(next_indent + "---------")
        else:
            print(indent + repr(elem))


if __name__ == "__main__":  # pragma: no cover
    filename = sys.argv[1]
    start_address = 0
    stop_address = None
    if len(sys.argv) > 2:  # then have start address
        start_address = eval(sys.argv[2])
    if len(sys.argv) > 3:
        stop_address = eval(sys.argv[3])

    print(filedump(filename, start_address, stop_address))
