/*
 * 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 Charles Reich
 */

package com.caucho.quercus.lib.zlib;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import java.util.Collection;

import java.util.logging.*;

import java.util.zip.*;

import com.caucho.quercus.QuercusModuleException;

import com.caucho.quercus.env.BooleanValue;
import com.caucho.quercus.env.Env;
import com.caucho.quercus.env.LongValue;
import com.caucho.quercus.env.ArrayValue;
import com.caucho.quercus.env.ArrayValueImpl;
import com.caucho.quercus.env.StringValue;
import com.caucho.quercus.env.BinaryValue;
import com.caucho.quercus.env.BinaryBuilderValue;
import com.caucho.quercus.env.StringValueImpl;
import com.caucho.quercus.env.TempBufferStringValue;
import com.caucho.quercus.env.Value;
import com.caucho.quercus.env.ServerArrayValue;

import com.caucho.quercus.module.AbstractQuercusModule;
import com.caucho.quercus.module.NotNull;
import com.caucho.quercus.module.ReturnNullAsFalse;
import com.caucho.quercus.module.Optional;

import com.caucho.quercus.lib.file.*;

import com.caucho.util.ByteBuffer;
import com.caucho.util.L10N;

import com.caucho.vfs.*;

/**
 * PHP Zlib
 */
public class ZlibModule extends AbstractQuercusModule {
  private static final Logger log
    = Logger.getLogger(ZlibModule.class.getName());
  private static final L10N L = new L10N(ZlibModule.class);

  public static final int FORCE_GZIP = 0x1;
  public static final int FORCE_DEFLATE = 0x2;

  public String []getLoadedExtensions()
  {
    return new String[] { "zlib" };
  }

  /**
   *
   * @param env
   * @param fileName
   * @param mode
   * @param useIncludePath always on
   * @return Zlib
   */
  @ReturnNullAsFalse
  public static BinaryStream gzopen(Env env,
			      String fileName,
			      String mode,
			      @Optional("false") boolean useIncludePath)
  {
    String filemode = getFileMode(mode);
    int compressionLevel = getCompressionLevel(mode);
    int compressionStrategy = getCompressionStrategy(mode);

    Object val = FileModule.fopen(env, fileName, mode, useIncludePath, null);

    if (val == null)
      return null;

    try {
      int ch = filemode.charAt(0);

      if (ch == 'r') {
        BinaryInput is = (BinaryInput) val;
        return new ZlibInputStream(is);
      }
      else if (ch == 'w') {
        return new ZlibOutputStream(((BinaryOutput) val).getOutputStream(),
				    compressionLevel,
				    compressionStrategy);
      }
      else if (ch == 'a') {
        return new ZlibOutputStream(((BinaryOutput) val).getOutputStream(),
				    compressionLevel,
				    compressionStrategy);
      }
      else if (ch == 'x') {
        return new ZlibOutputStream(((BinaryOutput) val).getOutputStream(),
				    compressionLevel,
				    compressionStrategy);
      }
    }
    catch (IOException e) {
      log.log(Level.FINE, e.getMessage(), e);
      env.warning(L.l(e.getMessage()));
    }

    return null;
  }

  /**
   *
   * @param env
   * @param fileName
   * @param useIncludePath
   * @return array of uncompressed lines from fileName
   */
  @ReturnNullAsFalse
  public static ArrayValue gzfile(Env env,
				  String fileName,
				  @Optional boolean useIncludePath)
  {
    BinaryInput is = (BinaryInput) gzopen(env, fileName, "r", useIncludePath);

    if (is == null)
      return null;

    try {
      ArrayValue result = new ArrayValueImpl();

      StringValue line;
      while ((line = is.readLine(Integer.MAX_VALUE)) != null &&
	     line.length() > 0)
	result.put(line);

      return result;
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    } finally {
      is.close();
    }
  }

  /**
   * outputs uncompressed bytes directly to browser, writes a warning message
   *   if an error has occured
   * Note: PHP5 is supposed to print an error message but it doesn't do it
   *
   * @param env
   * @param fileName
   * @param useIncludePath
   * @return number of bytes read from file, or FALSE if an error occurred
   */
  public static Value readgzfile(Env env,
				 String fileName,
				 @Optional boolean useIncludePath)
  {
    BinaryInput is = (BinaryInput) gzopen(env, fileName, "r", useIncludePath);

    if (is == null)
      return BooleanValue.FALSE;

    try {
      return LongValue.create(env.getOut().writeStream(is.getInputStream()));
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    } finally {
      is.close();
    }
  }

