#!/usr/bin/env python3
"""
EDITAREXIF v1.2  07/03/26  JSP (i Claude AI)

EditarEXIF.py - Modifica metadades EXIF d'imatges JPG d'una carpeta

Les imatges es modifiquen en la mateixa carpeta d'origen.

Ús:
    python EditarEXIF.py <carpeta_fotos> <acció> [paràmetres_opcionals]

Accions disponibles:

  DHFitxer
      Ajusta la data/hora del fitxer (mtime/atime) segons la data i hora
      de captura de la fotografia (tag EXIF DateTimeOriginal).
      No requereix paràmetres addicionals.

      Exemple:
          python EditaImatges.py /fotos DHFitxer

  DH  <hores>
      Suma (o resta si és negatiu) el nombre d'hores indicat als tags
      EXIF de data i hora de la fotografia:
          DateTimeOriginal  (0x9003)
          DateTimeDigitized (0x9004)
          DateTime          (0x0132)
      El valor pot ser decimal (p.ex. 1.5 = 1h 30min).

      Exemple (avançar 2 hores):
          python EditaImatges.py /fotos DH 2
      Exemple (endarrerir mitja hora):
          python EditaImatges.py /fotos DH -0.5

  DHSET  <data_hora>
      Estableix una data i hora concreta als tags EXIF de totes les imatges.
      Format acceptat: "YYYY-MM-DD HH:MM:SS"  o  "YYYY:MM:DD HH:MM:SS"

      Exemple:
          python EditaImatges.py /fotos DHSET "2024-06-15 10:30:00"

  CR  <text_copyright>
      Escriu el text indicat al tag EXIF Copyright (0x8298) de totes
      les imatges. Si el text és buit ("") esborra el copyright existent.

      Exemple:
          python EditarEXIF.py /fotos CR "(c) 2026 Joan Smith. All rights reserved."

  Autor  <nom_autor>
      Escriu el text indicat al tag EXIF de creador/artista:
          Artist     (0x013B)  — camp estàndard EXIF "artista/creador"
      Si el text és buit ("") esborra el camp existent.

      Exemple:
          python EditarEXIF.py /fotos Autor "Joan Smith"
      Exemple (esborrar):
          python EditarEXIF.py /fotos Autor ""

  Titol  <text_titol>
      Escriu el text indicat al tag EXIF ImageDescription (0x010E) de totes
      les imatges. Aquest tag és el que la majoria de programes mostren com
      a "Títol" o "Descripció" de la imatge (Lightroom, Windows, digiKam...).
      Si el text és buit ("") esborra el camp existent.

      Exemple:
          python EditarEXIF.py /fotos Titol "Sessió estudi - Núria - Març 2026"
      Exemple (esborrar):
          python EditarEXIF.py /fotos Titol ""
"""

# LLIBRERIES A INSTAL·LAR !!!
# pip install Pillow
# pip install piexif

import sys
import os
import shutil
from datetime import datetime, timedelta
from pathlib import Path

try:
    from PIL import Image
    PIL_OK = True
except ImportError:
    PIL_OK = False
    print("Error: Pillow no instal·lat. Instal·la-ho amb:  pip install Pillow")
    sys.exit(1)

try:
    import piexif
    PIEXIF_OK = True
except ImportError:
    PIEXIF_OK = False
    print("Error: piexif no instal·lat. Instal·la-ho amb:  pip install piexif")
    sys.exit(1)

# ─── Constants EXIF ────────────────────────────────────────────────────────────

EXIF_DATETIME_ORIGINAL  = 0x9003   # DateTimeOriginal
EXIF_DATETIME_DIGITIZED = 0x9004   # DateTimeDigitized
EXIF_DATETIME           = 0x0132   # DateTime  (IFD 0th)
EXIF_COPYRIGHT          = 0x8298   # Copyright (IFD 0th)
EXIF_ARTIST             = 0x013B   # Artist / Creator (IFD 0th)
EXIF_IMAGEDESCRIPTION   = 0x010E   # ImageDescription / Títol (IFD 0th)

EXIF_DT_FORMAT = "%Y:%m:%d %H:%M:%S"   # format intern EXIF

# ─── Extensies acceptades ──────────────────────────────────────────────────────

EXTENSIONS = {".jpg", ".jpeg", ".JPG", ".JPEG"}

# ─── Funcions auxiliars ────────────────────────────────────────────────────────

def get_jpg_files(folder: Path) -> list[Path]:
    """Retorna la llista de fitxers JPG de la carpeta, ordenada."""
    files = sorted([f for f in folder.iterdir() if f.suffix in EXTENSIONS])
    if not files:
        print(f"No s'han trobat imatges JPG a '{folder}'.")
        sys.exit(0)
    return files


