"""Renders the demo product art used by the seed (public/demo/*.jpg).
These are stylised studio illustrations of each product type, NOT photographs.
Replace them with real product photography from Admin → Media before launch."""
import math, os, random
from PIL import Image, ImageDraw, ImageFilter, ImageChops

OUT = os.path.join(os.path.dirname(__file__), "..", "..", "public", "demo")
os.makedirs(OUT, exist_ok=True)
W, H = 1000, 1193  # matches the 2000x2386 portrait ratio of the reference

def hexrgb(h): h=h.lstrip('#'); return tuple(int(h[i:i+2],16) for i in (0,2,4))
def mix(a,b,t): return tuple(int(a[i]+(b[i]-a[i])*t) for i in range(3))

def backdrop(bg="#EFE6DA", floor=0.78, w=W, h=H):
    top=hexrgb(bg); bot=mix(top,(255,255,255),0.25); flo=mix(top,(120,100,80),0.10)
    im=Image.new("RGB",(w,h)); d=ImageDraw.Draw(im)
    fy=int(h*floor)
    for y in range(h):
        c = mix(top,bot,y/fy) if y<fy else mix(flo,top,min(1,(y-fy)/(h-fy)))
        d.line([(0,y),(w,y)],fill=c)
    return im

def shadow(im, box, strength=90, blur=40):
    m=Image.new("L",im.size,0); ImageDraw.Draw(m).ellipse(box,fill=strength)
    m=m.filter(ImageFilter.GaussianBlur(blur))
    dark=Image.new("RGB",im.size,(60,45,35)); im.paste(dark,(0,0),m)

def shaded(im, mask, base, light=0.18, dark=0.30, angle="x"):
    """Fill mask with base colour and a soft left-light / right-shadow gradient."""
    bw,bh=im.size; b=hexrgb(base)
    grad=Image.new("RGB",(bw,bh)); gd=ImageDraw.Draw(grad)
    bbox=mask.getbbox() or (0,0,bw,bh); x0,y0,x1,y1=bbox
    if angle=="x":
        for x in range(bw):
            t=(x-x0)/max(1,(x1-x0)); t=min(1,max(0,t))
            c = mix(mix(b,(255,255,255),light), b, t*2) if t<0.5 else mix(b, mix(b,(0,0,0),dark),(t-0.5)*2)
            gd.line([(x,0),(x,bh)],fill=c)
    else:
        for y in range(bh):
            t=(y-y0)/max(1,(y1-y0)); t=min(1,max(0,t))
            c = mix(mix(b,(255,255,255),light), b, t*2) if t<0.5 else mix(b, mix(b,(0,0,0),dark),(t-0.5)*2)
            gd.line([(0,y),(bw,y)],fill=c)
    im.paste(grad,(0,0),mask)

def poly_mask(size, pts, r=0):
    m=Image.new("L",size,0); ImageDraw.Draw(m).polygon(pts,fill=255)
    if r: m=m.filter(ImageFilter.GaussianBlur(r)).point(lambda v:255 if v>127 else 0)
    return m

def rr_mask(size, box, rad):
    m=Image.new("L",size,0); ImageDraw.Draw(m).rounded_rectangle(box,rad,fill=255); return m

def stroke(im, pts, col, width=6, closed=False):
    d=ImageDraw.Draw(im); 
    if closed: pts=pts+[pts[0]]
    d.line(pts,fill=hexrgb(col) if isinstance(col,str) else col,width=width,joint="curve")

def arc_pts(cx,cy,rx,ry,a0,a1,n=80):
    return [(cx+rx*math.cos(math.radians(a0+(a1-a0)*i/n)), cy+ry*math.sin(math.radians(a0+(a1-a0)*i/n))) for i in range(n+1)]

