from __future__ import annotations

import math
import os
import random
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable

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


ROOT = Path(__file__).resolve().parents[1]
SOURCE = Path(r"C:\Users\conta\OneDrive\Desktop\cardapio PDF")
OUT = ROOT / "output" / "premium_ads"

W, H = 1080, 1350
FONT_DIR = Path(r"C:\Windows\Fonts")


@dataclass(frozen=True)
class AdSpec:
    slug: str
    file: str
    crop: tuple[int, int, int, int]
    bg_key: str
    title: str
    subtitle: str
    benefits: tuple[str, ...]
    seal: str
    cta: str
    accent: tuple[int, int, int]
    gold: tuple[int, int, int]
    product_max: tuple[int, int]
    product_xy: tuple[int, int]
    bg_label: str


SPECS = [
    AdSpec(
        "01_artesanais_qnb",
        "01.png",
        (2050, 930, 2470, 1365),
        "white",
        "ARTESANAIS QNB",
        "Hamburgueres altos, suculentos e feitos para impressionar.",
        ("Pao brioche", "Molho especial", "Carne artesanal"),
        "PREMIUM",
        "PEÇA SEU FAVORITO",
        (237, 42, 48),
        (244, 183, 69),
        (650, 610),
        (575, 720),
        "burger",
    ),
    AdSpec(
        "02_combos_artesanal",
        "02.png",
        (0, 70, 1080, 290),
        "white",
        "COMBOS ARTESANAL",
        "Burger, batata e bebida em composicoes prontas para vender mais.",
        ("Combo completo", "Sabor + praticidade", "Visual irresistivel"),
        "OFERTA",
        "MONTE SEU COMBO",
        (236, 45, 48),
        (255, 197, 70),
        (800, 390),
        (540, 715),
        "combo",
    ),
    AdSpec(
        "03_mundo_da_batata",
        "03.png",
        (55, 130, 395, 255),
        "white",
        "BATATAS CROCANTES",
        "Porcoes douradas, crocantes e perfeitas para acompanhar o pedido.",
        ("Crocancia maxima", "Porcoes generosas", "Cheddar e bacon"),
        "ADD-ON",
        "ADICIONE AO PEDIDO",
        (255, 132, 28),
        (255, 214, 74),
        (720, 440),
        (550, 725),
        "fries",
    ),
    AdSpec(
        "04_tradicional",
        "04.png",
        (1000, 890, 1350, 1220),
        "white",
        "TRADICIONAL QNB",
        "Classicos de lanchonete com aquele sabor que todo mundo conhece.",
        ("A partir de R$ 8,90", "Molho da casa", "Feito no capricho"),
        "CLASSICO",
        "PEÇA O SEU",
        (238, 46, 52),
        (246, 192, 67),
        (630, 560),
        (565, 720),
        "traditional",
    ),
    AdSpec(
        "05_mega_sanduiche",
        "05.png",
        (870, 2030, 1430, 2860),
        "white",
        "MEGA SANDUICHE",
        "Fome grande pede camadas generosas e presenca de respeito.",
        ("Muito recheio", "Camadas altas", "Impacto no primeiro olhar"),
        "GIGANTE",
        "GARANTA O SEU",
        (237, 42, 48),
        (250, 188, 69),
        (650, 760),
        (590, 745),
        "mega",
    ),
    AdSpec(
        "06_combos_tradicional",
        "06.png",
        (1420, 1870, 2580, 2280),
        "white",
        "COMBOS TRADICIONAL",
        "Combos prontos para pedir rapido e matar a fome sem pensar duas vezes.",
        ("Burger + bebida", "Preco de combo", "Ideal para grupos"),
        "APENAS",
        "CHAME A GALERA",
        (238, 45, 49),
        (255, 205, 82),
        (790, 500),
        (565, 735),
        "combo-trad",
    ),
    AdSpec(
        "07_espaguete_ao_forno",
        "07.png",
        (875, 2910, 1430, 3320),
        "white",
        "ESPAGUETE AO FORNO",
        "Cremoso, gratinado e servido bem quente para uma refeicao completa.",
        ("650g ou 950g", "Queijo derretido", "Molho a bolonhesa"),
        "FORNO",
        "ESCOLHA SEU SABOR",
        (226, 42, 48),
        (247, 196, 80),
        (720, 520),
        (560, 750),
        "pasta",
    ),
    AdSpec(
        "08_bebidas_geladas",
        "08.png",
        (90, 2450, 2500, 3400),
        "white",
        "BEBIDAS GELADAS",
        "O par perfeito para fechar o lanche com mais desejo e refrescancia.",
        ("Refrigerantes", "Sucos naturais", "Cervejas e aguas"),
        "GELADA",
        "COMPLETE O PEDIDO",
        (41, 156, 221),
        (255, 199, 74),
        (900, 520),
        (535, 770),
        "drinks",
    ),
]


