#!/usr/bin/env python3
"""40_wdefs.js from the game's Weapons/WeaponDefs.cs (STYLE-PORT, 29 Oct; the WEAPON RULE: the page draws the game's own weapons).
   python3 _tools/wdefs_gen.py [--cs <WeaponDefs.cs>] [--out 40_wdefs.js] [--check]
   Default source: the v4 worktree (/Volumes/Workspace/Projects/StickFight-dev). --check: print the diff vs the current file, write nothing.
   part: [kind 0 box | 1 capsule | 2 circle | 3 triangle | 4 glow, ax, ay, bx, by, rot, r, role 0 main 1 accent 2 dark 3 metal 4 wood 5 glow]"""
import json, re, sys, os
A = sys.argv[1:]
def opt(k, d):
    return A[A.index(k) + 1] if k in A else d
CS = opt('--cs', '/Volumes/Workspace/Projects/StickFight-dev/StickBrawl/Assets/_Game/Scripts/Weapons/WeaponDefs.cs')
OUT = opt('--out', os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '40_wdefs.js'))
src = open(CS).read()
src = re.sub(r'//[^\n]*', '', src)   # comments out (a part list's trailing note)

import math
def num(s):
    s = re.sub(r'(\d)[fF]\b', r'\1', s.strip()).replace('Mathf.PI', 'math.pi').replace('Mathf.Deg2Rad', '(math.pi/180)'); s = re.sub(r'\bPI\b', 'math.pi', s)
    if not s: return 0.0
    if not re.fullmatch(r'[-+*/(). 0-9a-z_]*', s): raise ValueError('part arg: ' + s)
    return float(eval(s, {'math': math, '__builtins__': {}}))
def call_args(s):
    return [x for x in (a.strip() for a in s.split(',')) if x != '']
def part(fn, args):
    v = [num(x) for x in args]
    if fn == 'B':   # B(cx, cy, hx, hy, role, rot = 0, r = 0.012)
        cx, cy, hx, hy, role = v[:5]; rot = v[5] if len(v) > 5 else 0.0; r = v[6] if len(v) > 6 else 0.012
        return [0, cx, cy, hx, hy, rot, r, int(role)]
    if fn == 'C':   # C(ax, ay, bx, by, r, role)
        return [1, v[0], v[1], v[2], v[3], 0, v[4], int(v[5])]
    if fn == 'O':   # O(cx, cy, r, role)
        return [2, v[0], v[1], v[2], 0, 0, v[2], int(v[3])]
    if fn == 'T':   # T(cx, cy, hx, hy, role, rot = 0)
        return [3, v[0], v[1], v[2], v[3], v[5] if len(v) > 5 else 0.0, 0, int(v[4])]
    if fn == 'G':   # G(cx, cy, r)
        return [4, v[0], v[1], v[2], 0, 0, v[2], 5]
    raise ValueError(fn)

def blocks(s):
    i = 0
    while True:
        i = s.find('Add(new WeaponDef', i)
        if i < 0: return
        j = s.index('{', i); d = 0; k = j
        while True:
            if s[k] == '{': d += 1
            elif s[k] == '}':
                d -= 1
                if d == 0: break
            k += 1
        yield s[j + 1:k]; i = k

def field(b, name, kind):
    if kind == 'str':
        m = re.search(r'\b' + name + r'\s*=\s*"([^"]*)"', b); return m.group(1) if m else None
    if kind == 'hex':
        m = re.search(r'\b' + name + r'\s*=\s*H\("([0-9A-Fa-f]{6})"\)', b); return m.group(1).upper() if m else None
    if kind == 'num':
        m = re.search(r'\b' + name + r'\s*=\s*(-?[0-9.]+)f?\b', b); return float(m.group(1)) if m else None
    if kind == 'bool':
        m = re.search(r'\b' + name + r'\s*=\s*(true|false)\b', b); return (m.group(1) == 'true') if m else False
    if kind == 'enum':
        m = re.search(r'\b' + name + r'\s*=\s*\w+\.(\w+)', b); return m.group(1) if m else None

