/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/*
 * We would like to eventually use Sun's base64 encoder, but it is not
 * public yet.  It is in the sun.misc package in JDK1.2.
 */
package org.mozilla.jss.util;

import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.PrintStream;

/**
 * An output stream filter that takes arbitrary bytes and outputs their
 * base64 encoding. Call flush() or close() to write out the final padding.
 * The class also automatically puts line breaks in the output stream.
 */
public class Base64OutputStream extends FilterOutputStream {

    /**
     * Create a stream that does not insert line breaks. To have line
     * breaks, use the other constructor.
     */
    public Base64OutputStream(PrintStream out) {
        super(out);
        charsOnLine = 0;
        inputCount = 0;
        input = new byte[3];
        doLineBreaks = false;
    }

    /**
     * @param quadsPerLine Number of 4-character blocks to write before
     *            outputting a line break. For example, for 76-characters per line,
     *            pass in 76/4 = 19.
     */
    public Base64OutputStream(PrintStream out, int quadsPerLine) {
        this(out);
        doLineBreaks = true;
        assert (quadsPerLine > 0);
        charsPerLine = quadsPerLine * 4;
    }

    @Override
    public void write(int oneByte) throws IOException {
        input[inputCount++] = (byte) (0xff & oneByte);
        if (inputCount == 3) {
            outputOneAtom();
            if (doLineBreaks) {
                if (charsOnLine == charsPerLine) {
                    ((PrintStream) out).println();
                    charsOnLine = 0;
                }
                assert (charsOnLine < charsPerLine);
            }
        }
        assert (inputCount < 3);
    }

    /**
     * This flushes the stream and closes the next stream downstream.
     */
    @Override
    public void close() throws IOException {
        flush();
        out.close();
    }

    /**
     * Calling this will put the ending padding on the base64 stream,
     * so don't call it until you have no data left. The class does no
     * unnecessary buffering, so you probably shouldn't call it at all.
     */
    @Override
    public void flush() throws IOException {
        if (inputCount > 0) {
            outputOneAtom();
        }
        if (doLineBreaks) {
            ((PrintStream) out).println();
            charsOnLine = 0;
        }
    }

    @Override
    public void write(byte[] buffer) throws IOException {
        write(buffer, 0, buffer.length);
    }

    @Override
    public void write(byte[] buffer, int offset, int count) throws IOException {
        int i;
        int byteCount;

        for (i = offset, byteCount = 0; byteCount < count; i++, byteCount++) {
            write(buffer[i]);
        }
    }

    private final static char encoding[] = {
            //   0   1   2   3   4   5   6   7
            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 0
            'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 1
            'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 2
            'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', // 3
            'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // 4
            'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // 5
            'w', 'x', 'y', 'z', '0', '1', '2', '3', // 6
            '4', '5', '6', '7', '8', '9', '+', '/' // 7
    };

    /**
     * Output 3 bytes of input as 4 bytes of base-64 encoded output.
     * If there are fewer than 3 bytes available, the output will be
     * padded according to the spec. Padding should only happen at the end
     * of the stream.
     */
    private void outputOneAtom() throws IOException {
        byte[] output = new byte[4];

        if (inputCount == 0) {
            return;
        }

        // If we have fewer than 3 bytes, pad the rest with zeroes
        if (inputCount < 3) {
            input[2] = 0;
            if (inputCount < 2) {
                input[1] = 0;
            }
        }

        // Do a base64 encoding
        output[0] = (byte) encoding[(input[0] >>> 2) & 0x3f];
        output[1] = (byte) encoding[(input[0] << 4) & 0x30 | (input[1] >>> 4) & 0xf];
        output[2] = (byte) encoding[(input[1] << 2) & 0x3c | (input[2] >>> 6) & 0x3];
        output[3] = (byte) encoding[input[2] & 0x3f];

        // add padding
        if (inputCount < 3) {
            output[3] = (byte) '=';
            if (inputCount < 2) {
                output[2] = (byte) '=';
            }
        }

        out.write(output);
        inputCount = 0;
        if (doLineBreaks) {
            charsOnLine += 4;
        }
    }

    //////////////////////////////////////////////////////////////////////
    // Private data
    //////////////////////////////////////////////////////////////////////
    private byte[] input; // buffered input, max 3 bytes (after 3 we write
                          // them out base64-encoded)
    private short inputCount; // number of bytes in input buffer
    private short charsOnLine; // number of bytes on the current line
    private int charsPerLine; // maximum characters per line
    private static final int DEFAULT_QUADS_PER_LINE = 16; //64 chars
    private boolean doLineBreaks;
}
