"""Reconstruct selected L2 as one editable, repeatable vector system."""
from pathlib import Path
from collections import deque
from io import BytesIO
import re
import json
import xml.etree.ElementTree as ET
import numpy as np
from PIL import Image
import vtracer
from shapely.geometry import Polygon, GeometryCollection
from shapely.geometry.polygon import orient
from shapely import affinity
from fontTools.pens.basePen import BasePen
from fontTools.svgLib.path import parse_path
from graphics import *

class FlattenPen(BasePen):
    def __init__(self):super().__init__(None);self.contours=[];self.current=[]
    def _moveTo(self,p):self.current=[p]
    def _lineTo(self,p):self.current.append(p)
    def _curveToOne(self,p1,p2,p3):
        p0=self._getCurrentPoint()
        # At native logo size this is considerably finer than one output pixel.
        for k in range(1,33):
            t=k/32;u=1-t
            self.current.append((u**3*p0[0]+3*u*u*t*p1[0]+3*u*t*t*p2[0]+t**3*p3[0],u**3*p0[1]+3*u*u*t*p1[1]+3*u*t*t*p2[1]+t**3*p3[1]))
    def _closePath(self):self.contours.append(self.current);self.current=[]
    def _endPath(self):self._closePath()

def geometry(d,m=(1,0,0,1,0,0)):
    p=FlattenPen();parse_path(d,p);out=GeometryCollection()
    for pts in p.contours:
        if len(pts)<3:continue
        q=Polygon(pts)
        if not q.is_valid:q=q.buffer(0)
        out=out.symmetric_difference(q)
    return affinity.affine_transform(out,[m[0],m[2],m[1],m[3],m[4],m[5]])

def geometry_path(g):
    if g.is_empty:return ''
    polys=[g] if g.geom_type=='Polygon' else list(g.geoms)
    parts=[]
    for poly in polys:
        if poly.geom_type!='Polygon':continue
        poly=orient(poly,sign=1)
        for ring in [poly.exterior,*poly.interiors]:
            coords=list(ring.coords)
            parts.append('M'+nums(coords[0])+'L'+' '.join(nums(p) for p in coords[1:])+'Z')
    return ''.join(parts)

