/*
 * 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.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;

import com.caucho.quercus.QuercusModuleException;

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

import com.caucho.quercus.lib.file.FileValue;
import com.caucho.quercus.lib.file.FileModule;
import com.caucho.quercus.lib.string.StringModule;

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

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

import com.caucho.vfs.Path;
import com.caucho.vfs.TempBuffer;

/**
 * Zlib object oriented API facade
 */

public class Zlib {
  private static final Logger log = Logger.getLogger(Zlib.class.getName());
  private static final L10N L = new L10N(Zlib.class);

  private InputStream _in;
  private ZlibOutputStream _gzout;

  private FileValue _fileValue;
  private boolean _isGZIPInputStream;

  /**
   * XXX: todo - implement additional read/write modes (a,etc)
   *
   * Creates and sets GZIP stream if mode is 'w'
   * Also creates _fileValue.  All write functions are wrappers around
   * the _fileValue functions using the private class GZFileValueWriter to
   * compress the byte stream.
   *
   *
   * @param fileName
   * @param mode (ie: "w9" or "r7f")
   * @param useIncludePath is always on
   */
  public Zlib(Env env, String filename, String mode, boolean useIncludePath)
  {
    String filemode = getFileMode(mode);
    int compressionLevel = getCompressionLevel(mode);
    int compressionStrategy = getCompressionStrategy(mode);

    Value val = null;
    // FileModule.fopen(env, filename, mode, useIncludePath, null);
    
    if (val != BooleanValue.FALSE)
      _fileValue = (FileValue)val;

      /*
    try {
      if (filemode.equals("r")) {
        _in = getGZIPInputStream();
      }
      else if (filemode.equals("w")) {
        _gzout = new ZlibOutputStream(_fileValue.getPath().openWrite(),
				      compressionLevel,
				      compressionStrategy);
      }
      else if (filemode.equals("a")) {
        _gzout = new ZlibOutputStream(_fileValue.getPath().openAppend(),
				      compressionLevel,
				      compressionStrategy);
      }
      else if (filemode.equals("x")) {
        _gzout = new ZlibOutputStream(_fileValue.getPath().openWrite(),
				      compressionLevel,
				      compressionStrategy);
      }
    }
    catch (IOException e) {
      log.log(Level.FINE, e.getMessage(), e);
      env.warning(L.l(e.getMessage()));
    }
      */
  }


  /**
   * Reads from the input and writes to the gzip stream
   * @param s
   * @param length # of bytes to compress
   * @return # of uncompressed bytes
   */
  public int gzwrite(Env env, InputStream is, @Optional("-1") int length)
  {
    if (_fileValue == null) {
      env.warning(L.l("file could not be open for writing"));
      return -1;
    }

    TempBuffer tb = TempBuffer.allocate();
    byte[] buffer = tb.getBuffer();

    int inputSize = 0;
    int sublen;

    if (length < 0)
      length = Integer.MAX_VALUE;

    try {
      while (length > 0) {
	if (buffer.length < length)
	  sublen = buffer.length;
	else
	  sublen = length;

	sublen = is.read(buffer, 0, sublen);

	if (sublen <= 0)
	  break;

	_gzout.write(buffer, 0, sublen);
	
	inputSize += sublen;
	length -= sublen;
      }
    }
    catch (IOException e) {
      log.log(Level.FINE, e.getMessage(), e);
      env.warning(L.l(e.getMessage()));
    }
    
    TempBuffer.free(tb);
    return inputSize;
  }


  /**
   * Closes the gzip stream
   * @return true if successful, false otherwise
   */
  public boolean gzclose()
  {
    if (_fileValue == null) {
      return false;
    }

    try {
      if (_gzout != null) {
        _gzout.close();
        _gzout = null;
      }
      
      if (_in != null) {
        _in.close();
        _in = null;
      }
    }
    catch (Exception e) {
      throw QuercusModuleException.create(e);
    }
    return true;
  }

  /**
   * alias of gzwrite
   * @param env
   * @param s
   * @param length
   * @return # of uncompressed bytes
   */
  public int gzputs(Env env,
		    @NotNull InputStream is,
		    @Optional("-1") int length)
  {
    return gzwrite(env, is, length);
  }

  /**
   *
   * @return the next character or BooleanValue.FALSE
   */
  public Value gzgetc()
  {
    try {
      int ch = _in.read();

      if (ch >= 0)
	return new StringValueImpl(Character.toString((char) ch));
      else
	return BooleanValue.FALSE;
    } catch (IOException e) {
      throw QuercusModuleException.create(e);
    }
  }