def load_exif(filepath: Path) -> dict | None:
    """Llegeix el diccionari EXIF d'una imatge. Retorna None si falla."""
    try:
        return piexif.load(str(filepath))
    except Exception as e:
        print(f"  Avis: no s'han pogut llegir dades EXIF de '{filepath.name}': {e}")
        return None


def save_exif(filepath: Path, exif_dict: dict) -> bool:
    """Escriu el diccionari EXIF al fitxer. Retorna True si va bé."""
    try:
        exif_bytes = piexif.dump(exif_dict)
        piexif.insert(exif_bytes, str(filepath))
        return True
    except Exception as e:
        print(f"  Error en desar EXIF de '{filepath.name}': {e}")
        return False


def parse_exif_datetime(dt_bytes) -> datetime | None:
    """Converteix un valor EXIF de data/hora (bytes o str) a datetime."""
    if not dt_bytes:
        return None
    if isinstance(dt_bytes, bytes):
        dt_str = dt_bytes.decode("utf-8", errors="ignore").strip("\x00").strip()
    else:
        dt_str = str(dt_bytes).strip()
    try:
        return datetime.strptime(dt_str, EXIF_DATETIME_FORMAT := EXIF_DT_FORMAT)
    except ValueError:
        return None


def format_exif_datetime(dt: datetime) -> bytes:
    """Converteix un datetime a bytes en format EXIF."""
    return dt.strftime(EXIF_DT_FORMAT).encode("utf-8")


def parse_user_datetime(s: str) -> datetime:
    """
    Accepta els formats:
        "YYYY-MM-DD HH:MM:SS"
        "YYYY:MM:DD HH:MM:SS"
    Retorna un datetime o llença ValueError.
    """
    s = s.replace(":", "-", 2) if s[4] == ":" else s  # normalitza separadors de data
    for fmt in ("%Y-%m-%d %H:%M:%S", "%Y/%m/%d %H:%M:%S"):
        try:
            return datetime.strptime(s, fmt)
        except ValueError:
            pass
    raise ValueError(f"Format de data/hora no reconegut: '{s}'\n"
                     "  Utilitza: \"YYYY-MM-DD HH:MM:SS\"  (p.ex. \"2024-06-15 10:30:00\")")


def result_line(filepath: Path, ok: bool, detail: str = ""):
    """Imprimeix una línia de resultat per a cada fitxer processat."""
    status = "OK" if ok else "ERROR"
    msg = f"  [{status}] {filepath.name}"
    if detail:
        msg += f"  →  {detail}"
    print(msg)


# ─── Accions ──────────────────────────────────────────────────────────────────

def accio_dhfitxer(files: list[Path]):
    """
    DHFitxer: ajusta mtime/atime del fitxer a la data EXIF DateTimeOriginal.
    Si no hi ha DateTimeOriginal, prova amb DateTime.
    """
    print("Acció: DHFitxer — Ajustar data/hora del fitxer segons EXIF\n")
    ok_count = err_count = skip_count = 0

    for f in files:
        exif = load_exif(f)
        if exif is None:
            err_count += 1
            continue

        dt = None
        # Intentem DateTimeOriginal primer (Exif IFD)
        exif_ifd = exif.get("Exif", {})
        dt = parse_exif_datetime(exif_ifd.get(EXIF_DATETIME_ORIGINAL))

        # Si no, DateTime de l'IFD principal
        if dt is None:
            dt = parse_exif_datetime(exif.get("0th", {}).get(EXIF_DATETIME))

        if dt is None:
            result_line(f, False, "no s'ha trobat data EXIF")
            skip_count += 1
            continue

        # Convertim datetime a timestamp Unix
        ts = dt.timestamp()
        try:
            os.utime(f, (ts, ts))
            result_line(f, True, dt.strftime("%Y-%m-%d %H:%M:%S"))
            ok_count += 1
        except Exception as e:
            result_line(f, False, str(e))
            err_count += 1

    resum = f"{ok_count} fitxers actualitzats"
    if skip_count: resum += f", {skip_count} sense data EXIF"
    if err_count:  resum += f", {err_count} errors"
    print(f"\nResum: {resum}.")