def reconstruct_mark():
    out=ROOT/'source/throw-master.svg'
    if out.exists():
        tree=ET.fromstring(out.read_text());s=Scene(278,466,title='Stand The Fight | Throw emblem')
        for p in tree.findall('{http://www.w3.org/2000/svg}path'):
            m=tuple(map(float,re.findall(r'[-+]?\d*\.?\d+',p.attrib.get('transform','matrix(1 0 0 1 0 0)'))))
            s.path(p.attrib['d'],p.attrib.get('fill',INK),m,role=p.attrib.get('data-role',''))
        return s
    im=np.array(Image.open(ROOT/'source/reference-l2.png').convert('RGB'))
    mask=(im[:,:,0]<90)&(im[:,:,1]<90)&(im[:,:,2]<90)
    mask[:130]=False;mask[640:]=False;mask[:,:640]=False;mask[:,940:]=False
    seen=np.zeros(mask.shape,bool);comps=[]
    for y,x in zip(*np.where(mask)):
        if seen[y,x]:continue
        seen[y,x]=True;q=deque([(y,x)]);pts=[]
        while q:
            yy,xx=q.popleft();pts.append((xx,yy))
            for ny,nx in [(yy-1,xx),(yy+1,xx),(yy,xx-1),(yy,xx+1)]:
                if 0<=ny<mask.shape[0] and 0<=nx<mask.shape[1] and mask[ny,nx] and not seen[ny,nx]:seen[ny,nx]=True;q.append((ny,nx))
        if len(pts)>1000:
            a=np.array(pts);lo=a.min(axis=0);hi=a.max(axis=0)
            if lo[0]<920 and hi[0]<935:comps.append((len(pts),pts,lo,hi))
    s=Scene(278,466,title='Stand The Fight | Throw emblem')
    for count,pts,lo,hi in sorted(comps,reverse=True,key=lambda c:c[0]):
        if 800<lo[0]<815 and 440<lo[1]<450:continue # Head is redrawn as an exact circle.
        # Native vectorisation only: isolate the approved silhouette, discard raster noise.
        raster=np.full((466,278,3),255,np.uint8)
        for x,y in pts:raster[y-164,x-653]=0
        buf=BytesIO();Image.fromarray(raster).save(buf,format='PNG')
        svg=vtracer.convert_raw_image_to_svg(buf.getvalue(),img_format='png',colormode='binary',mode='spline',filter_speckle=8,corner_threshold=70,length_threshold=3.5,max_iterations=10,splice_threshold=45,path_precision=3)
        tree=ET.fromstring(svg)
        role='body'
        if 700<lo[0]<730 and 225<lo[1]<250:role='foreground-curve'
        if lo[1]>590:role='foot'
        for p in tree.findall('{http://www.w3.org/2000/svg}path'):
            vals=list(map(float,re.findall(r'[-+]?\d*\.?\d+',p.attrib.get('transform','translate(0,0)'))))
            s.path(p.attrib['d'],INK,trans(*vals[:2]),role=role)
    red=(im[:,:,0]>130)&(im[:,:,1]<85)&(im[:,:,2]<100)
    red[:250]=False;red[420:]=False;red[:,:680]=False;red[:,810:]=False
    yy,xx=np.where(red)
    cx=(int(xx.min())+int(xx.max()))/2-653;cy=(int(yy.min())+int(yy.max()))/2-164
    rr=((xx.max()-xx.min())+(yy.max()-yy.min()))/4
    s.circle(cx,cy,float(rr),RED,role='red-head')
    s.circle(840.5-653,478.5-164,35.2,INK,role='black-head')
    out.write_text(s.svg())
    assert len(s.items)==7,(len(s.items),comps)
    return s

def patch_mark(mark):
    s=Scene(mark.w,mark.h,title='Stand The Fight | Open-gap throw emblem')
    for i in mark.items:
        g=geometry(i['d'],i['m'])
        # Inset separate filled shapes, opening their negative spaces without joining them.
        if i['role'] not in ['red-head','black-head']:g=g.buffer(-2.8,join_style='round').simplify(.12)
        s.path(geometry_path(g),i['fill'],role=i['role'])
    return s

def recolor(s,kind):
    maps={'colour':{},'black':{RED:INK},'white':{INK:WHITE,RED:WHITE},'reverse':{INK:WHITE},'red':{INK:RED}}
    n=Scene(s.w,s.h,title=s.title+' | '+kind);n.add(s,recolor=maps[kind]);return n

def japanese_column(s,font,x,top,bottom):
    chars=list('スタンド・ザ・ファイト');size=39
    boxes=[]
    for ch in chars:
        d,b,_=font.glyph(ch);sc=size/font.upem;boxes.append((d,b,sc,(b[3]-b[1])*sc,(b[2]-b[0])*sc))
    first=top+boxes[0][3]/2;last=bottom-boxes[-1][3]/2
    for k,(ch,(d,b,sc,h,w)) in enumerate(zip(chars,boxes)):
        center=first+(last-first)*k/(len(chars)-1)
        # Small kana retains its original relative size and a vertical-setting offset.
        offset=6 if ch=='ァ' else 0
        cy=center-4 if ch=='ァ' else center
        s.path(d,INK,(sc,0,0,-sc,x-w/2+offset-b[0]*sc,cy+h/2+b[1]*sc),role='katakana')

