#!/usr/bin/env python3
#
# Generate the keycodes/evdev file from the names defined in
# linux/input-event-codes.h
#
# Note that this script relies on libevdev to provide the key names and
# those are compiled in. Ensure you have a recent-enough libevdev to
# generate this list.
#

import argparse
import contextlib
import re
import sys

try:
    import libevdev
except ImportError:
    print(
        "WARNING: python-libevdev not available, cannot check for new evdev keycodes",
        file=sys.stderr,
    )
    sys.exit(77)


# The marker to search for in the template file, replaced with our generated
# codes.
replacement_marker = "@evdevkeys@"

# These markers are put into the result file and are used to detect
# the section that we added when parsing an existing file.
section_header = "Key codes below are autogenerated"
section_footer = "End of autogenerated key codes"


def evdev_codes():
    """
    Return the dict {code, name} for all known evdev codes.

    The list of names is compiled into libevdev.so, use a recent libevdev
    release to get the most up-to-date list.
    """
    codes = {}
    for c in libevdev.EV_KEY.codes:
        # 112 because that's where our 1:1 keycode entries historically
        # started.
        # Undefined keys are those with a code < KEY_MAX but without a
        # #define in the kernel header file
        if c.value < 112 or not c.is_defined:
            continue

        if c.name.startswith("BTN_") or c.name == "KEY_MAX":
            continue

        codes[c.value] = c.name

    return codes


def existing_keys(lines):
    """
    Return the dict {code, name} for all existing keycodes in the templates
    file.

    This is a very simple parser, good enough for the keycodes/evdev file
    but that's about it.
    """
    pattern = re.compile(r"\s+\<([^>]+)\>\s+=\s+(\d+);")
    keys = {}
    for line in lines:
        match = re.match(pattern, line)
        if not match:
            continue
        keys[int(match.group(2))] = match.group(1)

    return keys


def generate_keycodes_file(template, codes):
    """
    Generate a new keycodes/evdev file with line containing @evdevkeys@
    replaced by the full list of known evdev key codes, including our
    section_header/footer. Expected output:

    ::

    // $section_header
    <I$keycode> = <$keycode + 8> // #define $kernel_name
    ...
    // $section_footer

    """
    lines = template.readlines()
    existing = existing_keys(lines)

    output = []
    for line in lines:
        if replacement_marker not in line:
            output.append(line)
            continue

        output.append(f"\t// {section_header}\n")

        warned = False
        for code, name in codes.items():
            xkeycode = code + 8

            if xkeycode > 255 and not warned:
                warned = True
                output.append("\n")
                output.append("\t// Key codes below cannot be used in X\n")
                output.append("\n")

            if xkeycode in existing:
                output.append(
                    f"\talias <I{xkeycode}> = <{existing[xkeycode]}>;	// #define {name:23s} {code}\n"
                )
                continue

            # Special keys that need a comment
            special_keys = {
                211: "conflicts with AB11",
            }

            comment = special_keys.get(xkeycode, "")
            if comment:
                comment = f" {comment}"

            output.append(
                f"\t<I{xkeycode}> = {xkeycode};\t\t// #define {name:23s} {code}{comment}\n"
            )
        output.append(f"\t// {section_footer}\n")

    return output


def extract_generated_keycodes(fp):
    """
    Return an iterator the keycode of any <I123> keys between the section
    header and footer.
    """
    in_generated_section = False
    pattern = re.compile(".*<I([0-9]*)>.*")

    for line in fp:
        if section_header in line:
            in_generated_section = True
            continue
        elif section_footer in line:
            return
        elif in_generated_section:
            match = pattern.match(line)
            if match:
                yield int(match[1])


def compare_with(codes, oldfile):
    """
    Extract the <I123> keycodes from between the section_header/footer of
    oldfile and return a list of keycodes that are in codes but not in
    oldfile.
    """
    old_keycodes = extract_generated_keycodes(oldfile)
    keycodes = [c + 8 for c in codes]  # X keycode offset

    # This does not detect keycodes in old_keycode but not in the new
    # generated list - should never happen anyway.
    return sorted(set(keycodes).difference(old_keycodes))


def log_msg(msg):
    print(msg, file=sys.stderr)


def main():
    parser = argparse.ArgumentParser(description="Generate the evdev keycode lists.")
    parser.add_argument(
        "--template",
        type=argparse.FileType("r"),
        default=open(".gitlab-ci/evdev.in"),
        help="The template file (default: .gitlab-ci/evdev.in)",
    )
    parser.add_argument(
        "--output",
        type=str,
        default="keycodes/evdev",
        required=False,
        help="The file to be written to (default: keycodes/evdev)",
    )
    parser.add_argument(
        "--compare-with",
        type=argparse.FileType("r"),
        default=open("keycodes/evdev"),
        help="Compare generated output with the given file (default: keycodes/evdev)",
    )
    parser.add_argument(
        "--verbose",
        action=argparse.BooleanOptionalAction,
        help="Print verbose output to stderr",
    )
    ns = parser.parse_args()

    codes = evdev_codes()
    rc = 0
    if ns.verbose:
        kmin, kmax = min(codes.keys()), max(codes.keys())
        log_msg(f"evdev keycode range: {kmin} ({kmin:#x}) → {kmax} ({kmax:#x})")

    # We compare before writing so we can use the same filename for
    # --compare-with and --output. That's also why --output has to be type
    # str instead of FileType('w').
    if ns.compare_with:
        diff = compare_with(codes, ns.compare_with)
        if diff:
            rc = 1
            if ns.verbose:
                log_msg(
                    f"File {ns.compare_with.name} is out of date, missing keycodes:"
                )
                for k in diff:
                    name = codes[k - 8]  # remove the X offset
                    log_msg(f"  <I{k}> // #define {name}")

    with contextlib.ExitStack() as stack:
        if ns.output == "-":
            fd = sys.stdout
        else:
            fd = stack.enter_context(open(ns.output, "w"))
        output = generate_keycodes_file(ns.template, codes)
        fd.write("".join(output))

    sys.exit(rc)


if __name__ == "__main__":
    main()
