#!/usr/bin/env python3
"""
Recorta avatares cuadrados centrados en la cara, usando Haar cascade de OpenCV.

Uso:
  python3 face_crop_avatar.py <src> <dst> [--size 256] [--padding 0.6]
  python3 face_crop_avatar.py --batch <src_dir> <dst_dir> [--size 256]

Strategy:
- Detecta caras frontales (haarcascade_frontalface_default).
- Si encuentra >=1: coge la mayor, expande con padding (factor del lado de la cara)
  y recorta cuadrado.
- Si encuentra 0: fallback a crop cuadrado centrado en el TERCIO SUPERIOR
  de la imagen (donde suele estar la cabeza en fotos verticales).
- Redimensiona el cuadrado a `size x size`.

Mantiene aspecto natural — NO estira.
"""
import argparse, os, sys
import cv2

DEFAULT_SIZE = 256
DEFAULT_PADDING = 0.6  # factor del lado de la cara para añadir alrededor

cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
profile_cascade_path = cv2.data.haarcascades + 'haarcascade_profileface.xml'
_face_cascade = cv2.CascadeClassifier(cascade_path)
_profile_cascade = cv2.CascadeClassifier(profile_cascade_path)


def detect_face(gray):
    """Devuelve (x, y, w, h) de la cara más grande, o None."""
    for cascade in (_face_cascade, _profile_cascade):
        faces = cascade.detectMultiScale(
            gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30)
        )
        if len(faces):
            # más grande
            return max(faces, key=lambda r: r[2] * r[3])
        # también lateral en flip
        if cascade is _profile_cascade:
            flipped = cv2.flip(gray, 1)
            faces2 = cascade.detectMultiScale(flipped, 1.1, 5, minSize=(30, 30))
            if len(faces2):
                x, y, w, h = max(faces2, key=lambda r: r[2] * r[3])
                return (gray.shape[1] - x - w, y, w, h)
    return None


def square_crop_box(face, img_w, img_h, padding):
    """Caja cuadrada centrada en la cara. Si el padding no cabe sin invadir
    lados opuestos (típicamente texto), se REDUCE el side antes que extender.
    """
    x, y, w, h = face
    cx, cy = x + w / 2, y + h / 2
    ideal = max(w, h) * (1 + 2 * padding)
    # side máximo manteniendo cara centrada y dentro de la imagen
    max_x = 2 * min(cx, img_w - cx)
    max_y = 2 * min(cy, img_h - cy)
    side = min(ideal, max_x, max_y)
    half = side / 2
    x1, y1 = int(cx - half), int(cy - half)
    x2, y2 = int(cx + half), int(cy + half)
    return x1, y1, x2, y2


def fallback_box(img_w, img_h):
    """Sin cara detectada. Heurística por aspect ratio:
    - Horizontal (w > h*1.3): cuadrado a la IZQUIERDA (col_* suelen tener foto
      a la izquierda y texto a la derecha).
    - Vertical o cuadrado: cuadrado anclado al tercio superior, centrado en x.
    """
    if img_w > img_h * 1.3:
        side = img_h
        return 0, 0, side, side
    side = min(img_w, img_h)
    cx = img_w / 2
    x1 = max(0, int(cx - side / 2))
    return x1, 0, x1 + side, side


def process(src_path, dst_path, size=DEFAULT_SIZE, padding=DEFAULT_PADDING):
    img = cv2.imread(src_path, cv2.IMREAD_UNCHANGED)
    if img is None:
        return False, 'imread fail'
    # convertir alpha → blanco si hace falta
    if img.ndim == 3 and img.shape[2] == 4:
        # composite sobre blanco
        bgr = img[:, :, :3].copy()
        alpha = img[:, :, 3] / 255.0
        white = (1 - alpha[:, :, None]) * 255
        img = (bgr * alpha[:, :, None] + white).astype('uint8')
    elif img.ndim == 2:
        img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)

    h, w = img.shape[:2]
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    face = detect_face(gray)
    if face is not None:
        x1, y1, x2, y2 = square_crop_box(face, w, h, padding)
        used = 'face'
    else:
        x1, y1, x2, y2 = fallback_box(w, h)
        used = 'fallback'

    crop = img[y1:y2, x1:x2]
    resized = cv2.resize(crop, (size, size), interpolation=cv2.INTER_AREA)
    # asegurar JPEG-safe (sin alpha)
    if resized.ndim == 3 and resized.shape[2] == 4:
        resized = cv2.cvtColor(resized, cv2.COLOR_BGRA2BGR)
    os.makedirs(os.path.dirname(dst_path), exist_ok=True)
    cv2.imwrite(dst_path, resized, [cv2.IMWRITE_JPEG_QUALITY, 88])
    return True, used


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('src')
    ap.add_argument('dst')
    ap.add_argument('--size', type=int, default=DEFAULT_SIZE)
    ap.add_argument('--padding', type=float, default=DEFAULT_PADDING)
    ap.add_argument('--batch', action='store_true', help='src y dst son directorios')
    args = ap.parse_args()

    if args.batch:
        files = [f for f in os.listdir(args.src) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))]
        stats = {'face': 0, 'fallback': 0, 'fail': 0}
        for fn in files:
            ok, info = process(
                os.path.join(args.src, fn),
                os.path.join(args.dst, os.path.splitext(fn)[0] + '.jpg'),
                size=args.size, padding=args.padding,
            )
            stats['fail' if not ok else info] += 1
        print(f'face: {stats["face"]}, fallback: {stats["fallback"]}, fail: {stats["fail"]}')
    else:
        ok, info = process(args.src, args.dst, size=args.size, padding=args.padding)
        print(f'{"OK" if ok else "FAIL"}  {info}  → {args.dst}')


if __name__ == '__main__':
    main()
