#!/usr/bin/env python3
# SPDX-License-Identifier: WTFPL
# original: 2007
# rewritten for modernization in 2019

from argparse import ArgumentParser, FileType
import locale
import os
import signal
import sys

def has_colors():
    if os.environ.get("NO_COLOR"):
        return False
    if os.environ.get("CLICOLOR_FORCE") or os.environ.get("FORCE_COLOR"):
        return True
    return sys.stdout.isatty()

try:
    import termcolor
except ImportError:
    def styled(s, *args, **kwargs):
        return s
else:
    def styled(s, *args, **kwargs):
        if has_colors():
            s = termcolor.colored(s, *args, **kwargs)
        return s


REPLACES = {
    '\n': styled(r'\n', 'green'),
    '\t': styled(r'\t', 'green'),
    '\r': styled(r'\r', 'green'),
    '\f': styled(r'\f', 'green'),
    '\b': styled(r'\b', 'green'),
    '\a': styled(r'\a', 'green'),
    '\v': styled(r'\v', 'green'),
    '\x00': styled(r'\0', attrs=['reverse']),
    ' ': styled('sp', 'green'),
}

def transform(b):
    char = chr(b)

    if char in REPLACES:
        return f'{REPLACES[char]}'
    elif 0x20 < b < 0x7f:
        return f'{char} '
    else:
        return f"{styled(f'{b:02X}', attrs=['reverse'])}"


def print_line(line):
    print(' '.join(transform(b) for b in line))


def main():
    locale.setlocale(locale.LC_ALL, "")
    signal.signal(signal.SIGINT, signal.SIG_DFL)
    signal.signal(signal.SIGPIPE, signal.SIG_DFL)

    args = parse_args()
    with args.file:
        process_lines(args)


def parse_args():
    parser = ArgumentParser(description='Visual Hex Dump')
    parser.add_argument(
        "-L", "--line-numbers", action="store_true",
        help="Display line numbers (in decimal base, starting at 1)",
    )
    parser.add_argument(
        "--line-addresses", action="store_true",
        help="Display byte offsets at start of lines (in hexadecimal base)",
    )
    parser.add_argument(
        "--hex", action="store_true",
        help="Always display hex values for all bytes (instead of fancy display ASCII bytes)",
    )
    parser.add_argument(
        "-0", "--zero", action="store_const", const=b"\0", default=b"\n", dest="delimiter",
        help="Lines are separated by NULL characters",
    )
    parser.add_argument('file', nargs='?', type=FileType('rb'), default=sys.stdin.buffer)
    return parser.parse_args()


def iter_lines(fp, delimiter=b"\n"):
    buf = b""
    while True:
        chunk = fp.read(1024)
        if not chunk:
            if buf:
                yield buf
            break

        buf += chunk
        parts = buf.split(delimiter)
        buf = parts.pop(-1)
        for part in parts:
            yield part + delimiter


def process_lines(args):
    total = 0
    lineno = 1
    for line in iter_lines(args.file, args.delimiter):
        if args.line_addresses:
            print(f'{total:08x} ', end='')
        elif args.line_numbers:
            print(f"{lineno:08} ", end="")

        if args.hex:
            print(' '.join(f'{b:02x}' for b in line))
        else:
            print_line(line)

        total += len(line)
        lineno += 1


if __name__ == '__main__':
    main()
