#!/usr/bin/env python3
# signar.py — v2.3.1 06/03/26 JSP (i Claude AI)
#
# Ús:
#   python signar.py <signatura> <cantonada> <carpeta_origen> <carpeta_destí> [N] [L]
#   cantonada: A (Automàtic) | D (Dreta) | E (Esquerra) | C (Centre)
#   N:         factor de mida (la signatura ocuparà 1/N de l'amplada, default: 14)
#   L:         % de lluminositat per sota del qual s'inverteix la signatura (0-100, default: 50)
#              (si L = 0, la signatura no s'invertirà mai)
#
# Dependències obligatòries:  pip install Pillow piexif resvg_py
# Dependències opcionals:     pip install psd-tools pyexiftool
#                             ExifTool al sistema: https://exiftool.org

import sys
import argparse
from io import BytesIO
from pathlib import Path
from PIL import Image, ImageOps
import piexif

# Imports opcionals
try:
    import resvg_py
except ImportError:
    resvg_py = None

try:
    import exiftool
except ImportError:
    exiftool = None

try:
    from psd_tools import PSDImage
except ImportError:
    PSDImage = None


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

EXTENSIONS           = {".jpg", ".jpeg", ".png", ".tif", ".tiff", ".psd"}
QUALITAT_JPEG        = 90
SVG_ESCALA           = 3    # Factor de sobremostreig SVG per a fotos d'alta resolució
FACTOR_MIDA_DEFAULT  = 14   # La signatura ocuparà 1/N de l'amplada de la foto
LLINDAR_LLUMINOSITAT = 0.5  # Per sota d'aquest valor (0-1), la zona es considera fosca


# ---------------------------------------------------------------------------
# Càrrega de la signatura
# ---------------------------------------------------------------------------

def carregar_signatura(ruta: Path) -> Image.Image:
    ext = ruta.suffix.lower()

    if ext == ".svg":
        if not resvg_py:
            sys.exit("ERROR: pip install resvg_py  (necessari per a SVG)")
        svg = ruta.read_text(encoding="utf-8")
        # Renderitzem a mida natural i sobremostregen amb Pillow (LANCZOS)
        # per preservar proporcions i fidelitat dels elements interns del SVG.
        img = Image.open(BytesIO(resvg_py.svg_to_bytes(svg_string=svg))).convert("RGBA")
        w, h = img.size
        return img.resize((w * SVG_ESCALA, h * SVG_ESCALA), Image.LANCZOS)

    if ext == ".psd":
        if not PSDImage:
            sys.exit("ERROR: pip install psd-tools  (necessari per a PSD)")
        return PSDImage.open(ruta).composite().convert("RGBA")

    return Image.open(ruta).convert("RGBA")


# ---------------------------------------------------------------------------
# Anàlisi de zones
# ---------------------------------------------------------------------------

def lluminositat_mitjana(foto: Image.Image, x: int, y: int, sw: int, sh: int) -> float:
    """Retorna la lluminositat mitjana [0-1] de la zona (x, y, sw, sh)."""
    zona = foto.crop((x, y, x + sw, y + sh)).convert("L")
    pixels = list(zona.get_flattened_data())
    return sum(pixels) / len(pixels) / 255


def homogeneitat(foto: Image.Image, x: int, y: int, sw: int, sh: int) -> float:
    """
    Retorna la desviació estàndard de la lluminositat de la zona (x, y, sw, sh).
    Un valor més baix indica una zona més homogènia (millor per a la signatura).
    """
    zona = foto.crop((x, y, x + sw, y + sh)).convert("L")
    pixels = list(zona.get_flattened_data())
    n = len(pixels)
    mitjana = sum(pixels) / n
    desviacio = (sum((p - mitjana) ** 2 for p in pixels) / n) ** 0.5
    return desviacio


def cantonada_automatica(foto: Image.Image, sw: int, sh: int, marge: int) -> str:
    """
    Avalua les cantonades inferior-esquerra i inferior-dreta i retorna
    la lletra ('E' o 'D') de la zona més homogènia (menor desviació estàndard).
    """
    fw, fh = foto.size
    candidates = {
        "E": (marge,           fh - sh - marge),
        "D": (fw - sw - marge, fh - sh - marge),
    }
    return min(candidates, key=lambda c: homogeneitat(foto, *candidates[c], sw, sh))


# ---------------------------------------------------------------------------
# Inversió de la signatura
# ---------------------------------------------------------------------------

def invertir_signatura(signatura: Image.Image) -> Image.Image:
    """Retorna el negatiu de la signatura preservant el canal alfa."""
    r, g, b, a = signatura.split()
    rgb_inv = Image.merge("RGB", [
        r.point(lambda p: 255 - p),
        g.point(lambda p: 255 - p),
        b.point(lambda p: 255 - p),
    ])
    rgb_inv.putalpha(a)
    return rgb_inv


# ---------------------------------------------------------------------------
# Metadades
# ---------------------------------------------------------------------------

def extreure_exif(foto_path: Path) -> bytes | None:
    """Extreu EXIF de la foto original, netejant miniatura i resetejant Orientation."""
    try:
        d = piexif.load(str(foto_path))
        d.pop("thumbnail", None)
        d["1st"] = {}
        d["0th"][piexif.ImageIFD.Orientation] = 1
        return piexif.dump(d)
    except Exception:
        return None


