"""Small deterministic vector toolkit. SVG and PDF share the same paths."""
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
from html import escape
from io import BytesIO
import re
import xml.etree.ElementTree as ET
import math
import json
import numpy as np
from PIL import Image
from fontTools.ttLib import TTFont
from fontTools.varLib.instancer import instantiateVariableFont
from fontTools.pens.svgPathPen import SVGPathPen
from fontTools.pens.boundsPen import BoundsPen
from fontTools.pens.basePen import BasePen
from fontTools.svgLib.path import parse_path
from reportlab.pdfgen import canvas
from reportlab.lib.colors import HexColor
from reportlab.lib.utils import ImageReader
from base64 import b64encode,b64decode
import pypdfium2 as pdfium

ROOT = Path(__file__).resolve().parents[1]
INK = '#111111'
RED = '#C81D25'
PAPER = '#F4F1EB'
WHITE = '#FFFFFF'
MUTED = '#66625C'
RULE = '#D6D1C8'

def matrix(a=1,b=0,c=0,d=1,e=0,f=0): return (a,b,c,d,e,f)
def mul(a,b):
    return (a[0]*b[0]+a[2]*b[1],a[1]*b[0]+a[3]*b[1],
            a[0]*b[2]+a[2]*b[3],a[1]*b[2]+a[3]*b[3],
            a[0]*b[4]+a[2]*b[5]+a[4],a[1]*b[4]+a[3]*b[5]+a[5])
def trans(x,y,sx=1,sy=None): return (sx,0,0,sx if sy is None else sy,x,y)
def nums(v): return ' '.join(f'{x:.5f}'.rstrip('0').rstrip('.') if x else '0' for x in v)

class CanvasPen(BasePen):
    def __init__(self,c): super().__init__(None); self.path=c.beginPath()
    def _moveTo(self,p): self.path.moveTo(*p)
    def _lineTo(self,p): self.path.lineTo(*p)
    def _curveToOne(self,p1,p2,p3): self.path.curveTo(*p1,*p2,*p3)
    def _qCurveToOne(self,p1,p2):
        p0=self._getCurrentPoint()
        self._curveToOne((p0[0]+2/3*(p1[0]-p0[0]),p0[1]+2/3*(p1[1]-p0[1])),
                         (p2[0]+2/3*(p1[0]-p2[0]),p2[1]+2/3*(p1[1]-p2[1])),p2)
    def _closePath(self): self.path.close()
    def _endPath(self): pass

