"""Read-only EC RAM tools using the bundled WinRing0 driver.

Subcommands:
    install    register and start the kernel service (admin)
    uninstall  stop and delete the service (admin)
    status     show service state and try to open the device
    dump       dump 256 bytes of EC RAM (read-only)
    read       read a single EC RAM byte
    diff       diff two dump files

WARNING:
    EC RAM access talks directly to the embedded controller.
    This tool only reads. Do not modify it to write EC bytes
    unless you know exactly which offsets are safe.
"""

from __future__ import annotations

import argparse
import ctypes
import os
import shutil
import subprocess
import sys
import time
from ctypes import byref, wintypes
from datetime import datetime
from pathlib import Path

SERVICE_NAME = "WinRing0_1_2_0"
DEVICE_PATH = r"\\.\WinRing0_1_2_0"

DEFAULT_DRIVER_NAME = "蛟龙游戏控制中心.sys"
DEFAULT_DRIVER_DIRS = [
    Path(__file__).resolve().parent,
    Path(__file__).resolve().parent.parent / "蛟龙游戏控制中心",
]

EC_DATA = 0x62
EC_SC = 0x66
EC_OBF = 0x01
EC_IBF = 0x02
EC_CMD_READ = 0x80

IOCTL_OLS_READ_IO_PORT_BYTE = 0x9C4020CC
IOCTL_OLS_WRITE_IO_PORT_BYTE = 0x9C4020D8

GENERIC_READ = 0x80000000
GENERIC_WRITE = 0x40000000
OPEN_EXISTING = 3
INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value


def _setup_kernel32():
    k = ctypes.WinDLL("kernel32", use_last_error=True)

    k.CreateFileW.argtypes = [
        wintypes.LPCWSTR,
        wintypes.DWORD,
        wintypes.DWORD,
        ctypes.c_void_p,
        wintypes.DWORD,
        wintypes.DWORD,
        wintypes.HANDLE,
    ]
    k.CreateFileW.restype = wintypes.HANDLE

    k.DeviceIoControl.argtypes = [
        wintypes.HANDLE,
        wintypes.DWORD,
        ctypes.c_void_p,
        wintypes.DWORD,
        ctypes.c_void_p,
        wintypes.DWORD,
        ctypes.POINTER(wintypes.DWORD),
        ctypes.c_void_p,
    ]
    k.DeviceIoControl.restype = wintypes.BOOL

    k.CloseHandle.argtypes = [wintypes.HANDLE]
    k.CloseHandle.restype = wintypes.BOOL
    return k


KERNEL32 = _setup_kernel32()


class _WriteIoPortInput(ctypes.Structure):
    _fields_ = [("PortNumber", ctypes.c_uint32), ("Value", ctypes.c_uint32)]


class WinRing0:
    def __init__(self, device_path: str = DEVICE_PATH):
        self.device_path = device_path
        h = KERNEL32.CreateFileW(
            device_path,
            GENERIC_READ | GENERIC_WRITE,
            0,
            None,
            OPEN_EXISTING,
            0,
            None,
        )
        if not h or h == INVALID_HANDLE_VALUE:
            err = ctypes.get_last_error()
            raise OSError(
                f"open device failed: {device_path}; "
                f"GLE={err}. Run `ec_tools.py install` as Administrator first."
            )
        self.handle = h

    def close(self) -> None:
        if self.handle:
            KERNEL32.CloseHandle(self.handle)
            self.handle = None

    def __enter__(self):
        return self

    def __exit__(self, *_):
        self.close()

    def in_byte(self, port: int) -> int:
        in_buf = ctypes.c_uint32(port)
        out_buf = ctypes.c_uint32(0)
        ret = wintypes.DWORD(0)
        ok = KERNEL32.DeviceIoControl(
            self.handle,
            IOCTL_OLS_READ_IO_PORT_BYTE,
            byref(in_buf),
            ctypes.sizeof(in_buf),
            byref(out_buf),
            ctypes.sizeof(out_buf),
            byref(ret),
            None,
        )
        if not ok:
            raise OSError(
                f"in_byte port={port:#x} failed; GLE={ctypes.get_last_error()}"
            )
        return out_buf.value & 0xFF

    def out_byte(self, port: int, value: int) -> None:
        s = _WriteIoPortInput(port, value & 0xFF)
        ret = wintypes.DWORD(0)
        ok = KERNEL32.DeviceIoControl(
            self.handle,
            IOCTL_OLS_WRITE_IO_PORT_BYTE,
            byref(s),
            ctypes.sizeof(s),
            None,
            0,
            byref(ret),
            None,
        )
        if not ok:
            raise OSError(
                f"out_byte port={port:#x} failed; GLE={ctypes.get_last_error()}"
            )


