/*
 * Copyright (c) 1998-2008 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 Emil Ong
 */

package com.caucho.quercus.lib.file;

import com.caucho.quercus.QuercusModuleException;
import com.caucho.quercus.env.*;
import com.caucho.vfs.TempBuffer;

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

/**
 * A stream that has its operations mediated by a Quercus object.
 */
public class WrappedStream implements BinaryInput, BinaryOutput {
  private static final UnicodeBuilderValue STREAM_CLOSE
    = new UnicodeBuilderValue("stream_close");
  private static final UnicodeBuilderValue STREAM_EOF
    = new UnicodeBuilderValue("stream_eof");
  private static final UnicodeBuilderValue STREAM_FLUSH
    = new UnicodeBuilderValue("stream_flush");
  private static final UnicodeBuilderValue STREAM_OPEN
    = new UnicodeBuilderValue("stream_open");
  private static final UnicodeBuilderValue STREAM_READ
    = new UnicodeBuilderValue("stream_read");
  private static final UnicodeBuilderValue STREAM_SEEK
    = new UnicodeBuilderValue("stream_seek");
  private static final UnicodeBuilderValue STREAM_TELL
    = new UnicodeBuilderValue("stream_tell");
  private static final UnicodeBuilderValue STREAM_WRITE
    = new UnicodeBuilderValue("stream_write");
  
  private byte []printBuffer = new byte[1];

  private Env _env;
  private Value _wrapper;
  private LineReader _lineReader;

  private InputStream _is;
  private OutputStream _os;
  private int _buffer;
  private boolean _doUnread = false;

  private int _writeLength;

  private WrappedStream(Env env, Value wrapper)
  {
    _env = env;

    _wrapper = wrapper;

    _lineReader = new LineReader(env);
  }

  public WrappedStream(Env env,
                       QuercusClass qClass,
                       StringValue path,
                       StringValue mode,
                       LongValue options)
  {
    _env = env;

    _lineReader = new LineReader(env);

    _wrapper = qClass.callNew(_env, new Value[0]);

    _wrapper.callMethod(_env, STREAM_OPEN, 
                        path, mode, options, NullValue.NULL);
  }

  public InputStream getInputStream()
  {
    if (_is == null)
      _is = new WrappedInputStream();

    return _is;
  }

  public OutputStream getOutputStream()
  {
    if (_os == null)
      _os = new WrappedOutputStream();

    return _os;
  }

  /**
   * Opens a new copy.
   */
  public BinaryInput openCopy()
    throws IOException
  {
    return new WrappedStream(_env, _wrapper);
  }

  /**
   * Sets the current read encoding.  The encoding can either be a
   * Java encoding name or a mime encoding.
   *
   * @param encoding name of the read encoding
   */
  public void setEncoding(String encoding)
    throws UnsupportedEncodingException
  {
  }

  public void closeRead()
  {
    close();
  }

  public void closeWrite()
  {
    close();
  }

  public void close()
  {
    _wrapper.callMethod(_env, STREAM_CLOSE);
  }

  /**
   * Reads a character from a file, returning -1 on EOF.
   */
  public int read()
    throws IOException
  {
    if (_doUnread) {
      _doUnread = false;

      return _buffer;
    } else {
      Value output = _wrapper.callMethod(_env, STREAM_READ, LongValue.ONE);

      _buffer = (int) output.toLong();

      return _buffer;
    }
  }

  /**
   * Unread a character.
   */
  public void unread()
    throws IOException
  {
    _doUnread = true;
  }

  public int read(byte []buffer, int offset, int length)
  {
    // XXX: shgould be reimplemented

    Value output = _wrapper.callMethod(_env, STREAM_READ,
                                       LongValue.create(length));

    // XXX "0"?

    if (! output.toBoolean())
      return -1;

    byte []outputBytes = output.toString().getBytes();

    if (outputBytes.length < length)
      length = outputBytes.length;

    System.arraycopy(outputBytes, 0, buffer, offset, length);

    return length;
  }

