"""
Push WMI fan/temperature snapshot from MICommonInterface into RTSS on-screen display.

Requires RTSS running. Shared memory: RTSSSharedMemoryV2 (see RTSS SDK RTSSSharedMemory.h).

OSD slots start at index 1; slot 0 is reserved for MSI Afterburner-style hosts.
"""

from __future__ import annotations

import argparse
import ctypes
import struct
import sys
import time

kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)

FILE_MAP_ALL_ACCESS = 0xF001F

OpenFileMappingW = kernel32.OpenFileMappingW
OpenFileMappingW.argtypes = [ctypes.c_uint32, ctypes.c_bool, ctypes.c_wchar_p]
OpenFileMappingW.restype = ctypes.c_void_p

MapViewOfFile = kernel32.MapViewOfFile
MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
MapViewOfFile.restype = ctypes.c_void_p

UnmapViewOfFile = kernel32.UnmapViewOfFile
UnmapViewOfFile.argtypes = [ctypes.c_void_p]
UnmapViewOfFile.restype = ctypes.c_bool

CloseHandle = kernel32.CloseHandle
CloseHandle.argtypes = [ctypes.c_void_p]
CloseHandle.restype = ctypes.c_bool

def _interlocked_compare_exchange(ptr: ctypes.POINTER(ctypes.c_long), exchange: int, comparand: int) -> int:
    """Pure-ctypes fallback for InterlockedCompareExchange (not exported on x64 kernel32)."""
    old = ptr[0]
    if old == comparand:
        ptr[0] = exchange
    return old


def _interlocked_exchange(ptr: ctypes.POINTER(ctypes.c_long), value: int) -> int:
    old = ptr[0]
    ptr[0] = value
    return old

RTSS_SIGNATURE = 0x52545353

VERSION_EX_OSD = 0x00020007
VERSION_BUSY = 0x0002000E

MAP_BYTES = 40 * 1024 * 1024


OSDFRAME_OFFSET = 32  # byte offset of dwOSDFrame in RTSS_SHARED_MEMORY header


def _read_global_header(buf: memoryview) -> tuple[int, int, int, int, int]:
    sig, ver, _, _, _, osd_esize, osd_off, osd_asize = struct.unpack_from("<8I", buf, 0)
    return sig, ver, osd_esize, osd_off, osd_asize


def _entry_owner(buf: memoryview, ent_base: int) -> bytes:
    chunk = buf[ent_base + 256 : ent_base + 512]
    return bytes(chunk.tobytes().split(b"\0", 1)[0])


def _write_sz(buf: memoryview, offset: int, cap: int, text: str, encoding: str) -> None:
    raw = text.encode(encoding, errors="replace")
    if len(raw) >= cap:
        raw = raw[: cap - 1]
    region = buf[offset : offset + cap]
    region[:] = bytes(cap)
    region[: len(raw)] = raw


def update_rtss_osd(owner: str, text: str, *, encoding: str = "utf-8") -> bool:
    handle = OpenFileMappingW(FILE_MAP_ALL_ACCESS, False, "RTSSSharedMemoryV2")
    if not handle:
        return False

    base = MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, 0)
    if not base:
        CloseHandle(handle)
        return False

    raw = (ctypes.c_ubyte * MAP_BYTES).from_address(base)
    buf = memoryview(raw).cast("B")

    try:
        sig, ver, osd_esize, osd_off, osd_asize = _read_global_header(buf)
        if sig != RTSS_SIGNATURE or ver < 0x00020000:
            return False
        if osd_esize <= 0 or osd_asize <= 0 or osd_off <= 0:
            return False

        owner_b = owner.encode(encoding, errors="replace")
        if len(owner_b) >= 256:
            owner_b = owner_b[:255]

        use_ex = ver >= VERSION_EX_OSD
        busy_ptr = ctypes.cast(base + 36, ctypes.POINTER(ctypes.c_long)) if ver >= VERSION_BUSY else None

        def write_slot(ent_base: int) -> bool:
            locked = False
            if busy_ptr is not None and use_ex:
                prev = _interlocked_compare_exchange(busy_ptr, 1, 0)
                if prev != 0:
                    return False
                locked = True

            try:
                if use_ex:
                    _write_sz(buf, ent_base + 512, 4096, text, encoding)
                else:
                    _write_sz(buf, ent_base, 256, text, encoding)

                frame = struct.unpack_from("<I", buf, OSDFRAME_OFFSET)[0]
                struct.pack_into("<I", buf, OSDFRAME_OFFSET, frame + 1)
                return True
            finally:
                if locked:
                    _interlocked_exchange(busy_ptr, 0)

        for dw_pass in range(2):
            for dw_entry in range(1, int(osd_asize)):
                ent_base = int(osd_off) + dw_entry * int(osd_esize)
                if ent_base + int(osd_esize) > len(buf):
                    break

                current_owner = _entry_owner(buf, ent_base)

                if dw_pass == 1 and current_owner == b"":
                    _write_sz(buf, ent_base + 256, 256, owner, encoding)
                    current_owner = owner_b

                if current_owner == owner_b:
                    return write_slot(ent_base)

        return False
    finally:
        UnmapViewOfFile(base)
        CloseHandle(handle)


