/*
 * 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.InputStream;

/**
 * <code>Base64InputStream</code> is an <code>InputStream</code> that decodes Base64-encoded data provided by
 * an underlying <code>InputStream</code>. The underlying data must be valid base64-encoded data with respect to
 * the character table in use. If not, an <code>IOException</code> will be thrown when illegal data is encountered.
 *
 * @see Base64Decoder
 * @author Maxence Bernard
 */
public class Base64InputStream extends InputStream {

    /** Underlying stream data is read from */
    private InputStream in;

    /** The Base64 decoding table */
    private final int[] decodingTable;

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

    /** Decoded bytes available for reading */
    private int readBuffer[] = new int[3];

    /** Index of the next byte available for reading in the buffer */
    private int readOffset;

    /** Number of bytes left for reading in the buffer */
    private int bytesLeft;

    /** Buffer used temporarily for decoding */
    private int decodeBuffer[] = new int[4];


    /**
     * Equivalent to calling {@link #Base64InputStream(java.io.InputStream, Base64Table)} with
     * a {@link Base64Table#STANDARD_TABLE} table.
     *
     * @param in underlying InputStream the Base64-encoded data is read from
     */
    public Base64InputStream(InputStream in) {
        this(in, Base64Table.STANDARD_TABLE);
    }

    /**
     * Creates a new <code>Base64InputStream</code> that allows to decode data that has been Base64-encoded using the
     * given table, from the provided <code>InputStream</code>.
     *
     * @param in underlying InputStream the Base64-encoded data is read from
     * @param table the table to use for Base64 decoding
     */
    public Base64InputStream(InputStream in, Base64Table table) {
        this.in = in;
        this.decodingTable = table.getDecodingTable();
        this.paddingChar = table.getPaddingChar();
    }


    ////////////////////////////////
    // InputStream implementation //
    ////////////////////////////////

    @Override
    public int read() throws IOException {
        // Read buffer empty: read and decode a new base64-encoded 4-byte group
        if(bytesLeft==0) {
            int read;
            int nbRead = 0;

            while(nbRead<4) {
                read = in.read();
                // EOF reached
                if(read==-1) {
                    if(nbRead%4 != 0) {
                        // Base64 encoded data must come in a multiple of 4 bytes, throw an IOException if the underlying stream ended prematurely
                        throw new IOException("InputStream did not end on a multiple of 4 bytes");
                    }

                    if(nbRead==0)
                        return -1;
                    else    // nbRead==4
                        break;
                }

                decodeBuffer[nbRead] = decodingTable[read];

                // Discard any character that's not a base64 character, without throwing an IOException.
                // In particular, '\r' and '\n' characters that are usually found in email attachments are simply ignored.
                if(decodeBuffer[nbRead]==-1 && read!=paddingChar) {
                    continue;
                }

                nbRead++;
            }

            // Decode byte 0
            readBuffer[bytesLeft++] = ((decodeBuffer[0]<<2)&0xFC | ((decodeBuffer[1]>>4)&0x03));

            // Test if the character is not a padding character
            if(decodeBuffer[2]!=-1) {
                // Decode byte 1
                readBuffer[bytesLeft++] = (decodeBuffer[1]<<4)&0xF0 | ((decodeBuffer[2]>>2)&0x0F);

                // Test if the character is a padding character
                if(decodeBuffer[3]!=-1)
                    // Decode byte 2
                    readBuffer[bytesLeft++] = ((decodeBuffer[2]<<6)&0xC0) | (decodeBuffer[3]&0x3F);
            }

            readOffset = 0;
        }

        bytesLeft--;

        return readBuffer[readOffset++];
    }
}