def transferir_metadades(origen: Path, desti: Path) -> None:
    """Copia totes les metadades (EXIF, XMP, IPTC, copyright) via ExifTool."""
    with exiftool.ExifToolHelper() as et:
        et.execute(
            "-overwrite_original", "-tagsFromFile", str(origen),
            "-all:all",
            "--IFD1:all", "--ExifIFD:PixelXDimension", "--ExifIFD:PixelYDimension",
            "--IFD0:ImageWidth", "--IFD0:ImageLength",
            "--IFD0:Orientation",
            "--ExifIFD:Orientation",
            str(desti)
        )


# ---------------------------------------------------------------------------
# Processament d'una imatge
# ---------------------------------------------------------------------------

def signar_imatge(foto_path: Path, signatura: Image.Image,
                  cantonada: str, dest: Path, factor: int, llindar: float) -> None:

    # Obrir i auto-rotar segons EXIF Orientation
    with Image.open(foto_path) as raw:
        dpi  = raw.info.get("dpi")
        foto = ImageOps.exif_transpose(raw).convert("RGBA")

    fw, fh = foto.size
    sw, sh = signatura.size

    # Escala: la signatura ocupa 1/factor de l'amplada de la foto
    scale = (fw + fh) / (factor * 2) / sw
    sw, sh = max(1, int(sw * scale)), max(1, int(sh * scale))
    sign = signatura.resize((sw, sh), Image.LANCZOS)

    # Posició: resolem cantonada automàtica abans de calcular (x, y)
    marge = sw // 4
    cant = cantonada_automatica(foto, sw, sh, marge) if cantonada == "A" else cantonada
    x = {"E": marge, "C": (fw - sw) // 2}.get(cant, fw - sw - marge)
    y = fh - sh - marge

    # Invertir la signatura si la zona on caurà és fosca
    if lluminositat_mitjana(foto, x, y, sw, sh) < llindar:
        sign = invertir_signatura(sign)

    foto.paste(sign, (x, y), mask=sign)

    # Desar
    ruta_dest = dest / (foto_path.stem + "S.jpg")
    save_kw = {"format": "JPEG", "quality": QUALITAT_JPEG, "subsampling": 0}
    if dpi: save_kw["dpi"] = dpi
    exif_bytes = extreure_exif(foto_path)
    if exif_bytes: save_kw["exif"] = exif_bytes
    foto.convert("RGB").save(ruta_dest, **save_kw)

    # Metadades completes via ExifTool si disponible
    if exiftool:
        transferir_metadades(foto_path, ruta_dest)

    meta = "EXIF+XMP/IPTC" if exiftool else "EXIF"
    auto = f" [auto→{cant}]" if cantonada == "A" else ""
    print(f"  ✓  {foto_path.name}  →  {ruta_dest.name}  [{meta}]{auto}")


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main():
    p = argparse.ArgumentParser(description="Aplica una signatura a un conjunt de fotografies.")
    p.add_argument("signatura")
    p.add_argument("cantonada", help="D (Dreta) | E (Esquerra) | C (Centre) | A (Automàtic)")
    p.add_argument("carpeta_origen")
    p.add_argument("carpeta_desti")
    p.add_argument("factor", nargs="?", type=int, default=FACTOR_MIDA_DEFAULT,
                   help=f"1/N de l'amplada de la foto (default: {FACTOR_MIDA_DEFAULT})")
    p.add_argument("llindar", nargs="?", type=float, default=50.0,
                   help="%% de lluminositat per sota del qual s'inverteix la signatura (0-100, default: 50)")
    args = p.parse_args()

    cantonada = args.cantonada.upper()
    if cantonada not in ("D", "E", "C", "A"):
        sys.exit(f"ERROR: cantonada ha de ser D, E, C o A (rebut: '{args.cantonada}')")
    if args.factor < 1:
        sys.exit(f"ERROR: el factor N ha de ser >= 1 (rebut: {args.factor})")
    if not (0 <= args.llindar <= 100):
        sys.exit(f"ERROR: el llindar ha de ser entre 0 i 100 (rebut: {args.llindar})")
    llindar = args.llindar / 100

    sign_path = Path(args.signatura)
    src       = Path(args.carpeta_origen)
    dest      = Path(args.carpeta_desti)

    if not sign_path.exists(): sys.exit(f"ERROR: No es troba la signatura: {sign_path}")
    if not src.is_dir():       sys.exit(f"ERROR: Carpeta origen no existeix: {src}")
    dest.mkdir(parents=True, exist_ok=True)

    print(f"Carregant signatura: {sign_path.name}")
    signatura = carregar_signatura(sign_path)
    print(f"  Mida: {signatura.size[0]}×{signatura.size[1]} px\n")

    fitxers = sorted(f for f in src.iterdir()
                     if f.is_file() and f.suffix.lower() in EXTENSIONS)
    if not fitxers:
        sys.exit("No s'han trobat imatges a la carpeta origen.")

    print(f"📁Origen:      '{src}'")
    print(f"📁Destí:       '{dest}'")
    print(f"📐Cantonada:   {cantonada}\n")

    print(f"Processant {len(fitxers)} imatge(s) — cantonada: {cantonada}, factor: 1/{args.factor}, llindar: {args.llindar:.0f}%\n")
    errors = []
    for f in fitxers:
        try:
            signar_imatge(f, signatura, cantonada, dest, args.factor, llindar)
        except Exception as e:
            print(f"  ✗  {f.name}  →  ERROR: {e}")
            errors.append(f.name)

    print(f"\nProcés completat. {len(fitxers)-len(errors)}/{len(fitxers)} imatges signades.")
    if not exiftool:
        print("  ℹ️  Sense ExifTool: només EXIF bàsic. pip install pyexiftool + https://exiftool.org")
    if errors:
        print(f"  Errors en: {', '.join(errors)}")


if __name__ == "__main__":
    main()