#!/usr/bin/env python3
"""
Dump the full Framework EC keyboard matrix.
Reads all matrix positions and prints those with non-zero scancodes.

Usage:
  sudo python3 dump-matrix.py
  # or on NixOS:
  # nix-shell -p python3 --run 'sudo python3 dump-matrix.py'
"""

import subprocess
import sys

# PS/2 Scan Code Set 2 -> Key name mapping
SCANCODE_NAMES = {
    0x00: "(none)",
    0x01: "F9",
    0x03: "F5",
    0x04: "F3",
    0x05: "F1",
    0x06: "F2",
    0x07: "F12",
    0x09: "F10",
    0x0A: "F8",
    0x0B: "F6",
    0x0C: "F4",
    0x0D: "Tab",
    0x0E: "`",
    0x11: "L Alt",
    0x12: "L Shift",
    0x14: "L Ctrl",
    0x15: "q",
    0x16: "1",
    0x1A: "z",
    0x1B: "s",
    0x1C: "a",
    0x1D: "w",
    0x1E: "2",
    0x21: "c",
    0x22: "x",
    0x23: "d",
    0x24: "e",
    0x25: "4",
    0x26: "3",
    0x29: "Space",
    0x2A: "v",
    0x2B: "f",
    0x2C: "t",
    0x2D: "r",
    0x2E: "5",
    0x31: "n",
    0x32: "b",
    0x33: "h",
    0x34: "g",
    0x35: "y",
    0x36: "6",
    0x3A: "m",
    0x3B: "j",
    0x3C: "u",
    0x3D: "7",
    0x3E: "8",
    0x41: ",",
    0x42: "k",
    0x43: "i",
    0x44: "o",
    0x45: "0",
    0x46: "9",
    0x49: ".",
    0x4A: "/",
    0x4B: "l",
    0x4C: ";",
    0x4D: "p",
    0x4E: "-",
    0x52: "'",
    0x54: "[",
    0x55: "=",
    0x58: "Caps Lock",
    0x59: "R Shift",
    0x5A: "Enter",
    0x5B: "]",
    0x5D: "\\",
    0x66: "Backspace",
    0x69: "KP 1",
    0x6B: "KP 4",
    0x6C: "KP 7",
    0x70: "KP 0",
    0x71: "KP .",
    0x72: "KP 2",
    0x73: "KP 5",
    0x74: "KP 6",
    0x75: "KP 8",
    0x76: "Esc",
    0x77: "Num Lock",
    0x78: "F11",
    0x79: "KP +",
    0x7A: "KP 3",
    0x7B: "KP -",
    0x7C: "KP *",
    0x7D: "KP 9",
    0x7E: "Scroll Lock",
    0x83: "F7",
    0xFF: "Fn",
    # Extended keys (E0 prefix) - stored as 16-bit values
    0xE011: "R Alt",
    0xE014: "R Ctrl",
    0xE01F: "L GUI",
    0xE027: "R GUI",
    0xE02F: "Apps/Menu",
    0xE04A: "KP /",
    0xE05A: "KP Enter",
    0xE069: "End",
    0xE06B: "Left Arrow",
    0xE06C: "Home",
    0xE070: "Insert",
    0xE071: "Delete",
    0xE072: "Down Arrow",
    0xE074: "Right Arrow",
    0xE075: "Up Arrow",
    0xE07A: "Page Down",
    0xE07D: "Page Up",
}


def get_scancode_name(code):
    if code in SCANCODE_NAMES:
        return SCANCODE_NAMES[code]
    return f"unknown"


def read_matrix_position(row, col):
    """Read the scancode at a given matrix position. Returns the 16-bit scancode."""
    col_hex = format(col, 'x')
    cmd = f"ectool raw 0x3E0C d1,d0,b{row:x},b{col_hex},w0"
    try:
        result = subprocess.run(
            cmd.split(),
            capture_output=True, text=True, timeout=5
        )
        output = result.stdout + result.stderr
        # Parse the response bytes. Look for the "Read XXX bytes" line and
        # then parse the hex dump that follows.
        lines = output.strip().split('\n')
        response_bytes = []
        reading = False
        for line in lines:
            if "Read" in line and "bytes" in line:
                reading = True
                continue
            if reading and '|' in line:
                # Extract hex bytes before the |
                hex_part = line.split('|')[0].strip()
                for byte_str in hex_part.split():
                    if len(byte_str) == 2:
                        try:
                            response_bytes.append(int(byte_str, 16))
                        except ValueError:
                            pass

        # Scancode is at bytes 10-11 (little-endian 16-bit)
        if len(response_bytes) >= 12:
            scancode = response_bytes[10] | (response_bytes[11] << 8)
            return scancode
        return None
    except (subprocess.TimeoutExpired, Exception) as e:
        print(f"  Error reading ({row},{col:x}): {e}", file=sys.stderr)
        return None


def main():
    if subprocess.os.geteuid() != 0:
        print("This script must be run as root (sudo).", file=sys.stderr)
        sys.exit(1)

    # Scan a wide range: rows 0-15, cols 0-15
    # This covers well beyond the assumed 8x12 matrix
    max_row = 16
    max_col = 16

    print(f"Scanning matrix positions ({max_row} rows x {max_col} cols)...")
    print(f"{'Pos':>8}   {'Scancode':>10}   Key")
    print("-" * 40)

    found = []
    for row in range(max_row):
        for col in range(max_col):
            scancode = read_matrix_position(row, col)
            if scancode is not None and scancode != 0:
                name = get_scancode_name(scancode)
                label = f"({row:x},{col:x})"
                sc_str = f"{scancode:04x}" if scancode > 0xFF else f"{scancode:02x}"
                print(f"{label:>8}   {sc_str:>10}   {name}")
                found.append((row, col, scancode, name))
            # Print progress on stderr
            print(f"\r  Scanning ({row:x},{col:x})...   ", end='', file=sys.stderr, flush=True)

    print("\r                          ", file=sys.stderr)
    print("-" * 40)
    print(f"Found {len(found)} mapped positions.")

    # Print a visual grid
    print("\n=== Matrix Grid ===")
    print(f"{'':>6}", end='')
    for col in range(max_col):
        print(f"  {col:x}    ", end='')
    print()

    for row in range(max_row):
        print(f"  {row:x}  ", end='')
        for col in range(max_col):
            match = None
            for r, c, sc, name in found:
                if r == row and c == col:
                    match = name
                    break
            if match:
                print(f" {match:>6}", end='')
            else:
                print(f"     .", end='')
        print()


if __name__ == "__main__":
    main()
