"""PROTOTYP — wegwerfen. Färbt die grauen EV-Vorlagen ohne KI um: Licht und Schatten
kommen aus der Vorlage, die Farbe wird per Code darunter gelegt. Warnstreifen bleiben."""
import numpy as np
from PIL import Image
from scipy import ndimage


def hex_rgb(h):
    h = h.lstrip("#")
    return np.array([int(h[i:i + 2], 16) for i in (0, 2, 4)], float)


# --- sRGB <-> Lab (D65) ---
def _lin(c):
    c = c / 255
    return np.where(c <= 0.04045, c / 12.92, ((c + 0.055) / 1.055) ** 2.4)


def _gam(c):
    c = np.where(c <= 0.0031308, c * 12.92, 1.055 * np.clip(c, 0, None) ** (1 / 2.4) - 0.055)
    return np.clip(c * 255, 0, 255)


M = np.array([[0.4124, 0.3576, 0.1805], [0.2126, 0.7152, 0.0722], [0.0193, 0.1192, 0.9505]])
W = np.array([0.95047, 1.0, 1.08883])


def rgb_lab(rgb):
    xyz = _lin(rgb) @ M.T / W
    f = np.where(xyz > 0.008856, np.cbrt(xyz), 7.787 * xyz + 16 / 116)
    return np.stack([116 * f[..., 1] - 16, 500 * (f[..., 0] - f[..., 1]), 200 * (f[..., 1] - f[..., 2])], -1)


def lab_rgb(lab):
    fy = (lab[..., 0] + 16) / 116
    f = np.stack([fy + lab[..., 1] / 500, fy, fy - lab[..., 2] / 200], -1)
    xyz = np.where(f ** 3 > 0.008856, f ** 3, (f - 16 / 116) / 7.787) * W
    return _gam(xyz @ np.linalg.inv(M).T)


def kleinstes_rechteck(huelle):
    """Kleinstes gedrehtes Rechteck um eine konvexe Hülle (eine Kante liegt immer auf einer Hüllkante).
    Ein Reflektor ist ein Rechteck; die Hülle allein verliert dunklere weiße Ecken."""
    beste = None
    for i in range(len(huelle)):
        kante = huelle[(i + 1) % len(huelle)] - huelle[i]
        u = kante / np.linalg.norm(kante)
        n = np.array([-u[1], u[0]])
        a, b = huelle @ u, huelle @ n
        flaeche = (a.max() - a.min()) * (b.max() - b.min())
        if beste is None or flaeche < beste[0]:
            beste = (flaeche, u, n, a.min(), a.max(), b.min(), b.max())
    _, u, n, a0, a1, b0, b1 = beste
    return [a0 * u + b0 * n, a1 * u + b0 * n, a1 * u + b1 * n, a0 * u + b1 * n]


def reflektoren(rgba):
    """Form der Warnreflektoren. Jeder Reflektor ist ein konvexes Viereck aus roten und
    hellgrauen Streifen. Erst die roten Streifen eines Reflektors zu einer Gruppe zusammenfassen
    (auf den Seitenansichten liegen sie über 36 px auseinander), dann je Gruppe die konvexe Hülle aus
    roten und hellen Pixeln, davon das kleinste gedrehte Rechteck. Ein achsparalleles Rechteck reicht nicht, die Reflektoren stehen schräg."""
    from PIL import ImageDraw
    from scipy.spatial import ConvexHull
    rgb = rgba[..., :3].astype(float)
    r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
    rot = (r > 140) & (r - g > 60) & (r - b > 60)
    hell = (rgb.mean(-1) > 158) & (rgb.max(-1) - rgb.min(-1) < 40) & (rgba[..., 3] > 200)
    gruppen, n = ndimage.label(ndimage.binary_dilation(rot, iterations=35))
    huelle = Image.new("L", (rgba.shape[1], rgba.shape[0]), 0)
    zeichnen = ImageDraw.Draw(huelle)
    rechteck = Image.new("L", huelle.size, 0)
    zeichnen_r = ImageDraw.Draw(rechteck)
    for i in range(1, n + 1):
        bereich = gruppen == i
        if (rot & bereich).sum() < 50:
            continue
        ys, xs = np.where((rot | hell) & bereich)
        pkt = np.column_stack([xs, ys]).astype(float)
        ecken = pkt[ConvexHull(pkt).vertices]
        zeichnen.polygon([tuple(p) for p in ecken], fill=255)
        zeichnen_r.polygon([tuple(p) for p in kleinstes_rechteck(ecken)], fill=255)
    # Hülle plus 1 px Übergang; aus dem Rechteck nur, was heller ist als die Wand (dunklere weiße
    # Ecken, die aus der Hülle fallen). So nimmt das Rechteck bei leicht trapezförmigen
    # Frontreflektoren keine graue Wand mit.
    grau = rgb.mean(-1)
    wand = np.median(grau[(rgba[..., 3] > 250) & ~rot])
    heller = (grau > wand + 10) | rot
    return ndimage.binary_dilation(np.array(huelle) > 0, iterations=1) | ((np.array(rechteck) > 0) & heller)


