"""PROTOTYP — wegwerfen. Variante 2: Das Modell färbt nur um (und entfernt auf der
Seite das EV-Logo). Der Code richtet das Ergebnis am Rahmen der Vorlage aus und setzt
das Originallogo an die Stelle des EV-Logos. Ergebnis in gen2/, danach bericht.html neu."""
import base64
import io
import json
import os
import sys
from concurrent.futures import ThreadPoolExecutor

import numpy as np
import requests
from PIL import Image, ImageChops, ImageDraw, ImageFilter
from scipy import ndimage

from bilder import ANSICHTEN, GLEICH, HIER, MODELL
from lauf import KEY, bericht

# größte erlaubte Logobreite als Vielfaches der EV-Logobreite (Platz zwischen den Streben)
BREITE_MAX = {
    "absetzcontainer-grau-5cbm-seitenansicht-2": 1.9,
    "markdown-grauer-absetzcontainer-seitenansicht-2": 1.7,
    "absetzcontainer-grau-10cbm-seitlich-2": 1.6,
    "ev-abrollcontainer-grau-12cbm-2": 4.0,
    "grauer-40cbm-abrollcontainer-seitenansicht-2": 2.0,
}
TOLERANZ = 0.03  # Abweichung des Seitenverhältnisses, ab der neu erzeugt wird


def vorlage(datei):
    return Image.open(os.path.join(HIER, "ev", datei + ".webp")).convert("RGBA")


def rahmen(im):
    """Umriss des Containers: Pixel mit deutlicher Deckung (Schatten fällt weg)."""
    a = np.array(im.getchannel("A")) > 128
    ys, xs = np.where(a)
    return xs.min(), ys.min(), xs.max() + 1, ys.max() + 1


def ev_logo_box(im):
    a = np.array(im).astype(int)
    r, g, b, al = a[..., 0], a[..., 1], a[..., 2], a[..., 3]
    weiss = (r > 215) & (g > 215) & (b > 215) & (al > 250)
    rot = (r > 150) & (g < 100) & (b < 100)
    kand = weiss & ~ndimage.binary_dilation(rot, iterations=15)
    lab, _ = ndimage.label(ndimage.binary_closing(kand, iterations=4))
    objs = ndimage.find_objects(lab)
    i = int(np.argmax([(lab[o] == k + 1).sum() for k, o in enumerate(objs)]))
    o = objs[i]
    return o[1].start, o[0].start, o[1].stop, o[0].stop


def prompt(farbe, seite):
    if seite:
        return (f"Lackiere den Container in {farbe}. Entferne das weiße runde Logo mit den Pfeilen auf der Seitenwand vollständig, "
                "dort ist danach nur glatte lackierte Wand. Füge kein Logo und keine Schrift hinzu. " + GLEICH)
    return f"Lackiere den Container in {farbe}. Füge kein Logo und keine Schrift hinzu. " + GLEICH


def ki_umfaerben(datei, farbe, seite):
    v = vorlage(datei)
    weiss = Image.new("RGBA", v.size, (255, 255, 255, 255))
    weiss.alpha_composite(v)
    puffer = io.BytesIO()
    weiss.convert("RGB").save(puffer, "PNG")
    r = requests.post("https://api.openai.com/v1/images/edits", timeout=300,
                      headers={"Authorization": f"Bearer {KEY}"},
                      files=[("image[]", (datei + ".png", puffer.getvalue(), "image/png"))],
                      data={"model": MODELL, "prompt": prompt(farbe, seite), "size": "auto", "quality": "auto",
                            "background": "transparent", "output_format": "png"})
    r.raise_for_status()
    return Image.open(io.BytesIO(base64.b64decode(r.json()["data"][0]["b64_json"]))).convert("RGBA")


def ausrichten(ergebnis, v):
    """Skaliert und verschiebt das Ergebnis so, dass sein Umriss auf dem der Vorlage liegt.
    Gibt (Bild, Abweichung des Seitenverhältnisses) zurück."""
    vx0, vy0, vx1, vy1 = rahmen(v)
    ex0, ey0, ex1, ey1 = rahmen(ergebnis)
    sx, sy = (vx1 - vx0) / (ex1 - ex0), (vy1 - vy0) / (ey1 - ey0)
    abweichung = abs(sx / sy - 1)
    teil = ergebnis.crop((ex0, ey0, ex1, ey1)).resize((vx1 - vx0, vy1 - vy0), Image.LANCZOS)
    neu = Image.new("RGBA", v.size, (0, 0, 0, 0))
    neu.alpha_composite(teil, (vx0, vy0))
    # Bodenschatten der Vorlage übernehmen: alles unterhalb/außerhalb des Umrisses
    schatten = v.copy()
    maske = Image.new("L", v.size, 0)
    ImageDraw.Draw(maske).rectangle((vx0, vy0, vx1, vy1), fill=255)
    schatten.putalpha(ImageChops.subtract(v.getchannel("A"), maske))
    grund = Image.new("RGBA", v.size, (0, 0, 0, 0))
    grund.alpha_composite(schatten)
    grund.alpha_composite(neu)
    return grund, abweichung


