package fr.iutfbleau.sae.mpif;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Cette classe permet de lire un fichier .pif et de reconstruire l'image d'origine.
 * Elle lit d'abord l'en-tête puis les longueurs des codes canoniques.
 * Ensuite elle reconstruit les codes, construit les arbres de décodage
 * et décode les pixels un par un pour obtenir l'image RGB finale.
 * Elle est utilisée par le programme Viewer pour afficher une image .pif.
 */
public class PIFReader {

    private int width;
    private int height;
    private int[] lenR;
    private int[] lenG;
    private int[] lenB;

    /**
     * Cette methode sert de point d'entrer pour le decodage d'un pif.
     * Elle Lit complètement un fichier .pif et renvoie l'image décodée.
     * Elle lit l'en-tête, récupère les longueurs des codes,
     * reconstruit les codes canoniques puis les arbres nécessaires
     * au décodage. Enfin elle lit pixel par pixel.
     *
     * @param file fichier .pif à décoder
     * @return l'image obtenue après décodage
     * @throws Exception si le fichier est invalide ou si une erreur survient pendant la lecture
     */
    public RGBImage decodePifFile(File file) throws Exception {
        FileInputStream fis = new FileInputStream(file);
        BufferedInputStream bos = new BufferedInputStream(fis);
        BitInputStream lecteur = new BitInputStream(bos);
        
        // Je lis l'en-tête et les tables canoniques
        this.readHeader(lecteur);
        this.readCanonicalTables(lecteur);

        // Je reconstruis les tables canoniques car dans le fichier on a juste les longueurs en bits
        Map<String, Integer> canonR = rebuildCanonical(lenR);
        Map<String, Integer> canonG = rebuildCanonical(lenG);
        Map<String, Integer> canonB = rebuildCanonical(lenB);

        // Je construis les arbres de décodage
        DecodeNode trieR = buildDecodageTree(canonR);
        DecodeNode trieG = buildDecodageTree(canonG);
        DecodeNode trieB = buildDecodageTree(canonB);

        // Je décode les pixels
        RGBImage img = decodePixels(lecteur, trieR, trieG, trieB);

        lecteur.closeFlux();
        return img;
    }


    /**
     * Lit l'en-tête du fichier .pif. On y récupère la largeur
     * et la hauteur de l'image, chacune codée sur 16 bits.
     *
     * @param in flux binaire à lire
     * @throws IOException si la lecture échoue
     */
    public  void readHeader(BitInputStream in) throws IOException {
        this.width = in.readBits(16);
        this.height = in.readBits(16);
    }
    

    /**
     * Lit les longueurs des code canoniques pour les trois composantes
     * rouge, vert et bleu. Chaque table contient 256 valeurs sur 5 bits.
     * Ces longueurs permettront  de reconstruire les vrais codes plus tard
     * 
     * @param in flux binaire d'entrée pdfpdof
     * @throws IOException si erreur de lecture se produit
     */
    public void readCanonicalTables(BitInputStream in) throws IOException {
        // Table Rouge
        this.lenR = new int[256];
        for (int i = 0; i < 256; i++){
            this.lenR[i] = in.readBits(8);
        }

        // Table Vert
        this.lenG = new int[256];
        for (int i = 0; i < 256; i++){
            this.lenG[i] = in.readBits(8);
        }

        // Table Bleu
        this.lenB = new int[256];
        for (int i = 0; i < 256; i++){
            this.lenB[i] = in.readBits(8);
        }
    }


    /**
     * Reconstruit les codes canoniques à partir des longueurs stockées dans le fichier.
     * On trie d'abord les paires (symbole, longueur), puis on génère les codes
     * en appliquant la règle des codes canoniques.
     *
     * @param lengths tableau contenant les longueurs des codes pour 256 symboles
     * @return une table qui associe un code binaire (sous forme de texte) à son symbole
     */
    public Map<String, Integer> rebuildCanonical(int[] lengths) {
        // je cree une liste de paires (symbole, longueur)
        List<Map.Entry<Integer, Integer>> entiers = new ArrayList<>();

        for (int i = 0; i < lengths.length; i++) {
            if (lengths[i] > 0) {
                entiers.add(new java.util.AbstractMap.SimpleEntry<>(i, lengths[i]));
            }
        }

        // Je trie par longueur croissante, puis par symbole croissant
        ComparateurEntreeCanonique comparateur = new ComparateurEntreeCanonique();
        entiers.sort(comparateur);

        // je genere les codes canoniques
        Map<String, Integer> codes = new HashMap<>();
        int code = 0;
        int previousLength = 0;
        
        for (Map.Entry<Integer, Integer> entry : entiers) {
            int symbol = entry.getKey();
            int length = entry.getValue();
            
            // Decalage pour aligner le code sur la longueur courante
            code <<= (length - previousLength);

            // je convertit ce code en texte binaire 
            String codeStr = Integer.toBinaryString(code);
            // On s'assure que la chaîne a la bonne longueur en ajoutant des zéros à gauche si nécessaire
            while (codeStr.length() < length) {
                codeStr = "0" + codeStr;
            }        
            // je restocke le code + symbole en inversant car la map est inverse dans l'encodage
            codes.put(codeStr, symbol);
        
            code++;
            previousLength = length;
            
        }

        return codes;
    }

