# calibre_plugins/pdf_to_cbz/ui.py

import re
import unicodedata
import io
import sys
import os
import platform
import tempfile
import zipfile
from PIL import Image
from calibre.gui2.actions import InterfaceAction
from calibre.utils.logging import default_log as log
from calibre.gui2 import error_dialog
from calibre.ptempfile import TemporaryDirectory
from calibre.gui2.threaded_jobs import ThreadedJob, ThreadedJobServer


try:
    from PyQt5.Qt import (QMenu, QIcon, QPixmap,
                          QWidget, QVBoxLayout, QLabel, QProgressBar, Qt, QApplication)
except ImportError:
    from PyQt4.Qt import (QMenu, QIcon, QPixmap,
                          QWidget, QVBoxLayout, QLabel, QProgressBar, Qt, QApplication)



class ProgressWindow(QWidget):
    """
    Fenêtre simple de progression pour un plugin Calibre.

    Cette fenêtre contient :
    - un label supérieur pour indiquer l'action en cours
    - une barre de progression
    - un label avec le pourcentage actuel

    Utilisation :
        win = ProgressWindow("Traitement du fichier...")
        win.show()
        win.update_progress(50)
        ...
        win.close()
    """

    def __init__(self, texte="Progression"):
        super().__init__()

        # --- Réglage de la fenêtre ---
        self.setWindowTitle("PDF ==> CBZ")
        self.setWindowModality(Qt.ApplicationModal)  # bloque l'interface si nécessaire
        self.setMinimumWidth(400)

        # --- Layout principal ---
        layout = QVBoxLayout()
        self.setLayout(layout)

        # --- Label descriptif ---
        self.label_info = QLabel(texte)
        self.label_info.setAlignment(Qt.AlignCenter)
        layout.addWidget(self.label_info)

        # --- Barre de progression ---
        self.progress_bar = QProgressBar()
        self.progress_bar.setMinimum(0)
        self.progress_bar.setMaximum(100)
        layout.addWidget(self.progress_bar)

        # --- Label pour le pourcentage ---
        self.label_percent = QLabel("0 %")
        self.label_percent.setAlignment(Qt.AlignCenter)
        #layout.addWidget(self.label_percent)

    def update_progress(self, percent, message=None):
        """
        Met à jour l'avancement de la barre.

        Arguments :
            percent : entier entre 0 et 100
            message : texte optionnel pour mettre à jour le label supérieur
        """

        # Mise à jour du texte si fourni
        if message is not None:
            self.label_info.setText(message)

        # Mise à jour de la barre
        self.progress_bar.setValue(percent)

        # Mise à jour du % affiché
        self.label_percent.setText(f"{percent} %")

        # Forcer Qt à rafraîchir la fenêtre immédiatement
        self.repaint()
        QApplication.processEvents()







def load_pypdf_from_whl():

    # Détection du ZIP du plugin
    plugin_path = __file__
    if ".zip" in plugin_path:
        plugin_zip = plugin_path.split(".zip")[0] + ".zip"
    else:
        plugin_zip = None

    # Dossier temporaire fixe pour Calibre
    temp_dir = os.path.join(tempfile.gettempdir(), "pypdf_calibre")
    os.makedirs(temp_dir, exist_ok=True)

    # Recherche et extraction du .whl
    selected_whl = None
    if plugin_zip and zipfile.is_zipfile(plugin_zip):
        log.info(f"pdftocbz:📦 Plugin zip detected : {plugin_zip}")
        with zipfile.ZipFile(plugin_zip, "r") as z:
            # Chercher le .whl correspondant
            whl_files = [f for f in z.namelist() if f.endswith(".whl")]
            for f in whl_files:
                if "pypdf" in f:
                    selected_whl = f
                    break

            # Extraction du .whl dans le temp_dir
            log.info(f"pdftocbz:⏬ Extracting {selected_whl} to {temp_dir}")
            z.extract(selected_whl, temp_dir)
            whl_path = os.path.join(temp_dir, selected_whl)
            # Extraire le contenu du .whl
            with zipfile.ZipFile(whl_path, "r") as whl_zip:
                whl_zip.extractall(temp_dir)
    else:
        raise ImportError("Plugin zip not found, cannot load PyPDF2 ")

    # Ajouter le dossier temporaire au sys.path et importer fitz
    sys.path.insert(0, temp_dir)
    try:
        import pypdf
        return pypdf
    finally:
        sys.path.pop(0)
