"""
memtest.py — read the game's WASM linear memory (HEAPU8) and hunt for the address
of a value (e.g. HP), Cheat-Engine style:

  python memtest.py scan  <value> [type]   # first scan for a known value
  python memtest.py narrow <value>         # keep only candidates now holding <value>
  python memtest.py read  <offset> [type]  # read one address
  python memtest.py list                   # show current candidates (if few)
  python memtest.py watch <offset> [type]  # read an address repeatedly

types: i32 (default) u32 i16 u16 f32 u8

Candidates live in the page (window.__memcands) so the 256 MB never gets transferred.
Repeat scan -> change the value in-game -> narrow, until only a couple addresses remain.
Separate/removable experiment (memory READING only; we still act via clicks).
"""
import sys
import json
import urllib.request
from playwright.sync_api import sync_playwright

SCAN_JS = r"""(a) => {
  if (typeof HEAPU8 === 'undefined') return {err: 'no HEAPU8'};
  const buf = HEAPU8.buffer;
  const mk = {i32:[Int32Array,4], u32:[Uint32Array,4], i16:[Int16Array,2],
              u16:[Uint16Array,2], f32:[Float32Array,4], u8:[Uint8Array,1], f64:[Float64Array,8]};
  const [Ctor, sz] = mk[a.type] || mk.i32;
  const arr = new Ctor(buf);
  let cands;
  if (a.narrow && window.__memcands) {
    cands = window.__memcands.filter(off => arr[off / sz] === a.value);
  } else {
    cands = [];
    for (let i = 0; i < arr.length; i++) {
      if (arr[i] === a.value) { cands.push(i * sz); if (cands.length > 4000000) break; }
    }
  }
  window.__memcands = cands; window.__memtype = a.type;
  return {n: cands.length, sample: cands.slice(0, 40)};
}"""

BUILDPOOL_JS = r"""(specs) => {
  if (typeof HEAPU8 === 'undefined') return -1;
  const buf = HEAPU8.buffer;
  const mk = {i32:[Int32Array,4], u32:[Uint32Array,4], i16:[Int16Array,2],
              u16:[Uint16Array,2], f32:[Float32Array,4], u8:[Uint8Array,1], f64:[Float64Array,8]};
  const pool = [];
  for (const sp of specs) {
    const [Ctor, sz] = mk[sp.type]; const arr = new Ctor(buf);
    for (let i = 0; i < arr.length; i++) {
      if (arr[i] === sp.value) { pool.push([i * sz, sp.type]); if (pool.length > 6000) break; }
    }
  }
  window.__pool = pool; return pool.length;
}"""

LOGPOOL_JS = r"""() => {
  const buf = HEAPU8.buffer;
  const mk = {i32:[Int32Array,4], u32:[Uint32Array,4], i16:[Int16Array,2],
              u16:[Uint16Array,2], f32:[Float32Array,4], u8:[Uint8Array,1], f64:[Float64Array,8]};
  return (window.__pool || []).map(p => { const [Ctor, sz] = mk[p[1]]; return new Ctor(buf)[p[0] / sz]; });
}"""

READALL_JS = r"""(typ) => {
  const cands = window.__memcands || [];
  const buf = HEAPU8.buffer;
  const mk = {i32:[Int32Array,4], u32:[Uint32Array,4], i16:[Int16Array,2],
              u16:[Uint16Array,2], f32:[Float32Array,4], u8:[Uint8Array,1], f64:[Float64Array,8]};
  const [Ctor, sz] = mk[typ] || mk.i32;
  const arr = new Ctor(buf);
  return cands.map(off => arr[off / sz]);
}"""

READ_JS = r"""(a) => {
  const buf = HEAPU8.buffer;
  const mk = {i32:[Int32Array,4], u32:[Uint32Array,4], i16:[Int16Array,2],
              u16:[Uint16Array,2], f32:[Float32Array,4], u8:[Uint8Array,1], f64:[Float64Array,8]};
  const [Ctor, sz] = mk[a.type] || mk.i32;
  return (new Ctor(buf))[a.off / sz];
}"""


BUILDRANGE_JS = r"""(rg) => {
  const buf = HEAPU8.buffer, i32 = new Int32Array(buf), f32 = new Float32Array(buf), u16 = new Uint16Array(buf);
  const lo = rg[0], hi = rg[1], pool = [];
  for (let i = 0; i < i32.length; i++) {
    const v = i32[i]; if (v >= lo && v <= hi) pool.push([i * 4, 'i32']);
    const g = f32[i]; if (g >= lo && g <= hi && Number.isFinite(g)) pool.push([i * 4, 'f32']);
  }
  for (let i = 0; i < u16.length; i++) { if (u16[i] >= lo && u16[i] <= hi) pool.push([i * 2, 'u16']); if (pool.length > 700000) break; }
  window.__pool = pool; return pool.length;
}"""

