#!/usr/bin/env python3
import json,struct,os,re,sys,csv
from pathlib import Path

ROOT=Path(__file__).resolve().parent.parent

def load_charmap(path):
    data=json.load(open(path,encoding='utf-8-sig')); rev={}
    for g in data.values():
        refs=g.get('References')
        if refs is None: raise ValueError('glyph with null References')
        for ref in refs:
            script,code=ref[0],int(ref[1]); rev.setdefault(script,{})[g.get('Text','')]=code
    return rev

def extra(hextext):
    s=hextext.replace(' ','').strip()
    if not s:return b''
    return bytes(int(s[i:i+2],16) for i in range(0,len(s),2))

def glyph_code(script,c,rev):
    if c in rev.get(script,{}): return rev[script][c]
    if c in rev.get('BasicCharSet',{}): return rev['BasicCharSet'][c]
    raise KeyError(f'unmapped ordinary glyph {c!r} U+{ord(c):04X} for {script}')

def translate(script,s,rev):
    # Deterministic parser matching the control syntax used by Nova-Tools.
    s=s.replace('\\n','\n')
    out=bytearray(); i=0
    while i<len(s):
        c=s[i]
        if c=='\n':
            out+=struct.pack('<H',0x8080); i+=1; continue
        if c=='[':
            j=s.find(']',i)
            if j>=0:
                tok=s[i:j+1]
                if tok=='[b]': out+=struct.pack('<H',0x8082); i=j+1; continue
                if tok=='[d]' or tok=='[/ruby]': out+=struct.pack('<H',0x8091); i=j+1; continue
                if tok.startswith('[a ') and tok.endswith(']'):
                    out+=struct.pack('<H',0x8081)+extra(tok[3:-1]); i=j+1; continue
                if tok.startswith('[c ') and tok.endswith(']'):
                    out+=struct.pack('<H',0x8090)+extra(tok[3:-1])+b'\0'; i=j+1; continue
                if tok.startswith('[e ') and tok.endswith(']'):
                    out+=struct.pack('<H',0x8094)+extra(tok[3:-1]); i=j+1; continue
                if tok.startswith('[f ') and tok.endswith(']'):
                    out+=struct.pack('<H',0x8099)+extra(tok[3:-1]); i=j+1; continue
                if tok=='[ruby]':
                    # Explicit empty-reading ruby span used extensively by upstream translations.
                    out+=struct.pack('<H',0x8090)+b'\0'; i=j+1; continue
                if tok.startswith('[ruby ') and tok.endswith(']'):
                    out+=struct.pack('<H',0x8090)
                    ruby=tok[6:-1]
                    rmap=rev.get('BasicRubySet',{})
                    for rc in ruby:
                        if rc not in rmap: raise KeyError(f'unmapped ruby glyph {rc!r} U+{ord(rc):04X}')
                        code=rmap[rc]
                        val=code-0x881+1
                        if not (1<=val<=255): raise ValueError(f'ruby code out of byte range: {rc!r} {code:#x}')
                        out.append(val)
                    out.append(0); i=j+1; continue
                # Unrecognized bracketed text is literal text. Fall through one char at a time.
        out+=struct.pack('<H',glyph_code(script,c,rev)); i+=1
    out.append(0)
    return bytes(out)

def insert(src,dst,id_,text):
    b=Path(src).read_bytes()
    if len(b)<0x44 or b[:4]!=b' DMR': raise ValueError('invalid RMD template')
    fontOffset=struct.unpack_from('<i',b,4)[0]
    idx=struct.unpack_from('<i',b,0x30)[0]
    st=struct.unpack_from('<i',b,0x34)[0]
    ft=struct.unpack_from('<i',b,0x38)[0]
    for name,val in [('idx',idx),('st',st),('ft',ft),('fontOffset',fontOffset)]:
        if val<0 or val>len(b): raise ValueError(f'{name} out of bounds {val}')
    if not (idx<=st<=ft<=len(b)): raise ValueError(f'bad table ordering idx={idx} st={st} ft={ft} len={len(b)}')
    diff=ft-st; nft=ft-diff; nfo=fontOffset-diff
    padding=0x40-(len(text)%0x40); tlen=len(text)+padding; nft+=tlen; nfo+=tlen
    o=bytearray(); o+=b[:4]; o+=struct.pack('<i',nfo); o+=b[8:12]; o+=struct.pack('<i',nfo)
    o+=b[0x10:0x30]; o+=b[0x30:0x38]; o+=struct.pack('<i',nft); o+=struct.pack('<i',nfo-0x10)*2
    o+=b[0x44:idx+0x10]; o+=struct.pack('<Q',int(id_)); o+=b[idx+0x18:st+0x10]
    o+=text; o+=b'\0'*padding; o+=b[ft+0x10:]
    Path(dst).parent.mkdir(parents=True,exist_ok=True); Path(dst).write_bytes(o)

def compile_records(ids=None, output_name='compiled'):
    trans=ROOT/'corpus/translations.json'; glyph=ROOT/'glyphs.json'
    scripts=Path(os.environ.get('PSNOVA_SCRIPTS',ROOT/'scripts')); output=ROOT/output_name
    if not glyph.exists(): raise SystemExit('MISSING glyphs.json')
    rev=load_charmap(glyph); db=json.load(open(trans,encoding='utf-8-sig'))
    selected=set(map(str,ids)) if ids else None
    failures=[]; success=[]
    output.mkdir(parents=True,exist_ok=True)
    for id_,v in db.items():
        if not v.get('Enabled'): continue
        if selected is not None and str(id_) not in selected: continue
        base=str(v.get('Base') or id_)
        src=scripts/f'msg_{base}.rmd'; dst=output/f'msg_{id_}.rmd'
        try:
            if not src.exists(): raise FileNotFoundError(str(src))
            data=translate('msg_'+str(id_),v.get('Text',''),rev); insert(src,dst,id_,data)
            success.append({'id':str(id_),'base':base,'size':dst.stat().st_size,'encoded_bytes':len(data)})
        except Exception as e:
            failures.append({'id':str(id_),'base':base,'error':repr(e)})
    return success,failures

def main():
    ids=None; output='compiled'
    if '--ids' in sys.argv:
        ids=sys.argv[sys.argv.index('--ids')+1].split(',')
    if '--output' in sys.argv:
        output=sys.argv[sys.argv.index('--output')+1]
    success,failures=compile_records(ids,output)
    (ROOT/'reports').mkdir(exist_ok=True)
    json.dump({'compiled':len(success),'failures':failures},open(ROOT/'reports/compile_run.json','w'),indent=2)
    with open(ROOT/'compile_success.csv','w',newline='',encoding='utf-8') as f:
        w=csv.DictWriter(f,fieldnames=['id','base','size','encoded_bytes']);w.writeheader();w.writerows(success)
    with open(ROOT/'compile_failures.csv','w',newline='',encoding='utf-8') as f:
        w=csv.DictWriter(f,fieldnames=['id','base','error']);w.writeheader();w.writerows(failures)
    print('compiled',len(success),'failures',len(failures))
    if failures:
        for x in failures[:20]: print(x)
        sys.exit(2)
if __name__=='__main__': main()