  /**
   * Gets a (uncompressed) string of up to 'length' bytes read
   * from the given file pointer. Reading ends when 'length' bytes
   * have been read, on a newline, or on EOF (whichever comes first).
   *
   * @param length
   * @return StringValue
   */
  @ReturnNullAsFalse
  public StringValue gzgets(int length)
  {
    if (_in == null)
      return null;

    StringBuilderValue sbv = new StringBuilderValue();
    int readChar;

    try {
      for (int i = 0; i < length - 1; i++) {
        readChar = _in.read();
	
        if (readChar >= 0) {
          sbv.append((char) readChar);
	  
          if (readChar == '\n' || readChar == '\r')
            break;
        } else
          break;
      }
    } catch (Exception e) {
      throw QuercusModuleException.create(e);
    }

    if (sbv.length() > 0)
      return sbv;
    else
      return null;
  }

  /**
   * helper function for ZlibModule.gzfile
   * need to have created a Zlib before calling this
   *
   * @return array of uncompressed lines
   * @throws IOException
   * @throws DataFormatException
   */
  public ArrayValue gzfile()
  {
    Value line;
    int oldLength = 0; 

    ArrayValue array = new ArrayValueImpl();

    try {
      //read in String BuilderValue's initial capacity
      while ((line = gzgets(Integer.MAX_VALUE)) != BooleanValue.FALSE) {
        array.put(line);
      }

      return array;
    } catch (Exception e) {
      throw QuercusModuleException.create(e);
    }
  }

  /**
   * same as gzgets but does not stop at '\n' or '\r'
   * @param length
   * @return BinaryValue, an empty BinaryValue if no data read
   * @throws IOException
   * @throws DataFormatException
   */
  public BinaryValue gzread(int length)
  {
    BinaryBuilderValue sbv = new BinaryBuilderValue();
    int readChar;

    if (_in == null)
      return sbv;

    try {
      for (int i = 0; i < length; i++) {
        readChar = _in.read();
	
        if (readChar >= 0) {
          sbv.append(readChar);
        } else
          break;
      }
    } catch (Exception e) {
      throw QuercusModuleException.create(e);
    }

    return sbv;
  }

  /**
   *
   * @return true if eof
   */
  public boolean gzeof()
  {
    if (_isGZIPInputStream)
      return ((GZIPInputStream)_in).isEOS();
    else {
      try {
        _in.mark(1);
        int ch = _in.read();
        _in.reset();
        return (ch == -1);
      } catch (IOException e) {
        throw new QuercusModuleException(e);
      }
    }
  }

  /**
   *
   * @param length
   * @param allowedTags
   * @return next line stripping tags
   * @throws IOException
   * @throws DataFormatException
   */
  @ReturnNullAsFalse
  public StringValue gzgetss(int length,
			     @Optional String allowedTags)
  {
    try {
      if (_in == null)
        return null;

      StringBuilderValue sbv = new StringBuilderValue();
      int readChar;
      for (int i=0; i < length; i++) {
        readChar = _in.read();
        if (readChar >= 0) {
          sbv.append((char)readChar);
          if (readChar == '\n' || readChar == '\r')
            break;
        } else
          break;
      }
      if (sbv.length() > 0)
	return StringModule.strip_tags(sbv, allowedTags);
      else
	return null;
    } catch (Exception e) {
      throw QuercusModuleException.create(e);
    }
  }

  /**
   * resets to the beginning of the file stream.
   *
   * @return always true
   * @throws IOException
   */
  public boolean gzrewind()
  {
    try {
      if (_in != null )
        _in.close();

      _in = getGZIPInputStream();
    }
    catch (IOException e)
    {
      throw QuercusModuleException.create(e);
    }
    return true;
  }

  /**
   * helper function to open file for reading when necessary
   *
   * @throws IOException
   */
  protected InputStream getGZIPInputStream()
    throws IOException
  {
    try {
      _isGZIPInputStream = true;
      return new GZIPInputStream(_fileValue.getPath().openRead());
    }
    catch (IOException e) {
      //GZIPInputStream throws an Exception if not in gzip format
      //else open uncompressed stream
      _isGZIPInputStream = false;

      return _fileValue.getPath().openRead();
    }
  }

  /**
   * 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 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 like how PHP5 does it.
   * 	1. finds the compression level nearest to the end and returns that
   */
  private 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 like how PHP5 does it.
   * 	1. finds the compression strategy nearest to the end and returns that
   */
  private 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;
  }

  public String toString()
  {
    return "Zlib[]";
  }
}