def full_logo(f,mark,english=False,compact=False):
    w=1005 if english else 1070;h=910
    s=Scene(w,h,title='Stand The Fight | '+('English' if english else 'Bilingual')+' primary logo')
    f['display'].text(s,'STAND',514,270,200,width=410,role='name-stand')
    f['display'].text(s,'THE',512,502,73,width=78,role='name-the')
    f['display'].text(s,'FIGHT',608,502,208,width=316,role='name-fight')
    if not compact:
        f['light'].text(s,'TRADITIONAL JAPANESE',514,741,25,width=410,tracking=.11,role='descriptor')
        f['light'].text(s,'JIU-JITSU',719,783,25,tracking=.22,align='center',role='descriptor')
    if not english:japanese_column(s,f['jp'],964,270,808)
    # True transparent negative space: cut the name behind the foreground curve.
    curve=next(i for i in mark.items if i['role']=='foreground-curve')
    cg=geometry(curve['d'],mul(trans(80,80,1.6),curve['m'])).buffer(5.5)
    index=next(k for k,i in enumerate(s.items) if i['role']=='name-stand')
    item=s.items[index];sg=geometry(item['d'],item['m'])
    cut=sg.difference(cg)
    assert cut.area/sg.area>.80,'Too much of S has been removed'
    s.items[index]={'d':geometry_path(cut),'fill':INK,'m':matrix(),'stroke':None,'sw':1,'role':'name-stand-overlap'}
    s.add(mark,80,80,1.6)
    return s

def emblem_logo(mark):
    s=Scene(398,586,title='Stand The Fight | Standalone throw emblem');s.add(mark,60,60);return s

def compact_logo(f,mark):
    # Match L2's name, omit microcopy, and trim excess right/bottom space.
    s=full_logo(f,mark,english=True,compact=True)
    s.h=850;s.title='Stand The Fight | Compact English logo';return s

def wordmark(f):
    s=Scene(620,700,title='Stand The Fight | Name-only wordmark')
    f['display'].text(s,'STAND',70,70,215,width=480)
    f['display'].text(s,'THE',70,315,77,width=92)
    f['display'].text(s,'FIGHT',183,315,235,width=367)
    f['light'].text(s,'TRADITIONAL JAPANESE',70,585,23,width=480,tracking=.13)
    f['light'].text(s,'JIU-JITSU',310,626,23,tracking=.23,align='center')
    return s

def patch(f,mark,round_only=False):
    if round_only:
        s=Scene(750,750,title='Stand The Fight | 75 mm emblem patch artwork')
        s.circle(375,375,355,PAPER)
        # Merrow edge shown as a single black outer ring.
        s.path('M375 20A355 355 0 1 1 374.999 20Z',None,stroke=INK,sw=18)
        s.add(mark,200,83,1.25)
        return s
    s=Scene(1000,850,title='Stand The Fight | 100 x 85 mm name patch artwork')
    s.rect(0,0,1000,850,PAPER)
    s.rect(20,20,960,810,None,stroke=INK,sw=16)
    logo=compact_logo(f,mark)
    s.add(logo,15,-1,.965)
    return s

def build_identity():
    f=fonts();mark=reconstruct_mark();pmark=patch_mark(mark)
    pmark.export(ROOT/'source/throw-open-gap')
    logos={'primary':full_logo(f,mark),'english':full_logo(f,mark,english=True),'compact':compact_logo(f,mark),'emblem':emblem_logo(mark),'wordmark':wordmark(f)}
    for name,logo in logos.items():
        for kind in ['colour','black','white','reverse']:
            v=recolor(logo,kind)
            v.export(ROOT/'logos'/f'stf-{name}-{kind}',png_width=2400 if name!='emblem' else 1600)
    p=patch(f,pmark);p.export(ROOT/'patches/stf-name-patch-100x85mm-colour',png_width=1800,physical=(100/25.4*72,85/25.4*72))
    recolor(p,'black').export(ROOT/'patches/stf-name-patch-100x85mm-mono',png_width=1800,physical=(100/25.4*72,85/25.4*72))
    rp=patch(f,pmark,True);rp.export(ROOT/'patches/stf-emblem-patch-75mm-colour',png_width=1500,physical=(75/25.4*72,75/25.4*72))
    recolor(rp,'black').export(ROOT/'patches/stf-emblem-patch-75mm-mono',png_width=1500,physical=(75/25.4*72,75/25.4*72))
    return f,mark,pmark,logos

if __name__=='__main__':
    build_identity()
