/*
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * muCommander is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.mucommander.commons.io.base64;

import java.io.IOException;
import java.io.OutputStream;


/**
 * This <code>OuputStream</code> encodes supplied data to Base64 encoding and writes it to the underlying
 * <code>OutputStream</code>.
 *
 * @see Base64Encoder
 * @author Maxence Bernard, with the exception of the algorithm description which was found on the Web.
 */
public class Base64OutputStream extends OutputStream {
    /*
      Base64 uses a 65 character subset of US-ASCII,
      allowing 6 bits for each character so the character
      "m" with a Base64 value of 38, when represented
      in binary form, is 100110.

      With a text string, let's say "men" is encoded this
      is what happens :

      The text string is converted into its US-ASCII value.

      The character "m" has the decimal value of 109
      The character "e" has the decimal value of 101
      The character "n" has the decimal value of 110

      When converted to binary the string looks like this :

      m   01101101
      e   01100101
      n   01101110

      These three "8-bits" are concatenated to make a
      24 bit stream
      011011010110010101101110

      This 24 bit stream is then split up into 4 6-bit
      sections
      011011 010110 010101 101110

      We now have 4 values. These binary values are
      converted to decimal form
      27     22     21     46

      And the corresponding Base64 character are :
      b      W       V     u

      The encoding is always on a three characters basis
      (to have a set of 4 Base64 characters). To encode one
      or two then, we use the special character "=" to pad
      until 4 base64 characters is reached.

      ex. encode "me"

      01101101  01100101
      0110110101100101
      011011 010110 0101
      111111    (AND to fill the missing bits)
      011011 010110 010100
      b     W      U
      b     W      U     =  ("=" is the padding character)

      so "bWU="  is the base64 equivalent.

      encode "m"

      01101101
      011011 01
      111111         (AND to fill the missing bits)
      011011 010000
      b     Q     =  =   (two paddings are added)

      Finally, MIME specifies that lines are 76 characters wide maximum.

    */

    /** Underlying OutputStream encoded data is sent to */
    private OutputStream out;

    /** The Base64 encoding table */
    private final byte[] encodingTable;

    /** The character used for padding */
    private final byte paddingChar;

    /** Array used to accumulate the first 2 bytes of a 3-byte group */
    private byte byteAcc[] = new byte[2];
	
    /** Number of bytes accumulated to form a 3-byte group */
    private int nbBytesWaiting;

    /** Specifies whether line breaks should be inserted after 80 chars */
    private boolean insertLineBreaks;

    /** Current line length (to insert line return character after 80 chars)*/
    private int lineLength;


    /**
     * Equivalent to calling {@link #Base64OutputStream(java.io.OutputStream, boolean, Base64Table)} with
     * a {@link Base64Table#STANDARD_TABLE} table.
     *
     * @param out the underlying OutputStream to write the base64-encoded data to
     * @param insertLineBreaks if <code>true</code>, line breaks will be inserted after every 80 characters written
     */
    public Base64OutputStream(OutputStream out, boolean insertLineBreaks) {
        this(out, insertLineBreaks, Base64Table.STANDARD_TABLE);
    }

    /**
     * Creates a new <code>Base64OutputStream</code> using the underlying OutputStream and table to write the
     * base64-encoded data.
     *
     * @param out the underlying OutputStream to write the base64-encoded data to
     * @param insertLineBreaks if <code>true</code>, line breaks will be inserted after every 80 characters written
     * @param table the table to use to encode data
     */
    public Base64OutputStream(OutputStream out, boolean insertLineBreaks, Base64Table table) {
        this.out = out;
        this.insertLineBreaks = insertLineBreaks;
        this.encodingTable = table.getEncodingTable();
        this.paddingChar = table.getPaddingChar();
    }

    /**
     * Writes padding '=' characters to the underlying <code>OutputStream</code> if there currently is an
     * unfinished 3-byte group. If it's not the case, then this method is a no-op.
     *
     * @throws IOException if the padding characters could not be written to the underlying OutputStream.
     */
    public void writePadding() throws IOException {
        // No padding needed
        if(nbBytesWaiting==0)
            return;

        // 1 padding character
        if (nbBytesWaiting==2) {
            // 2 bytes left
            out.write(encodingTable[(byte)((byteAcc[0] & 0xFC) >> 2)]);
            out.write(encodingTable[(byte)(((byteAcc[0] & 0x03) << 4) | ((byteAcc[1] & 0xF0) >> 4))]);
            out.write(encodingTable[(byte)((byteAcc[1] & 0x0F) << 2)]);
            out.write(paddingChar);
        }
        // 2 padding characters
        else if (nbBytesWaiting==1) {
            // 1 byte left
            out.write(encodingTable[(byte)((byteAcc[0] & 0xFC) >> 2)]);
            out.write(encodingTable[(byte)((byteAcc[0] & 0x03) << 4)]);
            out.write(paddingChar);
            out.write(paddingChar);
        }

        // Just in case this method is called again
        nbBytesWaiting = 0;
    }


    /////////////////////////////////
    // OutputStream implementation //
    /////////////////////////////////

    @Override
    public void write(int i) throws IOException {
        // We have a 3-byte group
        if(nbBytesWaiting==2) {
            // Write 3 bytes as 4 base64 characters
            out.write(encodingTable[(byte)((byteAcc[0] & 0xFC) >> 2)]);
            out.write(encodingTable[(byte)(((byteAcc[0] & 0x03) << 4) | ((byteAcc[1] & 0xF0) >> 4))]);
            out.write(encodingTable[(byte)(((byteAcc[1] & 0x0F) << 2) | ((i & 0xC0) >> 6))]);
            out.write(encodingTable[(byte)(i & 0x3F)]);

            nbBytesWaiting = 0;

            // Insert a line break after every 80 characters written
            if (insertLineBreaks && (lineLength += 4) >= 76) {
                out.write('\r');
                out.write('\n');
                lineLength = 0;
            }
        }
        // Waiting for more bytes...
        else {
            byteAcc[nbBytesWaiting++] = (byte)i;
        }
    }

    /**
     * Writes padding if necessary and closes the underlying stream.
     */
    @Override
    public void close() throws IOException {
        writePadding();
        out.close();
    }
}
