#!/usr/bin/env python3
"""Summarize the original executable's DirectDraw setup and presentation flow."""
from __future__ import annotations

import argparse
import html
import json
import struct
from pathlib import Path

from probe_exe_scene_tables import read_sections, va_to_offset
from summarize_exe_imports import parse_imports


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
EXE = ROOT / "Hwanse2.exe"

DIRECTDRAW_CREATE_WRAPPER = 0x00435D98
DIRECTDRAW_CREATE_CALLS = [0x00415602, 0x00415618]
DIRECTDRAW_GLOBAL = 0x004676C0
PALETTE_GLOBAL = 0x004676C4
CLIPPER_GLOBAL = 0x004676C8
ERROR_GLOBAL = 0x004676CC
DISPLAY_MODE_GLOBAL = 0x004676EC
HWND_GLOBAL = 0x004676F0
FLAGS_GLOBAL = 0x004676D4
PALETTE_BUFFER_GLOBAL = 0x004676E4
PALETTE_ENTRIES_GLOBAL = 0x004676E8
SURFACE_TABLE = 0x0055ABD8

DDRAW_METHODS = {
    0x10: "CreateClipper",
    0x14: "CreatePalette",
    0x18: "CreateSurface",
    0x50: "SetCooperativeLevel",
    0x54: "SetDisplayMode",
}

SURFACE_METHODS = {
    0x08: "Release",
    0x10: "BltFast",
    0x14: "Blt",
    0x18: "BltBatch",
    0x2C: "Flip",
    0x30: "GetAttachedSurface",
    0x70: "SetClipper",
    0x7C: "SetPalette",
}

PALETTE_METHODS = {
    0x08: "Release",
    0x10: "GetEntries",
    0x18: "SetEntries",
}


def hex32(value: int | None) -> str | None:
    if value is None:
        return None
    return f"0x{value:08x}"


def read_at(exe: bytes, sections: list[dict], va: int, size: int) -> bytes:
    offset = va_to_offset(sections, va)
    if offset is None:
        raise ValueError(f"VA {hex32(va)} is outside raw sections")
    return exe[offset : offset + size]


def read_u8(exe: bytes, sections: list[dict], va: int) -> int:
    return read_at(exe, sections, va, 1)[0]


def rel32_target(exe: bytes, sections: list[dict], va: int) -> int:
    opcode = read_u8(exe, sections, va)
    if opcode != 0xE8:
        raise ValueError(f"expected near call at {hex32(va)}")
    rel = struct.unpack("<i", read_at(exe, sections, va + 1, 4))[0]
    return va + 5 + rel


def vtable_call_offset(exe: bytes, sections: list[dict], va: int) -> int:
    body = read_at(exe, sections, va, 3)
    if body[:2] != b"\xff\x50":
        raise ValueError(f"expected call DWORD PTR [eax+imm8] at {hex32(va)}")
    return body[2]


def find_import(imports: dict, name: str) -> dict:
    for dll in imports.get("imports") or []:
        for fn in dll.get("functions") or []:
            if fn.get("name") == name:
                return {"dll": dll.get("dll"), **fn}
    raise ValueError(f"import {name} not found")


