"""Original deterministic artwork. Run from author directory: python sources/draw_figures.py.
Chinese text uses exactly GEWU_CJK_FONT; font identity checked before rendering.
Body examples are small explicit configurations, not simulated research data.
"""
from pathlib import Path
import os, json, math
import numpy as np
from PIL import Image, ImageDraw, ImageFont, ImageFilter
from matplotlib.font_manager import FontProperties
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / 'images'
OUT.mkdir(exist_ok=True)
FONT_FILE = os.environ['GEWU_CJK_FONT']
assert FontProperties(fname=FONT_FILE).get_name() == 'Noto Sans CJK SC'
SCALE=2
BG='#f7f3e9'; INK='#173f46'; UP='#cf604b'; DOWN='#258f99'; CUT='#bd8218'; GRAY='#bdc9c4'; RING='#4c4279'

def canvas(w,h):
    im=Image.new('RGB',(w*SCALE,h*SCALE),BG)
    return im,ImageDraw.Draw(im)

def xy(p): return tuple(round(a*SCALE) for a in p)
def line(d,pts,col,width=3): d.line([xy(p) for p in pts],fill=col,width=round(width*SCALE),joint='curve')
def ellipse(d,box,col,outline=None,width=1): d.ellipse(xy(box),fill=col,outline=outline,width=width*SCALE)
def label(d,x,y,t,size=28,anchor='mm',color=INK):
    font=ImageFont.truetype(os.environ['GEWU_CJK_FONT'],size*SCALE)
    d.text(xy((x,y)),t,font=font,fill=color,anchor=anchor)
def dash(d,a,b,col,width=6):
    a=np.array(a); b=np.array(b); n=np.linalg.norm(b-a); v=(b-a)/n
    for z in np.arange(0,n,13): line(d,[a+v*z,a+v*min(z+8,n)],col,width)

def spin(d,p,isdown,r=18,ring=False):
    x,y=p
    if ring: ellipse(d,(x-r-6,y-r-6,x+r+6,y+r+6),BG,RING,2)
    ellipse(d,(x-r,y-r,x+r,y+r),DOWN if isdown else UP)
    # Explicit arrows accompany color; no font-based arrow glyphs.
    top=y+8 if isdown else y-8; base=y-8 if isdown else y+8
    line(d,[(x,base),(x,top)],'#ffffff',2.6)
    e=-1 if isdown else 1
    line(d,[(x-5,top+e*5),(x,top),(x+5,top+e*5)],'#ffffff',2.6)

def edges(L,M,periodic):
    ans=[]
    for y in range(M):
        for x in range(L):
            if x+1<L: ans.append(((x,y),(x+1,y),'normal'))
            elif periodic: ans.append(((x,y),(0,y),'hwrap'))
            if y+1<M: ans.append(((x,y),(x,y+1),'normal'))
            elif periodic: ans.append(((x,y),(x,0),'vwrap'))
    return ans

def bezier(a,c1,c2,b):
    return [(1-t)**3*np.array(a)+3*(1-t)**2*t*np.array(c1)+3*(1-t)*t*t*np.array(c2)+t**3*np.array(b) for t in np.linspace(0,1,70)]

def graph(d,L,M,selected,origin,gap,periodic=False,mark=False):
    ox,oy=origin; gx,gy=gap
    pos={(x,y):(ox+x*gx,oy+y*gy) for y in range(M) for x in range(L)}
    es=edges(L,M,periodic)
    # Periodic edges split into paired outward stubs; all interior edges exact.
    for a,b,typ in sorted(es,key=lambda e:e[2]=='normal'):
        pa,pb=pos[a],pos[b]; different=(a in selected)!=(b in selected)
        col=CUT if different else GRAY
        if typ=='normal':
            if different: dash(d,pa,pb,col,6)
            else: line(d,[pa,pb],col,3)
        else:
            # Split a periodic bond at the boundary: paired stubs on opposite
            # sides denote ONE bond. No curve traverses unrelated vertices.
            if typ=='hwrap':
                segs=[(pa,(pa[0]+38,pa[1])),(pb,(pb[0]-38,pb[1]))]
            else:
                segs=[(pa,(pa[0],pa[1]+38)),(pb,(pb[0],pb[1]-38))]
            for start,end in segs:
                if different: dash(d,start,end,col,6)
                else: line(d,[start,end],col,3)
                vec=(np.array(end)-np.array(start))/38
                ortho=np.array([-vec[1],vec[0]])
                line(d,[np.array(end)-vec*7+ortho*4,end,np.array(end)-vec*7-ortho*4],col,2.5)
    for v,p in pos.items(): spin(d,p,v in selected,16,mark and sum(v)%2==0)
    return sum((a in selected)!=(b in selected) for a,b,_ in es)

def legend(d,y,mark=False):
    spin(d,(42,y),False,13); label(d,66,y,'朝上',24,'lm')
    spin(d,(178,y),True,13); label(d,202,y,'朝下',24,'lm')
    dash(d,(310,y),(352,y),CUT,5); label(d,369,y,'反向连接',24,'lm')
    if mark:
        ellipse(d,(555,y-16,587,y+16),BG,RING,2)
        label(d,602,y,'翻转位置',23,'lm')

# Figure 1: exact tiny open-grid toy example.
im,d=canvas(720,1040); legend(d,42)
local=[]
for ytop,selected,title in [(120,{(1,1),(2,1)},'相邻的两个朝下点'),(600,{(1,1),(2,2)},'分离的两个朝下点')]:
    label(d,360,ytop,title,34)
    u=graph(d,4,4,selected,(216,ytop+85),(96,86))
    label(d,360,ytop+395,f'{u} 条反向连接',34)
    local.append(u)