@dataclass
class Scene:
    w: float
    h: float
    items: list = field(default_factory=list)
    title: str = 'Stand The Fight'
    text_runs: list = field(default_factory=list)

    def path(self,d,fill=INK,m=(1,0,0,1,0,0),stroke=None,sw=1,role=''):
        self.items.append({'d':d,'fill':fill,'m':m,'stroke':stroke,'sw':sw,'role':role})
        return self
    def rect(self,x,y,w,h,fill=INK,stroke=None,sw=1,role=''):
        return self.path(f'M{x} {y}h{w}v{h}h{-w}Z',fill,stroke=stroke,sw=sw,role=role)
    def circle(self,x,y,r,fill=INK,role=''):
        k=r*0.5522847498307936
        d=f'M{x+r} {y}C{x+r} {y+k} {x+k} {y+r} {x} {y+r}C{x-k} {y+r} {x-r} {y+k} {x-r} {y}C{x-r} {y-k} {x-k} {y-r} {x} {y-r}C{x+k} {y-r} {x+r} {y-k} {x+r} {y}Z'
        return self.path(d,fill,role=role)
    def line(self,x1,y1,x2,y2,col=RULE,sw=1):
        return self.path(f'M{x1} {y1}L{x2} {y2}',None,stroke=col,sw=sw)
    def image(self,path,x,y,w,h):
        data=Path(path).read_bytes()
        with Image.open(BytesIO(data)) as im:iw,ih=im.size
        scale=min(w/iw,h/ih);ww=iw*scale;hh=ih*scale
        self.items.append({'kind':'image','data':b64encode(data).decode(),'m':trans(x+(w-ww)/2,y+(h-hh)/2),'w':ww,'h':hh,'fill':None,'stroke':None,'role':'mockup-image'})
        return self
    def add(self,other,x=0,y=0,s=1,recolor=None):
        m=trans(x,y,s)
        for item in other.items:
            v=dict(item);v['m']=mul(m,v['m'])
            if recolor:
                v['fill']=recolor.get(v['fill'],v['fill'])
                v['stroke']=recolor.get(v['stroke'],v['stroke'])
            self.items.append(v)
        return self
    def svg(self,live_text=False):
        out=[f'<svg xmlns="http://www.w3.org/2000/svg" width="{self.w}" height="{self.h}" viewBox="0 0 {self.w} {self.h}" role="img" aria-labelledby="title">',f'<title id="title">{escape(self.title)}</title>']
        starts={r['start']:r for r in self.text_runs} if live_text else {}
        skip_until=-1
        for index,i in enumerate(self.items):
            if index<skip_until:continue
            if index in starts:
                r=starts[index];skip_until=r['end']
                attrs=f'fill="{r["fill"]}" transform="matrix({nums(r["m"])})" font-family="{r["family"]}" font-weight="{r["weight"]}" font-size="{r["size"]}" letter-spacing="{r["tracking"]}" font-kerning="none" style="font-feature-settings:&quot;kern&quot; 0,&quot;liga&quot; 0;white-space:pre"'
                out.append('<text '+attrs+'>'+escape(r['text'])+'</text>')
                continue
            if i.get('kind')=='image':
                out.append(f'<image href="data:image/png;base64,{i["data"]}" width="{i["w"]}" height="{i["h"]}" transform="matrix({nums(i["m"])})"/>')
                continue
            attrs=f'd="{i["d"]}" fill="{i["fill"] or "none"}" transform="matrix({nums(i["m"])})"'
            if i['stroke']: attrs+=f' stroke="{i["stroke"]}" stroke-width="{i["sw"]}"'
            if i['role']: attrs+=f' data-role="{escape(i["role"])}"'
            out.append('<path '+attrs+'/>')
        return '\n'.join(out+['</svg>'])
    def draw_pdf(self,c,physical=None,cmyk=False):
        c.saveState()
        pw,ph=physical or (self.w,self.h)
        c.translate(0,ph);c.scale(pw/self.w,-ph/self.h)
        for i in self.items:
            c.saveState();c.transform(*i['m'])
            if i.get('kind')=='image':
                c.translate(0,i['h']);c.scale(1,-1)
                c.drawImage(ImageReader(BytesIO(b64decode(i['data']))),0,0,width=i['w'],height=i['h'],mask='auto')
                c.restoreState();continue
            p=CanvasPen(c);parse_path(i['d'],p)
            def colour(value,stroke=False):
                if cmyk:
                    if value==INK:vals=(0,0,0,1)
                    else:
                        rgb=[int(value[j:j+2],16)/255 for j in (1,3,5)];k=1-max(rgb)
                        vals=(0,0,0,1) if k==1 else tuple((1-v-k)/(1-k) for v in rgb)+(k,)
                    (c.setStrokeColorCMYK if stroke else c.setFillColorCMYK)(*vals)
                else:(c.setStrokeColor if stroke else c.setFillColor)(HexColor(value))
            if i['fill']:colour(i['fill'])
            if i['stroke']:colour(i['stroke'],True);c.setLineWidth(i['sw'])
            c.drawPath(p.path,fill=bool(i['fill']),stroke=bool(i['stroke']),fillMode=1)
            c.restoreState()
        c.restoreState()
    def pdf(self,physical=None,cmyk=False):
        b=BytesIO(); c=canvas.Canvas(b,pagesize=physical or (self.w,self.h),pageCompression=1)
        c.setTitle(self.title);c.setAuthor('Stand The Fight');self.draw_pdf(c,physical,cmyk);c.showPage();c.save()
        return b.getvalue()
    def png(self,width=None,transparent=True):
        doc=pdfium.PdfDocument(self.pdf());page=doc[0]
        bm=page.render(scale=(width or self.w)/self.w,fill_color=(255,255,255,0 if transparent else 255))
        im=bm.to_pil().copy();bm.close();page.close();doc.close();return im
    def export(self,path,png_width=None,physical=None,transparent=True):
        path=Path(path);path.parent.mkdir(parents=True,exist_ok=True)
        path.with_suffix('.svg').write_text(self.svg())
        path.with_suffix('.pdf').write_bytes(self.pdf(physical))
        if png_width:self.png(png_width,transparent).save(path.with_suffix('.png'))

