/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.geronimo.mail.util;

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

/**
 * An implementation of a FilterOutputStream that encodes the
 * stream data in UUencoding format.  This version does the
 * encoding "on the fly" rather than encoding a single block of
 * data.  Since this version is intended for use by the MimeUtilty class,
 * it also handles line breaks in the encoded data.
 */
public class UUEncoderStream extends FilterOutputStream {

    // default values included on the "begin" prefix of the data stream.
    protected static final int DEFAULT_MODE = 644;
    protected static final String DEFAULT_NAME = "encoder.buf";

    protected static final int MAX_CHARS_PER_LINE = 45;

    // the configured name on the "begin" command.
    protected String name;
    // the configured mode for the "begin" command.
    protected int mode;

    // since this is a filtering stream, we need to wait until we have the first byte written for encoding
    // to write out the "begin" marker.  A real pain, but necessary.
    protected boolean beginWritten = false;


    // our encoder utility class.
    protected UUEncoder encoder = new UUEncoder();

    // Data is generally written out in 45 character lines, so we're going to buffer that amount before
    // asking the encoder to process this.

    // the buffered byte count
    protected int bufferedBytes = 0;

    // we'll encode this part once it is filled up.
    protected byte[] buffer = new byte[45];

    /**
     * Create a Base64 encoder stream that wraps a specifed stream
     * using the default line break size.
     *
     * @param out    The wrapped output stream.
     */
    public UUEncoderStream(final OutputStream out) {
        this(out, DEFAULT_NAME, DEFAULT_MODE);
    }


    /**
     * Create a Base64 encoder stream that wraps a specifed stream
     * using the default line break size.
     *
     * @param out    The wrapped output stream.
     * @param name   The filename placed on the "begin" command.
     */
    public UUEncoderStream(final OutputStream out, final String name) {
        this(out, name, DEFAULT_MODE);
    }


    public UUEncoderStream(final OutputStream out, final String name, final int mode) {
        super(out);
        // fill in the name and mode information.
        this.name = name;
        this.mode = mode;
    }


    private void checkBegin() throws IOException {
        if (!beginWritten) {
            // grumble...OutputStream doesn't directly support writing String data.  We'll wrap this in
            // a PrintStream() to accomplish the task of writing the begin command.

            final PrintStream writer = new PrintStream(out);
            // write out the stream with a CRLF marker
            writer.print("begin " + mode + " " + name + "\r\n");
            writer.flush();
            beginWritten = true;
        }
    }

    private void writeEnd() throws IOException {
        final PrintStream writer = new PrintStream(out);
        // write out the stream with a CRLF marker
        writer.print("\nend\r\n");
        writer.flush();
    }

    private void flushBuffer() throws IOException {
        // make sure we've written the begin marker first
        checkBegin();
        // ask the encoder to encode and write this out.
        if (bufferedBytes != 0) {
            encoder.encode(buffer, 0, bufferedBytes, out);
            // reset the buffer count
            bufferedBytes = 0;
        }
    }

    private int bufferSpace() {
        return MAX_CHARS_PER_LINE - bufferedBytes;
    }

    private boolean isBufferFull() {
        return bufferedBytes >= MAX_CHARS_PER_LINE;
    }


    // in order for this to work, we need to override the 3 different signatures for write

    @Override
    public void write(final int ch) throws IOException {
        // store this in the buffer.
        buffer[bufferedBytes++] = (byte)ch;

        // if we filled this up, time to encode and write to the output stream.
        if (isBufferFull()) {
            flushBuffer();
        }
    }

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

    @Override
    public void write(final byte [] data, int offset, int length) throws IOException {
        // first check to see how much space we have left in the buffer, and copy that over
        final int copyBytes = Math.min(bufferSpace(), length);

        System.arraycopy(buffer, bufferedBytes, data, offset, copyBytes);
        bufferedBytes += copyBytes;
        offset += copyBytes;
        length -= copyBytes;

        // if we filled this up, time to encode and write to the output stream.
        if (isBufferFull()) {
            flushBuffer();
        }

        // we've flushed the leading part up to the line break.  Now if we have complete lines
        // of data left, we can have the encoder process all of these lines directly.
        if (length >= MAX_CHARS_PER_LINE) {
            final int fullLinesLength = (length / MAX_CHARS_PER_LINE) * MAX_CHARS_PER_LINE;
            // ask the encoder to encode and write this out.
            encoder.encode(data, offset, fullLinesLength, out);
            offset += fullLinesLength;
            length -= fullLinesLength;
        }

        // ok, now we're down to a potential trailing bit we need to move into the
        // buffer for later processing.

        if (length > 0) {
            System.arraycopy(buffer, 0, data, offset, length);
            bufferedBytes += length;
            offset += length;
            length -= length;
        }
    }

    @Override
    public void flush() throws IOException {
        // flush any unencoded characters we're holding.
        flushBuffer();
        // write out the data end marker
        writeEnd();
        // and flush the output stream too so that this data is available.
        out.flush();
    }

    @Override
    public void close() throws IOException {
        // flush all of the streams and close the target output stream.
        flush();
        out.close();
    }

}


