# BEGIN CHANGE: set corpse OTB group+flags from visual scan
"""Set items.otb corpses to ITEM_GROUP_CONTAINER + pickupable/movable.

Does NOT set FLAG_USABLE: in OTX playerUseItem, usable=true means use-with
(runes) and rejects normal Use/open. Classic dead troll is flags 0x60.
Does NOT set stackable. Backup is mandatory unless --dry-run.

  python otb_set_corpse_flags.py --otb PATH --ids-json visual_item_scan.json --backup
"""
from __future__ import annotations

import argparse
import json
import shutil
import struct
import sys
import time
from pathlib import Path

ESCAPE, NODE_START, NODE_END = 0xFD, 0xFE, 0xFF
ATTR_SERVER = 16
ITEM_GROUP_CONTAINER = 2
FLAG_USABLE = 1 << 4
FLAG_PICKUPABLE = 1 << 5
FLAG_MOVABLE = 1 << 6
WANT_FLAGS = FLAG_PICKUPABLE | FLAG_MOVABLE


def read_node_buffer(raw: bytes, start: int) -> tuple[bytes, int]:
    buf = bytearray()
    p = start
    n = len(raw)
    while p < n:
        b = raw[p]
        p += 1
        if b == NODE_START:
            depth = 1
            while p < n and depth > 0:
                nb = raw[p]
                p += 1
                if nb == NODE_START:
                    depth += 1
                elif nb == NODE_END:
                    depth -= 1
                elif nb == ESCAPE and p < n:
                    p += 1
        elif b == NODE_END:
            break
        elif b == ESCAPE:
            if p >= n:
                break
            buf.append(raw[p])
            p += 1
        else:
            buf.append(b)
    return bytes(buf), p


def escape_bytes(data: bytes) -> bytes:
    out = bytearray()
    for b in data:
        if b in (ESCAPE, NODE_START, NODE_END):
            out.append(ESCAPE)
        out.append(b)
    return bytes(out)


def parse_otb(raw: bytes) -> tuple[bytes, list[bytearray]]:
    if len(raw) < 8 or raw[0:4] != b"\x00\x00\x00\x00" or raw[4] != NODE_START:
        raise ValueError("invalid otb")
    root_buf = bytearray()
    p = 5
    n = len(raw)
    while p < n:
        b = raw[p]
        p += 1
        if b == NODE_START:
            depth = 1
            while p < n and depth > 0:
                nb = raw[p]
                p += 1
                if nb == NODE_START:
                    depth += 1
                elif nb == NODE_END:
                    depth -= 1
                elif nb == ESCAPE and p < n:
                    p += 1
        elif b == NODE_END:
            break
        elif b == ESCAPE:
            if p < n:
                root_buf.append(raw[p])
                p += 1
        else:
            root_buf.append(b)

    children: list[bytearray] = []
    p = 5
    while p < n:
        b = raw[p]
        p += 1
        if b == NODE_START:
            cbuf, p = read_node_buffer(raw, p)
            children.append(bytearray(cbuf))
        elif b == NODE_END:
            break
        elif b == ESCAPE and p < n:
            p += 1
    return bytes(root_buf), children


def child_server_id(buf: bytes) -> int | None:
    if len(buf) < 5:
        return None
    i = 5
    while i < len(buf):
        attr = buf[i]
        i += 1
        if i + 2 > len(buf):
            break
        alen = buf[i] | (buf[i + 1] << 8)
        i += 2
        if i + alen > len(buf):
            break
        if attr == ATTR_SERVER and alen >= 2:
            return buf[i] | (buf[i + 1] << 8)
        i += alen
    return None


def build_otb(root_buf: bytes, children: list[bytearray]) -> bytes:
    out = bytearray()
    out += b"\x00\x00\x00\x00"
    out.append(NODE_START)
    out += escape_bytes(root_buf)
    for ch in children:
        out.append(NODE_START)
        out += escape_bytes(bytes(ch))
        out.append(NODE_END)
    out.append(NODE_END)
    return bytes(out)


def load_corpse_ids(path: Path) -> set[int]:
    raw = json.loads(path.read_text(encoding="utf-8"))
    if isinstance(raw, dict) and "corpses" in raw:
        ids = raw["corpses"]
    elif isinstance(raw, list):
        ids = raw
    else:
        raise ValueError("json must be {corpses:[...]} or a list")
    return {int(x) for x in ids}


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--otb", required=True)
    ap.add_argument("--ids-json", required=True)
    ap.add_argument("--dry-run", action="store_true")
    ap.add_argument("--backup", action="store_true")
    args = ap.parse_args()

    want = load_corpse_ids(Path(args.ids_json))
    path = Path(args.otb)
    raw = path.read_bytes()
    root_buf, children = parse_otb(raw)

    group_changed = 0
    flags_changed = 0
    already = 0
    found: set[int] = set()
    group_hist: dict[int, int] = {}

    for ch in children:
        sid = child_server_id(bytes(ch))
        if sid not in want or len(ch) < 5:
            continue
        found.add(sid)
        old_group = ch[0]
        old_flags = struct.unpack_from("<I", ch, 1)[0]
        group_hist[old_group] = group_hist.get(old_group, 0) + 1
        new_group = ITEM_GROUP_CONTAINER
        new_flags = (old_flags | WANT_FLAGS) & ~FLAG_USABLE
        if old_group == new_group and old_flags == new_flags:
            already += 1
            continue
        if old_group != new_group:
            group_changed += 1
        if old_flags != new_flags:
            flags_changed += 1
        ch[0] = new_group
        struct.pack_into("<I", ch, 1, new_flags)

    missing = sorted(want - found)
    print("want=%d found=%d missing=%d already_ok=%d" % (
        len(want), len(found), len(missing), already
    ))
    print("group_changed=%d flags_or=%d" % (group_changed, flags_changed))
    print("old_groups", group_hist)
    if missing:
        print("missing_sample", missing[:30], "..." if len(missing) > 30 else "")

    if args.dry_run:
        return 0
    if group_changed == 0 and flags_changed == 0:
        print("nothing to write")
        return 0
    if not args.backup:
        print("refusing to write without --backup", file=sys.stderr)
        return 2

    bak = path.with_name(path.name + ".bak_" + time.strftime("%Y%m%d_%H%M%S"))
    shutil.copy2(path, bak)
    if not bak.is_file() or bak.stat().st_size != path.stat().st_size:
        print("backup failed", bak, file=sys.stderr)
        return 3
    print("backup", bak)

    out = build_otb(root_buf, children)
    path.write_bytes(out)
    print("wrote", path, "size", len(out))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
# END CHANGE