def accio_dh(files: list[Path], hores: float):
    """
    DH <hores>: suma/resta el nombre d'hores als tags EXIF de data/hora.
    Modifica: DateTimeOriginal, DateTimeDigitized, DateTime.
    """
    signe = "+" if hores >= 0 else ""
    print(f"Acció: DH — Ajustar data/hora EXIF en {signe}{hores}h\n")
    ok_count = err_count = 0
    delta = timedelta(hours=hores)

    for f in files:
        exif = load_exif(f)
        if exif is None:
            err_count += 1
            continue

        changed = False

        # Tags dins l'IFD "Exif"
        exif_ifd = exif.get("Exif", {})
        for tag in (EXIF_DATETIME_ORIGINAL, EXIF_DATETIME_DIGITIZED):
            dt = parse_exif_datetime(exif_ifd.get(tag))
            if dt is not None:
                exif["Exif"][tag] = format_exif_datetime(dt + delta)
                changed = True

        # Tag DateTime de l'IFD principal ("0th")
        ifd0 = exif.get("0th", {})
        dt = parse_exif_datetime(ifd0.get(EXIF_DATETIME))
        if dt is not None:
            exif["0th"][EXIF_DATETIME] = format_exif_datetime(dt + delta)
            changed = True

        if not changed:
            result_line(f, False, "no s'ha trobat cap tag de data/hora EXIF")
            err_count += 1
            continue

        if save_exif(f, exif):
            result_line(f, True, f"offset {signe}{hores}h aplicat")
            ok_count += 1
        else:
            err_count += 1

    resum = f"{ok_count} fitxers modificats"
    if err_count: resum += f", {err_count} errors"
    print(f"\nResum: {resum}.")


def accio_dhset(files: list[Path], nova_dt: datetime):
    """
    DHSET <data_hora>: estableix una data/hora concreta als tags EXIF.
    Modifica: DateTimeOriginal, DateTimeDigitized, DateTime.
    """
    dt_str = nova_dt.strftime("%Y-%m-%d %H:%M:%S")
    print(f"Acció: DHSET — Establir data/hora EXIF a {dt_str}\n")
    ok_count = err_count = 0
    dt_bytes = format_exif_datetime(nova_dt)

    for f in files:
        exif = load_exif(f)
        if exif is None:
            err_count += 1
            continue

        # Assegurem que existeixen les seccions
        if "Exif" not in exif:
            exif["Exif"] = {}
        if "0th" not in exif:
            exif["0th"] = {}

        exif["Exif"][EXIF_DATETIME_ORIGINAL]  = dt_bytes
        exif["Exif"][EXIF_DATETIME_DIGITIZED] = dt_bytes
        exif["0th"][EXIF_DATETIME]            = dt_bytes

        if save_exif(f, exif):
            result_line(f, True, dt_str)
            ok_count += 1
        else:
            err_count += 1

    resum = f"{ok_count} fitxers modificats"
    if err_count: resum += f", {err_count} errors"
    print(f"\nResum: {resum}.")


def accio_cr(files: list[Path], copyright_text: str):
    """
    CR <text>: escriu (o esborra si és buit) el tag EXIF Copyright.
    """
    if copyright_text:
        print(f"Acció: CR — Establir copyright EXIF: \"{copyright_text}\"\n")
    else:
        print("Acció: CR — Esborrar copyright EXIF\n")

    ok_count = err_count = 0
    cr_bytes = copyright_text.encode("utf-8") if copyright_text else b""

    for f in files:
        exif = load_exif(f)
        if exif is None:
            err_count += 1
            continue

        if "0th" not in exif:
            exif["0th"] = {}

        if cr_bytes:
            exif["0th"][EXIF_COPYRIGHT] = cr_bytes
        else:
            exif["0th"].pop(EXIF_COPYRIGHT, None)

        if save_exif(f, exif):
            detail = f"\"{copyright_text}\"" if copyright_text else "(esborrat)"
            result_line(f, True, detail)
            ok_count += 1
        else:
            err_count += 1

    resum = f"{ok_count} fitxers modificats"
    if err_count: resum += f", {err_count} errors"
    print(f"\nResum: {resum}.")


def accio_autor(files: list[Path], autor_text: str):
    """
    Autor <text>: escriu (o esborra si és buit) el tag EXIF Artist (0x013B).
    """
    if autor_text:
        print(f"Acció: Autor — Establir autor EXIF: \"{autor_text}\"\n")
    else:
        print("Acció: Autor — Esborrar autor EXIF\n")

    ok_count = err_count = 0
    artist_bytes = autor_text.encode("utf-8") if autor_text else b""

    for f in files:
        exif = load_exif(f)
        if exif is None:
            err_count += 1
            continue

        if "0th" not in exif:
            exif["0th"] = {}

        if autor_text:
            exif["0th"][EXIF_ARTIST] = artist_bytes
        else:
            exif["0th"].pop(EXIF_ARTIST, None)

        if save_exif(f, exif):
            detail = f"\"{autor_text}\"" if autor_text else "(esborrat)"
            result_line(f, True, detail)
            ok_count += 1
        else:
            err_count += 1

    resum = f"{ok_count} fitxers modificats"
    if err_count: resum += f", {err_count} errors"
    print(f"\nResum: {resum}.")