class ECReader:
    def __init__(self, ring: WinRing0, timeout_s: float = 0.5):
        self.ring = ring
        self.timeout_s = timeout_s

    def _wait_status(self, mask: int, expect: int) -> int:
        end = time.perf_counter() + self.timeout_s
        last = 0
        while time.perf_counter() < end:
            last = self.ring.in_byte(EC_SC)
            if (last & mask) == expect:
                return last
        raise TimeoutError(
            f"EC status mask={mask:#x} expect={expect:#x} timeout, last={last:#x}"
        )

    def read(self, offset: int) -> int:
        if not 0 <= offset <= 0xFF:
            raise ValueError("offset must be in 0..255")
        self._wait_status(EC_IBF, 0)
        self.ring.out_byte(EC_SC, EC_CMD_READ)
        self._wait_status(EC_IBF, 0)
        self.ring.out_byte(EC_DATA, offset)
        self._wait_status(EC_OBF, EC_OBF)
        return self.ring.in_byte(EC_DATA)

    def dump(self) -> bytes:
        return bytes(self.read(i) for i in range(256))


def _resolve_driver_path(user_path: str | None) -> Path:
    if user_path:
        p = Path(user_path).expanduser()
        if not p.exists():
            raise SystemExit(f"driver not found: {p}")
        return p
    for d in DEFAULT_DRIVER_DIRS:
        cand = d / DEFAULT_DRIVER_NAME
        if cand.exists():
            return cand
    raise SystemExit(
        "driver .sys not found in default locations; pass --driver-path"
    )


def _run_sc(args: list[str]) -> subprocess.CompletedProcess:
    return subprocess.run(
        ["sc.exe", *args],
        capture_output=True,
        text=True,
        encoding="mbcs",
        errors="replace",
    )


def cmd_install(args) -> int:
    src = _resolve_driver_path(args.driver_path)

    target_dir = Path(os.environ.get("SystemRoot", r"C:\Windows")) / "System32" / "drivers"
    target = target_dir / "WinRing0_1_2_0.sys"
    if not target.exists() or src.stat().st_mtime > target.stat().st_mtime:
        shutil.copy2(src, target)
        print(f"copied {src.name} -> {target}")
    else:
        print(f"driver already present: {target}")

    rc = _run_sc(["query", SERVICE_NAME])
    if rc.returncode == 0:
        print(f"service {SERVICE_NAME} already exists; updating binPath")
        rc = _run_sc(
            [
                "config",
                SERVICE_NAME,
                "type=",
                "kernel",
                "start=",
                "demand",
                "binPath=",
                str(target),
            ]
        )
        print((rc.stdout + rc.stderr).strip())
        if rc.returncode != 0:
            raise SystemExit("sc config failed; run as Administrator")
    else:
        rc = _run_sc(
            [
                "create",
                SERVICE_NAME,
                "type=",
                "kernel",
                "start=",
                "demand",
                "binPath=",
                str(target),
                "DisplayName=",
                "WinRing0_1_2_0",
            ]
        )
        print(rc.stdout.strip() or rc.stderr.strip())
        if rc.returncode != 0:
            raise SystemExit("sc create failed; run as Administrator")

    rc = _run_sc(["start", SERVICE_NAME])
    out = (rc.stdout + rc.stderr).strip()
    print(out)
    if rc.returncode != 0 and "1056" not in out and "已经运行" not in out and "ALREADY_RUNNING" not in out:
        raise SystemExit(
            "sc start failed; common causes: not Administrator, "
            "Windows blocked vulnerable driver via DG/HVCI, or driver signature rejected"
        )
    return 0