  public int read(char []buffer, int offset, int length)
  {
    // XXX: shgould be reimplemented

    Value output = _wrapper.callMethod(_env, STREAM_READ,
                                       LongValue.create(length));

    // XXX "0"?

    if (! output.toBoolean())
      return -1;

    byte []outputBytes = output.toString().getBytes();

    if (outputBytes.length < length)
      length = outputBytes.length;

    System.arraycopy(outputBytes, 0, buffer, offset, length);

    return length;
  }

  /**
   * Appends to a string builder.
   */
  public StringValue appendTo(StringValue builder)
  {
    try {
      int ch;
    
      while ((ch = read()) >= 0) {
	builder.append((char) ch);
      }

      return builder;
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    }
  }


  /**
   * Reads a Binary string.
   */
  public StringValue read(int length)
    throws IOException
  {
    Value output = _wrapper.callMethod(_env, STREAM_READ, 
                                       LongValue.create(length));

    return output.toBinaryValue(_env);
  }

  /**
   * Reads the optional linefeed character from a \r\n
   */
  public boolean readOptionalLinefeed()
    throws IOException
  {
    int ch = read();

    if (ch == '\n') {
      return true;
    }
    else {
      unread();
      return false;
    }
  }

    /**
   * Reads a line from a file, returning null on EOF.
   */
  public StringValue readLine(long length)
    throws IOException
  {
    return _lineReader.readLine(_env, this, length);
  }
  
  public void write(byte []buffer, int offset, int length)
    throws IOException
  {
    StringValue bb = _env.createBinaryBuilder(buffer, offset, length);

    Value output = _wrapper.callMethod(_env, STREAM_WRITE, bb);

    _writeLength = (int) output.toLong();
  }

  /**
   * Writes to a stream.
   */
  public int write(InputStream is, int length)
  {
    int writeLength = 0;

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

    try {
      while (length > 0) {
        int sublen;

        if (length < buffer.length)
          sublen = length;
        else
          sublen = buffer.length;

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

        if (sublen < 0)
          break;

        for (int offset = 0; offset < sublen;) {
          write(buffer, offset, sublen);

          if (_writeLength > 0)
            offset += _writeLength;
          else
            return writeLength;
        }

        writeLength += sublen;
        length -= sublen;
      }

      return writeLength;
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    } finally {
      TempBuffer.free(tb);
    }
  }

  /**
   * Prints a string to a file.
   */
  public void print(char v)
    throws IOException
  {
    printBuffer[0] = (byte) v;

    write(printBuffer, 0, 1);
  }

  /**
   * Prints a string to a file.
   */
  public void print(String v)
    throws IOException
  {
    for (int i = 0; i < v.length(); i++)
      print(v.charAt(i));
  }

  /**
   * Returns true if end-of-file has been reached
   */
  public boolean isEOF()
  {
    return _wrapper.callMethod(_env, STREAM_EOF).toBoolean();
  }

  /**
   * Tells the position in the stream
   */
  public long getPosition()
  {
    return _wrapper.callMethod(_env, STREAM_TELL).toLong();
  }

  /**
   * Sets the position.
   */
  public boolean setPosition(long offset)
  {
    LongValue offsetValue = LongValue.create(offset);
    LongValue whenceValue = LongValue.create(SEEK_SET);

    return _wrapper.callMethod(_env, STREAM_SEEK, 
                               offsetValue, whenceValue).toBoolean();
  }

  public long seek(long offset, int whence)
  {
    LongValue offsetValue = LongValue.create(offset);
    LongValue whenceValue = LongValue.create(whence);

    return _wrapper.callMethod(_env, STREAM_SEEK, 
                               offsetValue, whenceValue).toLong();
  }

  public void flush()
    throws IOException
  {
    if (! _wrapper.callMethod(_env, STREAM_FLUSH).toBoolean())
      throw new IOException(); // Get around java.io.Flushable
  }

  public Value stat()
  {
    return _wrapper.callMethod(_env, STREAM_FLUSH);
  }

  private class WrappedInputStream extends InputStream {
    public int read()
      throws IOException
    {
      return WrappedStream.this.read();
    }
  }

  private class WrappedOutputStream extends OutputStream {
    public void write(int b)
      throws IOException
    {
      _wrapper.callMethod(_env, STREAM_WRITE, LongValue.create(b));
    }
  }
}