def reflektor_rechtecke(rgba):
    """Reflektoren, die gerade Rechtecke sind (PSD-Vorlagen): je Gruppe roter Streifen das
    Rechteck der kräftig roten und hellgrauen Pixel, ohne Aufweiten. Gruppen, die ihr Rechteck
    nicht zu mindestens 90 % füllen (schräge Reflektoren alter Vorlagen), kommen nicht zurück."""
    rgb = rgba[..., :3].astype(float)
    r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
    rot = (r > 140) & (r - g > 60) & (r - b > 60)
    # Für die Kanten nur eindeutige Reflektorpixel: die Mischpixel am Rand (halb Wand, halb Streifen)
    # gehören in den Übergangsring und werden anteilig gefärbt, sonst bleibt eine graue Linie stehen
    rot_klar = (r > 175) & (g < 65) & (b < 60)
    hell_klar = (rgb.mean(-1) > 170) & (rgb.max(-1) - rgb.min(-1) < 20) & (rgba[..., 3] > 250)
    gruppen, n = ndimage.label(ndimage.binary_dilation(rot, iterations=35))
    rechtecke = []
    for i in range(1, n + 1):
        bereich = gruppen == i
        if (rot & bereich).sum() < 50:
            continue
        klar = (rot_klar | hell_klar) & bereich
        ys, xs = np.where(klar)
        x0, x1, y0, y1 = xs.min(), xs.max() + 1, ys.min(), ys.max() + 1
        if klar[y0:y1, x0:x1].mean() >= 0.9:
            rechtecke.append((x0, y0, x1, y1))
    return rechtecke


def umfaerbegewicht(rgba):
    """Anteil, zu dem jedes Pixel umgefärbt wird (0..1). Container samt Kantenpixeln = 1,
    Bodenschatten und Hintergrund = 0. Rechteckige Reflektoren sind exakt 0; das eine
    Übergangspixel ringsum bekommt seinen Mischanteil: halb Wand, halb Reflektor -> 0,5.
    Schräge Reflektoren alter Vorlagen fallen über reflektoren() heraus."""
    a = rgba[..., 3]
    w = ((a > 0) & ndimage.binary_dilation(a > 200, iterations=3)).astype(float)
    rechtecke = reflektor_rechtecke(rgba)
    if not rechtecke:
        return w * ~reflektoren(rgba)
    rgb = rgba[..., :3].astype(float)
    grau = rgb.mean(-1)
    wand_rgb = np.median(rgb[(a > 250) & (w > 0)], axis=0)
    r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
    klar = ((r > 175) & (g < 65) & (b < 60)) | ((grau > 170) & (rgb.max(-1) - rgb.min(-1) < 20) & (a > 250))
    H, W = a.shape
    for x0, y0, x1, y1 in rechtecke:
        # Kern (1 px eingerückt): fest Reflektor. Dort liegen auch die schrägen Rot/Weiß-Mischpixel,
        # die sonst fälschlich als halbe Wand gälten.
        w[y0 + 1:y1 - 1, x0 + 1:x1 - 1] = 0
        # Randband (±2 px um die Kante): Mischanteil je Pixel gegen das nächste eindeutige
        # Reflektorpixel. Folgt so auch einer leicht schrägen Kante.
        by0, by1, bx0, bx1 = max(y0 - 2, 0), min(y1 + 2, H), max(x0 - 2, 0), min(x1 + 2, W)
        band_klar = klar[by0:by1, bx0:bx1]
        _, (iy, ix) = ndimage.distance_transform_edt(~band_klar, return_indices=True)
        teil = rgb[by0:by1, bx0:bx1]
        naechstes = teil[iy, ix]
        abstand = np.linalg.norm(naechstes - wand_rgb, axis=-1)
        anteil = np.clip(np.linalg.norm(teil - wand_rgb, axis=-1) / np.maximum(abstand, 1), 0, 1)
        kern = np.zeros(band_klar.shape, bool)
        kern[y0 + 1 - by0:y1 - 1 - by0, x0 + 1 - bx0:x1 - 1 - bx0] = True
        fenster = w[by0:by1, bx0:bx1]
        w[by0:by1, bx0:bx1] = np.where(kern, 0, np.minimum(fenster, 1 - anteil))
    return w