def cmd_uninstall(args) -> int:
    rc = _run_sc(["stop", SERVICE_NAME])
    print((rc.stdout + rc.stderr).strip())
    rc = _run_sc(["delete", SERVICE_NAME])
    print((rc.stdout + rc.stderr).strip())
    return 0


def cmd_status(args) -> int:
    rc = _run_sc(["query", SERVICE_NAME])
    print((rc.stdout + rc.stderr).strip())
    try:
        with WinRing0() as r:
            status = r.in_byte(EC_SC)
            print(f"device open OK; EC SC=0x{status:02X}")
    except OSError as exc:
        print(f"device open failed: {exc}")
        return 1
    return 0


def _format_hex_dump(data: bytes) -> str:
    lines = []
    for row in range(0, 256, 16):
        chunk = data[row : row + 16]
        hex_part = " ".join(f"{b:02X}" for b in chunk)
        ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
        lines.append(f"{row:02X}: {hex_part}  {ascii_part}")
    return "\n".join(lines)


def cmd_dump(args) -> int:
    with WinRing0() as r:
        ec = ECReader(r, timeout_s=args.timeout)
        data = ec.dump()

    text = _format_hex_dump(data)
    print(text)

    if args.out:
        out = Path(args.out)
    else:
        ts = datetime.now().strftime("%Y%m%d_%H%M%S")
        out = Path(__file__).resolve().parent / f"ec_dump_{ts}.bin"

    out.write_bytes(data)
    print(f"\nwrote {len(data)} bytes -> {out}")
    return 0


def cmd_read(args) -> int:
    with WinRing0() as r:
        ec = ECReader(r, timeout_s=args.timeout)
        for offset in args.offsets:
            value = ec.read(offset)
            print(f"EC[0x{offset:02X}] = 0x{value:02X} ({value})")
    return 0


def cmd_diff(args) -> int:
    a = Path(args.file_a).read_bytes()
    b = Path(args.file_b).read_bytes()
    n = min(len(a), len(b), 256)
    diffs = [(i, a[i], b[i]) for i in range(n) if a[i] != b[i]]
    if not diffs:
        print("no differences in first 256 bytes")
        return 0
    print(f"{len(diffs)} differing bytes (offset hex: A -> B):")
    for off, va, vb in diffs:
        print(f"  0x{off:02X}: 0x{va:02X} ({va:3d}) -> 0x{vb:02X} ({vb:3d})  delta={vb - va:+d}")
    return 0


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    sub = parser.add_subparsers(dest="cmd", required=True)

    sp = sub.add_parser("install", help="register and start kernel service (admin)")
    sp.add_argument("--driver-path", help="path to .sys file; default: auto-detect")
    sp.set_defaults(func=cmd_install)

    sp = sub.add_parser("uninstall", help="stop and delete the service (admin)")
    sp.set_defaults(func=cmd_uninstall)

    sp = sub.add_parser("status", help="show service state and try to open device")
    sp.set_defaults(func=cmd_status)

    sp = sub.add_parser("dump", help="dump 256 bytes of EC RAM (read-only)")
    sp.add_argument("--out", help="output binary file path")
    sp.add_argument("--timeout", type=float, default=0.5, help="EC status timeout seconds")
    sp.set_defaults(func=cmd_dump)

    sp = sub.add_parser("read", help="read single EC RAM bytes")
    sp.add_argument("offsets", nargs="+", type=lambda s: int(s, 0), help="byte offsets, decimal or 0x-prefixed")
    sp.add_argument("--timeout", type=float, default=0.5)
    sp.set_defaults(func=cmd_read)

    sp = sub.add_parser("diff", help="diff two EC dump files")
    sp.add_argument("file_a")
    sp.add_argument("file_b")
    sp.set_defaults(func=cmd_diff)

    args = parser.parse_args()
    return args.func(args)


if __name__ == "__main__":
    sys.exit(main())