    /**
     * Construit un arbre de décodage à partir des codes canoniques.
     * Chaque code binaire définit un chemin dans l'arbre jusqu'à une feuille
     * contenant le symbole à décoder.
     *
     * @param codes dictionnaire associant le code binaire au symbole
     * @return la racine de l'arbre de décodage
     */
    public DecodeNode buildDecodageTree(Map<String,Integer> codes) {
        DecodeNode root = new DecodeNode(); //  la racine de larbre

        for (Map.Entry<String, Integer> entry : codes.entrySet()) {
            String code = entry.getKey(); // 101011101110 par exemple
            int symbol = entry.getValue(); // 0,1,2,3 etc.  par exemple

            DecodeNode current = root;

            // je parcours le code bit à bit 
            for (int i = 0; i < code.length(); i++) {
                char bit = code.charAt(i);
                
                if(i == code.length() - 1) {
                    // Dernier bit:  je cree une feuille avec la valeur du symbol
                    if(bit == '0') {
                        current.left = new DecodeNode(null, null, symbol);
                    } else {
                        current.right = new DecodeNode(null, null, symbol);
                    }
                } else {
                   // Si c'est pas le dernier bit : je creer un node interne 
                    if(bit == '0') {
                        if(current.left == null) {
                            current.left = new DecodeNode(); // Node interne
                        }
                        current = current.left; 
                    } else {
                        if(current.right == null) {
                            current.right = new DecodeNode(); // node intern
                        }
                        current = current.right;
                    }
                }
            }
        }
        return root;
    }



    /**
     * Décode l'ensemble des pixels de l'image en utilisant les trois arbres
     * correspondant aux composantes rouge, verte et bleue.
     * Chaque symbole est lu en parcourant l'arbre bit par bit.
     *
     * @param in flux binaire contenant les données des pixels
     * @param red arbre de décodage pour le rouge
     * @param green arbre pour le vert
     * @param blue arbre pour le bleu
     * @return l'image RGB reconstruite
     * @throws IOException si un symbole est invalide ou si la lecture échoue
    */
    public RGBImage decodePixels(BitInputStream in, DecodeNode red, DecodeNode green, DecodeNode blue) throws IOException{
       RGBImage image = new RGBImage(width, height);
        
       for(int y = 0; y < height; y++) {
           for(int x = 0; x < width; x++) {
              // je decode chaque composante en parcourant son arbre
               int r = decodeSymbole(in, red);
               int g = decodeSymbole(in, green);
               int b = decodeSymbole(in, blue);

               // je cree et je place le pixel
               Pixel pixel = new Pixel(r, g, b);
               image.setPixel(x, y, pixel);
           }
        }
        return image;
    }
    
    /**
     * Décode un seul symbole en parcourant l'arbre tant qu'on n'est pas sur une feuille.
     * Chaque bit lu détermine si on part à gauche ou à droite dans l'arbre.
     *
     * @param in flux binaire source
     * @param root racine de l'arbre de décodage
     * @return la valeur du symbole trouvé
     * @throws IOException si un chemin est invalide ou si la lecture échoue
     */

    private int decodeSymbole(BitInputStream in, DecodeNode root) throws IOException {
        DecodeNode current = root;
        
        // je parcours l'arbre en suivant les bits du flux jusqu'à atteindre une feuille
        while (!current.isLeaf()) {
            int bit = in.readBit();
            if (bit == 0) {
                current = current.left;
            } else {
                current = current.right;
            }
            
            if (current == null) {
                throw new IOException("code invalide: noeud null rencontre");
            }
        }
        // si on est arrivé à une feuille, on retourne la valeur
        if (current.value == -1) {
            throw new IOException("Feuille sans valeur assignée");
        }
        return current.value;
    }


    /**
     * Vérifie si un fichier est un fichier .pif valide.
     * On teste l'existence du fichier, son extension et une taille minimale
     * permettant au moins de contenir l'en-tête et les tables de longueurs.
     *
     * @param f fichier à tester
     * @return true si le fichier semble être un .pif valide, sinon false
     */
    public static boolean isPIFFile(File f) {
        if (f == null){
            return false;
        }
        if (!f.exists() || !f.isFile()){
            return false;
        }

        //je verifi l'extension
        String name = f.getName().toLowerCase();
        if (!name.endsWith(".pif")) {
            return false;
        }
        
        return f.length() >= 772; // taille minimal pour un fichier pif longueur largeur et tables de frequance
    }
}