STARTTRACK_JS = r"""() => {
  const p = window.__pool, n = p.length, buf = HEAPU8.buffer;
  const i32 = new Int32Array(buf), f32 = new Float32Array(buf), u16 = new Uint16Array(buf);
  const rd = (o, t) => t === 'i32' ? i32[o >> 2] : t === 'f32' ? f32[o >> 2] : u16[o >> 1];
  const mn = new Float64Array(n), mx = new Float64Array(n);
  for (let k = 0; k < n; k++) { const v = rd(p[k][0], p[k][1]); mn[k] = v; mx[k] = v; }
  window.__mn = mn; window.__mx = mx;
  window.__timer = setInterval(() => {
    const buf = HEAPU8.buffer, i32 = new Int32Array(buf), f32 = new Float32Array(buf), u16 = new Uint16Array(buf);
    const p = window.__pool, mn = window.__mn, mx = window.__mx;
    for (let k = 0; k < p.length; k++) { const o = p[k][0], t = p[k][1];
      const v = t === 'i32' ? i32[o >> 2] : t === 'f32' ? f32[o >> 2] : u16[o >> 1];
      if (v < mn[k]) mn[k] = v; if (v > mx[k]) mx[k] = v; }
  }, 50);
  return n;
}"""

STOPTRACK_JS = r"""(minRange) => {
  clearInterval(window.__timer);
  const p = window.__pool, mn = window.__mn, mx = window.__mx, out = [];
  for (let k = 0; k < p.length; k++) {
    if (mx[k] <= 280 && mn[k] >= -1 && mx[k] - mn[k] >= minRange)   // stays HP-plausible (no reused/garbage)
      out.push([p[k][0], p[k][1], Math.round(mn[k]), Math.round(mx[k]), Math.round(mx[k] - mn[k])]);
  }
  out.sort((a, b) => b[4] - a[4]);
  return out.slice(0, 200);
}"""


def find_port(a=9222, b=9250):
    for port in range(a, b):
        try:
            urllib.request.urlopen(f"http://127.0.0.1:{port}/json/version", timeout=0.3)
            return port
        except Exception:
            pass
    return None


def get_page(p):
    port = find_port()
    if not port:
        raise SystemExit("no flyffbot CDP port found")
    return p.chromium.connect_over_cdp(f"http://127.0.0.1:{port}").contexts[0].pages[0]