def known_vtable_calls(exe: bytes, sections: list[dict]) -> list[dict]:
    rows = [
        (0x00415660, "IDirectDraw", 0x50, "windowed cooperative level candidate", "flags 0x00000008"),
        (0x0041568D, "IDirectDraw", 0x50, "exclusive fullscreen cooperative level", "flags 0x00000011"),
        (0x004156C8, "IDirectDraw", 0x54, "fullscreen display mode", "640x480x8"),
        (0x00415766, "IDirectDraw", 0x18, "primary flipping surface candidate", "DDSD_CAPS|DDSD_BACKBUFFERCOUNT, caps 0x218"),
        (0x0041579D, "IDirectDraw", 0x18, "simple primary surface candidate", "DDSD_CAPS, caps 0x200"),
        (0x00415836, "IDirectDrawSurface", 0x30, "attached backbuffer lookup", "caps 0x00000004"),
        (0x004158D1, "IDirectDraw", 0x18, "offscreen 640x480 surface candidate", "DDSD_CAPS|HEIGHT|WIDTH, caps include 0x40"),
        (0x0041596F, "IDirectDrawSurface", 0x30, "attached backbuffer lookup", "caps 0x00000004"),
        (0x00415A0F, "IDirectDraw", 0x18, "offscreen 640x480 surface candidate", "DDSD_CAPS|HEIGHT|WIDTH, caps include 0x40"),
        (0x00415A9F, "IDirectDraw", 0x10, "windowed clipper creation", "out 0x004676c8"),
        (0x00415AD6, "IDirectDrawClipper", 0x20, "clipper SetHWnd candidate", "hwnd 0x004676f0"),
        (0x00415B0F, "IDirectDrawSurface", 0x70, "primary SetClipper candidate", "clipper 0x004676c8"),
        (0x0041652E, "IDirectDraw", 0x14, "palette creation", "256-entry palette buffer"),
        (0x00416555, "IDirectDrawSurface", 0x7C, "primary SetPalette", "palette 0x004676c4"),
        (0x00416606, "IDirectDrawSurface", 0x7C, "refresh primary SetPalette", "palette 0x004676c4"),
        (0x00416649, "IDirectDrawPalette", 0x18, "SetEntries", "writes changed palette range"),
        (0x00416708, "IDirectDrawPalette", 0x18, "SetEntries", "writes changed palette range"),
        (0x00416856, "IDirectDrawPalette", 0x10, "GetEntries", "reads palette range"),
        (0x0041769B, "IDirectDrawSurface", 0x2C, "Flip", "fullscreen/page-flip path"),
        (0x00417722, "IDirectDrawSurface", 0x14, "Blt", "copy/update path"),
        (0x0041784B, "IDirectDrawSurface", 0x14, "Blt", "copy/update path"),
        (0x00417938, "IDirectDrawSurface", 0x14, "Blt", "copy/update path"),
        (0x00417B4B, "IDirectDrawSurface", 0x14, "Blt", "windowed/scaled copy path"),
        (0x00417B8A, "IDirectDrawSurface", 0x14, "Blt", "windowed/scaled copy path"),
        (0x00417BDF, "IDirectDrawSurface", 0x14, "Blt", "windowed/scaled copy path"),
    ]
    methods = {
        "IDirectDraw": DDRAW_METHODS,
        "IDirectDrawSurface": SURFACE_METHODS,
        "IDirectDrawPalette": PALETTE_METHODS,
        "IDirectDrawClipper": {0x20: "SetHWnd"},
    }
    output = []
    for va, interface, expected_offset, role, notes in rows:
        actual_offset = vtable_call_offset(exe, sections, va)
        output.append(
            {
                "vaHex": hex32(va),
                "interface": interface,
                "vtableOffsetHex": f"0x{actual_offset:02x}",
                "expectedOffsetHex": f"0x{expected_offset:02x}",
                "method": methods.get(interface, {}).get(actual_offset, "unknown"),
                "role": role,
                "notes": notes,
                "verified": actual_offset == expected_offset,
            }
        )
    return output


