"""Generate mech front-view silhouette + Chinese label glyphs as axis-aligned rect lists.
Output: src/art.lua (data only). Offline tool; the game never loads PIL or fonts."""
from pathlib import Path
from PIL import Image,ImageDraw,ImageFont
import json
R=Path(__file__).resolve().parents[1]
GW,GH=170,146
def mirror(poly): return [(GW-x,y) for x,y in poly]
def mir(polys): return [mirror(p) for p in polys]
def box(x0,y0,x1,y1,c=3): return [(x0+c,y0),(x1-c,y0),(x1,y0+c),(x1,y1-c),(x1-c,y1),(x0+c,y1),(x0,y1-c),(x0,y0+c)]
# v1.8 icon, traced from the in-game exosuit renders (Patriot / Emancipator /
# Lumberer / Breacher share one hull; only the arms differ). Flat outline style.
# Pilot's-eye orientation (seen from behind): mech LEFT on screen LEFT, so the
# front-view renders are mirrored. Columns never overlap: arms x<=40,
# legs 42..65, hull 46..124 (upper wing) / 68..102 (spine). 1 unit = 1 px.
Z={}
Z['cockpit_rear']=[[(52,0),(118,0),(122,4),(122,12),(48,12),(48,4)]]
Z['cockpit_front_left']=[[(46,15),(85,15),(85,72),(72,72),(68,68),(68,44),(46,44)]]
Z['cockpit_front_right']=mir(Z['cockpit_front_left'])
Z['hips']=[box(70,75,100,87,3)]
Z['leg_left']=[[(41,47),(67,47),(67,108),(61,110),(61,132),(66,136),(66,145),(38,145),(38,136),(43,132),(43,110),(41,108)]]
Z['leg_right']=mir(Z['leg_left'])
LABEL={'cockpit_rear':(85,6.5),'cockpit_front_left':(65,29),'cockpit_front_right':(105,29),'hips':(85,81),
       'leg_left':(54,72),'leg_right':(116,72)}
# Per-model arm shapes (left = mech's left hand, screen left).
SHOULDER_L=box(8,2,40,24,3)
ARMS={
 'default':{'arm_left':[SHOULDER_L,box(6,24,38,62,3)],'arm_right':None,'lab':((22,43),None)},
 # EXO-45 Patriot: rocket pod slab (left); shoulder + gatling with ammo drum (right).
 'EXO-45':{'arm_left':[box(4,4,40,60,4)],'arm_right':mir([SHOULDER_L,box(22,24,38,46,2),box(8,38,28,60,7)]),'lab':((22,32),(GW-24,13))},
 # EXO-49 Emancipator: autocannon pod under each shoulder, twin barrels outward.
 'EXO-49':{'arm_left':[SHOULDER_L,box(10,24,40,60,3),[(2,30),(10,30),(10,36),(2,36)],[(2,46),(10,46),(10,52),(2,52)]],'arm_right':None,'lab':((25,42),None)},
 # EXO-51 Lumberer: shoulder + flamethrower with side tank (left); AT cannon block + barrel (right).
 'EXO-51':{'arm_left':[SHOULDER_L,box(16,24,38,64,3),box(4,30,16,50,3)],'arm_right':mir([box(6,2,40,58,4),[(18,58),(26,58),(26,86),(18,86)]]),'lab':((28,42),(GW-23,30))},
 # EXO-55 Breacher: tall shield slab (left); shoulder + shotgun drum (right).
 'EXO-55':{'arm_left':[box(0,0,34,100,4),[(34,10),(42,10),(42,22),(34,22)]],'arm_right':mir([SHOULDER_L,[(20,24),(30,24),(30,30),(20,30)],box(8,30,40,62,9)]),'lab':((17,50),(GW-24,46))},
}
for k,v in ARMS.items():
    if v['arm_right'] is None: v['arm_right']=mir(v['arm_left'])
    l,r=v['lab'];v['lab']=(l,r or (GW-l[0],l[1]))
# Neutral connectors: shoulder joints, spine/waist link, hip joints.
N={'waist':[(79,72),(91,72),(91,75),(79,75)],'hj_l':[(65,78),(70,78),(70,84),(65,84)]}
N['hj_r']=mirror(N['hj_l'])
DETAIL=[]
def mask(poly):
    im=Image.new('1',(GW,GH),0);d=ImageDraw.Draw(im)
    for p in (poly if isinstance(poly[0][0],(list,tuple)) else [poly]):d.polygon([tuple(q) for q in p],fill=1,outline=1)
    return im
def rects_of(im):
    w,h=im.size;px=im.load();open_={};out=[]
    for y in range(h+1):
        runs=[];x=0
        while y<h and x<w:
            if px[x,y]:
                s=x
                while x<w and px[x,y]:x+=1
                runs.append((s,x))
            else:x+=1
        nxt={}
        for r in runs:
            if r in open_:nxt[r]=open_.pop(r)
            else:nxt[r]=y
        for (s,e),y0 in open_.items():out.append((s,y0,e-s,y-y0))
        open_=nxt
    return out
def outline(im):
    w,h=im.size;px=im.load();o=Image.new('1',(w,h),0);q=o.load()
    for y in range(h):
        for x in range(w):
            if px[x,y] and any(not(0<=x+dx<w and 0<=y+dy<h) or not px[x+dx,y+dy] for dx,dy in((1,0),(-1,0),(0,1),(0,-1))):q[x,y]=1
    return o
