#!/usr/bin/env python3
"""
Tatiana's final embroidered patch composite.
Exact placement: just below the American flag on the astronaut's left arm.
Full image coords: center ~(750, 760), patch 80x62px
"""

from PIL import Image, ImageDraw, ImageFilter
import numpy as np

np.random.seed(42)

# Load
base = Image.open("v1-03-mid-full-body.png").convert("RGBA")
logo_raw = Image.open("ff-logo-mark.png").convert("RGBA")

print(f"Base: {base.size}, Logo: {logo_raw.size}")

base_rgb = np.array(base.convert("RGB"), dtype=np.float32)

# ============================================================
# PLACEMENT — just below the flag, left upper arm
# ============================================================
PATCH_W = 76
PATCH_H = 59
PASTE_X = 712   # left edge
PASTE_Y = 740   # top edge, just below flag

# Get local lighting from the suit surface
arm_region = base_rgb[PASTE_Y:PASTE_Y+PATCH_H, PASTE_X:PASTE_X+PATCH_W]
arm_lum = (arm_region[:,:,0]*0.299 + arm_region[:,:,1]*0.587 + arm_region[:,:,2]*0.114)
avg_lum = arm_lum.mean()
print(f"Local arm luminance: avg={avg_lum:.1f}/255 ({avg_lum/255*100:.0f}%)")

# ============================================================
# PATCH FABRIC
# Off-white woven fabric — its own material, not matching background
# Modulated by local lighting so it reads as physically lit
# ============================================================

# Base fabric color: warm off-white
# Scale by local luminance (darker zone = patch appears darker, like it's in shadow)
lum_factor = np.clip(avg_lum / 180.0, 0.6, 1.1)  # normalize to 0.6–1.1
fab_r = int(np.clip(232 * lum_factor, 120, 240))
fab_g = int(np.clip(226 * lum_factor, 115, 235))
fab_b = int(np.clip(215 * lum_factor, 108, 225))
print(f"Patch fabric color: ({fab_r}, {fab_g}, {fab_b}) — lum_factor={lum_factor:.2f}")

patch = Image.new("RGBA", (PATCH_W, PATCH_H), (fab_r, fab_g, fab_b, 255))
patch_arr = np.array(patch, dtype=np.float32)