def build_summary(exe_path: Path = EXE, exe_imports: dict | None = None) -> dict:
    exe = exe_path.read_bytes()
    sections = read_sections(exe)
    imports = exe_imports or parse_imports(exe_path)
    directdraw_create = find_import(imports, "DirectDrawCreate")
    call_targets = [rel32_target(exe, sections, va) for va in DIRECTDRAW_CREATE_CALLS]
    vtable_calls = known_vtable_calls(exe, sections)
    display_mode_bytes = {
        "bppPush": read_at(exe, sections, 0x004156AF, 2).hex(" "),
        "heightPush": read_at(exe, sections, 0x004156B1, 5).hex(" "),
        "widthPush": read_at(exe, sections, 0x004156B6, 5).hex(" "),
    }
    palette_header_bytes = {
        "versionWrite": read_at(exe, sections, 0x00415C58, 5).hex(" "),
        "countWrite": read_at(exe, sections, 0x00415C62, 6).hex(" "),
    }
    verification = {
        "directDrawCreateImportMatches": directdraw_create.get("iatVa") == 0x005A0374,
        "directDrawCreateCallsReachWrapper": all(target == DIRECTDRAW_CREATE_WRAPPER for target in call_targets),
        "directDrawOutputGlobalMatches": read_at(exe, sections, 0x004155FB, 5) == b"\x68\xc0\x76\x46\x00",
        "hardwareDeviceBranchPushes2": read_at(exe, sections, 0x00415616, 2) == b"\x6a\x02",
        "displayModeIs640x480x8": (
            display_mode_bytes["bppPush"] == "6a 08"
            and display_mode_bytes["heightPush"] == "68 e0 01 00 00"
            and display_mode_bytes["widthPush"] == "68 80 02 00 00"
        ),
        "paletteHeaderIs256Entries": (
            palette_header_bytes["versionWrite"] == "66 c7 00 00 03"
            and palette_header_bytes["countWrite"] == "66 c7 40 02 00 01"
        ),
        "allKnownVtableOffsetsMatch": all(row["verified"] for row in vtable_calls),
        "createSurfaceCallCountAtLeast4": sum(1 for row in vtable_calls if row["method"] == "CreateSurface") >= 4,
        "presentationHasFlipAndBlt": (
            any(row["method"] == "Flip" for row in vtable_calls)
            and any(row["method"] == "Blt" for row in vtable_calls)
        ),
    }
    summary = {
        "source": str(exe_path),
        "directDrawCreate": {
            "dll": directdraw_create.get("dll"),
            "iatVaHex": hex32(directdraw_create.get("iatVa")),
            "wrapperVaHex": hex32(DIRECTDRAW_CREATE_WRAPPER),
            "callSites": [hex32(va) for va in DIRECTDRAW_CREATE_CALLS],
            "callTargets": [hex32(target) for target in call_targets],
            "outputGlobalHex": hex32(DIRECTDRAW_GLOBAL),
            "errorGlobalHex": hex32(ERROR_GLOBAL),
            "flagGlobalHex": hex32(FLAGS_GLOBAL),
            "displayModeGlobalHex": hex32(DISPLAY_MODE_GLOBAL),
            "hwndGlobalHex": hex32(HWND_GLOBAL),
        },
        "displayMode": {
            "setCooperativeLevelCalls": [
                row for row in vtable_calls if row["method"] == "SetCooperativeLevel"
            ],
            "setDisplayModeCall": next(row for row in vtable_calls if row["method"] == "SetDisplayMode"),
            "width": 640,
            "height": 480,
            "bpp": 8,
            "bytes": display_mode_bytes,
        },
        "surfaces": {
            "surfaceTableBaseHex": hex32(SURFACE_TABLE),
            "primarySurfaceSlots": [
                {"slot": 0, "wrapperGlobalHex": "0x0055abd8", "role": "primary/front surface wrapper"},
                {"slot": 1, "wrapperGlobalHex": "0x0055abdc", "role": "offscreen/back surface wrapper"},
                {"slot": 0xBF, "wrapperGlobalHex": "0x0055aed4", "role": "attached backbuffer wrapper"},
            ],
            "createSurfaceCalls": [
                row for row in vtable_calls if row["method"] == "CreateSurface"
            ],
            "surfaceCallSamples": [
                row
                for row in vtable_calls
                if row["interface"] == "IDirectDrawSurface" and row["method"] != "SetPalette"
            ],
        },
        "palette": {
            "paletteGlobalHex": hex32(PALETTE_GLOBAL),
            "bufferGlobalHex": hex32(PALETTE_BUFFER_GLOBAL),
            "entriesGlobalHex": hex32(PALETTE_ENTRIES_GLOBAL),
            "entryCount": 256,
            "headerBytes": palette_header_bytes,
            "calls": [
                row
                for row in vtable_calls
                if row["method"] in {"CreatePalette", "SetPalette", "SetEntries", "GetEntries"}
            ],
        },
        "presentation": {
            "classification": "directdraw-640x480x8-paletted-surfaces",
            "flipCalls": [row for row in vtable_calls if row["method"] == "Flip"],
            "bltCalls": [row for row in vtable_calls if row["method"] == "Blt"],
            "implication": "The web runtime should preserve 640x480 indexed-image assumptions, then present through canvas scaling rather than inventing a higher-resolution native layout.",
        },
        "vtableCalls": vtable_calls,
        "verification": verification,
        "promotionStatus": "render-pipeline-grounded",
        "openQuestions": [
            "Exact draw-order and dirty-rectangle rules still require deeper renderer tracing.",
            "The static vtable labels assume DirectDraw 1 era COM layouts, which match the import shape but are not symbol names from the binary.",
            "Event and route promotion are not affected by this report.",
        ],
    }
    return summary