  /**
   * Writes a string to the gzip stream.
   */
  public static int gzwrite(@NotNull BinaryOutput os,
			    InputStream is,
			    @Optional("0x7fffffff") int length)
  {
    if (os == null)
      return 0;

    try {
      return os.write(is, length);
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    }
  }

  /**
   *
   * @param env
   * @param zp
   * @param s
   * @param length
   * @return alias of gzwrite
   */
  public int gzputs(Env env,
                    @NotNull BinaryOutput os,
                    InputStream is,
                    @Optional("0x7ffffff") int length)
  {
    if (os == null)
      return 0;

    try {
      return os.write(is, length);
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    }
  }

  /**
   * Closes the stream.
   */
  public boolean gzclose(@NotNull BinaryStream os)
  {
    if (os == null)
      return false;
    
    os.close();

    return true;
  }

  /**
   * Returns true if the GZip stream is ended.
   */
  public boolean gzeof(@NotNull BinaryStream binaryStream)
  {
    if (binaryStream == null)
      return true;

    return binaryStream.isEOF();
  }

  /**
   * Reads a character from the stream.
   */
  public static Value gzgetc(@NotNull BinaryInput is)
  {
    if (is == null)
      return BooleanValue.FALSE;

    try {
      int ch = is.read();

      if (ch < 0)
        return BooleanValue.FALSE;
      else
        return new BinaryBuilderValue(new byte[] { (byte) ch });
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    }
  }

  /**
   * Reads a chunk of data from the gzip stream.
   */
  @ReturnNullAsFalse
  public BinaryValue gzread(@NotNull BinaryInput is, int length)
  {
    if (is == null)
      return null;

    try {
      return is.read(length);
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    }
  }

  /**
   * Reads a line from the input stream.
   */
  public static Value gzgets(Env env,
			      @NotNull BinaryInput is,
			      int length)
  {
    return FileModule.fgets(env, is, length);
  }

  /**
   * Reads a line from the zip stream, stripping tags.
   */
  public static Value gzgetss(Env env,
			      @NotNull BinaryInput is,
			      int length,
			      @Optional String allowedTags)
  {
    return FileModule.fgetss(env, is, length, allowedTags);
  }

  /**
   * Rewinds the stream to the very beginning
   */
  public boolean gzrewind(@NotNull BinaryStream binaryStream)
  {
    if (binaryStream == null)
      return false;

    return binaryStream.setPosition(0);
  }

  /**
   * Set stream position to the offset
   * @param offset absolute position to set stream to
   * @return 0 upon success, else -1 for error
   */
  public int gzseek(@NotNull BinaryStream binaryStream, long offset)
  {
    if (binaryStream == null)
      return -1;
    if (binaryStream.setPosition(offset) == false)
      return -1;
    if (binaryStream.getPosition() != offset)
      return -1;

    return 0;
  }

  /**
   * Gets the current position in the stream
   * @return the position in the stream, or FALSE for error
   */
  public Value gztell(@NotNull BinaryStream binaryStream)
  {
    if (binaryStream == null)
      return BooleanValue.FALSE;
    return new LongValue(binaryStream.getPosition());
  }

  /**
   * Prints out the remaining data in the stream to stdout
   */
  public Value gzpassthru(Env env, @NotNull BinaryInput is)
  {
    WriteStream out = env.getOut();
    TempBuffer tempBuf = TempBuffer.allocate();
    byte[] buffer = tempBuf.getBuffer();

    int length = 0;
    try {
      int sublen = is.read(buffer, 0, buffer.length);
      while (sublen > 0) {
        out.write(buffer, 0, sublen);
        length += sublen;
        sublen = is.read(buffer, 0, buffer.length);
      }

      return new LongValue(length);

    } catch (IOException e) {
      log.log(Level.FINE, e.toString(), e);

      return BooleanValue.FALSE;

    } finally {
      TempBuffer.free(tempBuf);
    }
  }