out = {}
# the game hides weapons after the defs: AddItemProjectiles' list (throwables / gear) and WeaponRoster.Removed (retired: not a game weapon any more)
HIDE = set()
m = re.search(r'AddItemProjectiles\(\)\s*\{\s*foreach \(var id in new\[\] \{([^}]*)\}', src)
if m: HIDE |= set(re.findall(r'"([a-z0-9_]+)"', m.group(1)))
ROSTER = os.path.join(os.path.dirname(CS), 'WeaponRoster.cs'); REMOVED = set()
if os.path.exists(ROSTER):
    r = re.sub(r'//[^\n]*', '', open(ROSTER).read()); m = re.search(r'Removed\s*=\s*\{([^}]*)\}', r)
    if m: REMOVED = set(re.findall(r'"([a-z0-9_]+)"', m.group(1)))
for b in blocks(src):
    wid = field(b, 'id', 'str')
    if not wid: continue
    m = re.search(r'\bparts\s*=\s*\{', b); P = []
    if m:
        k = m.end(); d = 1; e = k
        while d:
            if b[e] == '{': d += 1
            elif b[e] == '}': d -= 1
            e += 1
        body = b[k:e - 1]
        for fm in re.finditer(r'\b([BCOTG])\(([^()]*)\)', body): P.append(part(fm.group(1), call_args(fm.group(2))))
    rec = {'id': wid, 'name': field(b, 'name', 'str'), 'cat': field(b, 'cat', 'enum'), 'kind': field(b, 'kind', 'enum'),
           'main': field(b, 'main', 'hex') or '3A3F4B', 'accent': field(b, 'accent', 'hex') or 'FFD23F',
           'length': field(b, 'length', 'num') or 0.3, 'muzzleY': field(b, 'muzzleY', 'num') or 0.0}
    ho = field(b, 'holdOffset', 'num')
    if ho is not None: rec['holdOffset'] = ho
    rec['twoHanded'] = field(b, 'twoHanded', 'bool'); rec['hidden'] = field(b, 'hidden', 'bool') or wid in HIDE or wid in REMOVED
    if wid in REMOVED: rec['removed'] = True   # (retired from the game: a page drawing of it is NOT a game weapon - ink-cast)
    if wid in HIDE: rec['gear'] = True
    rec['parts'] = [[round(x, 5) if isinstance(x, float) else x for x in p] for p in P]
    out[wid] = rec

# a retired weapon whose game def is an empty slot (timewatch, wingblade) keeps the page's last parts, so a page that still draws it shows
# something until its fixer ink-casts it (WEAPON_AUDIT); it is marked removed either way
try:
    _t = open(OUT).read(); _old = json.loads(_t[_t.index('{'):_t.rindex('}') + 1])
    for k, r in out.items():
        if not r['parts'] and k in _old and _old[k].get('parts'): r['parts'] = _old[k]['parts']; r['main'] = _old[k].get('main', r['main']); r['accent'] = _old[k].get('accent', r['accent']); r['length'] = _old[k].get('length', r['length']); r['kept58'] = True
except Exception: pass
head = ('/* Every weapon\'s drawn parts, GENERATED from Weapons/WeaponDefs.cs by _tools/wdefs_gen.py (STYLE-PORT, the WEAPON RULE: the page\n'
        '   draws the game\'s own weapons). Do not hand-edit: re-run the generator when the game changes a weapon.\n'
        '   part: [kind 0 box | 1 capsule | 2 circle | 3 triangle | 4 glow, ax, ay, bx, by, rot, r, role 0 main 1 accent 2 dark 3 metal 4 wood 5 glow] */\n')
js = head + 'window.WDEFS = ' + json.dumps(out, separators=(',', ':')) + ';\n'
if '--check' in A:
    old = {}
    try:
        t = open(OUT).read(); old = json.loads(t[t.index('{'):t.rindex('}') + 1])
    except Exception as e: print('old file unreadable:', e)
    added = sorted(set(out) - set(old)); gone = sorted(set(old) - set(out)); changed = []
    for k in sorted(set(out) & set(old)):
        a, b2 = out[k], old[k]
        diffs = [f for f in ('main', 'accent', 'length', 'muzzleY', 'twoHanded', 'hidden') if a.get(f) != b2.get(f)]
        if json.dumps(a['parts']) != json.dumps(b2['parts']): diffs.append('parts(%d->%d)' % (len(b2['parts']), len(a['parts'])))
        if diffs: changed.append(k + ': ' + ', '.join(diffs))
    print('weapons: %d (old %d)\nadded: %s\ngone: %s\nchanged (%d):\n  %s' % (len(out), len(old), ' '.join(added) or '-', ' '.join(gone) or '-', len(changed), '\n  '.join(changed) or '-'))
else:
    open(OUT, 'w').write(js); print('wrote', OUT, len(out), 'weapons')