def logo_vorbereiten(pfad):
    """Beschneidet das Logo auf seinen Inhalt und legt es auf ein weißes Schild,
    wenn es keinen eigenen deckenden Hintergrund hat."""
    lg = Image.open(pfad).convert("RGBA")
    a = np.array(lg).astype(int)
    inhalt = (a[..., 3] > 20) & ~((a[..., 0] > 245) & (a[..., 1] > 245) & (a[..., 2] > 245))
    ys, xs = np.where(inhalt)
    lg = lg.crop((xs.min(), ys.min(), xs.max() + 1, ys.max() + 1))
    a = np.array(lg)
    ecken = [a[0, 0], a[0, -1], a[-1, 0], a[-1, -1]]
    eigener_grund = all(e[3] > 250 for e in ecken) and len({tuple(e[:3] // 16) for e in ecken}) == 1 and not all(e[0] > 240 and e[1] > 240 and e[2] > 240 for e in ecken)
    if eigener_grund:
        return lg, False
    rand = int(max(lg.size) * 0.08)
    schild = Image.new("RGBA", (lg.width + 2 * rand, lg.height + 2 * rand), (255, 255, 255, 255))
    schild.alpha_composite(lg, (rand, rand))
    return schild, True


def logo_einsetzen(bild, box, logo, breite_max):
    x0, y0, x1, y1 = box
    bw, bh = x1 - x0, y1 - y0
    cx, cy = (x0 + x1) / 2, (y0 + y1) / 2
    hoehe_max = bh * 1.15
    faktor = min(hoehe_max / logo.height, bw * breite_max / logo.width)
    w, h = max(round(logo.width * faktor), 1), max(round(logo.height * faktor), 1)
    # das Schild muss die alte EV-Fläche ganz verdecken
    if w < bw * 1.05 or h < bh * 1.05:
        s = max(bw * 1.05 / w, bh * 1.05 / h)
        w, h = round(w * s), round(h * s)
    lg = logo.resize((w, h), Image.LANCZOS)
    radius = max(2, min(w, h) // 10)
    maske = Image.new("L", (w, h), 0)
    ImageDraw.Draw(maske).rounded_rectangle((0, 0, w - 1, h - 1), radius=radius, fill=255)
    lg.putalpha(ImageChops.multiply(lg.getchannel("A"), maske))
    # Licht der Wand übernehmen: Helligkeitsverlauf unter dem Schild relativ zum Mittel
    px, py = round(cx - w / 2), round(cy - h / 2)
    wand = bild.crop((px, py, px + w, py + h)).convert("L").filter(ImageFilter.GaussianBlur(max(w, h) / 6))
    wa = np.array(wand).astype(float)
    licht = np.clip(wa / max(np.median(wa), 1), 0.88, 1.06)
    la = np.array(lg).astype(float)
    la[..., :3] = np.clip(la[..., :3] * licht[..., None], 0, 255)
    lg = Image.fromarray(la.astype(np.uint8), "RGBA")
    # leichter Kantenschatten, damit das Schild aufliegt
    schatten = Image.new("RGBA", (w + 8, h + 8), (0, 0, 0, 0))
    ImageDraw.Draw(schatten).rounded_rectangle((4, 5, w + 3, h + 4), radius=radius, fill=(0, 0, 0, 60))
    bild.alpha_composite(schatten.filter(ImageFilter.GaussianBlur(2)), (px - 4, py - 4))
    bild.alpha_composite(lg, (px, py))
    return bild


def erzeugen(auftrag):
    slug, farbe, datei, seite = auftrag
    ziel = os.path.join(HIER, "gen2", slug, datei + ".png")
    if os.path.exists(ziel):
        return auftrag, {"pfad": ziel}
    v = vorlage(datei)
    versuche = []
    for versuch in range(3):
        try:
            roh = ki_umfaerben(datei, farbe, seite)
        except requests.RequestException as e:
            versuche.append(f"Fehler: {e}")
            continue
        bild, abw = ausrichten(roh.resize(v.size, Image.LANCZOS), v)
        versuche.append(round(float(abw), 3))
        if abw <= TOLERANZ:
            break
    else:
        return auftrag, {"pfad": None, "versuche": versuche}
    if seite:
        logo, schild = logo_vorbereiten(os.path.join(HIER, "gen", slug, "logo.png"))
        bild = logo_einsetzen(bild, ev_logo_box(v), logo, BREITE_MAX[datei])
    bild.save(ziel)
    return auftrag, {"pfad": ziel, "versuche": versuche}


if __name__ == "__main__":
    ergebnisse = json.load(open(os.path.join(HIER, "ergebnis.json")))
    slugs = json.load(open(os.path.join(HIER, "bilder.json")))
    namen = {s: n for n in ergebnisse for s in slugs if s == __import__("re").sub(r"[^a-z0-9]+", "-", n.lower()).strip("-")}
    auftraege = []
    for slug, name in namen.items():
        if len(sys.argv) > 1 and not any(a.lower() in name.lower() for a in sys.argv[1:]):
            continue
        os.makedirs(os.path.join(HIER, "gen2", slug), exist_ok=True)
        auftraege += [(slug, ergebnisse[name]["farbe"], d, seite) for d, _, seite in ANSICHTEN]
    print(f"{len(auftraege)} Bilder")
    pfad = os.path.join(HIER, "bilder2.json")
    ausgabe = json.load(open(pfad)) if os.path.exists(pfad) else {}
    with ThreadPoolExecutor(8) as pool:
        for (slug, _, datei, _), info in pool.map(erzeugen, auftraege):
            print(slug, datei, info, flush=True)
            ausgabe.setdefault(slug, {})[datei] = info
    json.dump(ausgabe, open(pfad, "w"), indent=2)
    open(os.path.join(HIER, "bericht.html"), "w").write(bericht(ergebnisse))
