#!/usr/bin/env python3
"""
FULL CONTACTES v1.3 07/03/26 JSP (i Claude AI)

FullContactes.py - Genera fulles de contacte a partir d'imatges JPG

Les cel·les són quadrades i la fotografia s'hi mostra amb la seva orientació
original (horitzontal o vertical) centrada i maximitzada dins la cel·la.
Cada fulla inclou una capçalera i un peu de pàgina amb el copyright de la 1a foto.

Ús:
    python FullContactes.py <carpeta_origen> <carpeta_desti> ["Titol"] [mida_cel·la] [columnes] [files]

Paràmetres:
    carpeta_origen  Carpeta amb les imatges JPG d'origen  (obligatori)
    carpeta_desti   Carpeta on es desaran els fulls de contacte  (obligatori)
    Títol           Text de capçalera: model, data sessió, etc. (opcional, entre cometes)
                    Si s'omet o és buit (""), s'agafa el tag EXIF ImageDescription de la 1a imatge.
    mida_cel·la     Costat de la cel·la quadrada en píxels (per defecte: 350)
    columnes        Nombre d'imatges per fila (per defecte: NCOL (8))
    files           Nombre de files per fulla (per defecte: NFIL (4))
"""

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

import sys
import os
import math
from datetime import date
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
try:
    import piexif
    PIEXIF_OK = True
except ImportError:
    PIEXIF_OK = False
    print("Avis: piexif no instal·lat. Les fulles no tindran metadades EXIF.\n"
          "      Instal·la-ho amb:  pip install piexif")

# ─── Configuració ────────────────────────────────────────────────────────────

DIM_CELLA = 350
NCOL = 8
NFIL = 4

BACKGROUND   = (240, 240, 240)   # Gris molt clar de fons
CELL_BG      = (255, 255, 255)   # Blanc dins la cel·la quadrada
LABEL_COLOR  = (30, 30, 30)
MARGIN       = 30                # Marge exterior (px)
GAP          = 8                 # Espai entre cel·les (px)
LABEL_HEIGHT = 30                # Alçada del text identificador sota la foto
FONT_SIZE    = 26

# ── Capçalera i peu ──────────────────────────────────────────────────────────
HEADER_FONT_SIZE = 36
HEADER_COLOR     = (0, 0, 0)
HEADER_HEIGHT    = 40            # Alçada reservada per a capçalera i peu (px)

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

def get_font(size: int) -> ImageFont.ImageFont:
    candidates = [
        "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
        "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
        "/usr/share/fonts/truetype/freefont/FreeSansBold.ttf",
        "/System/Library/Fonts/Helvetica.ttc",
        "C:/Windows/Fonts/arialbd.ttf",
        "C:/Windows/Fonts/arial.ttf",
    ]
    for path in candidates:
        if os.path.exists(path):
            try:
                return ImageFont.truetype(path, size)
            except Exception:
                pass
    return ImageFont.load_default()


def read_exif_copyright(filepath: Path) -> str:
    """
    Llegeix els tags EXIF de copyright de la imatge indicada.
    Retorna una cadena amb Copyright i/o Artist si existeixen,
    o FOOTER_TEXT si no se'n troba cap.

    Tags consultats:
        0x8298  Copyright  (tag 33550)
        0x013B  Artist     (tag 315)
    """

    foottext = "(c) " + str(date.today().year) + ". All rights reserved."

    try:
        img = Image.open(filepath)
        exif = img.getexif()

        copyright_val = exif.get(0x8298, "").strip()
        artist_val    = exif.get(0x013B, "").strip()

        parts = []
        if copyright_val:
            parts.append(copyright_val)
        if artist_val and artist_val not in copyright_val:
            parts.append(artist_val)

        if parts:
            foottext = "  |  ".join(parts)
            foottext = "(c) " + str(date.today().year) + " " + foottext + (". All rights reserved.")
            return foottext
    except Exception:
        pass

    return foottext


def read_exif_title(filepath: Path) -> str:
    """
    Llegeix el tag EXIF ImageDescription (0x010E) de la imatge indicada.
    Retorna la cadena si existeix, o "" si no n'hi ha.
    """
    try:
        img  = Image.open(filepath)
        exif = img.getexif()
        val  = exif.get(0x010E, "")
        if isinstance(val, bytes):
            val = val.decode("utf-8", errors="ignore")
        return val.strip("\x00").strip()
    except Exception:
        return ""