  /**
   * Returns the encoding type both allowed by the server
   *   and supported by the user's browser.
   */
  public Value zlib_get_coding_type(Env env)
  {
    String ini = env.getIniString("zlib.output_compression");

    if (ini == null || ini == "")
      return BooleanValue.FALSE;

    //zlib_get_coding_type can also be an integer > 0
    if (! ini.equalsIgnoreCase("on")) {
      int ch = ini.charAt(0);

      if (ch < '0' || ch > '9')
        return BooleanValue.FALSE;
    }

    ServerArrayValue sav = new ServerArrayValue(env);
    Value val = sav.get(new StringValueImpl("HTTP_ACCEPT_ENCODING"));

    if (!val.isset())
      return BooleanValue.FALSE;

    String s = val.toString();
    if (s.contains("gzip"))
      return new StringValueImpl("gzip");
    else if(s.contains("deflate"))
      return new StringValueImpl("deflate");
    else
      return BooleanValue.FALSE;
  }

  /**
   * compresses data using zlib
   *
   * @param data
   * @param level (default is Deflater.DEFAULT_COMPRESSION)
   * @return compressed string
   */
  public Value gzcompress(InputStream data,
                          @Optional("6") int level)
  {
    TempBuffer tempBuf = TempBuffer.allocate();
    byte []buffer = tempBuf.getBuffer();

    try {
      Deflater deflater = new Deflater(level, true);
      Adler32 crc = new Adler32();

      boolean isFinished = false;
      TempStream out = new TempStream();

      buffer[0] = (byte) 0x78;

      if (level <= 1)
        buffer[1] = (byte) 0x01;
      else if (level < 6)
        buffer[1] = (byte) 0x5e;
      else if (level == 6)
        buffer[1] = (byte) 0x9c;
      else
        buffer[1] = (byte) 0xda;

      out.write(buffer, 0, 2, false);

      int len;
      while (! isFinished) {
        while (! isFinished && deflater.needsInput()) {
          len = data.read(buffer, 0, buffer.length);

          if (len > 0) {
            crc.update(buffer, 0, len);
            deflater.setInput(buffer, 0, len);
          }
          else {
            isFinished = true;
            deflater.finish();
          }
        }

        while ((len = deflater.deflate(buffer, 0, buffer.length)) > 0) {
          out.write(buffer, 0, len, false);
        }
      }

      long value = crc.getValue();
    
      buffer[0] = (byte) (value >> 24);
      buffer[1] = (byte) (value >> 16);
      buffer[2] = (byte) (value >> 8);
      buffer[3] = (byte) (value >> 0);

      out.write(buffer, 0, 4, true);

      return new TempBufferStringValue(out.getHead());
    } catch (Exception e) {
      throw QuercusModuleException.create(e);
    } finally {
      TempBuffer.free(tempBuf);
    }
  }

  /**
   *
   * @param data
   * @param length (maximum length of string returned)
   * @return uncompressed string
   */
  public Value gzuncompress(InputStream is,
                            @Optional("0") long length)
  {
    TempBuffer tempBuf = TempBuffer.allocate();
    byte []buffer = tempBuf.getBuffer();

    try {
      if (length == 0)
        length = Long.MAX_VALUE;

      InflaterInputStream in = new InflaterInputStream(is);
      TempStream out = new TempStream();



      int len;
      while ((len = in.read(buffer, 0, buffer.length)) >= 0) {
        out.write(buffer, 0, len, false);
      }

      in.close();

      return new TempBufferStringValue(out.getHead());
    } catch (Exception e) {
      throw QuercusModuleException.create(e);
    } finally {
      TempBuffer.free(tempBuf);
    }
  }

  private int _dbg;
  
  /**
   *
   * @param level
   * @return compressed using DEFLATE algorithm
   */
  public Value gzdeflate(InputStream data,
                         @Optional("6") int level)
  {
    TempBuffer tempBuf = TempBuffer.allocate();
    byte []buffer = tempBuf.getBuffer();

    try {
      Deflater deflater = new Deflater(level, true);

      boolean isFinished = false;
      TempStream out = new TempStream();

      int len;
      while (! isFinished) {
        if (! isFinished && deflater.needsInput()) {
          len = data.read(buffer, 0, buffer.length);

          if (len > 0)
            deflater.setInput(buffer, 0, len);
          else {
            isFinished = true;
            deflater.finish();
          }
        }

        while ((len = deflater.deflate(buffer, 0, buffer.length)) > 0) {
          out.write(buffer, 0, len, false);
        }
      }
      deflater.end();

      return new TempBufferStringValue(out.getHead());

    } catch (Exception e) {
      throw QuercusModuleException.create(e);
    } finally {
      TempBuffer.free(tempBuf);
    }
  }