def release_rtss_osd(owner: str, *, encoding: str = "utf-8") -> None:
    handle = OpenFileMappingW(FILE_MAP_ALL_ACCESS, False, "RTSSSharedMemoryV2")
    if not handle:
        return

    base = MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, 0)
    if not base:
        CloseHandle(handle)
        return

    raw = (ctypes.c_ubyte * MAP_BYTES).from_address(base)
    buf = memoryview(raw).cast("B")

    try:
        sig, ver, osd_esize, osd_off, osd_asize = _read_global_header(buf)
        if sig != RTSS_SIGNATURE or ver < 0x00020000:
            return

        owner_b = owner.encode(encoding, errors="replace")
        if len(owner_b) >= 256:
            owner_b = owner_b[:255]

        busy_ptr = ctypes.cast(base + 36, ctypes.POINTER(ctypes.c_long)) if ver >= VERSION_BUSY else None

        for dw_entry in range(1, int(osd_asize)):
            ent_base = int(osd_off) + dw_entry * int(osd_esize)
            if ent_base + int(osd_esize) > len(buf):
                break

            if _entry_owner(buf, ent_base) != owner_b:
                continue

            locked = False
            if busy_ptr is not None:
                prev = _interlocked_compare_exchange(busy_ptr, 1, 0)
                locked = prev == 0

            try:
                buf[ent_base : ent_base + int(osd_esize)] = bytes(int(osd_esize))
                frame = struct.unpack_from("<I", buf, OSDFRAME_OFFSET)[0]
                struct.pack_into("<I", buf, OSDFRAME_OFFSET, frame + 1)
            finally:
                if locked:
                    _interlocked_exchange(busy_ptr, 0)
            break
    finally:
        UnmapViewOfFile(base)
        CloseHandle(handle)


def main() -> None:
    try:
        import pythoncom
    except ImportError as exc:
        raise SystemExit("Missing pywin32 (pythoncom). pip install pywin32") from exc
    from read_fan_realtime import FanReader, WMI_INSTANCE_NAME, format_full_snapshot

    parser = argparse.ArgumentParser(description="Feed WMI laptop fan stats to RTSS OSD.")
    parser.add_argument("--owner", default="WMI_FanFeed", help="RTSS OSD owner id string")
    parser.add_argument("--instance", default=WMI_INSTANCE_NAME, help="MICommonInterface InstanceName")
    parser.add_argument("-i", "--interval", type=float, default=1.0, help="poll seconds")
    parser.add_argument("--compact", action="store_true", help="shorter OSD line")
    parser.add_argument("--encoding", default="utf-8", help="encode OSD text")
    parser.add_argument("--release-on-exit", action="store_true", help="clear OSD slot on exit")
    parser.add_argument("--once", action="store_true", help="single update then exit")
    args = parser.parse_args()

    pythoncom.CoInitialize()
    reader = FanReader(args.instance)
    try:
        while True:
            fields = reader.read_full_snapshot()
            if args.compact:
                mf = fields.get("max_fan_switch")
                mfv = fields.get("max_fan_speed")
                line = (
                    f"CPU {fields['cpu_rpm']}RPM GPU {fields['gpu_rpm']}RPM "
                    f"{fields['cpu_temp']}C MF {mf}({mfv})"
                )
            else:
                line = format_full_snapshot(fields)

            if update_rtss_osd(args.owner, line, encoding=args.encoding):
                if not args.once:
                    sys.stdout.write("\rRTSS OSD OK  ")
                    sys.stdout.flush()
            else:
                print(
                    "RTSS: cannot open RTSSSharedMemoryV2 — is RTSS running?",
                    file=sys.stderr,
                )
                if args.once:
                    raise SystemExit(1)

            if args.once:
                break
            time.sleep(args.interval)
    except KeyboardInterrupt:
        print()
    finally:
        if args.release_on_exit:
            release_rtss_osd(args.owner, encoding=args.encoding)
        reader = None
        pythoncom.CoUninitialize()


if __name__ == "__main__":
    main()