def sheet_size(cols: int, rows: int, cell: int) -> tuple[int, int]:
    w = 2 * MARGIN + cols * cell + (cols - 1) * GAP
    h = (2 * HEADER_HEIGHT + 2 * MARGIN
         + rows * (cell + LABEL_HEIGHT) + (rows - 1) * GAP)
    return w, h


def apply_exif_orientation(img: Image.Image) -> Image.Image:
    """Aplica la rotació/flip indicada pel tag EXIF Orientation."""
    try:
        exif = img.getexif()
        orientation = exif.get(0x0112, 1)
    except Exception:
        orientation = 1

    ops = {
        2: (Image.FLIP_LEFT_RIGHT,),
        3: (Image.ROTATE_180,),
        4: (Image.FLIP_TOP_BOTTOM,),
        5: (Image.FLIP_LEFT_RIGHT, Image.ROTATE_90),
        6: (Image.ROTATE_270,),
        7: (Image.FLIP_LEFT_RIGHT, Image.ROTATE_270),
        8: (Image.ROTATE_90,),
    }
    for op in ops.get(orientation, ()):
        img = img.transpose(op)
    return img


def fit_image_in_square(img: Image.Image, cell: int) -> Image.Image:
    """
    Corregeix l'orientació EXIF, redimensiona mantenint l'aspecte original
    i centra la foto dins un quadrat de costat `cell` sobre fons blanc.
    """
    img = apply_exif_orientation(img)

    iw, ih = img.size
    scale = min(cell / iw, cell / ih)
    nw = int(iw * scale)
    nh = int(ih * scale)

    resized = img.resize((nw, nh), Image.LANCZOS)

    square = Image.new("RGB", (cell, cell), CELL_BG)
    ox = (cell - nw) // 2
    oy = (cell - nh) // 2
    square.paste(resized, (ox, oy))
    return square


def draw_centered_text(draw: ImageDraw.ImageDraw, text: str,
                       y: int, sheet_w: int,
                       font: ImageFont.ImageFont,
                       color: tuple, band_h: int):
    """Dibuixa text centrat horitzontalment dins una banda vertical."""
    try:
        bbox = font.getbbox(text)
        tw = bbox[2] - bbox[0]
        th = bbox[3] - bbox[1]
    except AttributeError:
        tw, th = font.getsize(text)
    tx = (sheet_w - tw) // 2
    ty = y + (band_h - th) // 2
    draw.text((tx, ty), text, fill=color, font=font)


