/*
 * 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 SoftwareFoundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.vfs;

import com.caucho.util.ByteBuffer;
import com.caucho.util.CharBuffer;

import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;

/*
 * A stream for reading multipart/mime files.
 *
 * <p/>Each call to openRead() will return the next part and return null
 * when complete.
 *
 * <pre><code>
 * MultipartStream multi = new MultipartStream(rawIs, boundary);
 * ReadStream is;
 *
 * while ((is = multi.openRead()) != null) {
 *   // read from is as a normal stream
 * }
 */
public class MultipartStream extends StreamImpl {
  private ByteBuffer _boundary = new ByteBuffer();
  private byte []_boundaryBuffer;
  private int _boundaryLength;

  private ByteBuffer _peekBuffer = new ByteBuffer();
  private byte []_peek;
  private int _peekOffset;
  private int _peekLength;

  private byte []_dummyBuffer = new byte[32];
  
  private ReadStream _is;
  private ReadStream _readStream;
  private boolean _isPartDone;
  private boolean _isDone;
  private boolean _isComplete;
  private HashMap<String,String> _headers = new HashMap<String,String>();
  private CharBuffer _line = new CharBuffer();

  private String _defaultEncoding;

  public MultipartStream()
    throws IOException
  {
    _boundary = new ByteBuffer();
  }

  public MultipartStream(ReadStream is, String boundary)
    throws IOException
  {
    this();

    init(is, boundary);
  }

  /**
   * Returns the default encoding.
   */
  public String getEncoding()
  {
    return _defaultEncoding;
  }

  /**
   * Sets the default encoding.
   */
  public void setEncoding(String encoding)
  {
    _defaultEncoding = encoding;
  }

  /**
   * Initialize the multipart stream with a given boundary.  The boundary
   * passed to <code>init</code> will have "--" prefixed.
   *
   * @param is the underlying stream
   * @param headerBoundary the multipart/mime boundary.
   */
  public void init(ReadStream is, String headerBoundary)
    throws IOException
  {
    _is = is;
    
    _boundary.clear();
    _boundary.add("--");
    _boundary.add(headerBoundary);

    _boundaryBuffer = _boundary.getBuffer();
    _boundaryLength = _boundary.getLength();

    _peekBuffer.setLength(_boundaryLength + 5);
    _peek = _peekBuffer.getBuffer();
    _peekOffset = 0;
    _peekLength = 0;
    _peek[_peekLength++] = (byte) '\n';

    _isPartDone = false;
    _isDone = false;
    _isComplete = false;
    
    while (read(_dummyBuffer, 0, _dummyBuffer.length) >= 0) {
    }
    
    _isPartDone = true;
  }

  /**
   * Returns true if complete.
   */
  public boolean isComplete()
  {
    return _isComplete;
  }

  /**
   * Opens the next part of the multipart/mime stream for reading.  Returns
   * null when the last section is read.
   */
  public ReadStream openRead()
    throws IOException
  {
    if (_isDone)
      return null;
    else if (_readStream == null)
      _readStream = new ReadStream(this, null);
    else if (! _isPartDone) {
      int len;
      while ((len = read(_dummyBuffer, 0, _dummyBuffer.length)) >= 0) {
      }

      if (_isDone)
        return null;
    }
    
    _readStream.init(this, null);

    _isPartDone = false;
    
    if (scanHeaders()) {
      String contentType = (String) getAttribute("content-type");
      
      String charset = getAttributePart(contentType, "charset");
      
      if (charset != null)
        _readStream.setEncoding(charset);
      else if (_defaultEncoding != null)
        _readStream.setEncoding(_defaultEncoding);
      
      return _readStream;
    }
    else {
      _isDone = true;
      _readStream.close();
      return null;
    }
  }

  /**
   * Returns a read attribute from the multipart mime.
   */
  public Object getAttribute(String key)
  {
    return _headers.get(key.toLowerCase());
  }

  /**
   * Returns the headers from the mime.
   */
  public Iterator getAttributeNames()
  {
    return _headers.keySet().iterator();
  }

  /**
   * Scans the mime headers.  The mime headers are in standard mail/http
   * header format: "key: value".
   */
  private boolean scanHeaders()
    throws IOException
  {
    int ch = read() ;

    _headers.clear();
    while (ch > 0 && ch != '\n' && ch != '\r') {
      _line.clear();

      _line.append((char) ch);
      for (ch = read();
           ch >= 0 && ch != '\n' && ch != '\r';
           ch = read()) {
        _line.append((char) ch);
      }

      if (ch == '\r') {
        if ((ch = read()) == '\n')
          ch = read();
      } else if (ch == '\n')
        ch = read();

      int i = 0;
      for (; i < _line.length() && _line.charAt(i) != ':'; i++) {
      }

      String key = null;
      String value = null;
      if (i < _line.length()) {
        key = _line.substring(0, i).trim().toLowerCase();
        value = _line.substring(i + 1).trim();

        _headers.put(key, value);
      }
    }
    
    if (ch == '\r') {
      if ((ch = read()) != '\n') {
        _peek[0] = (byte) ch;
        _peekOffset = 0;
        _peekLength = 1;
      }
    }

    return true;
  }
  
