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

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

/**
 * This class represents an ASCII85 output stream.
 *
 * @author <a href="mailto:ben@benlitchfield.com">Ben Litchfield</a>
 *
 */
public class ASCII85OutputStream extends FilterOutputStream
{

    private int lineBreak;
    private int count;

    private byte[] indata;
    private byte[] outdata;

    /**
     * Function produces five ASCII printing characters from
     * four bytes of binary data.
     */
    private int maxline;
    private boolean flushed;
    private char terminator;
    private static final char OFFSET = '!';
    private static final char NEWLINE = '\n';
    private static final char Z = 'z';

    /**
     * Constructor.
     *
     * @param out The output stream to write to.
     */
    public ASCII85OutputStream(OutputStream out)
    {
        super(out);
        lineBreak = 36 * 2;
        maxline = 36 * 2;
        count = 0;
        indata = new byte[4];
        outdata = new byte[5];
        flushed = true;
        terminator = '~';
    }

    /**
     * This will set the terminating character.
     *
     * @param term The terminating character.
     */
    public void setTerminator(char term)
    {
        if (term < 118 || term > 126 || term == Z)
        {
            throw new IllegalArgumentException("Terminator must be 118-126 excluding z");
        }
        terminator = term;
    }

    /**
     * This will get the terminating character.
     *
     * @return The terminating character.
     */
    public char getTerminator()
    {
        return terminator;
    }

    /**
     * This will set the line length that will be used.
     *
     * @param l The length of the line to use.
     */
    public void setLineLength(int l)
    {
        if (lineBreak > l)
        {
            lineBreak = l;
        }
        maxline = l;
    }

    /**
     * This will get the length of the line.
     *
     * @return The line length attribute.
     */
    public int getLineLength()
    {
        return maxline;
    }

    /**
     * This will transform the next four ascii bytes.
     */
    private final void transformASCII85()
    {
        long word = ((((indata[0] << 8) | (indata[1] & 0xFF)) << 16) | ((indata[2] & 0xFF) << 8) | (indata[3] & 0xFF)) & 0xFFFFFFFFL;

        if (word == 0)
        {
            outdata[0] = (byte) Z;
            outdata[1] = 0;
            return;
        }
        long x;
        x = word / (85L * 85L * 85L * 85L);
        outdata[0] = (byte) (x + OFFSET);
        word -= x * 85L * 85L * 85L * 85L;

        x = word / (85L * 85L * 85L);
        outdata[1] = (byte) (x + OFFSET);
        word -= x * 85L * 85L * 85L;

        x = word / (85L * 85L);
        outdata[2] = (byte) (x + OFFSET);
        word -= x * 85L * 85L;

        x = word / 85L;
        outdata[3] = (byte) (x + OFFSET);

        outdata[4] = (byte) ((word % 85L) + OFFSET);
    }

    /**
     * This will write a single byte.
     *
     * @param b The byte to write.
     *
     * @throws IOException If there is an error writing to the stream.
     */
    public final void write(int b) throws IOException
    {
        flushed = false;
        indata[count++] = (byte) b;
        if (count < 4)
        {
            return;
        }
        transformASCII85();
        for (int i = 0; i < 5; i++)
        {
            if (outdata[i] == 0)
            {
                break;
            }
            out.write(outdata[i]);
            if (--lineBreak == 0)
            {
                out.write(NEWLINE);
                lineBreak = maxline;
            }
        }
        count = 0;
    }

    /**
     * This will flush the data to the stream.
     *
     * @throws IOException If there is an error writing the data to the stream.
     */
    public final void flush() throws IOException
    {
        if (flushed)
        {
            return;
        }
        if (count > 0)
        {
            for (int i = count; i < 4; i++)
            {
                indata[i] = 0;
            }
            transformASCII85();
            if (outdata[0] == Z)
            {
                for (int i = 0; i < 5; i++) // expand 'z',
                {
                    outdata[i] = (byte) OFFSET;
                }
            }
            for (int i = 0; i < count + 1; i++)
            {
                out.write(outdata[i]);
                if (--lineBreak == 0)
                {
                    out.write(NEWLINE);
                    lineBreak = maxline;
                }
            }
        }
        if (--lineBreak == 0)
        {
            out.write(NEWLINE);
        }
        out.write(terminator);
        out.write('>');
        out.write(NEWLINE);
        count = 0;
        lineBreak = maxline;
        flushed = true;
        super.flush();
    }

    /**
     * This will close the stream.
     *
     * @throws IOException If there is an error closing the wrapped stream.
     */
    public void close() throws IOException
    {
        try
        {
            flush();
            super.close();
        }
        finally
        {
            indata = outdata = null;
        }
    }
}
