#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Conversor de imágenes a formato 1-bit para e-ink con interfaz gráfica
Permite convertir imágenes a formato binario con diferentes opciones de tamaño
Incluye previsualización de la imagen convertida
"""

import tkinter as tk
from tkinter import ttk, filedialog, messagebox, Canvas
from PIL import Image, ImageOps, ImageEnhance, ImageFilter, ImageTk
import os
from pathlib import Path

# -----------------------------
#  DITHERING STUCKI
# -----------------------------
STUCKI_KERNEL = [
    (1, 0,  8/42), (2, 0, 4/42),
    (-2, 1, 2/42), (-1, 1, 4/42), (0, 1, 8/42), (1, 1, 4/42), (2, 1, 2/42),
    (-2, 2, 1/42), (-1, 2, 2/42), (0, 2, 4/42), (1, 2, 2/42), (2, 2, 1/42),
]

def dither_stucki(img):
    """Aplica dithering Stucki manualmente"""
    w, h = img.size
    pix = img.load()
    for y in range(h):
        for x in range(w):
            old = pix[x, y]
            new = 255 if old > 127 else 0
            pix[x, y] = new
            err = old - new
            for dx, dy, factor in STUCKI_KERNEL:
                nx = x + dx
                ny = y + dy
                if 0 <= nx < w and 0 <= ny < h:
                    pix[nx, ny] = max(0, min(255, pix[nx, ny] + int(err * factor)))
    return img

def convertir_imagen(input_path, output_path, ancho=None, alto=None):
    """
    Convierte imagen a formato 1-bit para e-ink
    Si ancho y alto son None, mantiene dimensiones originales
    Retorna la imagen procesada además de las estadísticas
    """
    # Abrir imagen
    img = Image.open(input_path)
    
    # Determinar dimensiones finales
    if ancho is None or alto is None:
        ANCHO, ALTO = img.size
    else:
        ANCHO, ALTO = ancho, alto
    
    # Convertir a escala de grises
    img = ImageOps.grayscale(img)
    
    # Auto-contraste moderado
    img = ImageOps.autocontrast(img, cutoff=2)
    
    # Ajuste de brillo/contraste
    img = ImageEnhance.Brightness(img).enhance(1.05)
    img = ImageEnhance.Contrast(img).enhance(1.15)
    
    # Reescalar si es necesario
    if img.size != (ANCHO, ALTO):
        img = img.resize((ANCHO, ALTO), Image.LANCZOS)
    
    # Suavizado leve
    img = img.filter(ImageFilter.GaussianBlur(radius=0.4))
    
    # Dithering
    img = dither_stucki(img)
    
    # Convertir a 1-bit
    pix = img.load()
    data = bytearray()
    
    for y in range(ALTO):
        for x_byte in range((ANCHO + 7) // 8):
            byte = 0
            for bit in range(8):
                x = x_byte * 8 + bit
                if x < ANCHO:
                    if pix[x, y] < 128:
                        byte |= (0x80 >> bit)
            data.append(byte)
    
    # Guardar archivo binario
    with open(output_path, "wb") as f:
        f.write(data)
    
    return img, ANCHO, ALTO, len(data)

# -----------------------------
#  INTERFAZ GRÁFICA
# -----------------------------
class ConversorEInkGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("Conversor de Imágenes E-Ink")
        self.root.geometry("1000x500")
        
        # Variables
        self.archivo_seleccionado = tk.StringVar()
        self.opcion_tamaño = tk.IntVar(value=1)
        self.ancho_custom = tk.StringVar(value="122")
        self.alto_custom = tk.StringVar(value="250")
        self.imagen_preview = None
        
        # Crear carpetas si no existen
        self.crear_carpetas()
        
        # Construir interfaz
        self.construir_interfaz()
    
    def crear_carpetas(self):
        """Crea las carpetas originales y binarios si no existen"""
        Path("originales").mkdir(exist_ok=True)
        Path("binarios").mkdir(exist_ok=True)
    
    def construir_interfaz(self):
        # Frame principal dividido en dos columnas
        frame_principal = ttk.Frame(self.root)
        frame_principal.pack(fill="both", expand=True, padx=5, pady=5)
        
        # Columna izquierda (controles)
        frame_izquierda = ttk.Frame(frame_principal)
        frame_izquierda.pack(side="left", fill="both", expand=False, padx=5)
        
        # Columna derecha (preview)
        frame_derecha = ttk.LabelFrame(frame_principal, text="Vista Previa", padding=10)
        frame_derecha.pack(side="right", fill="both", expand=True, padx=5)
        
        # === COLUMNA IZQUIERDA ===
        
        # Título
        titulo = tk.Label(frame_izquierda, text="Conversor E-Ink",
                         font=("Arial", 14, "bold"))
        titulo.pack(pady=10)
        
        # Frame de selección de archivo
        frame_archivo = ttk.LabelFrame(frame_izquierda, text="Archivo de Entrada", padding=10)
        frame_archivo.pack(pady=10, fill="x")
        
        self.label_archivo = ttk.Label(frame_archivo, text="Ningún archivo seleccionado",
                                       foreground="gray", wraplength=350)
        self.label_archivo.pack(pady=5)
        
        ttk.Button(frame_archivo, text="Seleccionar imagen",
                  command=self.seleccionar_archivo).pack(pady=5)
        
        # Frame de opciones de tamaño
        frame_opciones = ttk.LabelFrame(frame_izquierda, text="Opciones de Tamaño", padding=10)
        frame_opciones.pack(pady=10, fill="x")
        
        # Opción 1: Mantener tamaño original
        ttk.Radiobutton(frame_opciones, text="Mantener tamaño original",
                       variable=self.opcion_tamaño, value=1,
                       command=self.actualizar_estado_campos).pack(anchor="w", pady=3)
        
        # Opción 2: Tamaño fijo 122x250
        ttk.Radiobutton(frame_opciones, text="122 × 250 píxeles",
                       variable=self.opcion_tamaño, value=2,
                       command=self.actualizar_estado_campos).pack(anchor="w", pady=3)
        
        # Opción 3: Tamaño personalizado
        ttk.Radiobutton(frame_opciones, text="Tamaño personalizado:",
                       variable=self.opcion_tamaño, value=3,
                       command=self.actualizar_estado_campos).pack(anchor="w", pady=3)
        
        # Frame para entrada personalizada
        frame_custom = ttk.Frame(frame_opciones)
        frame_custom.pack(anchor="w", padx=20, pady=3)
        
        ttk.Label(frame_custom, text="Ancho:").grid(row=0, column=0, padx=3)
        self.entry_ancho = ttk.Entry(frame_custom, textvariable=self.ancho_custom, width=8)
        self.entry_ancho.grid(row=0, column=1, padx=3)
        
        ttk.Label(frame_custom, text="Alto:").grid(row=0, column=2, padx=3)
        self.entry_alto = ttk.Entry(frame_custom, textvariable=self.alto_custom, width=8)
        self.entry_alto.grid(row=0, column=3, padx=3)
        
        # Botón de conversión
        self.btn_convertir = ttk.Button(frame_izquierda, text="Convertir Imagen",
                                       command=self.convertir, state="disabled")
        self.btn_convertir.pack(pady=15)
        
        # Área de estado
        frame_estado = ttk.LabelFrame(frame_izquierda, text="Estado", padding=10)
        frame_estado.pack(pady=5, fill="x")
        
        self.label_estado = tk.Label(frame_estado, text="Seleccione una imagen",
                                     justify="left", anchor="w", wraplength=350)
        self.label_estado.pack(fill="x")
        
        # === COLUMNA DERECHA (PREVIEW) ===
        
        # Canvas para mostrar la imagen
        self.canvas = Canvas(frame_derecha, bg="white", highlightthickness=1, 
                            highlightbackground="gray")
        self.canvas.pack(fill="both", expand=True)
        
        # Label para información de la imagen
        self.label_info_preview = ttk.Label(frame_derecha, text="", 
                                           justify="center", font=("Arial", 9))
        self.label_info_preview.pack(pady=5)
        
        # Inicializar estado de campos
        self.actualizar_estado_campos()
    
    def actualizar_estado_campos(self):
        """Habilita/deshabilita campos según la opción seleccionada"""
        if self.opcion_tamaño.get() == 3:
            self.entry_ancho.config(state="normal")
            self.entry_alto.config(state="normal")
        else:
            self.entry_ancho.config(state="disabled")
            self.entry_alto.config(state="disabled")
    
    def seleccionar_archivo(self):
        """Abre diálogo para seleccionar archivo """
        archivo = filedialog.askopenfilename(
            title="Seleccionar imagen",
            initialdir="originales",
            filetypes=[
                      ("Imágenes", "*.png *.jpg *.jpeg *.bmp *.gif *.tiff *.webp"),
                      ("Archivos PNG", "*.png"),
                      ("Archivos JPG", "*.jpg *.jpeg"),
                      ("Todos los archivos", "*.*")
                      ]
        )
        
        if archivo:
            self.archivo_seleccionado.set(archivo)
            self.btn_convertir.config(state="normal")
            nombre_archivo = os.path.basename(archivo)
            self.label_archivo.config(text=nombre_archivo, foreground="blue")
            self.label_estado.config(text=f"Listo para convertir: {nombre_archivo}")
    
    def mostrar_preview(self, img, ancho, alto, tamaño_bytes):
        """Muestra la imagen convertida en el canvas"""
        # Limpiar canvas
        self.canvas.delete("all")
        
        # Calcular escala para ajustar al canvas
        canvas_ancho = self.canvas.winfo_width()
        canvas_alto = self.canvas.winfo_height()
        
        # Esperar a que el canvas tenga dimensiones válidas
        if canvas_ancho <= 1 or canvas_alto <= 1:
            self.root.update()
            canvas_ancho = self.canvas.winfo_width()
            canvas_alto = self.canvas.winfo_height()
        
        # Calcular escala manteniendo proporción
        escala_ancho = (canvas_ancho - 40) / ancho
        escala_alto = (canvas_alto - 40) / alto
        escala = min(escala_ancho, escala_alto, 1.0)  # No ampliar más allá del tamaño original
        
        # Redimensionar para preview si es necesario
        if escala < 1.0:
            nuevo_ancho = int(ancho * escala)
            nuevo_alto = int(alto * escala)
            img_preview = img.resize((nuevo_ancho, nuevo_alto), Image.NEAREST)
        else:
            img_preview = img
        
        # Convertir para mostrar en Tkinter
        self.imagen_preview = ImageTk.PhotoImage(img_preview)
        
        # Centrar en canvas
        x_pos = canvas_ancho // 2
        y_pos = canvas_alto // 2
        self.canvas.create_image(x_pos, y_pos, image=self.imagen_preview)
        
        # Actualizar información
        info = f"{ancho} × {alto} px | {tamaño_bytes} bytes"
        self.label_info_preview.config(text=info)
    
    def convertir(self):
        """Ejecuta la conversión de la imagen"""
        archivo_entrada = self.archivo_seleccionado.get()
        
        if not archivo_entrada:
            messagebox.showwarning("Advertencia", "Debe seleccionar un archivo primero")
            return
        
        try:
            # Determinar dimensiones según opción
            if self.opcion_tamaño.get() == 1:
                # Mantener original
                ancho, alto = None, None
            elif self.opcion_tamaño.get() == 2:
                # Tamaño fijo
                ancho, alto = 122, 250
            else:
                # Personalizado
                try:
                    ancho = int(self.ancho_custom.get())
                    alto = int(self.alto_custom.get())
                    if ancho <= 0 or alto <= 0:
                        raise ValueError
                except ValueError:
                    messagebox.showerror("Error", "Las dimensiones deben ser números enteros positivos")
                    return
            
            # Generar nombre de archivo de salida
            nombre_base = os.path.splitext(os.path.basename(archivo_entrada))[0]
            archivo_salida = os.path.join("binarios", f"{nombre_base}.bin")
            
            # Convertir
            self.label_estado.config(text="Convirtiendo imagen...")
            self.root.update()
            
            img_resultado, ancho_final, alto_final, tamaño_bytes = convertir_imagen(
                archivo_entrada, archivo_salida, ancho, alto
            )
            
            # Mostrar preview
            self.mostrar_preview(img_resultado, ancho_final, alto_final, tamaño_bytes)
            
            # Actualizar estado
            self.label_estado.config(
                text=f"✓ Conversión exitosa\n{archivo_salida}\n{ancho_final}×{alto_final} px, {tamaño_bytes} bytes"
            )
            
            messagebox.showinfo("Éxito", f"Imagen convertida correctamente:\n\n{archivo_salida}")
            
        except Exception as e:
            messagebox.showerror("Error", f"Error durante la conversión:\n{str(e)}")
            self.label_estado.config(text="Error en la conversión")

# -----------------------------
#  PUNTO DE ENTRADA
# -----------------------------
if __name__ == "__main__":
    root = tk.Tk()
    app = ConversorEInkGUI(root)
    root.mainloop()