/**
 * Copyright (c) 2003-2005, www.pdfbox.org
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of pdfbox; nor the names of its
 *    contributors may be used to endorse or promote products derived from this
 *    software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * http://www.pdfbox.org
 *
 */
package org.pdfbox.filter;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;

import org.pdfbox.cos.COSDictionary;

/**
 * This is the used for the FlateDecode filter.
 * 
 * @author <a href="mailto:ben@benlitchfield.com">Ben Litchfield</a>
 * @author Marcel Kammer
 * @version $Revision: 1.10 $
 */
public class FlateFilter implements Filter
{
    private static final int    BUFFER_SIZE    = 2048;

    /**
     * This will decode some compressed data.
     * 
     * @param compressedData
     *            The compressed byte stream.
     * @param result
     *            The place to write the uncompressed byte stream.
     * @param options
     *            The options to use to encode the data.
     * 
     * @throws IOException
     *             If there is an error decompressing the stream.
     */

    public void decode(InputStream compressedData, OutputStream result, COSDictionary options) throws IOException
    {
        COSDictionary dict = (COSDictionary) options.getDictionaryObject("DecodeParms");
        int predictor = -1;
        int colors = -1;
        int bitsPerPixel = -1;
        int columns = -1;
        InflaterInputStream decompressor = null;
        ByteArrayInputStream bais = null;
        ByteArrayOutputStream baos = null;
        if (dict!=null)
        {
            predictor = dict.getInt("Predictor");
            colors = dict.getInt("Colors");
            bitsPerPixel = options.getInt("BitsPerComponent");
            columns = dict.getInt("Columns");
        }

        try
        {
            // Decompress data to temporary ByteArrayOutputStream
            decompressor = new InflaterInputStream(compressedData);
            byte[] buffer = new byte[BUFFER_SIZE];
            int amountRead;

            // Decode data using given predictor
            if (predictor==-1 || predictor == 1 && predictor == 10)
            {
                // decoding not needed
                while ((amountRead = decompressor.read(buffer, 0, BUFFER_SIZE)) != -1)
                {
                    result.write(buffer, 0, amountRead);
                }
            }
            else
            {
                if (colors==-1 || bitsPerPixel==-1 || columns==-1)
                {
                    throw new IOException("Could not read all parameters to decode image");
                }
                
                baos = new ByteArrayOutputStream();
                while ((amountRead = decompressor.read(buffer, 0, BUFFER_SIZE)) != -1)
                {
                    baos.write(buffer, 0, amountRead);
                }
                baos.flush();

                // Copy data to ByteArrayInputStream for reading
                bais = new ByteArrayInputStream(baos.toByteArray());
                baos.close();
                baos = null;

                byte[] decodedData = decodePredictor(predictor, colors, bitsPerPixel, columns, bais);
                bais.close();
                bais = new ByteArrayInputStream(decodedData);

                // write decoded data to result
                while ((amountRead = bais.read(buffer)) != -1)
                {
                    result.write(buffer, 0, amountRead);
                }
                bais.close();
                bais = null;
            }            
            
            
            result.flush();
        }
        finally
        {
            if (decompressor != null)
            {
                decompressor.close();
            }
            if (bais != null)
            {
                bais.close();
            }
            if (baos != null)
            {
                baos.close();
            }
        }
    }

    private byte[] decodePredictor(int predictor, int colors, int bitsPerComponent, int columns, InputStream data) 
        throws IOException
    {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buffer = new byte[2048];

        if (predictor == 1 || predictor == 10)
        {
            // No prediction or PNG NONE
            int i = 0;
            while ((i = data.read(buffer)) != -1)
            {
                baos.write(buffer, 0, i);
            }
        }
        else
        {
            // calculate sizes
            int bpp = (colors * bitsPerComponent + 7) / 8;
            int rowlength = (columns * colors * bitsPerComponent + 7) / 8 + bpp;
            byte[] actline = new byte[rowlength];
            byte[] lastline = new byte[rowlength];// Initialize lastline with
                                                    // Zeros according to
                                                    // PNG-specification
            boolean done = false;
            int linepredictor = predictor;

            while (!done)
            {
                if (predictor == 15)
                {
                    linepredictor = data.read();// read per line predictor
                    if (linepredictor == -1)
                    {
                        done = true;// reached EOF
                        break;
                    }
                    else
                    {
                        linepredictor += 10; // add 10 to tread value 1 as 11
                    }
                    // (instead of PRED NONE) and 2
                    // as 12 (instead of PRED TIFF)
                }

                // read line
                int i = 0;
                int offset = bpp;
                while (offset < rowlength && ((i = data.read(actline, offset, rowlength - offset)) != -1))
                {
                    offset += i;
                }

                // Do prediction as specified in PNG-Specification 1.2
                switch (linepredictor)
                {
                    case 2:// PRED TIFF SUB
                        /**
                         * @todo decode tiff
                         */
                        throw new IOException("TIFF-Predictor not supported");
                    case 11:// PRED SUB
                        for (int p = bpp; p < rowlength; p++)
                        {
                            int sub = actline[p] & 0xff;
                            int left = actline[p - bpp] & 0xff;
                            actline[p] = (byte) (sub + left);
                        }
                        break;
                    case 12:// PRED UP
                        for (int p = bpp; p < rowlength; p++)
                        {
                            int up = actline[p] & 0xff;
                            int prior = lastline[p] & 0xff;
                            actline[p] = (byte) (up + prior);
                        }
                        break;
                    case 13:// PRED AVG
                        for (int p = bpp; p < rowlength; p++)
                        {
                            int avg = actline[p] & 0xff;
                            int left = actline[p - bpp] & 0xff;
                            int up = lastline[p] & 0xff;
                            actline[p] = (byte) (avg + ((left + up) / 2));
                        }
                        break;
                    case 14:// PRED PAETH
                        for (int p = bpp; p < rowlength; p++)
                        {
                            int paeth = actline[p] & 0xff;
                            int a = actline[p - bpp] & 0xff;// left
                            int b = lastline[p] & 0xff;// upper
                            int c = lastline[p - bpp] & 0xff;// upperleft
                            int value = a + b - c;
                            int absa = Math.abs(value - a);
                            int absb = Math.abs(value - b);
                            int absc = Math.abs(value - c);

                            if (absa <= absb && absa <= absc)
                            {
                                actline[p] = (byte) (paeth + absa);
                            }
                            else if (absb <= absc)
                            {
                                actline[p] += (byte) (paeth + absb);
                            }
                            else
                            {
                                actline[p] += (byte) (paeth + absc);
                            }
                        }
                        break;
                    default:
                        break;
                }

                lastline = actline;
                baos.write(actline, bpp, actline.length - bpp);
            }
        }

        return baos.toByteArray();
    }

    /**
     * This will encode some data.
     * 
     * @param rawData
     *            The raw data to encode.
     * @param result
     *            The place to write to encoded results to.
     * @param options
     *            The options to use to encode the data.
     * 
     * @throws IOException
     *             If there is an error compressing the stream.
     */
    public void encode(InputStream rawData, OutputStream result, COSDictionary options) throws IOException
    {
        DeflaterOutputStream out = new DeflaterOutputStream(result);
        byte[] buffer = new byte[BUFFER_SIZE];
        int amountRead = 0;
        while ((amountRead = rawData.read(buffer, 0, BUFFER_SIZE)) != -1)
        {
            out.write(buffer, 0, amountRead);
        }
        out.close();
        result.flush();
    }
}