/*
 *  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.mina.filter.compression;

import java.io.IOException;

import org.apache.mina.core.buffer.IoBuffer;

import com.jcraft.jzlib.JZlib;
import com.jcraft.jzlib.ZStream;

/**
 * A helper class for interfacing with the JZlib library. This class acts both
 * as a compressor and decompressor, but only as one at a time.  The only
 * flush method supported is {@code Z_SYNC_FLUSH} also known as {@code Z_PARTIAL_FLUSH}
 *
 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
 */
class Zlib {
    /** Try o get the best possible compression */
    public static final int COMPRESSION_MAX = JZlib.Z_BEST_COMPRESSION;

    /** Favor speed over compression ratio */
    public static final int COMPRESSION_MIN = JZlib.Z_BEST_SPEED;

    /** No compression */
    public static final int COMPRESSION_NONE = JZlib.Z_NO_COMPRESSION;

    /** Default compression */
    public static final int COMPRESSION_DEFAULT = JZlib.Z_DEFAULT_COMPRESSION;

    /** Compression mode */
    public static final int MODE_DEFLATER = 1;

    /** Uncompress mode */
    public static final int MODE_INFLATER = 2;

    /** The requested compression level */
    private int compressionLevel;

    /** The inner stream used to inflate or deflate the data */
    private ZStream zStream = null;

    /** The selected operation mode : INFLATE or DEFLATE */
    private int mode = -1;

    /**
     * Creates an instance of the ZLib class.
     * 
     * @param compressionLevel the level of compression that should be used. One of
     * {@code COMPRESSION_MAX}, {@code COMPRESSION_MIN},
     * {@code COMPRESSION_NONE} or {@code COMPRESSION_DEFAULT}
     * @param mode the mode in which the instance will operate. Can be either
     * of {@code MODE_DEFLATER} or {@code MODE_INFLATER}
     * @throws IllegalArgumentException if the mode is incorrect
     */
    public Zlib(int compressionLevel, int mode) {
        switch (compressionLevel) {
        case COMPRESSION_MAX:
        case COMPRESSION_MIN:
        case COMPRESSION_NONE:
        case COMPRESSION_DEFAULT:
            this.compressionLevel = compressionLevel;
            break;
        default:
            throw new IllegalArgumentException("invalid compression level specified");
        }

        // create a new instance of ZStream. This will be done only once.
        zStream = new ZStream();

        switch (mode) {
        case MODE_DEFLATER:
            zStream.deflateInit(this.compressionLevel);
            break;
        case MODE_INFLATER:
            zStream.inflateInit();
            break;
        default:
            throw new IllegalArgumentException("invalid mode specified");
        }
        this.mode = mode;
    }

    /**
     * Uncompress the given buffer, returning it in a new buffer.
     * 
     * @param inBuffer the {@link IoBuffer} to be decompressed. The contents
     * of the buffer are transferred into a local byte array and the buffer is
     * flipped and returned intact.
     * @return the decompressed data
     * @throws IOException if the decompression of the data failed for some reason.
     * @throws IllegalArgumentException if the mode is not <code>MODE_DEFLATER</code>
     */
    public IoBuffer inflate(IoBuffer inBuffer) throws IOException {
        if (mode == MODE_DEFLATER) {
            throw new IllegalStateException("not initialized as INFLATER");
        }

        byte[] inBytes = new byte[inBuffer.remaining()];
        inBuffer.get(inBytes).flip();

        // We could probably do this better, if we're willing to return multiple buffers
        // (e.g. with a callback function)
        byte[] outBytes = new byte[inBytes.length * 2];
        IoBuffer outBuffer = IoBuffer.allocate(outBytes.length);
        outBuffer.setAutoExpand(true);

        synchronized (zStream) {
            zStream.next_in = inBytes;
            zStream.next_in_index = 0;
            zStream.avail_in = inBytes.length;
            zStream.next_out = outBytes;
            zStream.next_out_index = 0;
            zStream.avail_out = outBytes.length;
            int retval = 0;

            do {
                retval = zStream.inflate(JZlib.Z_SYNC_FLUSH);
                switch (retval) {
                case JZlib.Z_OK:
                    // completed decompression, lets copy data and get out
                case JZlib.Z_BUF_ERROR:
                    // need more space for output. store current output and get more
                    outBuffer.put(outBytes, 0, zStream.next_out_index);
                    zStream.next_out_index = 0;
                    zStream.avail_out = outBytes.length;
                    break;
                default:
                    // unknown error
                    outBuffer = null;
                    if (zStream.msg == null) {
                        throw new IOException("Unknown error. Error code : " + retval);
                    } else {
                        throw new IOException("Unknown error. Error code : " + retval + " and message : " + zStream.msg);
                    }
                }
            } while (zStream.avail_in > 0);
            
            cleanUp();
        }

        return outBuffer.flip();
    }

    /**
     * Compress the input. The result will be put in a new buffer.
     *  
     * @param inBuffer the buffer to be compressed. The contents are transferred
     * into a local byte array and the buffer is flipped and returned intact.
     * @return the buffer with the compressed data
     * @throws IOException if the compression of teh buffer failed for some reason
     * @throws IllegalStateException if the mode is not <code>MODE_DEFLATER</code>
     */
    public IoBuffer deflate(IoBuffer inBuffer) throws IOException {
        if (mode == MODE_INFLATER) {
            throw new IllegalStateException("not initialized as DEFLATER");
        }

        byte[] inBytes = new byte[inBuffer.remaining()];
        inBuffer.get(inBytes);

        // according to spec, destination buffer should be 0.1% larger
        // than source length plus 12 bytes. We add a single byte to safeguard
        // against rounds that round down to the smaller value
        int outLen = (int) Math.round(inBytes.length * 1.001) + 1 + 12;
        byte[] outBytes = new byte[outLen];

        synchronized (zStream) {
            zStream.next_in = inBytes;
            zStream.next_in_index = 0;
            zStream.avail_in = inBytes.length;
            zStream.next_out = outBytes;
            zStream.next_out_index = 0;
            zStream.avail_out = outBytes.length;

            int retval = zStream.deflate(JZlib.Z_SYNC_FLUSH);
            if (retval != JZlib.Z_OK) {
                outBytes = null;
                inBytes = null;
                throw new IOException("Compression failed with return value : " + retval);
            }

            IoBuffer outBuf = IoBuffer.wrap(outBytes, 0, zStream.next_out_index);

            cleanUp();
            
            return outBuf;
        }
    }

    /**
     * Cleans up the resources used by the compression library.
     */
    public void cleanUp() {
        if (zStream != null) {
            zStream.free();
        }
    }
}