class Typeface:
    def __init__(self,path,weight=None):
        self.family={'anton':'STF Anton','epilogue':'STF Epilogue','notosansjp':'STF Japanese'}[Path(path).parent.name]
        self.weight=weight or 400
        self.font=TTFont(path)
        if weight is not None and 'fvar' in self.font:self.font=instantiateVariableFont(self.font,{'wght':weight},inplace=False)
        self.gs=self.font.getGlyphSet();self.cmap=self.font.getBestCmap();self.upem=self.font['head'].unitsPerEm
        self.cache={}
    def glyph(self,ch):
        if ch not in self.cache:
            name=self.cmap.get(ord(ch))
            if name is None:raise ValueError(f'Missing glyph: {ch!r}')
            g=self.gs[name];p=SVGPathPen(self.gs);g.draw(p)
            b=BoundsPen(self.gs);g.draw(b)
            self.cache[ch]=(p.getCommands(),b.bounds,self.font['hmtx'][name][0])
        return self.cache[ch]
    def layout(self,text,tracking=0):
        x=0;parts=[];bounds=[]
        for ch in text:
            d,b,advance=self.glyph(ch)
            if b:
                parts.append((d,x));bounds.append((x+b[0],b[1],x+b[2],b[3]))
            x+=advance+tracking
        if not bounds:return [],(0,0,0,0)
        return parts,(min(p[0] for p in bounds),min(p[1] for p in bounds),max(p[2] for p in bounds),max(p[3] for p in bounds))
    def text(self,scene,text,x,y,height,width=None,tracking=0,fill=INK,align='left',role=''):
        parts,b=self.layout(text,tracking*self.upem)
        sy=height/(b[3]-b[1]);sx=width/(b[2]-b[0]) if width else sy
        w=(b[2]-b[0])*sx
        if align=='center':x-=w/2
        elif align=='right':x-=w
        start=len(scene.items)
        for d,px in parts:
            scene.path(d,fill,(sx,0,0,-sy,x+(px-b[0])*sx,y+b[3]*sy),role=role)
        scene.text_runs.append({'start':start,'end':len(scene.items),'text':text,'family':self.family,'weight':self.weight,'size':sy*self.upem,'tracking':tracking*self.upem*sy,'m':(sx/sy,0,0,1,x-b[0]*sx,y+b[3]*sy),'fill':fill})
        return w
    def paragraph(self,scene,text,x,y,width,size=18,leading=29,fill=INK,max_lines=20):
        words=text.split();lines=[];line=''
        # Body size is a nominal em; maintain the same scale across all lines.
        cap=self.glyph('H')[1];scale=size/self.upem
        def extent(s):
            _,b=self.layout(s);return (b[2]-b[0])*scale
        for word in words:
            candidate=(line+' '+word).strip()
            if line and extent(candidate)>width:lines.append(line);line=word
            else:line=candidate
        if line:lines.append(line)
        assert len(lines)<=max_lines,(text,len(lines),max_lines)
        for k,line in enumerate(lines):
            parts,b=self.layout(line)
            start=len(scene.items)
            for d,px in parts:scene.path(d,fill,(scale,0,0,-scale,x+(px-b[0])*scale,y+k*leading+cap[3]*scale))
            scene.text_runs.append({'start':start,'end':len(scene.items),'text':line,'family':self.family,'weight':self.weight,'size':size,'tracking':0,'m':(1,0,0,1,x-b[0]*scale,y+k*leading+cap[3]*scale),'fill':fill})
        return y+len(lines)*leading

def fonts():
    return {
        'display':Typeface(ROOT/'fonts/anton/Anton-Regular.ttf'),
        'light':Typeface(ROOT/'fonts/epilogue/Epilogue[wght].ttf',300),
        'body':Typeface(ROOT/'fonts/epilogue/Epilogue[wght].ttf',400),
        'medium':Typeface(ROOT/'fonts/epilogue/Epilogue[wght].ttf',600),
        'jp':Typeface(ROOT/'fonts/notosansjp/NotoSansJP[wght].ttf',400),
    }

def save_multipage(scenes,path,physical=(841.8898,595.2756)):
    c=canvas.Canvas(str(path),pagesize=physical,pageCompression=1)
    c.setTitle('Stand The Fight | Brand guide | Review edition 1.2');c.setAuthor('Stand The Fight')
    for s in scenes:s.draw_pdf(c,physical);c.showPage()
    c.save()

def load_svg_scene(path):
    tree=ET.fromstring(Path(path).read_text());s=Scene(float(tree.attrib['width']),float(tree.attrib['height']))
    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'],None if p.attrib.get('fill')=='none' else p.attrib.get('fill',INK),m,stroke=p.attrib.get('stroke'),sw=float(p.attrib.get('stroke-width',1)),role=p.attrib.get('data-role',''))
    return s

def render_pdf(path,out_dir,width=1200):
    out_dir=Path(out_dir);out_dir.mkdir(parents=True,exist_ok=True)
    doc=pdfium.PdfDocument(str(path));files=[]
    for n in range(len(doc)):
        p=doc[n];bm=p.render(scale=width/p.get_width());im=bm.to_pil();dest=out_dir/f'page-{n+1:02d}.png';im.save(dest);files.append(dest);bm.close();p.close()
    doc.close();return files