# Micro-texture (fabric weave)
noise1 = np.random.normal(0, 4, (PATCH_H, PATCH_W, 3))
noise2_small = np.random.normal(0, 2.5, ((PATCH_H+3)//4, (PATCH_W+3)//4, 3))
noise2 = np.kron(noise2_small, np.ones((4,4,1)))[:PATCH_H,:PATCH_W,:]
patch_arr[:,:,:3] = np.clip(patch_arr[:,:,:3] + noise1 + noise2, 0, 255)

# Apply suit's lighting gradient onto patch
# This is the key to making it look real: patch receives same light as its surface
arm_lum_norm = arm_lum / 255.0
# Subtle: 30% lighting influence (keeps it from going too dark)
light_blend = 0.70 + 0.55 * arm_lum_norm  # range ~0.70 to ~1.25
light_blend = np.clip(light_blend, 0.55, 1.15)[:,:,np.newaxis]
patch_arr[:,:,:3] = np.clip(patch_arr[:,:,:3] * light_blend, 0, 255)

# Edge darkening (patch lift shadow)
for d in range(5):
    fade = 1.0 - (5 - d) * 0.02
    patch_arr[d,:,:3] *= fade
    patch_arr[PATCH_H-1-d,:,:3] *= fade
    patch_arr[:,d,:3] *= fade
    patch_arr[:,PATCH_W-1-d,:3] *= fade

patch_arr[:,:,3] = 255
patch = Image.fromarray(np.clip(patch_arr, 0, 255).astype(np.uint8), "RGBA")

# ============================================================
# LOGO → EMBROIDERY LOOK
# ============================================================
logo_area_w = PATCH_W - 20
logo_area_h = PATCH_H - 16
logo_copy = logo_raw.copy()
logo_copy.thumbnail((logo_area_w, logo_area_h), Image.LANCZOS)
lw, lh = logo_copy.size
print(f"Logo: {lw}x{lh} on patch {PATCH_W}x{PATCH_H}")

logo_arr = np.array(logo_copy, dtype=np.float32)
r, g, b, a = logo_arr[:,:,0], logo_arr[:,:,1], logo_arr[:,:,2], logo_arr[:,:,3]
lum_logo = r*0.299 + g*0.587 + b*0.114

# Dark + opaque = logo mark pixels
logo_px = (lum_logo < 130) & (a > 40)

# Build embroidery
emb = np.zeros((lh, lw, 4), dtype=np.float32)

# Thread base: very dark charcoal (18, 14, 9)
# With horizontal thread sheen (satin stitch)
y_idx = np.arange(lh)[:, np.newaxis]
x_idx = np.arange(lw)[np.newaxis, :]
sheen = np.sin(y_idx * 2.8) * 9 + np.sin(x_idx * 0.8) * 3
thread_noise = np.random.normal(0, 1.5, (lh, lw))

emb[:,:,0] = logo_px * np.clip(18 + sheen * 0.6 + thread_noise, 0, 60)
emb[:,:,1] = logo_px * np.clip(14 + sheen * 0.5 + thread_noise, 0, 50)
emb[:,:,2] = logo_px * np.clip(9  + sheen * 0.4 + thread_noise, 0, 40)
emb[:,:,3] = logo_px * 238  # mostly opaque

emb_img = Image.fromarray(np.clip(emb, 0, 255).astype(np.uint8), "RGBA")
emb_img = emb_img.filter(ImageFilter.GaussianBlur(0.4))  # slight blur = thread softness

# Center on patch
lx = (PATCH_W - lw) // 2
ly = (PATCH_H - lh) // 2
patch.paste(emb_img, (lx, ly), emb_img)

# ============================================================
# STITCH BORDER — hand-sewn feel
# ============================================================
draw = ImageDraw.Draw(patch)
bm = 5  # border margin

thread_color = (28, 22, 14, 215)
thread_light = (55, 46, 30, 155)

def stitches(draw, x1, y1, x2, y2, clr, orient, slen=3, sgap=3, w=1):
    if orient == 'h':
        for x in range(x1, x2, slen + sgap):
            draw.line([(x, y1), (min(x+slen, x2), y1)], fill=clr, width=w)
    else:
        for y in range(y1, y2, slen + sgap):
            draw.line([(x1, y), (x1, min(y+slen, y2))], fill=clr, width=w)

# Outer border
stitches(draw, bm,    bm,             PATCH_W-bm, bm,             thread_color, 'h')
stitches(draw, bm,    PATCH_H-bm-1,   PATCH_W-bm, PATCH_H-bm-1,  thread_color, 'h')
stitches(draw, bm,    bm,             bm,          PATCH_H-bm,    thread_color, 'v')
stitches(draw, PATCH_W-bm-1, bm,     PATCH_W-bm-1, PATCH_H-bm,   thread_color, 'v')

# Inner border (double stitch)
ib = bm + 4
stitches(draw, ib,    ib,             PATCH_W-ib, ib,             thread_light, 'h', slen=2, sgap=2)
stitches(draw, ib,    PATCH_H-ib-1,   PATCH_W-ib, PATCH_H-ib-1,  thread_light, 'h', slen=2, sgap=2)
stitches(draw, ib,    ib,             ib,          PATCH_H-ib,    thread_light, 'v', slen=2, sgap=2)
stitches(draw, PATCH_W-ib-1, ib,     PATCH_W-ib-1, PATCH_H-ib,   thread_light, 'v', slen=2, sgap=2)

# Final micro-blur to integrate patch (removes that "crisp cutout" look)
patch = patch.filter(ImageFilter.GaussianBlur(0.35))

# ============================================================
# COMPOSITE
# ============================================================
result = base.copy()

# --- Drop shadow (upper-left light source → shadow goes lower-right) ---
sh_layer = Image.new("RGBA", base.size, (0,0,0,0))
for i in range(7):
    alpha = max(0, 38 - i * 6)
    offset = i
    sh = Image.new("RGBA", (PATCH_W, PATCH_H), (4, 4, 4, alpha))
    sh_layer.paste(sh, (PASTE_X + offset, PASTE_Y + offset), sh)
sh_blur = Image.new("RGBA", base.size, (0,0,0,0))
# Use a blurred version of the shadow
sh_arr = np.array(sh_layer, dtype=np.float32)
sh_layer_final = Image.fromarray(sh_arr.astype(np.uint8), "RGBA").filter(ImageFilter.GaussianBlur(2))
result = Image.alpha_composite(result, sh_layer_final)

# --- Patch ---
pl = Image.new("RGBA", base.size, (0,0,0,0))
pl.paste(patch, (PASTE_X, PASTE_Y), patch)
result = Image.alpha_composite(result, pl)

# ============================================================
# SAVE
# ============================================================
result_rgb = result.convert("RGB")
result_rgb.save("tatiana-patch-v2.jpg", quality=96)
print("✓ Saved: tatiana-patch-v2.jpg")

# Wide context view
ctx = result_rgb.crop((600, 620, 900, 900))
ctx.save("tatiana-patch-v2-context.jpg", quality=95)
print(f"✓ Context: tatiana-patch-v2-context.jpg")

# 4x zoom of just the patch
zoom_px = result_rgb.crop((PASTE_X - 15, PASTE_Y - 15, PASTE_X + PATCH_W + 15, PASTE_Y + PATCH_H + 15))
zoom = zoom_px.resize((zoom_px.width * 4, zoom_px.height * 4), Image.LANCZOS)
zoom.save("tatiana-patch-v2-zoom.jpg", quality=95)
print(f"✓ Zoom: tatiana-patch-v2-zoom.jpg")