def maske(rgba):
    """Pixel, die überwiegend umgefärbt werden (für Statistik wie den Wandmedian)."""
    return umfaerbegewicht(rgba) > 0.5


def referenz_l(rgba, m):
    """Helligkeit der typischen Wandfläche: Median über die umzufärbenden Pixel."""
    return np.median(rgb_lab(rgba[..., :3].astype(float))[..., 0][m & (rgba[..., 3] > 250)])


def multiplizieren(rgba, farbe):
    """Farbe × Helligkeitsverhältnis zur Wand. Lichter über der Wandhelligkeit hellen Richtung Weiß auf."""
    gew = umfaerbegewicht(rgba)
    m = gew > 0.5
    grau = rgba[..., :3].astype(float).mean(-1)
    ref = np.median(grau[m & (rgba[..., 3] > 250)])
    v = grau / ref
    ziel = hex_rgb(farbe)
    dunkel = ziel * np.minimum(v, 1)[..., None]
    hell = ziel + (255 - ziel) * np.clip((v - 1) / (255 / ref - 1), 0, 1)[..., None]
    neu = np.where((v <= 1)[..., None], dunkel, hell)
    out = rgba.astype(float).copy()
    out[..., :3] = out[..., :3] * (1 - gew[..., None]) + neu * gew[..., None]
    return out.clip(0, 255).astype(np.uint8)


def lab_tausch(rgba, farbe, kontrast=1.0):
    """Helligkeit L aus der Vorlage (um die Wandhelligkeit auf die Zielhelligkeit verschoben),
    Farbton a/b aus der Zielfarbe. Schattierung bleibt exakt, der Farbton auch."""
    m = maske(rgba)
    lab = rgb_lab(rgba[..., :3].astype(float))
    ziel = rgb_lab(hex_rgb(farbe)[None, None])[0, 0]
    ref = referenz_l(rgba, m)
    L = ziel[0] + (lab[..., 0] - ref) * kontrast
    # in Schatten und Lichtern die Sättigung zurücknehmen, sonst glühen sie
    sat = np.clip(1 - np.abs(L - ziel[0]) / 70, 0.35, 1)
    neu = np.stack([np.clip(L, 0, 100), ziel[1] * sat, ziel[2] * sat], -1)
    out = rgba.astype(float).copy()
    out[..., :3] = np.where(m[..., None], lab_rgb(neu), out[..., :3])
    return out.clip(0, 255).astype(np.uint8)


def weiches_licht(rgba, farbe):
    """Photoshop-Mischmodus „Weiches Licht“: Vorlage als Licht-/Schattenebene über einer Farbfläche."""
    m = maske(rgba)
    grau = rgba[..., :3].astype(float).mean(-1) / 255
    ref = np.median(grau[m & (rgba[..., 3] > 250)])
    s = np.clip(grau - ref + 0.5, 0, 1)[..., None]  # Wandhelligkeit -> neutrales Grau 0.5
    c = hex_rgb(farbe) / 255
    d = np.where(c <= 0.25, ((16 * c - 12) * c + 4) * c, np.sqrt(c))
    neu = np.where(s <= 0.5, c - (1 - 2 * s) * c * (1 - c), c + (2 * s - 1) * (d - c))
    out = rgba.astype(float).copy()
    out[..., :3] = np.where(m[..., None], neu * 255, out[..., :3])
    return out.clip(0, 255).astype(np.uint8)


VERFAHREN = {"Multiplizieren": multiplizieren, "Lab-Tausch": lab_tausch, "Weiches Licht": weiches_licht}


def einfaerben(pfad, farbe, verfahren="Lab-Tausch"):
    rgba = np.array(Image.open(pfad).convert("RGBA"))
    return Image.fromarray(VERFAHREN[verfahren](rgba, farbe), "RGBA")


def nachschaerfen(rgba_neu, rgba_vorlage, kontrast=1.0, absenken=0.0):
    """Spreizt die Helligkeit des umgefärbten Bildes um die Wandhelligkeit (×kontrast)
    und legt die Wand um `absenken` L-Punkte tiefer. Farbton und Sättigung bleiben."""
    gew = umfaerbegewicht(rgba_vorlage)
    m = gew > 0.5
    lab = rgb_lab(rgba_neu[..., :3].astype(float))
    wand = np.median(lab[..., 0][m & (rgba_vorlage[..., 3] > 250)])
    lab[..., 0] = np.clip((lab[..., 0] - wand) * kontrast + wand - absenken, 0, 100)
    out = rgba_neu.astype(float).copy()
    out[..., :3] = out[..., :3] * (1 - gew[..., None]) + lab_rgb(lab) * gew[..., None]
    return out.clip(0, 255).astype(np.uint8)
