"""Fast MCSJ Java Access Bridge operator.
Requires restarted MCSJ after Java Access Bridge enable.
Run with 32-bit Python:
  C:\MCSJAgent\Python311-32-embed\python.exe C:\MCSJAgent\mcsj-jab-operator.py tree

Commands:
  state
  tree [--max-depth N] [--max-nodes N] [--json]
  find --text TEXT [--json]
  click --text TEXT
  set --text TEXT --value VALUE
  focused
"""
import argparse, json, sys, time
from pyjab.jabdriver import JABDriver

TITLE_PREFIX = 'MCSJ Special - 2026.1'
DLL = r'C:\Edmunds\MCSJ26.1\jre\bin\WindowsAccessBridge-32.dll'

def connect():
    # pyjab title matching is exact; discover current visible SunAwtFrame title via win32gui.
    import win32gui
    titles=[]
    def cb(hwnd, _):
        t=win32gui.GetWindowText(hwnd)
        if t.startswith(TITLE_PREFIX):
            titles.append(t)
    win32gui.EnumWindows(cb, None)
    title = titles[0] if titles else TITLE_PREFIX
    return JABDriver(title=title, bridge_dll=DLL, timeout=10)

def info(el, n=None, depth=0):
    try:
        raw = el.get_element_information()
    except Exception:
        raw = {}
    def j(v):
        if isinstance(v, (str, int, float, bool)) or v is None: return v
        if isinstance(v, (list, tuple)): return [j(x) for x in v]
        if isinstance(v, dict): return {str(k): j(val) for k,val in v.items()}
        return str(v)
    return {
        'n': n, 'depth': depth,
        'name': j(raw.get('name','')),
        'role': j(raw.get('role','')),
        'role_en_us': j(raw.get('role_en_us') or raw.get('role_en_US') or raw.get('role','')),
        'description': j(raw.get('description','')),
        'states': j(raw.get('states', [])),
        'text': '',
        'children_count': j(raw.get('children_count', 0)),
        'bounds': j(raw.get('bounds', {})),
        'visible': 'visible' in (raw.get('states') or []),
        'showing': 'showing' in (raw.get('states') or []),
        'enabled': 'enabled' in (raw.get('states') or []),
        'editable': 'editable' in (raw.get('states') or []),
    }

def walk(el, max_depth=8, max_nodes=1000):
    out=[]
    stack=[(el,0)]
    while stack and len(out)<max_nodes:
        cur,depth=stack.pop()
        row=info(cur, len(out)+1, depth)
        row['_el']=cur
        out.append(row)
        if depth < max_depth:
            try:
                kids=list(cur._generate_childs_from_element(cur, visible=False))
            except Exception:
                kids=[]
            for child in reversed(kids):
                stack.append((child, depth+1))
    return out

def strip(rows):
    return [{k:v for k,v in r.items() if k!='_el'} for r in rows]

def match_row(r, text):
    hay=' | '.join(str(r.get(k,'')) for k in ('name','description','role','role_en_us','text')).lower()
    return text.lower() in hay

def choose(rows, text):
    hits=[r for r in rows if match_row(r,text)]
    # Prefer visible/showing enabled controls with more specific names.
    hits.sort(key=lambda r: (not r.get('showing'), not r.get('enabled'), r.get('depth',99), len(str(r.get('name','')))))
    return hits

def print_table(rows):
    for r in rows:
        b=r.get('bounds') or {}
        indent='  '*r['depth']
        name=(r.get('name') or '').replace('\n',' ')[:70]
        desc=(r.get('description') or '').replace('\n',' ')[:50]
        txt=str(r.get('text') or '').replace('\n',' ')[:50]
        print(f"{r['n']:04d} {indent}{r.get('role')} name={name!r} desc={desc!r} text={txt!r} states={r.get('states')} bounds=({b.get('x')},{b.get('y')},{b.get('width')},{b.get('height')}) children={r.get('children_count')}")

def main():
    ap=argparse.ArgumentParser()
    sub=ap.add_subparsers(dest='cmd', required=True)
    sub.add_parser('state')
    p=sub.add_parser('tree'); p.add_argument('--max-depth',type=int,default=8); p.add_argument('--max-nodes',type=int,default=1000); p.add_argument('--json',action='store_true')
    p=sub.add_parser('find'); p.add_argument('--text',required=True); p.add_argument('--json',action='store_true'); p.add_argument('--max-nodes',type=int,default=700); p.add_argument('--max-depth',type=int,default=10)
    p=sub.add_parser('click'); p.add_argument('--text',required=True); p.add_argument('--max-nodes',type=int,default=700); p.add_argument('--max-depth',type=int,default=10)
    p=sub.add_parser('set'); p.add_argument('--text',required=True); p.add_argument('--value',required=True); p.add_argument('--max-nodes',type=int,default=700); p.add_argument('--max-depth',type=int,default=10)
    sub.add_parser('focused')
    args=ap.parse_args()
    d=connect(); root=d.root_element
    if args.cmd=='state':
        print(json.dumps({'ok': True, 'hwnd': int(d.hwnd), 'pid': d.pid, 'vmid': d.vmid, 'version': d.get_version_info(), 'root': info(root)}, indent=2)); return
    if args.cmd=='focused':
        rows=walk(root, max_depth=12, max_nodes=2000)
        hits=[r for r in rows if 'focused' in (r.get('states') or [])]
        print(json.dumps(strip(hits), indent=2)); return
    if args.cmd=='tree':
        rows=walk(root, args.max_depth, args.max_nodes)
        if args.json: print(json.dumps(strip(rows), indent=2))
        else: print_table(rows)
        return
    rows=walk(root, getattr(args, 'max_depth', 10), getattr(args, 'max_nodes', 700))
    hits=choose(rows, args.text)
    if args.cmd=='find':
        if args.json: print(json.dumps(strip(hits), indent=2))
        else: print_table(hits)
        return
    if not hits:
        print(f'No element found matching {args.text!r}', file=sys.stderr); sys.exit(2)
    el=hits[0]['_el']
    if args.cmd=='click':
        el.click(); print(json.dumps({'ok': True, 'clicked': strip([hits[0]])[0]}, indent=2)); return
    if args.cmd=='set':
        try: el.clear()
        except Exception: pass
        el.send_text(args.value)
        print(json.dumps({'ok': True, 'set': strip([hits[0]])[0], 'value': args.value}, indent=2)); return

if __name__=='__main__': main()
