package fr.iutfbleau.sae.mpif;
import java.io.IOException;
import java.io.OutputStream;

/**
 * Décorateur de flux permettant l'écriture binaire à granularité du bit.
 * <p>
 * Cette classe encapsule un {@link OutputStream} existant et permet
 * l'écriture de bits individuellement ou par groupes.
 * Les bits sont accumulés afin de former des octets avant écriture.
 * </p>
 * <p>
 * Utilisée notamment pour l'encodage des fichiers compressés
 * (ex : format PIF utilisant des codes de Huffman).
 * </p>
 * @author Algassimou Pellel Diallo
 * @version 1.0
 * @since 2025-12-13
 */
public class BitOutputStream {

    /** Flux de sortie sous-jacent */
    private final OutputStream fluxSortie;

    /** Octet en cours de construction */
    private int octetEnConstruction;

    /** Position du prochain bit à écrire (de 7 à 0) */
    private int positionBit;

    /** Indique si le flux est fermé */
    private boolean fluxFerme;

    /**
     * Construit un écrivain binaire à partir d'un flux existant.
     *
     * @param fluxSortie flux de sortie à décorer
     * @throws IllegalArgumentException si le flux est nul
     */
    public BitOutputStream(OutputStream fluxSortie) {
        if (fluxSortie == null) {
            throw new IllegalArgumentException("Le flux de sortie ne peut pas être nul");
        }
        this.fluxSortie = fluxSortie;
        this.octetEnConstruction = 0;
        this.positionBit = 7;
        this.fluxFerme = false;
    }

    /**
     * Écrit un bit dans le flux binaire.
     *
     * @param bit bit à écrire (0 ou 1)
     * @throws IOException si une erreur d'écriture survient
     * @throws IllegalArgumentException si le bit n'est ni 0 ni 1
     */
    public void writeBit(int bit) throws IOException {
        if (bit != 0 && bit != 1) {
            throw new IllegalArgumentException("Le bit doit être 0 ou 1");
        }
        if (fluxFerme) {
            throw new IOException("Le flux de sortie est fermé");        
        }
        if (bit == 1) {
            this.octetEnConstruction = this.octetEnConstruction | (1 << this.positionBit);
        }
        this.positionBit--;

        // si on atteint la fin de l'octet, on le grave dans le flux et rebolotte
        if(this.positionBit < 0){
            this.fluxSortie.write(this.octetEnConstruction);
            this.octetEnConstruction = 0;
            this.positionBit = 7;
        }

    }

    /**
     * Écrit une séquence de bits correspondant à une valeur entière.
     *
     * @param valeur valeur contenant les bits à écrire
     * @param nombreBits nombre de bits à écrire (strictement positif)
     * @throws IOException si une erreur d'écriture survient
     */
    public void writeBits(int valeur, int nombreBits) throws IOException {
        for (int i = nombreBits - 1; i >= 0; i--) {
            int bit = (valeur >> i) & 1;
            writeBit(bit);
        }
    }

    /**
    * Force l'écriture immédiate des données accumulées dans le flux sous-jacent.
    *
    * @throws IOException si une erreur survient lors du flush
    */
    public void flush() throws IOException {
        if (fluxFerme) {
            throw new IOException("Le flux de sortie est fermé");
        }

        // Si l'octet nes pas vide on le complete avec des 0
        if(this.positionBit < 7){
            this.fluxSortie.write(this.octetEnConstruction);
            this.octetEnConstruction = 0;
            this.positionBit = 7;
        }
            
        this.fluxSortie.flush(); // Force l'écriture dans le flux sous-jacent dans le but de vider le buffer
    }

    /**
     * Écrit une séquence de bits à partir d'une chaîne de '0' et '1'.
     *
     * @param codeBinaire chaîne contenant uniquement '0' et '1'
     * @throws IOException si une erreur d'écriture survient
     * @throws IllegalArgumentException si la chaîne contient autre chose que '0' ou '1'
     */
    public void writeBitString(String codeBinaire) throws IOException {
        if (codeBinaire == null) {
            throw new IllegalArgumentException("Le code binaire ne peut pas être null");
        }
        
        for (int i = 0; i < codeBinaire.length(); i++) {
            char c = codeBinaire.charAt(i);
            if (c == '0') {
                writeBit(0);
            } else if (c == '1') {
                writeBit(1);
            } else {
                throw new IllegalArgumentException("Le code binaire ne doit contenir que '0' et '1'");
            }
        }
    }

    /**
     * Vide les buffers internes et ferme le flux de sortie.
     *
     * @throws IOException si une erreur survient lors de la fermeture
     */
    public void fermerFlux() throws IOException {
        // si le flux n'est pas déjà fermé
        if (!fluxFerme) {
            this.flush(); // compléter l'octet et forcer l'écriture
            this.fluxSortie.close(); // fermer le flux sous-jacent
            this.fluxFerme = true; // marquer le flux comme fermé
        }
    }

   
}
