/*
 * Copyright (c) 1998-2006 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Nam Nguyen
 */

package com.caucho.quercus.lib.zlib;

import java.io.InputStream;
import java.io.PushbackInputStream;
import java.io.IOException;

import java.util.zip.CRC32;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;

/**
 * Similar to GZIPInputStream but with ability to read appended gzip.
 */
public class GZInputStream extends InputStream
{
  private PushbackInputStream _in;
  private Inflater _inflater;

  private CRC32 _crc;
  private boolean _eof;
  private boolean _isGzip;

  private byte[] _readBuffer;	//raw input data buffer
  private byte[] _tbuffer;	//temporary buffer

  private int _readBufferSize;	//amount of raw data read into _readBuffer
  private int _inputSize;	//decompressed bytes read so far
			//  for the current 'append' stream

  private long _totalInputSize;	//total decompressed bytes read

  public GZInputStream(InputStream in)
    throws IOException
  {
    this(in, 512);
  }

  public GZInputStream(InputStream in, int size)
    throws IOException
  {
    // Need to use same buffer size for pushback and _readBuffer
    // because will need to unread <= _readBuffer.length.
    _in = new PushbackInputStream(in, size);

    _inflater = new Inflater(true);
    _crc = new CRC32();
    _eof = false;

    _readBuffer = new byte[size];
    _tbuffer = new byte[128];

    _totalInputSize = 0;

    init();
  }

  /**
   * Returns 0 if gzip EOF has been reached, 1 otherwise
   */
  public int available()
    throws IOException
  {
    if (!_isGzip)
      return _in.available();

    if (_eof == true)
      return 0;
    return 1;
  }

  public void close()
    throws IOException
  {
    _inflater.end();
  }

  /**
   * mark() and reset() are not supported by this class.
   * @return false always
   */
  public boolean markSupported()
  {
    return false;
  }

  /**
   * Returns the byte read, -1 if EOF
   * @return number of bytes read, or -1 if EOF
   */
  public int read() throws IOException
  {
    byte[] b = new byte[1];
    int n = read(b);
    if (n < 0)
      return -1;
    return b[0];
  }

  /**
   * Reads from the compressed stream and
   *  stores the resulting uncompressed data into the byte array.
   * @return number of bytes read, or -1 upon EOF
   */
  public int read(byte[] b)
    throws IOException
  {
    return read(b, 0, b.length);
  }

  /**
   * Reads from the compressed stream and
   *  stores the resulting uncompressed data into the byte array.
   * @return number of bytes read, or -1 upon EOF
   */
  public int read(byte[] b, int off, int len)
    throws IOException
  {
    if (len <= 0 || off < 0 || off + len > b.length)
      return 0;

    if (_eof)
      return -1;

    // Read from uncompressed stream
    if (! _isGzip)
      return _in.read(b, off, len);

    try {
      int sublen;
      int length = 0;
      while(length < len) {
        if (_inflater.needsInput()) {
          _readBufferSize = _in.read(_readBuffer, 0, _readBuffer.length);
          if (_readBufferSize < 0)
            break;

          _inflater.setInput(_readBuffer, 0, _readBufferSize);
        }

       sublen = _inflater.inflate(b, off + length, len - length);
        _crc.update(b, off + length, sublen);
       _inputSize += sublen;
       _totalInputSize += sublen;
       length += sublen;

        // Unread gzip trailer and possibly beginning of appended gzip data.
        if (_inflater.finished()) {
          int remaining = _inflater.getRemaining();
          _in.unread(_readBuffer, _readBufferSize - remaining, remaining);
          readTrailer();
          break;
        }
      }

      return length;
    }
    catch (DataFormatException e) {
      throw new IOException(e.getMessage());
    }
  }

  /**
   * Skips over and discards n bytes.
   * @param n number of bytes to skip
   * @return actual number of bytes skipped
   */
  public long skip(long n)
    throws IOException
  {
    if (_eof || n <= 0)
      return 0;
    
    long remaining = n;
    while (remaining > 0) {
      int length = (int)Math.min(_tbuffer.length, remaining);
      int sublen = read(_tbuffer, 0, length);
      if (sublen < 0)
        break;
      remaining -= sublen;
    }
    return (n - remaining);
  }

  /**
   * Inits/resets this class to be ready to read the start of a gzip stream.
   */
  private void init()
    throws IOException
  {
    _inflater.reset();
    _crc.reset();
    _inputSize = 0;
    _readBufferSize = 0;

    byte flg;

    int length = _in.read(_tbuffer, 0, 10);
    if (length != 10) {
      _isGzip = false;
      _in.unread(_tbuffer, 0, length);
      return;
    }
    if (_tbuffer[0] != (byte)0x1f || _tbuffer[1] != (byte)0x8b) {
      _isGzip = false;
      _in.unread(_tbuffer, 0, length);
      return;
    }

    flg = _tbuffer[3];

    // Skip optional field
    if ((flg & (byte)0x04) > 0) {
      length = _in.read(_tbuffer, 0, 2);
      if (length != 2)
        throw new IOException("Bad GZIP (FEXTRA) header.");
      length = (((int)_tbuffer[1]) << 4) | _tbuffer[0];
      _in.skip(length);
    }

    int c;

    // Skip optional field
    if ((flg & (byte)0x08) > 0) {
      c = _in.read();
      while (c != 0) {
        if (c < 0)
          throw new IOException("Bad GZIP (FNAME) header.");
        c = _in.read();
      }
    }

    // Skip optional field
    if ((flg & 0x10) > 0) {
      c = _in.read();
      while (c != 0) {
        if (c < 0)
          throw new IOException("Bad GZIP (FCOMMENT) header.");
        c = _in.read();
      }
    }

    // Skip optional field
    if ((flg & 0x02) > 0) {
      length = _in.read(_tbuffer, 0, 2);
      if (length != 2)
        throw new IOException("Bad GZIP (FHCRC) header.");
    }

    _isGzip = true;
  }

  /**
   * Reads the trailer and prepare this class for the possibility
   *   of an appended gzip stream.
   */
  private void readTrailer()
    throws IOException
  {
    int length = _in.read(_tbuffer, 0, 8);
    if (length !=  8)
      throw new IOException("Bad GZIP trailer.");

    int refValue = _tbuffer[3] & 0xff;
    refValue <<= 8;
    refValue |= _tbuffer[2] & 0xff;
    refValue <<= 8;
    refValue |= _tbuffer[1] & 0xff;
    refValue <<= 8;
    refValue |= _tbuffer[0] & 0xff;

    int value = (int)_crc.getValue();

    if (refValue != value)
      throw new IOException("Bad GZIP trailer (CRC32).");

    refValue = _tbuffer[7] & 0xff;
    refValue <<= 8;
    refValue |= _tbuffer[6] & 0xff;
    refValue <<= 8;
    refValue |= _tbuffer[5] & 0xff;
    refValue <<= 8;
    refValue |= _tbuffer[4] & 0xff;

    if (refValue != _inputSize)
      throw new IOException("Bad GZIP trailer (LENGTH).");

    // Check to see if this gzip stream is appended with a valid gzip stream.
    // If it is appended, then can continue reading from stream.
    int c = _in.read();
    if (c < 0)
      _eof = true;
    else {
      _in.unread(c);
      init();
      if (!_isGzip)
        _eof = true;
    }
  }

  /*
   * Returns true if stream is in gzip format.
   */
  public boolean isGzip()
  {
    return _isGzip;
  }
}