def font(name: str, size: int) -> ImageFont.FreeTypeFont:
    return ImageFont.truetype(str(FONT_DIR / name), size=size)


F_TITLE = font("impact.ttf", 84)
F_TITLE_SMALL = font("impact.ttf", 70)
F_SUB = font("bahnschrift.ttf", 32)
F_BODY = font("bahnschrift.ttf", 29)
F_BODY_BOLD = font("arialbd.ttf", 27)
F_CTA = font("arialbd.ttf", 31)
F_SEAL = font("arialbd.ttf", 25)
F_LOGO = font("impact.ttf", 34)


def fit_impact(text: str, max_width: int, start_size: int = 84, min_size: int = 48) -> ImageFont.FreeTypeFont:
    probe = ImageDraw.Draw(Image.new("RGB", (1, 1)))
    for size in range(start_size, min_size - 1, -2):
        fnt = font("impact.ttf", size)
        if probe.textbbox((0, 0), text, font=fnt, stroke_width=3)[2] <= max_width:
            return fnt
    return font("impact.ttf", min_size)


def lerp(a: int, b: int, t: float) -> int:
    return int(a + (b - a) * t)


def gradient_bg(spec: AdSpec) -> Image.Image:
    rng = random.Random(spec.slug)
    arr = np.zeros((H, W, 3), dtype=np.uint8)
    top = np.array((9, 9, 9), dtype=np.float32)
    mid = np.array((24, 20, 18), dtype=np.float32)
    accent = np.array(spec.accent, dtype=np.float32)
    for y in range(H):
        t = y / (H - 1)
        base = top * (1 - min(t * 1.45, 1)) + mid * min(t * 1.45, 1)
        warmth = accent * (0.10 * math.sin(t * math.pi))
        arr[y, :, :] = np.clip(base + warmth, 0, 255)

    noise = rng.normalvariate if False else None
    grain = np.random.default_rng(abs(hash(spec.slug)) % 2**32).normal(0, 5, (H, W, 1))
    arr = np.clip(arr.astype(np.float32) + grain, 0, 255).astype(np.uint8)
    img = Image.fromarray(arr, "RGB").convert("RGBA")
    draw = ImageDraw.Draw(img, "RGBA")

    # Soft studio light beams and premium counter surface.
    draw.polygon([(0, 0), (W * 0.62, 0), (W * 0.32, H)], fill=(255, 255, 255, 13))
    draw.polygon([(W, 70), (W, H), (W * 0.55, H)], fill=(*spec.accent, 24))
    draw.rectangle((0, 890, W, H), fill=(18, 13, 11, 225))
    for i in range(18):
        y = 910 + i * 26 + rng.randint(-5, 5)
        draw.line((0, y, W, y + rng.randint(-18, 18)), fill=(255, 255, 255, rng.randint(8, 18)), width=1)
    draw.rectangle((0, 888, W, 908), fill=(*spec.gold, 55))
    draw.rectangle((0, 908, W, 913), fill=(255, 255, 255, 22))

    # Vignette.
    vignette = Image.new("L", (W, H), 0)
    vd = ImageDraw.Draw(vignette)
    for r in range(900, 80, -22):
        alpha = int(170 * (1 - r / 900) ** 1.7)
        vd.ellipse((W // 2 - r, H // 2 - r, W // 2 + r, H // 2 + r), fill=alpha)
    inv = ImageOps.invert(vignette)
    dark = Image.new("RGBA", (W, H), (0, 0, 0, 165))
    img = Image.composite(img, Image.alpha_composite(img, dark), inv)
    return img


def wrap_text(text: str, fnt: ImageFont.FreeTypeFont, max_width: int) -> list[str]:
    words = text.split()
    lines: list[str] = []
    line = ""
    probe = ImageDraw.Draw(Image.new("RGB", (1, 1)))
    for word in words:
        candidate = word if not line else f"{line} {word}"
        if probe.textbbox((0, 0), candidate, font=fnt)[2] <= max_width:
            line = candidate
        else:
            if line:
                lines.append(line)
            line = word
    if line:
        lines.append(line)
    return lines


def draw_text_block(
    draw: ImageDraw.ImageDraw,
    xy: tuple[int, int],
    lines: Iterable[str],
    fnt: ImageFont.FreeTypeFont,
    fill: tuple[int, int, int, int],
    line_gap: int = 8,
) -> int:
    x, y = xy
    for line in lines:
        draw.text((x, y), line, font=fnt, fill=fill)
        y += draw.textbbox((x, y), line, font=fnt)[3] - draw.textbbox((x, y), line, font=fnt)[1] + line_gap
    return y


def extract_product(path: Path, box: tuple[int, int, int, int], key: str, max_size: tuple[int, int]) -> Image.Image:
    src = Image.open(path).convert("RGBA")
    crop = src.crop(box)
    rgb = np.array(crop.convert("RGB")).astype(np.int16)
    if key == "black":
        dist = np.sqrt(np.sum(rgb.astype(np.float32) ** 2, axis=2))
    else:
        dist = np.sqrt(np.sum((255 - rgb).astype(np.float32) ** 2, axis=2))
    alpha = np.clip((dist - 28) * 5.0, 0, 255).astype(np.uint8)

    # Keep white highlights inside food by preserving pixels with enough saturation.
    mx = rgb.max(axis=2)
    mn = rgb.min(axis=2)
    sat = mx - mn
    alpha = np.where((sat > 34) & (mx > 80), np.maximum(alpha, 230), alpha).astype(np.uint8)

    alpha = remove_small_alpha_components(alpha, min_area=1400)
    a = Image.fromarray(alpha, "L").filter(ImageFilter.GaussianBlur(0.65))
    crop.putalpha(a)
    bbox = crop.getbbox()
    if bbox:
        crop = crop.crop(bbox)
    crop.thumbnail(max_size, Image.Resampling.LANCZOS)
    return crop


def remove_small_alpha_components(alpha: np.ndarray, min_area: int) -> np.ndarray:
    mask = alpha > 42
    h, w = mask.shape
    visited = np.zeros(mask.shape, dtype=bool)
    keep = np.zeros(mask.shape, dtype=bool)
    neighbors = (-w - 1, -w, -w + 1, -1, 1, w - 1, w, w + 1)
    flat = mask.ravel()
    seen = visited.ravel()
    kept = keep.ravel()
    total = flat.size

    for start in np.flatnonzero(flat):
        if seen[start]:
            continue
        stack = [int(start)]
        seen[start] = True
        comp: list[int] = []
        while stack:
            idx = stack.pop()
            comp.append(idx)
            x = idx % w
            for step in neighbors:
                ni = idx + step
                if ni < 0 or ni >= total or seen[ni] or not flat[ni]:
                    continue
                nx = ni % w
                if abs(nx - x) > 1:
                    continue
                seen[ni] = True
                stack.append(ni)
        if len(comp) >= min_area:
            kept[comp] = True

    cleaned = np.where(keep, alpha, 0).astype(np.uint8)
    return cleaned


def paste_with_shadow(base: Image.Image, product: Image.Image, center: tuple[int, int]) -> None:
    x = int(center[0] - product.width / 2)
    y = int(center[1] - product.height / 2)
    alpha = product.getchannel("A")
    shadow = Image.new("RGBA", product.size, (0, 0, 0, 0))
    shadow.putalpha(alpha.filter(ImageFilter.GaussianBlur(18)))
    shadow = ImageOps.colorize(shadow.getchannel("A"), (0, 0, 0), (0, 0, 0)).convert("RGBA")
    shadow.putalpha(alpha.filter(ImageFilter.GaussianBlur(18)).point(lambda p: int(p * 0.55)))
    base.alpha_composite(shadow, (x + 18, y + 30))

    # Reflection on the premium surface.
    refl = ImageOps.flip(product)
    refl_alpha = refl.getchannel("A")
    fade = Image.new("L", refl.size, 0)
    fd = ImageDraw.Draw(fade)
    for yy in range(refl.height):
        fd.line((0, yy, refl.width, yy), fill=max(0, 72 - int(yy * 0.22)))
    refl.putalpha(ImageChops_multiply(refl_alpha, fade))
    refl = refl.filter(ImageFilter.GaussianBlur(1.1))
    base.alpha_composite(refl, (x, y + product.height - 20))
    base.alpha_composite(product, (x, y))


def ImageChops_multiply(a: Image.Image, b: Image.Image) -> Image.Image:
    aa = np.array(a).astype(np.uint16)
    bb = np.array(b).astype(np.uint16)
    return Image.fromarray(((aa * bb) // 255).astype(np.uint8), "L")


def draw_logo(draw: ImageDraw.ImageDraw, x: int, y: int, spec: AdSpec) -> None:
    draw.ellipse((x, y, x + 78, y + 78), fill=(167, 20, 19, 255), outline=spec.gold + (255,), width=3)
    draw.text((x + 14, y + 17), "QNB", font=F_LOGO, fill=spec.gold + (255,), stroke_width=1, stroke_fill=(90, 0, 0, 255))


def draw_chip(draw: ImageDraw.ImageDraw, x: int, y: int, text: str, spec: AdSpec) -> int:
    pad_x, pad_y = 18, 10
    bbox = draw.textbbox((0, 0), text, font=F_BODY_BOLD)
    w = bbox[2] - bbox[0] + pad_x * 2
    h = bbox[3] - bbox[1] + pad_y * 2
    draw.rounded_rectangle((x, y, x + w, y + h), radius=18, fill=(0, 0, 0, 132), outline=spec.gold + (190,), width=2)
    draw.text((x + pad_x, y + pad_y - 2), text, font=F_BODY_BOLD, fill=(255, 255, 255, 245))
    return y + h + 14


def draw_price_seal(draw: ImageDraw.ImageDraw, spec: AdSpec) -> None:
    x, y = 765, 150
    draw.ellipse((x - 12, y - 12, x + 176, y + 176), fill=spec.accent + (242,), outline=spec.gold + (235,), width=4)
    draw.text((x + 28, y + 28), spec.seal, font=F_SEAL, fill=(255, 255, 255, 255))
    draw.text((x + 31, y + 74), "QNB", font=font("impact.ttf", 54), fill=spec.gold + (255,))


def render(spec: AdSpec) -> Path:
    img = gradient_bg(spec)
    product = extract_product(SOURCE / spec.file, spec.crop, spec.bg_key, spec.product_max)
    draw = ImageDraw.Draw(img, "RGBA")

    draw_logo(draw, 54, 48, spec)
    title_font = fit_impact(spec.title, 690)
    draw.text((54, 143), spec.title, font=title_font, fill=(255, 255, 255, 255), stroke_width=3, stroke_fill=(0, 0, 0, 210))
    draw.rectangle((56, 238, 118 + min(620, len(spec.title) * 31), 248), fill=spec.accent + (245,))

    subtitle_lines = wrap_text(spec.subtitle, F_SUB, 575)
    y = draw_text_block(draw, (58, 276), subtitle_lines, F_SUB, (36, 30, 28, 245), 9)

    y += 28
    for benefit in spec.benefits:
        y = draw_chip(draw, 58, y, benefit, spec)

    draw_price_seal(draw, spec)

    paste_with_shadow(img, product, spec.product_xy)
    draw = ImageDraw.Draw(img, "RGBA")

    # CTA bar.
    cta_y = 1194
    draw.rounded_rectangle((54, cta_y, 690, cta_y + 82), radius=18, fill=spec.accent + (246,))
    draw.rectangle((84, cta_y + 78, 660, cta_y + 82), fill=spec.gold + (230,))
    draw.text((92, cta_y + 21), spec.cta, font=F_CTA, fill=(255, 255, 255, 255))
    draw.text((720, cta_y + 12), "Anuncio premium\npara venda online", font=font("bahnschrift.ttf", 27), fill=(255, 255, 255, 190), spacing=4)

    # Small quality marks.
    qy = 1050
    for i, label in enumerate(("FOTO REAL", "FUNDO NOVO", "ALTO DESEJO")):
        xx = 60 + i * 210
        draw.rounded_rectangle((xx, qy, xx + 178, qy + 44), radius=10, fill=(0, 0, 0, 110), outline=(255, 255, 255, 55), width=1)
        draw.text((xx + 15, qy + 11), label, font=font("arialbd.ttf", 18), fill=spec.gold + (245,))

    out = OUT / f"{spec.slug}.png"
    img.convert("RGB").save(out, quality=95)
    return out


def main() -> None:
    OUT.mkdir(parents=True, exist_ok=True)
    for spec in SPECS:
        print(render(spec))


if __name__ == "__main__":
    main()