def text_summary(summary: dict) -> str:
    lines = [
        "# DirectDraw Flow",
        "",
        f"Executable: `{summary['source']}`",
        f"Promotion status: `{summary['promotionStatus']}`",
        "",
        "## Import And Entry",
        "",
        f"- `DirectDrawCreate` IAT: `{summary['directDrawCreate']['iatVaHex']}`",
        f"- import thunk/wrapper: `{summary['directDrawCreate']['wrapperVaHex']}`",
        f"- call sites: {', '.join(f'`{item}`' for item in summary['directDrawCreate']['callSites'])}",
        f"- output global: `{summary['directDrawCreate']['outputGlobalHex']}`",
        f"- mode/window globals: display mode `{summary['directDrawCreate']['displayModeGlobalHex']}`, hwnd `{summary['directDrawCreate']['hwndGlobalHex']}`, flags `{summary['directDrawCreate']['flagGlobalHex']}`",
        "",
        "The two call sites pass either device selector `0` or `2` based on the graphics flag at `0x004676d4`, then store the created `IDirectDraw*` in `0x004676c0`.",
        "",
        "## Display Mode",
        "",
        f"- target mode: `{summary['displayMode']['width']}x{summary['displayMode']['height']}x{summary['displayMode']['bpp']}`",
        f"- `SetDisplayMode` call: `{summary['displayMode']['setDisplayModeCall']['vaHex']}`",
        "",
        "| call | method | notes |",
        "| --- | --- | --- |",
    ]
    for row in summary["displayMode"]["setCooperativeLevelCalls"]:
        lines.append(f"| `{row['vaHex']}` | `{row['method']}` | {row['notes']} |")
    lines.append(
        f"| `{summary['displayMode']['setDisplayModeCall']['vaHex']}` | `SetDisplayMode` | 640x480x8 |"
    )
    lines.extend([
        "",
        "## Surfaces",
        "",
        f"Surface wrappers are indexed from `{summary['surfaces']['surfaceTableBaseHex']}`. The initialization path creates a primary/front wrapper, an offscreen/back wrapper, and in fullscreen flip mode an attached backbuffer wrapper.",
        "",
        "| slot | wrapper | role |",
        "| ---: | --- | --- |",
    ])
    for row in summary["surfaces"]["primarySurfaceSlots"]:
        lines.append(f"| {row['slot']} | `{row['wrapperGlobalHex']}` | {row['role']} |")
    lines.extend(["", "| call | method | role | notes |", "| --- | --- | --- | --- |"])
    for row in summary["surfaces"]["createSurfaceCalls"]:
        lines.append(f"| `{row['vaHex']}` | `{row['method']}` | {row['role']} | {row['notes']} |")
    lines.extend([
        "",
        "## Palette",
        "",
        f"The palette buffer at `{summary['palette']['bufferGlobalHex']}` stores a 256-entry DirectDraw palette; the raw entries start at `{summary['palette']['entriesGlobalHex']}`.",
        "",
        "| call | method | role | notes |",
        "| --- | --- | --- | --- |",
    ])
    for row in summary["palette"]["calls"]:
        lines.append(f"| `{row['vaHex']}` | `{row['method']}` | {row['role']} | {row['notes']} |")
    lines.extend([
        "",
        "## Presentation",
        "",
        f"- classification: `{summary['presentation']['classification']}`",
        f"- flip calls: {', '.join(f'`{row['vaHex']}`' for row in summary['presentation']['flipCalls']) or '-'}",
        f"- blt calls: {', '.join(f'`{row['vaHex']}`' for row in summary['presentation']['bltCalls']) or '-'}",
        f"- implication: {summary['presentation']['implication']}",
        "",
        "## Verification",
        "",
        "| check | value |",
        "| --- | --- |",
    ])
    for key, value in summary["verification"].items():
        lines.append(f"| `{key}` | `{value}` |")
    lines.extend([
        "",
        "## Open Questions",
        "",
    ])
    lines.extend(f"- {item}" for item in summary["openQuestions"])
    return "\n".join(lines) + "\n"


def html_page(summary: dict) -> str:
    body = text_summary(summary)
    return (
        "<!doctype html>\n"
        "<html lang=\"en\">\n"
        "<head>\n"
        "  <meta charset=\"utf-8\">\n"
        "  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"
        "  <title>DirectDraw Flow</title>\n"
        "  <style>body{font-family:system-ui,sans-serif;max-width:1100px;margin:24px auto;line-height:1.45}"
        "pre{white-space:pre-wrap;background:#f6f6f6;padding:16px;border-radius:6px}</style>\n"
        "</head>\n"
        "<body><pre>"
        + html.escape(body)
        + "</pre></body>\n"
        "</html>\n"
    )


def write_outputs(summary: dict, out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "directdraw_flow.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "directdraw_flow.html").write_text(html_page(summary), encoding="utf-8")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--exe", type=Path, default=EXE)
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(args.exe)
    write_outputs(summary, args.out_dir)
    print(f"wrote DirectDraw flow summary -> {args.out_dir / 'directdraw_flow.html'}")


if __name__ == "__main__":
    main()