def inner(im,o):
    a=Image.new('1',im.size,0);pa=a.load();pi=im.load();po=o.load()
    for y in range(im.size[1]):
        for x in range(im.size[0]):
            if pi[x,y] and not po[x,y]:pa[x,y]=1
    return a
def hatch(im):
    a=Image.new('1',im.size,0);pa=a.load();pi=im.load()
    for y in range(im.size[1]):
        for x in range(im.size[0]):
            if pi[x,y] and (x+y)%5==0:pa[x,y]=1
    return a
art={'grid':[GW,GH],'zones':{},'neutral':{},'detail':[]}
stats={}
def zone_art(p,lab):
    m=mask(p);o=outline(m)
    return {'fill':rects_of(inner(m,o)),'edge':rects_of(o),'anchor':list(lab)}
for k,p in Z.items():
    art['zones'][k]=zone_art(p,LABEL[k]);stats[k]=len(art['zones'][k]['edge'])
art['models']={}
for code,v in ARMS.items():
    art['models'][code]={'arm_left':zone_art(v['arm_left'],v['lab'][0]),'arm_right':zone_art(v['arm_right'],v['lab'][1])}
art['zones']['arm_left']=art['models']['default']['arm_left'];art['zones']['arm_right']=art['models']['default']['arm_right']
nm=Image.new('1',(GW,GH),0)
for p in N.values():nm.paste(1,mask=mask(p))
no=outline(nm)
art['neutral']={'fill':rects_of(inner(nm,no)),'edge':rects_of(no)}
dm=Image.new('1',(GW,GH),0);dd=ImageDraw.Draw(dm)
for a,b in DETAIL:dd.line([a,b],fill=1)
art['detail']=rects_of(dm)
# v1.10 HD text: anti-aliased Noto Sans CJK SC (SIL OFL 1.1) rendered at the
# exact on-screen pixel size for each UI scale (1x/1.5x/2x), alpha quantised to
# LV levels; each level is a rect list drawn with proportional alpha. No
# scaling of bitmaps -> crisp at 1080p, 1440p and 4K.
TEXT='0123456789%-/EXO5'+'驾驶舱左前右后部髋关节腿臂武器一二机体结构完好受损危急毁突破护盾霰弹枪爱国者解放伐木火箭机枪炮喷反坦克'
SMALL='0123456789%-/损毁'
LV=4
NOTO_R='/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc'
NOTO_B='/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc'
def aa_set(path,px,chars,gamma=0.85):
    f=ImageFont.truetype(path,px,index=2)  # index 2 = SC
    asc,desc=f.getmetrics();H=asc+desc+2
    out={}
    for ch in dict.fromkeys(chars):
        W=int(f.getlength(ch))+4
        im=Image.new('L',(W,H),0);ImageDraw.Draw(im).text((1,1),ch,font=f,fill=255)
        px_=im.load();levels=[]
        for l in range(1,LV+1):
            m=Image.new('1',(W,H),0);q=m.load()
            for y in range(H):
                for x in range(W):
                    v=(px_[x,y]/255)**gamma
                    if v>0.06 and min(LV,max(1,round(v*LV)))==l:q[x,y]=1
            levels.append(rects_of(m))
        out[ch]={'l':levels,'adv':round(f.getlength(ch))}
    # baseline = asc+1 from glyph-box top; cap = height of digit '0'
    bb=f.getbbox('0')
    return {'em':px,'base':asc+1,'cap':bb[3]-bb[1],'lv':LV,'g':out}
FONTS={}
for sc in (1,1.5,2):
    FONTS[str(sc).rstrip('0').rstrip('.') if sc!=1 else '1']={
        'big':aa_set(NOTO_R,round(13*sc),TEXT),
        'small':aa_set(NOTO_B,round(9*sc),SMALL)}
art['fonts']=FONTS
# legacy keys (tests / coverage checks)
art['glyphs']=FONTS['1']['big']['g'];art['small']=FONTS['1']['small']['g']
glyphs=art['glyphs']
def lua(v):
    if isinstance(v,dict):return '{'+','.join(f'[{json.dumps(k,ensure_ascii=False)}]={lua(x)}' for k,x in v.items())+'}'
    if isinstance(v,(list,tuple)):
        if v and isinstance(v[0],(list,tuple)) and len(v[0])==4 and all(isinstance(t,int) for t in v[0]):
            return '"'+''.join(f'{a},{b},{c},{d};' for a,b,c,d in v)+'"'
        return '{'+','.join(lua(x) for x in v)+'}'
    if isinstance(v,float):return f'{v:.2f}'
    return json.dumps(v,ensure_ascii=False)
src='-- GENERATED by art/gen_art.py. Data only.\n-- Glyph bitmaps derived from Fusion Pixel Font 12px (SIL Open Font License 1.1).\nreturn '+lua(art)+'\n'
(R/'src/art.lua').write_text(src)
tot_g=sum(sum(len(l) for l in g['l']) for g in glyphs.values())
print(json.dumps({'zones':stats,'neutral':{k:len(v) for k,v in art['neutral'].items()},'detail':len(art['detail']),'glyphs':len(glyphs),'glyph_rects_total':tot_g,'avg_glyph':round(tot_g/len(glyphs),1),'bytes':len(src)},ensure_ascii=False))