def accio_titol(files: list[Path], titol_text: str):
    """
    Titol <text>: escriu (o esborra si és buit) el tag EXIF ImageDescription (0x010E).
    """
    if titol_text:
        print(f"Acció: Titol — Establir títol EXIF: \"{titol_text}\"\n")
    else:
        print("Acció: Titol — Esborrar títol EXIF\n")

    ok_count = err_count = 0
    titol_bytes = titol_text.encode("utf-8") if titol_text else b""

    for f in files:
        exif = load_exif(f)
        if exif is None:
            err_count += 1
            continue

        if "0th" not in exif:
            exif["0th"] = {}

        if titol_text:
            exif["0th"][EXIF_IMAGEDESCRIPTION] = titol_bytes
        else:
            exif["0th"].pop(EXIF_IMAGEDESCRIPTION, None)

        if save_exif(f, exif):
            detail = f"\"{titol_text}\"" if titol_text else "(esborrat)"
            result_line(f, True, detail)
            ok_count += 1
        else:
            err_count += 1

    resum = f"{ok_count} fitxers modificats"
    if err_count: resum += f", {err_count} errors"
    print(f"\nResum: {resum}.")


# ─── Principal ────────────────────────────────────────────────────────────────

ACCIONS = ("DHFitxer", "DH", "DHSET", "CR", "Autor", "Titol")


def main():
    if len(sys.argv) < 3:
        print(__doc__)
        sys.exit(1)

    # Carpeta origen
    src_dir = Path(sys.argv[1])
    if not src_dir.is_dir():
        print(f"Error: '{src_dir}' no és una carpeta vàlida.")
        sys.exit(1)

    # Acció
    accio = sys.argv[2].upper().strip()
    accio_orig = sys.argv[2].strip()  # preservem capitalització per al missatge

    # Llista de fitxers
    files = get_jpg_files(src_dir)
    print(f"Carpeta  : {src_dir}")
    print(f"Imatges  : {len(files)} fitxers JPG\n")

    # ── Despatx de l'acció ────────────────────────────────────────────────────

    if accio == "DHFITXER":
        accio_dhfitxer(files)

    elif accio == "DH":
        if len(sys.argv) < 4:
            print("Error: l'acció 'DH' requereix el paràmetre <hores>.\n"
                  "  Exemple: python EditaImatges.py /fotos DH -2")
            sys.exit(1)
        try:
            hores = float(sys.argv[3])
        except ValueError:
            print(f"Error: '{sys.argv[3]}' no és un nombre d'hores vàlid.")
            sys.exit(1)
        accio_dh(files, hores)

    elif accio == "DHSET":
        if len(sys.argv) < 4:
            print("Error: l'acció 'DHSET' requereix el paràmetre <data_hora>.\n"
                  "  Exemple: python EditaImatges.py /fotos DHSET \"2024-06-15 10:30:00\"")
            sys.exit(1)
        try:
            nova_dt = parse_user_datetime(sys.argv[3])
        except ValueError as e:
            print(f"Error: {e}")
            sys.exit(1)
        accio_dhset(files, nova_dt)

    elif accio == "CR":
        if len(sys.argv) < 4:
            print("Error: l'acció 'CR' requereix el paràmetre <text_copyright>.\n"
                  "  Exemple: python EditarEXIF.py /fotos CR \"(c) 2026 Joan Smith\"")
            sys.exit(1)
        accio_cr(files, sys.argv[3])

    elif accio == "AUTOR":
        if len(sys.argv) < 4:
            print("Error: l'acció 'Autor' requereix el paràmetre <nom_autor>.\n"
                  "  Exemple: python EditarEXIF.py /fotos Autor \"Joan Smith\"")
            sys.exit(1)
        accio_autor(files, sys.argv[3])

    elif accio == "TITOL":
        if len(sys.argv) < 4:
            print("Error: l'acció 'Titol' requereix el paràmetre <text_titol>.\n"
                  "  Exemple: python EditarEXIF.py /fotos Titol \"Sessió estudi - Març 2026\"")
            sys.exit(1)
        accio_titol(files, sys.argv[3])

    else:
        print(f"Error: acció desconeguda '{accio_orig}'.")
        print(f"  Accions disponibles: {', '.join(ACCIONS)}")
        sys.exit(1)

    print("\nFet!")


if __name__ == "__main__":
    main()