try:

    #fitz = load_fitz_from_whl()
    PyPDF2 = load_pypdf_from_whl()

    log.info("pdftocbz: ✅ PyMuPDF/fitz/PyPDF2  ok")
except Exception as e:
    log.info("pdftocbz: ⚠️ error import PyMuPDF :", e)

from functools import partial




class PDFtoCBZ(InterfaceAction):
    name = 'PDFtoCBZ'
    action_spec = ("PDF → CBZ", None, "Convert selected PDF books to CBZ", None)
    action_type = "global"

    def genesis(self):
        """
        Init GUI : créer l'action et la connecter.
        """
        log.info("pdftocbz: genesis() — initialisation GUI")
        base_plugin = self.interface_action_base_plugin
        self.dpi = base_plugin.options['dpi']
        self.ext = base_plugin.options['output_format']
        self.quality = base_plugin.options['compression']
        self.method = base_plugin.options['method']
        #log.info(self.dpi, self.ext)
        self.menu = QMenu(self.gui)
        # Get the icon for this interface action
        icon = self.get_icon('images/plugin.png')

        # The qaction is automatically created from the action_spec defined
        # above
        self.qaction.setMenu(self.menu)
        self.qaction.setIcon(icon)
        self.qaction.triggered.connect(self.convert_pdf_to_cbz)


        # build menu
        self.menu.clear()
        self.build_menu()
        #self.toggle_menu_items()


    def location_selected(self, loc):
        pass

    def library_changed(self, db):
        pass


    def convert_pdf_to_cbz(self):
        log.info("pdftocbz: convert_pdf_to_cbz called")
        db = self.gui.current_db.new_api
        base_plugin = self.interface_action_base_plugin
        self.dpi = base_plugin.options['dpi']
        self.ext = base_plugin.options['output_format']
        self.quality = base_plugin.options['compression']
        self.method = base_plugin.options['method']
        #log.info(f"pdftocbz: format {self.ext} avec compression {self.quality}")

        # récup IDs sélectionnés (essayer l'API standard, fallback si nécessaire)
        try:
            #log.info("pdftocbz: get selected")

            book_ids = list(self.gui.library_view.get_selected_ids())
            self.max_book = len(book_ids)
        except Exception:
            log.info("pdftocbz: view model")

            model = self.gui.library_view.model()
            rows = self.gui.library_view.selectionModel().selectedRows()
            book_ids = [model.id(r) for r in rows]

        if not book_ids:
            log.info("pdftocbz: aucun livre sélectionné")
            from calibre.gui2 import info_dialog
            info_dialog(self.gui, "PDF → CBZ", "Aucun livre sélectionné.")
            return

        done = []
        skipped = []
        errors = []

        for index, bid in enumerate(book_ids): #, start=1):
            meta = db.get_metadata(bid)
            self.titre = self.safe_filename(meta.title)
            log.info(f"pdftocbz: le livre {bid} est {meta.title}-{meta.series}")
            self.index_book = index
            try:
                fmts = tuple(db.formats(bid))  # tuple of format names like ('EPUB','PDF')
                fmts_up = [f.upper() for f in fmts]
                log.info(f"pdftocbz: le livre {bid} contient les formats = {fmts_up}")

                if "PDF" not in fmts_up:
                    skipped.append((bid, "Pas de PDF"))
                    log.info(f"pdftocbz: {bid} ignoré (pas de PDF)")
                    continue
                if "CBZ" in fmts_up or "CBR" in fmts_up:
                    skipped.append((bid, "CBZ/CBR déjà présent"))
                    log.info(f"pdftocbz: {bid} ignoré (CBZ/CBR déjà présent)")
                    continue

                # 5️⃣ Récupérer le chemin absolu du fichier PDF
                pdf_path = db.format_abspath(bid, 'PDF')
                if not os.path.exists(pdf_path):
                    log.info(f"pdftocbz:❌ Fichier PDF introuvable : {pdf_path}")
                    return

                # 6️⃣ Ouvrir le fichier avec PyMuPDF et convertir
                try:
                    #doc = fitz.open(pdf_path)
                    #log.info(f"pdftocbz:📄 Le document contient {doc.page_count} pages.")

                    #self.convert_one(doc, bid, db)
                    self.do_heavy_task(pdf_path, bid, db)

                    #doc.close()

                except Exception as e:
                    log.info(f"pdftocbz:⚠️ Erreur lors de l'ouverture du PDF : {e}")


            except Exception as e:
                log.exception(f"pdftocbz: ERREUR inattendue pour {bid}: {e}\n{traceback.format_exc()}")
                errors.append((bid, str(e)))


    def convert_one (self, pdf_path, bid, db, **kwargs):
        """ converti un doc pdf en image dans un rep temporaire puis ajoute a la libririe"""
        log.info(f"pdftocbz:✅ co1 : {bid}")
        #job = kwargs.get('job')
        base_plugin = self.interface_action_base_plugin
        self.dpi = base_plugin.options['dpi']
        self.ext = base_plugin.options['output_format']
        self.quality = base_plugin.options['compression']
        self.dimension = base_plugin.options['dimension']
        if self.dimension =='manga : 14x21 cm':
            hauteur = 21
        elif self.dimension =="comic : 17x26 cm":
            hauteur = 26
        else :
            hauteur = 32
        # --- récupération des objets spéciaux passés par Calibre ---
        notification = kwargs.get("notification")
        abort = kwargs.get("abort")
        log2 = kwargs.get("log")

        items_kwargs = list(kwargs.items())
        #doc = fitz.open(pdf_path)
        docpypdf = PyPDF2.PdfReader(pdf_path)
        #log.info(f"pdftocbz:📄 Le document contient {doc.page_count} pages.")
        log.info(f"pdftocbz:📄 document has {len(docpypdf.pages)} pages.")

        #log.info(f"pdftocbz:📄 kwargs: {items_kwargs} ")

        done = []
        skipped = []
        errors = []

        #log.info(f"pdftocbz:✅ co2 : {bid}")
        if log2 : log2.info(f"[PDFtoCBZ] 🔧 Début du traitement pour le livre {bid}, {self.dimension}")
        with TemporaryDirectory("pdf2cbz") as tdir:
            #log.info(f"pdftocbz:📄 document 1 pages.")

            for page_index in range(len(docpypdf.pages)):  # iterate over pdf pages
                #job.set_progress (page_index*100/len(doc))
                #log.info(f"pdftocbz:📄 document 2 pages.")

                pourcentage = int((
                                      (self.index_book + (page_index / len(docpypdf.pages)))
                                      / self.max_book
                              ) * 100)

                progress = page_index  / len(docpypdf.pages)
                if self.win:
                    self.win.update_progress(pourcentage, f"Book {self.index_book+1}/{self.max_book} page {page_index+1}/{len(docpypdf.pages)}")
                if notification:
                    notification.put(("progress", progress))
                    self.set_progress(progress)

                #page = doc[page_index]  # get the page
                #image_list = page.get_images(full=True)
                #log.info(f"pdftocbz:📄 document 3 pages.")

                try:
                    # Essayer de prendre la première image de la page
                    image_pypdf = docpypdf.pages[page_index].images[0]

                    # Essayer de charger l'image
                    image = Image.open(io.BytesIO(image_pypdf.data))

                except Exception:
                    # Si l'image est absente ou illisible → image blanche
                    image = Image.new("RGB", (800, 1200), "white")

                #image_pypdf = docpypdf.pages[page_index].images[0]
                #image = Image.open(io.BytesIO(image_pypdf.data))
                #log.info(f"pdftocbz:📄 document 4 pages.")

                if abort and abort.is_set():
                    log.info("[PDFtoCBZ] 🚫 Cancel job ...")
                    if notification:
                        notification.put(("abort", f"Job {bid} canceled"))
                    return
                # print the number of images found on the page

                #xref = img[0]  # get the XREF of the image
                #width = img[2]

                #height = img[3]
                height = image.height
                #pix = fitz.Pixmap(doc, xref)  # create a Pixmap
                #log.info(f"pdftocbz:dim {height}__")

                #if pix.n - pix.alpha > 3:  # CMYK: convert to RGB first
                #    pix = fitz.Pixmap(fitz.csRGB, pix)

                temppix = os.path.join(tdir, f"{self.titre}_p{page_index:03d}.{self.ext}")                # Calcul de la hauteur en cm
                reso = image.info.get("dpi", (72,72))  # défaut 72 si non spécifié
                hauteur_cm = (height / reso[1]) * 2.54

                # Si la hauteur est supérieure à 30cm, ajuster la résolution
                if hauteur_cm > hauteur:
                    resolution = int((height / hauteur) * 2.54)
                reso = (resolution, resolution)  # Nouvelle résolution
                #parametre de sauvegarde pillow
                save_params = {
                    "optimize": True,
                    "dpi": reso
                }
                #log.info(f"pdftocbz:dim  {height};{resolution}; ")

                # --- Sauvegarde selon le format ---
                if self.ext.lower() in ("jpg", "jpeg"):
                    if image.mode in ("RGBA", "P"):
                        image = image.convert("RGB")
                    save_params["quality"] = self.quality
                    #log.info("pdftocbz: jpg", self.quality)
                    format_ext = "JPEG"

                    #pix.save(temppix) #, jpg_quality = self.quality)
                    # open(temppix, "wb") as fp:
                    #    fp.write(image_pypdf.data)
                else:
                    #log.info("pdftocbz: png")
                    format_ext = 'PNG'

                    #pix.save(temppix)
                #log.info(f"pdftocbz: save...{save_params}")

                image.save(temppix, format= format_ext, **save_params)  # save the image
                log.info(f"pdftocbz: saved {bid} so {int(progress*100)}%")

            cbz_path = os.path.join(tdir, f"{bid}.cbz")
            try:
                self.make_cbz_from_files_in_dir(tdir, cbz_path)
            except Exception as e:
                log.exception(f"pdftocbz: création CBZ échouée pour {bid}: {e}")
                errors.append((bid, str(e)))
            # ajouter le CBZ au livre (ne remplace pas le PDF)
            try:
                added = db.add_format(bid, "CBZ", cbz_path, replace=False)
                if added:
                    log.info(f"pdftocbz: CBZ ajouté en base pour {bid}")
                    done.append(bid)
                else:
                    log.error(f"pdftocbz: db.add_format a retourné False pour {bid}")
                    errors.append((bid, "db.add_format a retourné False"))
            except Exception as e:
                log.exception(f"pdftocbz: erreur ajout CBZ DB pour {bid}: {e}")
                errors.append((bid, str(e)))
        #return cbz_path
        #progress.end()
        #doc.close()

    def make_cbz_from_files_in_dir(self, temp_dir, cbz_path):
        """
        make_cbz_from_files_in_dir(temp_dir, cbz_path)
        ----------------------------------------------
        Crée un fichier CBZ à partir de toutes les images présentes dans un répertoire temporaire.

        Paramètres :
            temp_dir (str) : chemin du répertoire contenant les images (ex: 'pdf2cbz_temp')
            cbz_path (str) : chemin complet du fichier CBZ à créer (ex: 'output/book.cbz')

        Fonctionnement :
            - Liste toutes les images du dossier temporaire
            - Trie les fichiers par ordre alphabétique (pour garder les pages dans le bon ordre)
            - Crée un fichier .cbz (Comic Book Zip)
        """
        # Vérifie que le dossier existe
        if not os.path.isdir(temp_dir):
            raise FileNotFoundError(f"Le répertoire {temp_dir} n'existe pas.")

        # Récupère toutes les images du dossier
        image_files = [f for f in os.listdir(temp_dir)
                       if f.lower().endswith((".png", ".jpg", ".jpeg"))]

        if not image_files:
            raise ValueError("Aucune image trouvée dans le répertoire temporaire.")

        # Trie les fichiers pour conserver l’ordre des pages
        image_files.sort()

        # Création du CBZ (en réalité une archive ZIP renommée)
        with zipfile.ZipFile(cbz_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
            for img_name in image_files:
                img_path = os.path.join(temp_dir, img_name)
                zipf.write(img_path, arcname=img_name)

        log.info(f"pdftocbz:✅ CBZ créé avec succès : {cbz_path}")

    def show_settings(self):
        log.info("pdftocbz: show_settings called")
        self.do_user_config()

    def build_menu(self):

        # add configuration entry
        self.menu_action("configure", "Configure",
                         partial(self.interface_action_base_plugin.do_user_config, (self.gui)))

    def menu_action(self, name, title, triggerfunc):
        action = self.create_menu_action(self.menu, name, title, icon=None,
                                         shortcut=None, description=None,
                                         triggered=triggerfunc, shortcut_name=None)
        setattr(self, name, action)

    def get_icon(self, icon_name):
        import os
        from calibre.utils.config import config_dir

        # Check to see whether the icon exists as a Calibre resource
        # This will enable skinning if the user stores icons within a folder like:
        # ...\AppData\Roaming\calibre\resources\images\Plugin Name\
        icon_path = os.path.join(config_dir, 'resources', 'images', self.name,
                                 icon_name.replace('images/', ''))
        if os.path.exists(icon_path):
            pixmap = QPixmap()
            pixmap.load(icon_path)
            return QIcon(pixmap)
        # As we did not find an icon elsewhere, look within our zip resources
        return get_icons(icon_name)


    def do_heavy_task(self, pdf, bid, db):
        """en test : Lancée depuis un bouton du plugin."""
        log.info(f"pdftocbz:✅ 1 : {bid}")
        base_plugin = self.interface_action_base_plugin

        self.method = base_plugin.options['method']
        # Créer server + job
        server = ThreadedJobServer()
        log.info(f"pdftocbz:✅ 2 : {bid}")

        job = ThreadedJob(
            f'convert one pdf in cbz {bid}',                   # Identifiant du job
            f'run conversion PDF... {bid}',  # Message dans la barre de progression
            self.convert_one,           # Fonction à exécuter en tâche de fond
            (pdf, bid, db),                           # Arguments de la fonction
            {},                         # Mêmes choses, mais nommés
            self.on_done               # Callback quand fini
            #gui=True

        )
        log.info(f"pdftocbz:✅ conversion method : {self.method}")


        # job souci KeyError: 'convert one pdf in cbz'
        #self.gui.job_manager.run_job(job, 'convert one pdf in cbz')
        win = None
        if self.method == 3:
            # job en tache
            self.gui.job_manager.run_threaded_job(job)
            gui.status_bar.show_message(_('convert pdf in cbz started'), 3000)

        elif self.method == 1:
            #les fichiers les un derrieres les autres en taches principales
            self.win = ProgressWindow("Analyse des métadonnées…")
            self.win.show()
            self.convert_one(pdf, bid, db)
            self.win.close()
        else:
            # Lancer : add_job démarre le serveur si nécessaire et lance le job
            server.add_job(job) # processus en parallele

        log.info(f"pdftocbz:✅ 4 : {bid}")



    def on_done(self, job):

        """📩 Callback appelée quand la tâche est terminée."""
        if job.failed:
            log.info('Erreur Le traitement a échoué')

            #log.error(f"Le job {job.name} a échoué")
            log.error(f"Exception : {job.exception}")  # objet Exception
            #log.error(f"Traceback :\n{job.traceback}")  # chaîne complète
        else:
            result = job.result
            log.info("pdf2cbz: conversion réalisée :", result)
            #self.gui.status_bar.show_message(f"pdf2cbz: Tâche terminée : {result}", 3000)



    def safe_filename(self, titre: str) -> str:
        """
        Transforme une chaîne en un nom de fichier sûr pour tous les OS :
        - Supprime les caractères interdits
        - Remplace les lettres accentuées par leur équivalent non accentué
        - Conserve uniquement lettres, chiffres, espaces, tirets et underscores

        Paramètres :
            titre : str -> chaîne d'origine

        Retour :
            str -> nom de fichier sécurisé
        """
        # Normaliser pour enlever les accents
        titre_norm = unicodedata.normalize('NFKD', titre)
        titre_norm = "".join([c for c in titre_norm if not unicodedata.combining(c)])

        # Supprimer tous les caractères non autorisés (conserver lettres, chiffres, espaces, - et _)
        titre_sure = re.sub(r'[^a-zA-Z0-9 _-]', '', titre_norm)

        # Supprimer espaces/tirets/underscores en début et fin
        titre_sure = titre_sure.strip(" _-")

        # Limiter à 255 caractères
        return titre_sure[:30]