  public boolean canRead()
  {
    return true;
  }

  /**
   * Returns the number of available bytes.
   */
  public int getAvailable()
    throws IOException
  {
    if (_isPartDone)
      return 0;
    else if (_peekOffset < _peekLength)
      return _peekLength - _peekOffset;
    else {
      int ch = read();
      if (ch < 0)
        return 0;
      _peekOffset = 0;
      _peekLength = 1;
      _peek[0] = (byte) ch;

      return 1;
    }
  }

  /**
   * Reads from the multipart mime buffer.
   */
  public int read(byte []buffer, int offset, int length) throws IOException
  {
    int b = -1;

    if (_isPartDone)
      return -1;

    int i = 0;
    // Need the last peek or would miss the initial '\n'
    while (_peekOffset + 1 < _peekLength && length > 0) {
      buffer[offset + i++] = _peek[_peekOffset++];
      length--;
    }

    while (i < length && (b = read()) >= 0) {
      boolean hasCr = false;

      if (b == '\r') {
        hasCr = true;
        b = read();

        // XXX: Macintosh?
        if (b != '\n') {
          buffer[offset + i++] = (byte) '\r';
          _peek[0] = (byte) b;
          _peekOffset = 0;
          _peekLength = 1;
          continue;
        }
      }
      else if (b != '\n') {
        buffer[offset + i++] = (byte) b;
        continue;
      }

      int j;
      for (j = 0;
           j < _boundaryLength && (b = read()) >= 0 && _boundaryBuffer[j] == b;
           j++) {
      }

      if (j == _boundaryLength) {
        _isPartDone = true;
        if ((b = read()) == '-') {
          if ((b = read()) == '-') {
            _isDone = true;
	    _isComplete = true;
	  }
	}
	
        for (; b > 0 && b != '\r' && b != '\n'; b = read()) {
        }
        if (b == '\r' && (b = read()) != '\n') {
          _peek[0] = (byte) b;
          _peekOffset = 0;
          _peekLength = 1;
        }

        return i > 0 ? i : -1;
      }

      _peekLength = 0;
      if (hasCr && i + 1 < length) {
        buffer[offset + i++] = (byte) '\r';
        buffer[offset + i++] = (byte) '\n';
      }
      else if (hasCr) {
        buffer[offset + i++] = (byte) '\r';
        _peek[_peekLength++] = (byte) '\n';
      }
      else {
        buffer[offset + i++] = (byte) '\n';
      }

      int k = 0;
      while (k < j && i + 1 < length)
        buffer[offset + i++] = _boundaryBuffer[k++];

      while (k < j)
        _peek[_peekLength++] = _boundaryBuffer[k++];

      _peek[_peekLength++] = (byte) b;
      _peekOffset = 0;
    }

    if (i <= 0) {
      _isPartDone = true;
      if (b < 0)
        _isDone = true;
      return -1;
    }
    else {
      return i;
    }
  }

  /**
   * Read the next byte from the peek or from the underlying stream.
   */
  private int read()
    throws IOException
  {
    if (_peekOffset < _peekLength)
      return _peek[_peekOffset++] & 0xff;
    else
      return _is.read();
  }

  private static String getAttributePart(String attr, String name)
  {
    if (attr == null)
      return null;
    
    int length = attr.length();
    int i = attr.indexOf(name);
    if (i < 0)
      return null;

    for (i += name.length(); i < length && attr.charAt(i) != '='; i++) {
    }
    
    for (i++; i < length && attr.charAt(i) == ' '; i++) {
    }

    CharBuffer value = CharBuffer.allocate();
    if (i < length && attr.charAt(i) == '\'') {
      for (i++; i < length && attr.charAt(i) != '\''; i++)
        value.append(attr.charAt(i));
    }
    else if (i < length && attr.charAt(i) == '"') {
      for (i++; i < length && attr.charAt(i) != '"'; i++)
        value.append(attr.charAt(i));
    }
    else if (i < length) {
      char ch;
      for (; i < length && (ch = attr.charAt(i)) != ' ' && ch != ';'; i++)
        value.append(ch);
    }

    return value.close();
  }
}