def gold(im, pts, width=7):
    stroke(im, pts, "#8E6A2C", width+3); stroke(im, pts, "#D2AE62", width); stroke(im, pts, "#F1DCA4", max(1,width//3))

# ───────────── bags ─────────────
def bag_shoulder(c, im):
    S=im.size; body=poly_mask(S,[(250,560),(750,560),(800,900),(200,900)],8)
    shadow(im,(170,860,830,960)); shaded(im,body,c)
    for p in [arc_pts(500,560,190,330,180,360)]: 
        stroke(im,p,mix(hexrgb(c),(0,0,0),0.35),28); stroke(im,p,mix(hexrgb(c),(255,255,255),0.1),20)
    d=ImageDraw.Draw(im); flap=poly_mask(S,[(250,560),(750,560),(760,700),(500,740),(240,700)],6); shaded(im,flap,c,0.25,0.2)
    d.ellipse((485,705,515,735),fill=hexrgb("#C9A55C"),outline=hexrgb("#8E6A2C"),width=3)
    stroke(im,[(255,566),(745,566)],mix(hexrgb(c),(255,255,255),0.35),2)

def bag_tote(c, im):
    S=im.size; body=poly_mask(S,[(290,470),(710,470),(740,900),(260,900)],4)
    shadow(im,(230,865,770,955)); shaded(im,body,c)
    for x0 in (360,560):
        p=arc_pts(x0+40,470,60,150,180,360); stroke(im,p,mix(hexrgb(c),(0,0,0),0.4),20)
    stroke(im,[(290,520),(710,520)],mix(hexrgb(c),(255,255,255),0.3),2)
    d=ImageDraw.Draw(im); d.rectangle((470,600,530,630),fill=hexrgb("#C9A55C"))

def bag_crossbody(c, im):
    S=im.size; body=rr_mask(S,(300,600,700,880),60)
    shadow(im,(260,850,740,940)); 
    stroke(im,[(320,620),(250,250),(500,150),(750,250),(680,620)],mix(hexrgb(c),(0,0,0),0.3),14)
    shaded(im,body,c)
    flap=rr_mask(S,(300,600,700,740),60); shaded(im,flap,c,0.28,0.15)
    d=ImageDraw.Draw(im); d.rounded_rectangle((470,720,530,770),8,fill=hexrgb("#C9A55C"),outline=hexrgb("#8E6A2C"),width=3)

def bag_hobo(c, im):
    S=im.size; m=Image.new("L",S,0); ImageDraw.Draw(m).chord((230,380,770,920),0,180,fill=255)
    m2=Image.new("L",S,0); ImageDraw.Draw(m2).ellipse((230,560,770,760),fill=255); m=ImageChops.lighter(m,m2)
    shadow(im,(220,860,780,950)); shaded(im,m,c)
    p=arc_pts(500,660,250,420,200,340); stroke(im,p,mix(hexrgb(c),(0,0,0),0.3),30)
    stroke(im,arc_pts(500,650,270,30,0,180),mix(hexrgb(c),(0,0,0),0.25),3)

def bag_clutch(c, im):
    S=im.size; body=rr_mask(S,(230,560,770,850),24)
    shadow(im,(200,820,800,910)); shaded(im,body,c,0.2,0.35)
    lid=rr_mask(S,(230,560,770,640),24); shaded(im,lid,c,0.35,0.2)
    gold(im,[(230,640),(770,640)],4)
    d=ImageDraw.Draw(im); d.ellipse((480,620,520,660),fill=hexrgb("#D2AE62"),outline=hexrgb("#8E6A2C"),width=3)
    gold(im,arc_pts(500,560,250,240,190,350),4)

def bag_bucket(c, im):
    S=im.size; body=poly_mask(S,[(300,480),(700,480),(740,880),(260,880)],10)
    shadow(im,(230,850,770,945)); shaded(im,body,c)
    d=ImageDraw.Draw(im); dk=mix(hexrgb(c),(0,0,0),0.25); lt=mix(hexrgb(c),(255,255,255),0.2)
    for y in range(500,880,28):
        for x in range(280,730,28): d.line([(x,y),(x+14,y+14)],fill=dk,width=3); d.line([(x+14,y),(x,y+14)],fill=lt,width=2)
    d.ellipse((300,455,700,515),fill=mix(hexrgb(c),(0,0,0),0.45))
    stroke(im,arc_pts(500,480,170,300,180,360),mix(hexrgb(c),(0,0,0),0.35),16)

# ───────────── accessories ─────────────
def acc_necklace(c, im):
    S=im.size; m=rr_mask(S,(330,250,670,960),170); shaded(im,m,"#E8DCCD",0.15,0.15)  # bust form
    pts=arc_pts(500,420,170,300,20,160,60)
    gold(im,pts,5) if c=="gold" else (stroke(im,pts,"#8A8F96",8),stroke(im,pts,"#E3E6EA",4))
    d=ImageDraw.Draw(im); col=hexrgb("#D2AE62") if c=="gold" else hexrgb("#C9CDD2")
    d.ellipse((485,705,515,735),fill=col,outline=(90,70,40) if c=="gold" else (110,115,120),width=2)

def acc_scarf(c, im):
    S=im.size; base=hexrgb(c[0]); acc=hexrgb(c[1])
    tile=Image.new("RGB",(560,560),base); d=ImageDraw.Draw(tile)
    random.seed(sum(base))
    d.rectangle((20,20,540,540),outline=acc,width=14)
    for i in range(9):
        x=random.randint(70,470); y=random.randint(70,470); r=random.randint(30,70)
        for k in range(5):
            a=k*72+random.randint(0,30)
            d.ellipse((x+r*math.cos(math.radians(a))-r//2, y+r*math.sin(math.radians(a))-r//4, x+r*math.cos(math.radians(a))+r//2, y+r*math.sin(math.radians(a))+r//4), fill=acc)
    tile=tile.rotate(12,resample=Image.BICUBIC,expand=True,fillcolor=(0,0,0))
    mask=tile.convert("L").point(lambda v:255 if v>0 else 0)
    shadow(im,(200,860,800,940)); im.paste(tile,(500-tile.width//2,560-tile.height//2),mask)
    # fold sheen
    sh=Image.new("L",S,0); ImageDraw.Draw(sh).polygon([(300,300),(380,300),(760,840),(680,840)],fill=50); sh=sh.filter(ImageFilter.GaussianBlur(25))
    im.paste((255,255,255),(0,0),sh)

def acc_hoops(c, im):
    shadow(im,(260,840,740,900),60,30)
    for cx in (390,610):
        pts=arc_pts(cx,600,110,110,0,360,120)
        if c=="gold": gold(im,pts,16)
        else: stroke(im,pts,"#7E848B",22); stroke(im,pts,"#D9DDE1",14); stroke(im,pts,"#F7F8F9",4)

def acc_pearls(c, im):
    shadow(im,(280,860,720,910),60,30); d=ImageDraw.Draw(im)
    for cx in (400,600):
        gold(im,[(cx,360),(cx,520)],5); d.ellipse((cx-22,330,cx+22,374),fill=hexrgb("#D2AE62"))
        for (r,y,col) in [(70,600,"#F4EFE7")]:
            m=Image.new("L",im.size,0); ImageDraw.Draw(m).ellipse((cx-r,y-r,cx+r,y+r),fill=255); shaded(im,m,col,0.4,0.25)
            hl=Image.new("L",im.size,0); ImageDraw.Draw(hl).ellipse((cx-40,y-45,cx-5,y-15),fill=140); hl=hl.filter(ImageFilter.GaussianBlur(8)); im.paste((255,255,255),(0,0),hl)

def acc_clip(c, im):
    S=im.size; shadow(im,(260,780,760,870),70,30)
    m=Image.new("L",S,0); dd=ImageDraw.Draw(m)
    for i in range(5):
        x=310+i*95; dd.rounded_rectangle((x,380,x+70,800),35,fill=255)
    dd.rounded_rectangle((290,520,730,640),40,fill=255)
    base=Image.new("RGB",S,hexrgb(c)); d=ImageDraw.Draw(base); random.seed(4)
    for i in range(120):
        x=random.randint(280,740); y=random.randint(370,810); r=random.randint(10,40)
        d.ellipse((x-r,y-r,x+r,y+r),fill=mix(hexrgb(c),(40,20,5),random.random()*0.6))
    base=base.filter(ImageFilter.GaussianBlur(6)); im.paste(base,(0,0),m)
    gl=Image.new("L",S,0); ImageDraw.Draw(gl).rectangle((300,380,380,800),fill=60); gl=gl.filter(ImageFilter.GaussianBlur(20)); im.paste((255,255,255),(0,0),ImageChops.multiply(gl,m))

def acc_bracelet(c, im):
    shadow(im,(250,800,750,880),70,30)
    pts=arc_pts(500,620,230,140,0,360,140)
    gold(im,pts,10) if c=="gold" else (stroke(im,pts,"#7E848B",14),stroke(im,pts,"#E3E6EA",8))
    S=im.size
    for k,a in enumerate((60,90,120)):
        x=500+230*math.cos(math.radians(a)); y=620+140*math.sin(math.radians(a))
        m=poly_mask(S,[(x,y),(x-28,y+50),(x,y+95),(x+28,y+50)],3); shaded(im,m,"#D2AE62" if c=="gold" else "#C9CDD2",0.4,0.3)

# ───────────── dresses ─────────────
def hanger(im):
    gold(im,[(500,150),(500,120)],4); gold(im,arc_pts(500,110,22,22,180,450,30),4)
    stroke(im,[(330,240),(500,160),(670,240)],"#6B5B4B",8)

def dress(im, c, pts, details=None, angle="x"):
    S=im.size; hanger(im); m=poly_mask(S,pts,10); shadow(im,(260,1080,740,1150),55,30); shaded(im,m,c,0.2,0.3,angle)
    if details: details(im, m)

def dr_midi(c,im):
    def det(im,m):
        stroke(im,[(420,470),(580,470)],mix(hexrgb(c),(0,0,0),0.3),6)
        for x in range(430,580,30): stroke(im,[(x,480),(x-40+(x-500)//2,1000)],mix(hexrgb(c),(0,0,0),0.12),2)
    dress(im,c,[(400,230),(450,240),(500,300),(550,240),(600,230),(640,330),(590,470),(700,1010),(300,1010),(410,470),(360,330)],det)

def dr_shirt(c,im):
    def det(im,m):
        d=ImageDraw.Draw(im)
        stroke(im,[(500,280),(500,990)],mix(hexrgb(c),(0,0,0),0.2),3)
        for y in range(330,960,70): d.ellipse((492,y,508,y+16),fill=(245,240,232))
        stroke(im,[(390,560),(610,560)],mix(hexrgb(c),(0,0,0),0.35),14)
        stroke(im,[(440,240),(500,300),(560,240)],(250,248,244),6)
    dress(im,c,[(390,230),(440,230),(500,290),(560,230),(610,230),(720,330),(690,420),(640,380),(650,990),(350,990),(360,380),(310,420),(280,330)],det)

def dr_wrap(c,im):
    def det(im,m):
        stroke(im,[(420,250),(560,560)],mix(hexrgb(c),(0,0,0),0.3),4); stroke(im,[(580,250),(470,520)],mix(hexrgb(c),(255,255,255),0.2),3)
        stroke(im,[(400,560),(620,540),(660,700)],mix(hexrgb(c),(0,0,0),0.35),10)
    dress(im,c,[(410,230),(460,235),(500,330),(540,235),(590,230),(650,320),(600,560),(730,1060),(270,1060),(400,560),(350,320)],det)

def dr_maxi(c,im):
    def det(im,m):
        for y,wid in [(600,190),(800,240),(980,290)]: stroke(im,[(500-wid,y),(500+wid,y)],mix(hexrgb(c),(0,0,0),0.18),4)
    dress(im,c,[(420,240),(440,240),(500,330),(560,240),(580,240),(600,430),(690,600),(740,800),(800,1070),(200,1070),(260,800),(310,600),(400,430)],det)

def dr_slip(c,im):
    def det(im,m):
        stroke(im,[(430,230),(440,330)],mix(hexrgb(c),(0,0,0),0.4),3); stroke(im,[(570,230),(560,330)],mix(hexrgb(c),(0,0,0),0.4),3)
        for x in range(380,640,22): stroke(im,[(x,520),(x+(x-500)//3,1030)],mix(hexrgb(c),(255,255,255),0.22) if (x//22)%2 else mix(hexrgb(c),(0,0,0),0.15),3)
    dress(im,c,[(440,330),(500,350),(560,330),(590,520),(690,1040),(310,1040),(410,520)],det)

def dr_linen(c,im):
    def det(im,m):
        d=ImageDraw.Draw(im)
        d.rectangle((560,640,630,720),outline=mix(hexrgb(c),(0,0,0),0.25),width=3)
        stroke(im,[(450,250),(500,290),(550,250)],mix(hexrgb(c),(0,0,0),0.3),4)
    dress(im,c,[(400,235),(450,240),(500,290),(550,240),(600,235),(700,320),(660,380),(620,350),(660,980),(340,980),(380,350),(340,380),(300,320)],det)

KINDS = {
 "shoulder":bag_shoulder,"tote":bag_tote,"crossbody":bag_crossbody,"hobo":bag_hobo,"clutch":bag_clutch,"bucket":bag_bucket,
 "necklace":acc_necklace,"scarf":acc_scarf,"hoops":acc_hoops,"pearls":acc_pearls,"clip":acc_clip,"bracelet":acc_bracelet,
 "midi":dr_midi,"shirt":dr_shirt,"wrap":dr_wrap,"maxi":dr_maxi,"slip":dr_slip,"linen":dr_linen,
}
BGS=["#EFE6DA","#ECE3D9","#E9E1D6","#F1E9DF"]

def render(kind, colour, name, bg_i=0):
    im=backdrop(BGS[bg_i%len(BGS)]); KINDS[kind](colour, im)
    im=im.filter(ImageFilter.SMOOTH)
    im.save(f"{OUT}/{name}-1.jpg",quality=86,optimize=True,progressive=True)
    # detail view: tighter crop on the object
    bx=(150,250,850,1085) if kind not in ("midi","shirt","wrap","maxi","slip","linen") else (250,180,750,776)
    det=im.crop(bx).resize((W,H),Image.LANCZOS); det=backdrop(BGS[(bg_i+1)%4]).copy() if False else det
    det.save(f"{OUT}/{name}-2.jpg",quality=86,optimize=True,progressive=True)

def scene(name, items, w=1600, h=1000, bg="#E9DFD2"):
    """Editorial still life: product renders side by side, each scaled to the canvas height and centre-cropped."""
    canvas=backdrop(bg,0.8,w,h)
    n=len(items); slot=w//n
    for i,(kind,col) in enumerate(items):
        tile=backdrop(bg,0.8); KINDS[kind](col,tile)
        sc=h/H; tile=tile.resize((int(W*sc),h),Image.LANCZOS)
        left=(tile.width-slot)//2
        canvas.paste(tile.crop((left,0,left+slot,h)),(i*slot,0))
    canvas.save(f"{OUT}/{name}.jpg",quality=85,optimize=True,progressive=True)

if __name__=="__main__":
    import json, sys
    spec=json.load(open(sys.argv[1]))
    for i,r in enumerate(spec["renders"]): render(r["kind"], r["colour"], r["name"], i)
    for s in spec.get("scenes",[]): scene(s["name"], [tuple(x) for x in s["items"]], s.get("w",1600), s.get("h",1000), s.get("bg","#E9DFD2"))
    print("rendered", len(spec["renders"]), "products and", len(spec.get("scenes",[])), "scenes")