def main():
    if len(sys.argv) < 2:
        print(__doc__); return
    cmd = sys.argv[1]
    with sync_playwright() as p:
        page = get_page(p)
        if cmd == "scan":
            value = float(sys.argv[2]) if "." in sys.argv[2] else int(sys.argv[2])
            typ = sys.argv[3] if len(sys.argv) > 3 else "i32"
            r = page.evaluate(SCAN_JS, {"value": value, "type": typ, "narrow": False})
            print(f"scan {value} ({typ}): {r.get('n')} candidates; sample {r.get('sample')}")
        elif cmd == "narrow":
            value = float(sys.argv[2]) if "." in sys.argv[2] else int(sys.argv[2])
            typ = page.evaluate("window.__memtype || 'i32'")
            r = page.evaluate(SCAN_JS, {"value": value, "type": typ, "narrow": True})
            print(f"narrow {value} ({typ}): {r.get('n')} candidates; sample {r.get('sample')}")
        elif cmd == "track":
            import time
            secs = float(sys.argv[2]) if len(sys.argv) > 2 else 15
            typ = page.evaluate("window.__memtype || 'i32'")
            cands = page.evaluate("window.__memcands || []")
            initial = page.evaluate(READALL_JS, typ)
            lo = list(initial); hi = list(initial); changed = set()
            print(f"tracking {len(cands)} candidates for {secs}s — TAKE DAMAGE NOW (repeatedly)…")
            t0 = time.time()
            while time.time() - t0 < secs:
                vals = page.evaluate(READALL_JS, typ)
                for i, v in enumerate(vals):
                    if v != initial[i]:
                        changed.add(i)
                    if v < lo[i]:
                        lo[i] = v
                    if v > hi[i]:
                        hi[i] = v
                time.sleep(0.1)
            print(f"{len(changed)} candidate(s) changed:")
            for i in sorted(changed):
                print(f"  off={cands[i]} start={initial[i]} range=[{lo[i]}..{hi[i]}]")
        elif cmd == "starttrack":
            lo = int(sys.argv[2]); hi = int(sys.argv[3])
            n = page.evaluate(BUILDRANGE_JS, [lo, hi])
            page.evaluate(STARTTRACK_JS)
            print(f"started tracking {n} addresses in [{lo},{hi}] — play now (take real damage); "
                  f"run 'stophp' when done. (Tracker keeps running in the page.)")
        elif cmd == "stophp":
            mxlo, mxhi, mnlo, mnhi = (int(sys.argv[i]) for i in range(2, 6))
            fl = page.evaluate("""(a)=>{ if(window.__timer) clearInterval(window.__timer);
              const p=window.__pool, mn=window.__mn, mx=window.__mx, out=[];
              for(let k=0;k<p.length;k++){ if(mx[k]>=a[0]&&mx[k]<=a[1]&&mn[k]>=a[2]&&mn[k]<=a[3])
                out.push([p[k][0],p[k][1],Math.round(mn[k]),Math.round(mx[k])]); }
              return out; }""", [mxlo, mxhi, mnlo, mnhi])
            print(f"{len(fl)} HP candidates (max {mxlo}-{mxhi}, min {mnlo}-{mnhi}):")
            for off, t, mn, mx in sorted(fl, key=lambda x: x[2])[:40]:
                print(f"  off={off} {t} min={mn} max={mx}")
        elif cmd == "rangepool":
            lo = int(sys.argv[2]); hi = int(sys.argv[3])
            n = page.evaluate(BUILDRANGE_JS, [lo, hi])
            print(f"range pool [{lo},{hi}]: {n} addresses")
        elif cmd == "findhp":
            import time
            secs = float(sys.argv[2]) if len(sys.argv) > 2 else 20
            n = page.evaluate(STARTTRACK_JS)
            print(f"tracking {n} addresses for {secs}s — FIGHT NOW (keep taking damage)…")
            time.sleep(secs)
            res = page.evaluate(STOPTRACK_JS, 10)
            json.dump(res, open("hpcands.json", "w"))
            print(f"{len(res)} changing addresses (top by range). Player HP = max~253, min in ~150-235:")
            for off, t, mn, mx, rng in res[:60]:
                flag = "  <-- HP?" if (268 <= mx <= 278 and 180 <= mn <= 268) else ""
                print(f"  off={off} {t} min={mn} max={mx} range={rng}{flag}")
        elif cmd == "log":
            import time
            secs = float(sys.argv[2]) if len(sys.argv) > 2 else 120
            base = int(sys.argv[3]) if len(sys.argv) > 3 else 253
            n = page.evaluate(BUILDPOOL_JS, [{"value": base, "type": "i32"},
                                             {"value": base, "type": "f32"},
                                             {"value": base, "type": "f64"},
                                             {"value": base, "type": "u16"}])
            pool = page.evaluate("window.__pool")
            f = open("memlog.csv", "w")
            f.write(json.dumps(pool) + "\n")
            print(f"logging {n} candidates every 200ms for {secs}s -> memlog.csv")
            t0 = time.time()
            while time.time() - t0 < secs:
                vals = page.evaluate(LOGPOOL_JS)
                f.write(f"{time.time()-t0:.2f}," + ",".join(str(v) for v in vals) + "\n")
                f.flush()
                time.sleep(0.2)
            f.close()
            print("done.")
        elif cmd == "analyze":
            targets = set(int(x) for x in sys.argv[2:])
            lines = open("memlog.csv").read().splitlines()
            pool = json.loads(lines[0])
            seen = [set() for _ in pool]
            for ln in lines[1:]:
                parts = ln.split(",")
                for i, v in enumerate(parts[1:]):
                    try:
                        seen[i].add(int(float(v)))
                    except Exception:
                        pass
            matches = [(pool[i], sorted(s)) for i, s in enumerate(seen) if targets.issubset(s)]
            print(f"{len(matches)} address(es) whose recorded values include {sorted(targets)}:")
            for (off, typ), s in matches[:25]:
                print(f"  off={off} type={typ}  seen={s[:14]}")
        elif cmd == "list":
            c = page.evaluate("window.__memcands || []")
            print(f"{len(c)} candidates" + (f": {c}" if len(c) <= 40 else f" (first 40): {c[:40]}"))
        elif cmd == "read":
            off = int(sys.argv[2]); typ = sys.argv[3] if len(sys.argv) > 3 else "i32"
            print(f"[{off}] ({typ}) =", page.evaluate(READ_JS, {"off": off, "type": typ}))
        elif cmd == "watch":
            import time
            off = int(sys.argv[2]); typ = sys.argv[3] if len(sys.argv) > 3 else "i32"
            for _ in range(20):
                print(f"[{off}] =", page.evaluate(READ_JS, {"off": off, "type": typ}))
                time.sleep(0.5)


if __name__ == "__main__":
    main()