  /**
   * @param data compressed using Deflate algorithm
   * @param length (maximum length of string returned)
   *
   * @return uncompressed string
   */
  public Value gzinflate(Env env,
                         InputStream data,
                         @Optional("0") long length)
  {
    TempBuffer tempBuf = TempBuffer.allocate();
    byte []buffer = tempBuf.getBuffer();

    try {
      Inflater inflater = new Inflater(true);

      boolean isFinished = false;
      TempStream out = new TempStream();

      int len;
      while (! isFinished) {
        if (! isFinished && inflater.needsInput()) {
          len = data.read(buffer, 0, buffer.length);

          if (len > 0) {
            inflater.setInput(buffer, 0, len);
	  }
          else
            isFinished = true;
        }

        while ((len = inflater.inflate(buffer, 0, buffer.length)) > 0) {
          out.write(buffer, 0, len, false);
        }
      }

      inflater.end();

      return new TempBufferStringValue(out.getHead());
    } catch (Exception e) {
      env.warning(e);
      return BooleanValue.FALSE;
    } finally {
      TempBuffer.free(tempBuf);
    }
  }

  /**
   *
   * Compresses data using the Deflate algorithm, output is
   * compatible with gzwrite's output
   *
   * @param data compressed with the Deflate algorithm
   * @param level Deflate compresion level [0-9]
   * @param encodingMode CRC32 trailer is not written if encoding mode
   *    is FORCE_DEFLATE, default is to write CRC32
   * @return StringValue with gzip header and trailer
   */
  public Value gzencode(InputStream is,
                        @Optional("6") int level,
                        @Optional("1") int encodingMode)
  {
    TempBuffer tempBuf = TempBuffer.allocate();
    byte[] buffer = tempBuf.getBuffer();

    TempStream ts = new TempStream();
    StreamImplOutputStream out = new StreamImplOutputStream(ts);

    try {
      ZlibOutputStream gzOut;

      gzOut = new ZlibOutputStream(out, level,
			   Deflater.DEFAULT_STRATEGY,
			   encodingMode);

      int len;
      while ((len = is.read(buffer, 0, buffer.length)) > 0) {
        gzOut.write(buffer, 0, len);
      }
      gzOut.close();
      return new TempBufferStringValue(ts.getHead());

    } catch(IOException e) {
      throw QuercusModuleException.create(e);
    } finally {
      TempBuffer.free(tempBuf);
    }
  }

  /**
   * Helper function to retrieve the filemode closest to the end
   * Note: PHP5 unexpectedly fails when 'x' is the mode.
   *
   * XXX todo: toss a warning if '+' is found
   *    (gzip cannot be open for both reading and writing at the same time)
   *
   */
  private static String getFileMode(String input)
  {
    String modifier = "";
    String filemode = input.substring(0, 1);

    for (int i = 1; i < input.length(); i++ )
    {
      char ch = input.charAt(i);
      switch (ch) {
        case 'r':
          filemode = "r";
          break;
        case 'w':
          filemode = "w";
          break;
        case 'a':
          filemode = "a";
          break;
        case 'b':
          modifier = "b";
          break;
        case 't':
          modifier = "t";
          break;
      }
    }
    return filemode + modifier;
  }

  /**
   * Helper function to retrieve the compression level
   *     - finds the compression level nearest to the end and returns that
   */
  private static int getCompressionLevel(String input)
  {
    for (int i = input.length() - 1; i >= 0; i--) {
      char ch = input.charAt(i);
      
      if (ch >= '0' && ch <= '9')
        return ch - '0';
    }
    
    return Deflater.DEFAULT_COMPRESSION;
  }

  /**
   * Helper function to retrieve the compression strategy.
   *     - finds the compression strategy nearest to the end and returns that
   */
  private static int getCompressionStrategy(String input)
  {
    for (int i = input.length() - 1; i >= 0; i--) {
      char ch = input.charAt(i);
      
      switch (ch) {
      case 'f':
        return Deflater.FILTERED;
	
      case 'h':
        return Deflater.HUFFMAN_ONLY;
      }
    }
    
    return Deflater.DEFAULT_STRATEGY;
  }
}