def make_sheet(images_info: list[tuple[str, Image.Image]],
               cols: int, rows: int, cell: int,
               font_cell: ImageFont.ImageFont,
               font_header: ImageFont.ImageFont,
               sheet_index: int, output_dir: Path,
               header_text: str,
               footer_text: str):
    """Crea i guarda una fulla de contacte amb capçalera i peu."""
    sw, sh = sheet_size(cols, rows, cell)
    sheet = Image.new("RGB", (sw, sh), BACKGROUND)
    draw = ImageDraw.Draw(sheet)

    # ── Capçalera ────────────────────────────────────────────────────────────
    draw_centered_text(draw, header_text, y=0, sheet_w=sw,
                       font=font_header, color=HEADER_COLOR,
                       band_h=HEADER_HEIGHT)
    draw.line([(MARGIN, HEADER_HEIGHT), (sw - MARGIN, HEADER_HEIGHT)],
              fill=(180, 180, 180), width=2)

    # ── Fotografies ──────────────────────────────────────────────────────────
    grid_top = HEADER_HEIGHT + MARGIN

    for idx, (label, img) in enumerate(images_info):
        row = idx // cols
        col = idx % cols

        cx = MARGIN + col * (cell + GAP)
        cy = grid_top + row * (cell + LABEL_HEIGHT + GAP)

        square = fit_image_in_square(img, cell)
        sheet.paste(square, (cx, cy))

        draw.line([(cx, cy + cell), (cx + cell, cy + cell)],
                  fill=(200, 200, 200), width=1)

        lx0, ly0 = cx, cy + cell
        try:
            bbox = font_cell.getbbox(label)
            tw = bbox[2] - bbox[0]
            th = bbox[3] - bbox[1]
        except AttributeError:
            tw, th = font_cell.getsize(label)

        tx = lx0 + (cell - tw) // 2
        ty = ly0 + (LABEL_HEIGHT - th) // 2
        draw.text((tx, ty), label, fill=LABEL_COLOR, font=font_cell)

    # ── Peu ──────────────────────────────────────────────────────────────────
    footer_y = sh - HEADER_HEIGHT
    draw.line([(MARGIN, footer_y), (sw - MARGIN, footer_y)],
              fill=(180, 180, 180), width=2)
    draw_centered_text(draw, footer_text, y=footer_y, sheet_w=sw,
                       font=font_header, color=HEADER_COLOR,
                       band_h=HEADER_HEIGHT)

    out_path = output_dir / f"FullContacte_{sheet_index:02d}.jpg"

    if PIEXIF_OK:
        exif_dict = {
            "0th": {
                piexif.ImageIFD.Copyright: footer_text.encode("utf-8"),
                piexif.ImageIFD.Artist:    footer_text.encode("utf-8"),
                piexif.ImageIFD.ImageDescription: header_text.encode("utf-8"),
            }
        }
        exif_bytes = piexif.dump(exif_dict)
        sheet.save(str(out_path), "JPEG", quality=95, exif=exif_bytes)
    else:
        sheet.save(str(out_path), "JPEG", quality=95)
    size_mb = out_path.stat().st_size / (1024 * 1024)
    print(f"  Guardada: {out_path.name}  ({size_mb:.1f} MB  |  {sw}x{sh} px  |  cel·la {cell}x{cell} px)")


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

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

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

    output_dir = Path(sys.argv[2])
    output_dir.mkdir(parents=True, exist_ok=True)

    # Paràmetres opcionals: Títol, mida_cel·la, columnes, files
    header_text = str(sys.argv[3]).strip() if len(sys.argv) > 3 else ""
    cell        = int(sys.argv[4])         if len(sys.argv) > 4 else DIM_CELLA
    cols        = int(sys.argv[5])         if len(sys.argv) > 5 else NCOL
    rows        = int(sys.argv[6])         if len(sys.argv) > 6 else NFIL

    if cols < 1 or rows < 1:
        print("Error: columnes i files han de ser >= 1.")
        sys.exit(1)
    if cell < 50:
        print("Error: la mida de cel·la ha de ser >= 50 px.")
        sys.exit(1)

    extensions = {".jpg", ".jpeg", ".JPG", ".JPEG"}
    jpg_files = sorted([f for f in src_dir.iterdir() if f.suffix in extensions])
    if not jpg_files:
        print(f"No s'han trobat imatges JPG a '{src_dir}'.")
        sys.exit(0)

    sw, sh = sheet_size(cols, rows, cell)

    # Llegir copyright EXIF de la primera imatge
    footer_text = read_exif_copyright(jpg_files[0])

    # Si el títol és buit, intentem agafar-lo del tag EXIF ImageDescription
    if not header_text:
        header_text = read_exif_title(jpg_files[0])
        if header_text:
            print(f"Títol llegit d'EXIF : {header_text}")

    print(f"Imatges trobades  : {len(jpg_files)}")
    print(f"Configuracio      : {cols} columnes x {rows} files  ({cols*rows} fotos/fulla)")
    print(f"Cel·la quadrada   : {cell}x{cell} px")
    print(f"Mida de la fulla  : {sw}x{sh} px  (~{sw*sh*3/1024/1024:.0f} MB RAW)")
    print(f"Capcalera         : {header_text}")
    print(f"Peu               : {footer_text}")
    print(f"Carpeta destí     : {output_dir}")

    font_cell   = get_font(FONT_SIZE)
    font_header = get_font(HEADER_FONT_SIZE)

    per_sheet    = cols * rows
    total_sheets = math.ceil(len(jpg_files) / per_sheet)
    print(f"Fulles a generar  : {total_sheets}\n")

    for sheet_idx in range(total_sheets):
        batch = jpg_files[sheet_idx * per_sheet: (sheet_idx + 1) * per_sheet]
        images_info = []
        for f in batch:
            label = f.stem[-3:]
            try:
                img = Image.open(f).convert("RGB")
                images_info.append((label, img))
            except Exception as e:
                print(f"  Avis: no s'ha pogut obrir '{f.name}': {e}")

        make_sheet(images_info, cols, rows, cell,
                   font_cell, font_header,
                   sheet_idx + 1, output_dir,
                   header_text=header_text,
                   footer_text=footer_text)

    print("\nFet!")


if __name__ == "__main__":
    main()