assert local==[6,8]
im.save(OUT/'cut-comparison.png')

# Figure 2: source paper Theorem 8 construction + Theorem 4 involution.
im,d=canvas(720,1640); legend(d,44,True)
bar={(1,0),(2,0)}
union=bar|{(2,2),(3,2),(4,2)}
color={(x,y) for x in range(6) for y in range(4) if (x+y)%2==0}
mirror=union^color
res=[]
for top,sel,title,mark in [(150,bar,'一段连续翻转',False),(655,union,'隔行放两段',True),(1160,mirror,'翻转棋盘格的一色',False)]:
    label(d,360,top,title,34)
    u=graph(d,6,4,sel,(160,top+95),(80,70),True,mark)
    label(d,360,top+415,f'{u} 条反向连接',34)
    res.append(u)
assert res==[6,14,34]
assert len(edges(6,4,True))==48
assert all(((a in union)!=(b in union)) != ((a in mirror)!=(b in mirror)) for a,b,_ in edges(6,4,True))
im.save(OUT/'bars-and-mirror.png')

# Cover: oblique layered lattice; highlighted cut bonds have exact endpoint correspondence.
W,H=1600,900
rng=np.random.default_rng(22820)
y,x=np.mgrid[0:H*SCALE,0:W*SCALE]
v=np.clip(1-np.sqrt(((x-W*SCALE*.55)/(W*SCALE))**2+((y-H*SCALE*.5)/(H*SCALE))**2),0,1)
noise=rng.normal(0,1.4,v.shape)
a=np.zeros((H*SCALE,W*SCALE,3),dtype=np.uint8)
for ch,base in enumerate([9,29,35]): a[:,:,ch]=np.clip(base+v*[10,20,22][ch]+noise,0,255)
im=Image.fromarray(a); d=ImageDraw.Draw(im)
# Perspective is editorial staging; open graph is not the paper's 24-point torus.
L,M=10,6
pos={(i,j):(180+i*100+j*57,176+j*89-i*11) for j in range(M) for i in range(L)}
selected={(i,j) for j in range(M) for i in range(L) if 2<=i<=6 and 1<=j<=4 and not(i==6 and j==1) and not(i==2 and j==4)}
# Thick cut-paper field floating above the ground.
poly=[pos[(0,0)],pos[(L-1,0)],pos[(L-1,M-1)],pos[(0,M-1)]]
poly=[(p[0]+(-45 if k in [0,3] else 45),p[1]+(-45 if k in [0,1] else 45)) for k,p in enumerate(poly)]
shadow=Image.new('RGBA',im.size); sd=ImageDraw.Draw(shadow)
sd.polygon([xy((p[0]+12,p[1]+54)) for p in poly],fill=(0,0,0,150))
shadow=shadow.filter(ImageFilter.GaussianBlur(26)); im=Image.alpha_composite(im.convert('RGBA'),shadow);d=ImageDraw.Draw(im)
d.polygon([xy((p[0],p[1]+16)) for p in poly],fill='#152e32')
d.polygon([xy(p) for p in poly],fill='#204850')
# Soft paper cells connect local geometric surface to spin colors.
for j in range(M-1):
    for i in range(L-1):
        vs=[(i,j),(i+1,j),(i+1,j+1),(i,j+1)]
        tint='#285e66' if sum(v in selected for v in vs)>=3 else '#3c4b4b'
        d.polygon([xy(pos[v]) for v in vs],fill=tint)
# Bond shadows, then bands, then golden cut ligatures.
es=edges(L,M,False)
for va,vb,_ in es:
    a,b=pos[va],pos[vb]; cut=(va in selected)!=(vb in selected)
    line(d,[(a[0],a[1]+9),(b[0],b[1]+9)],'#112f35',14)
    line(d,[a,b],'#efb945' if cut else '#57787a',10 if cut else 5)
    if cut:
        line(d,[(a[0],a[1]-2),(b[0],b[1]-2)],'#ffe7a0',3)
        mid=((a[0]+b[0])/2,(a[1]+b[1])/2)
        ellipse(d,(mid[0]-4,mid[1]-4,mid[0]+4,mid[1]+4),'#fff2bc')
for v,p in pos.items():
    xx,yy=p; down=v in selected
    ellipse(d,(xx-22,yy-10,xx+25,yy+32),'#0c252b')
    ellipse(d,(xx-22,yy-25,xx+22,yy+19),'#36aeb4' if down else '#e66e55')
    ellipse(d,(xx-16,yy-22,xx+16,yy+6),'#72ced0' if down else '#ffac82')
    # Each face has an arrow, represented in projection as a short raised inlay.
    top=yy+9 if down else yy-16; base=yy-16 if down else yy+9
    line(d,[(xx,base),(xx,top)],'#f9f1d6',4)
    e=-1 if down else 1
    line(d,[(xx-7,top+e*7),(xx,top),(xx+7,top+e*7)],'#f9f1d6',4)
# Grain belongs to a drawn medium, not fabricated observation.
im.convert('RGB').save(OUT/'cover.png')

report={'font_path':FONT_FILE,'font_family':FontProperties(fname=FONT_FILE).get_name(),
        'cut_comparison':local,'bars_and_mirror':res,'torus_bonds':48,
        'status':'exact illustrative configurations; no large computations'}
(ROOT/'review'/'drawing-checks.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n')
print(json.dumps(report,ensure_ascii=False))
