/*
 * Copyright (c) 1998-2012 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 Scott Ferguson
 */

package com.caucho.server.hmux;

import com.caucho.util.Alarm;
import com.caucho.util.CurrentTime;
import com.caucho.vfs.*;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.Socket;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Underlying stream handling HTTP requests.
 */
class HmuxStream extends StreamImpl {
  private static final Logger log
    = Logger.getLogger(HmuxStream.class.getName());
  // reserved headers that should not be passed to the HTTP server
  private static HashMap<String,String> _reserved;

  private static final Object LOCK = new Object();
  
  // Saved keepalive stream for a new request.
  private static HmuxStream _savedStream;
  // Time the stream was saved
  private static long _saveTime;
  
  private long _socketTimeout = 30000L;

  private boolean _isSSL;

  private Socket _s;
  private InputStream _is;
  private OutputStream _os;
  private ReadStream _rs;
  private WriteStream _ws;

  // The server's host name
  private String _host;
  // The server's port
  private int _port;

  private String _virtualHost;

  // the method
  private String _method;
  // true for a HEAD stream
  private boolean _isHead;
  // true for a POST stream
  private boolean _isPost;

  // buffer containing the POST data
  private MemoryStream _tempStream;

  // true if keepalive is allowed
  private boolean _isKeepalive = true;
  // true after the request has been sent
  private boolean _didGet;
  // length of the current chunk, -1 on eof
  private int _chunkLength;
  // the request is done
  private boolean _isRequestDone;

  private HashMap<String,Object> _attributes;

  // Used to read unread bytes on recycle.
  private byte []_tempBuffer;

  /**
   * Create a new HTTP stream.
   */
  private HmuxStream(Path path, String host, int port, Socket s)
    throws IOException
  {
    _s = s;

    _host = host;
    _port = port;
    
    _is = _s.getInputStream();
    _os = _s.getOutputStream();

    _ws = VfsStream.openWrite(_os);
    _rs = VfsStream.openRead(_is, _ws);

    _attributes = new HashMap<String,Object>();

    init(path);
  }

  /**
   * Opens a new HTTP stream for reading, i.e. a GET request.
   *
   * @param path the URL for the stream
   *
   * @return the opened stream
   */
  static HmuxStreamWrapper openRead(HmuxPath path) throws IOException
  {
    HmuxStream stream = createStream(path);
    stream._isPost = false;

    return new HmuxStreamWrapper(stream);
  }

  /**
   * Opens a new HTTP stream for reading and writing, i.e. a POST request.
   *
   * @param path the URL for the stream
   *
   * @return the opened stream
   */
  static HmuxStreamWrapper openReadWrite(HmuxPath path) throws IOException
  {
    HmuxStream stream = createStream(path);
    stream._isPost = true;

    return new HmuxStreamWrapper(stream);
  }

  /**
   * Creates a new HTTP stream.  If there is a saved connection to
   * the same host, use it.
   *
   * @param path the URL for the stream
   *
   * @return the opened stream
   */
  static private HmuxStream createStream(HmuxPath path) throws IOException
  {
    String host = path.getHost();
    int port = path.getPort();

    HmuxStream stream = null;
    long streamTime = 0;
    synchronized (LOCK) {
      if (_savedStream != null &&
          host.equals(_savedStream.getHost()) &&
          port == _savedStream.getPort()) {
        stream = _savedStream;
        streamTime = _saveTime;
        _savedStream = null;
      }
    }

    if (stream == null) {
    }
    // if the stream is still valid, use it
    else if (CurrentTime.getCurrentTime() < streamTime + 5000) {
      stream.init(path);
      return stream;
    }
    // if the stream has timed out, close it
    else {
      try {
        stream._isKeepalive = false;
        stream.close();
      } catch (IOException e) {
        log.log(Level.FINE, e.toString(), e);
      }
    }

    Socket s;

    try {
      s = new Socket(host, port);
    } catch (ConnectException e) {
      throw new ConnectException(path.getURL() + ": " + e.getMessage());
    } catch (Exception e) {
      throw new ConnectException(path.getURL() + ": " + e.toString());
    }

    int socketTimeout = 300 * 1000;

    try {
      s.setSoTimeout(socketTimeout);
    } catch (Exception e) {
    }
          
    return new HmuxStream(path, host, port, s);
  }

  /**
   * Initializes the stream for the next request.
   */
  private void init(Path path)
  {
    _isRequestDone = false;
    _didGet = false;
    _isPost = false;
    _isHead = false;
    _method = null;
    _attributes.clear();
    
    setPath(path);

    if (path instanceof HmuxPath)
      _virtualHost = ((HmuxPath) path).getVirtualHost();
  }

  /**
   * Set if this should be an SSL connection.
   */
  public void setSSL(boolean isSSL)
  {
    _isSSL = isSSL;
  }

  /**
   * Set if this should be an SSL connection.
   */
  public boolean isSSL()
  {
    return _isSSL;
  }

  /**
   * Sets the method
   */
  public void setMethod(String method)
  {
    _method = method;
  }

  /**
   * Sets true if we're only interested in the head.
   */
  public void setHead(boolean isHead)
  {
    _isHead = isHead;
  }

  /**
   * Returns the stream's host.
   */
  public String getHost()
  {
    return _host;
  }

  /**
   * Returns the stream's port.
   */
  public int getPort()
  {
    return _port;
  }
  
  /**
   * Returns a header from the response returned from the HTTP server.
   *
   * @param name name of the header
   * @return the header value.
   */
  public Object getAttribute(String name)
    throws IOException
  {
    if (! _didGet)
      getConnInput();

    return _attributes.get(name.toLowerCase(Locale.ENGLISH));
  }

  /**
   * Returns an iterator of the returned header names.
   */
  public Iterator getAttributeNames()
    throws IOException
  {
    if (! _didGet)
      getConnInput();

    return _attributes.keySet().iterator();
  }

  /**
   * Sets a header for the request.
   */
  public void setAttribute(String name, Object value)
  {
    if (name.equals("method"))
      setMethod((String) value);
    else if (name.equals("socket-timeout")) {
      if (value instanceof Integer) {
        int socketTimeout = ((Integer) value).intValue();

        if (socketTimeout > 0) {
          try {
            if (_s != null)
              _s.setSoTimeout(socketTimeout);
          } catch (Exception e) {

          }
        }
      }
    }
    else
      _attributes.put(name.toLowerCase(Locale.ENGLISH), value);
  }

  /**
   * Remove a header for the request.
   */
  public void removeAttribute(String name)
  {
    _attributes.remove(name.toLowerCase(Locale.ENGLISH));
  }

  /**
   * Sets the timeout.
   */
  public void setSocketTimeout(long timeout)
    throws SocketException
  {
    if (_s != null)
      _s.setSoTimeout((int) timeout);
  }

  /**
   * The stream is always writable (?)
   */
  public boolean canWrite()
  {
    return true;
  }

  /**
   * Writes a buffer to the underlying stream.
   *
   * @param buffer the byte array to write.
   * @param offset the offset into the byte array.
   * @param length the number of bytes to write.
   * @param isEnd true when the write is flushing a close.
   */
  public void write(byte []buf, int offset, int length, boolean isEnd)
    throws IOException
  {
    if (! _isPost)
      return;

    if (_tempStream == null)
      _tempStream = new MemoryStream();

    _tempStream.write(buf, offset, length, isEnd);
  }

  /**
   * The stream is readable.
   */
  public boolean canRead()
  {
    return true;
  }

  /**
   * Read data from the connection.  If the request hasn't yet been sent
   * to the server, send it.
   */
  public int read(byte []buf, int offset, int length) throws IOException
  {
    try {
      return readInt(buf, offset, length);
    } catch (IOException e) {
      _isKeepalive = false;
      throw e;
    } catch (RuntimeException e) {
      _isKeepalive = false;
      throw e;
    }
  }
  
  /**
   * Read data from the connection.  If the request hasn't yet been sent
   * to the server, send it.
   */
  public int readInt(byte []buf, int offset, int length) throws IOException
  {
    if (! _didGet)
      getConnInput();

    if (_isRequestDone)
      return -1;

    try {
      int len = length;

      if (_chunkLength == 0) {
        if (! readData())
          _chunkLength = -1;
      }
      
      if (_chunkLength < 0)
        return -1;
      
      if (_chunkLength < len)
        len = _chunkLength;

      len = _rs.read(buf, offset, len);

      if (len < 0) {
      }
      else
        _chunkLength -= len;
    
      return len;
    } catch (IOException e) {
      _isKeepalive = false;
      throw e;
    } catch (RuntimeException e) {
      _isKeepalive = false;
      throw e;
    }
  }

  /**
   * Sends the request and initializes the response.
   */
  private void getConnInput() throws IOException
  {
    if (_didGet)
      return;

    try {
      getConnInputImpl();
    } catch (IOException e) {
      _isKeepalive = false;
      throw e;
    } catch (RuntimeException e) {
      _isKeepalive = false;
      throw e;
    }
  }

  /**
   * Send the request to the server, wait for the response and parse
   * the headers.
   */
  private void getConnInputImpl() throws IOException
  {
    if (_didGet)
      return;

    _didGet = true;

    _ws.write('C');
    _ws.write(0);
    _ws.write(0);

    if (_method != null) {
      writeString(HmuxRequest.HMUX_METHOD, _method);
    }
    else if (_isPost) {
      writeString(HmuxRequest.HMUX_METHOD, "POST");
    }
    else if (_isHead)
      writeString(HmuxRequest.HMUX_METHOD, "HEAD");
    else
      writeString(HmuxRequest.HMUX_METHOD, "GET");
    
    if (_virtualHost != null)
      writeString(HmuxRequest.HMUX_SERVER_NAME, _virtualHost);
    else {
      writeString(HmuxRequest.HMUX_SERVER_NAME, _path.getHost());
      _ws.print(_path.getHost());
      if (_path.getPort() != 80) {
        writeString(HmuxRequest.CSE_SERVER_PORT,
                    String.valueOf(_path.getPort()));
      }
    }

    // Not splitting query? Also fullpath?
    writeString(HmuxRequest.HMUX_URI, _path.getPath());

    if (_path.getQuery() != null)
      writeString(HmuxRequest.CSE_QUERY_STRING, _path.getQuery());
    
    Iterator iter = getAttributeNames();
    while (iter.hasNext()) {
      String name = (String) iter.next();
      if (_reserved.get(name.toLowerCase(Locale.ENGLISH)) == null) {
        writeString(HmuxRequest.HMUX_HEADER, name);
        writeString(HmuxRequest.HMUX_STRING, getAttribute(name));
      }
    }

    if (_isPost) {
      MemoryStream tempStream = _tempStream;
      _tempStream = null;
      if (tempStream != null) {
        TempBuffer tb = TempBuffer.allocate();
        byte []buffer = tb.getBuffer();
        int sublen;

        ReadStream postIn = tempStream.openReadAndSaveBuffer();

        while ((sublen = postIn.read(buffer, 0, buffer.length)) > 0) {
          _ws.write('D');
          _ws.write(sublen >> 8);
          _ws.write(sublen);
          _ws.write(buffer, 0, sublen);
        }

        tempStream.destroy();

        TempBuffer.free(tb);
        tb = null;
      }
    }

    _attributes.clear();

    _ws.write('Q');

    readData();

    if (_isHead)
      _isRequestDone = true;
  }

  private void writeString(int code, String string)
    throws IOException
  {
    WriteStream ws = _ws;

    ws.write((byte) code);
    int len = string.length();
    ws.write(len >> 8);
    ws.write(len);
    ws.print(string);
  }

  private void writeString(int code, Object obj)
    throws IOException
  {
    String string = String.valueOf(obj);
    
    WriteStream ws = _ws;

    ws.write((byte) code);
    int len = string.length();
    ws.write(len >> 8);
    ws.write(len);
    ws.print(string);
  }

  /**
   * Parse the headers returned from the server.
   */
  private boolean readData()
    throws IOException
  {
    boolean isDebug = log.isLoggable(Level.FINE);
    
    int code;

    ReadStream is = _rs;

    while ((code = is.read()) > 0) {
      switch (code) {
      case HmuxRequest.HMUX_CHANNEL:
        is.read();
        is.read();
        break;
      case HmuxRequest.HMUX_QUIT:
      case HmuxRequest.HMUX_EXIT:
        is.close();

        if (isDebug)
          log.fine("HMUX: " + (char) code);

        return false;

      case HmuxRequest.HMUX_YIELD:
        break;

      case HmuxRequest.HMUX_STATUS:
        String value = readString(is);
        _attributes.put("status", value.substring(0, 3));

        if (isDebug)
          log.fine("HMUX: " + (char) code + " " + value);
        break;

      case HmuxRequest.HMUX_DATA:
        _chunkLength = 256 * (is.read() & 0xff) + (is.read() & 0xff);

        if (isDebug)
          log.fine("HMUX: " + (char) code + " " + _chunkLength);

        return true;

      default:
        int len = 256 * (is.read() & 0xff) + (is.read() & 0xff);

        if (isDebug)
          log.fine("HMUX: " + (char) code + " " + len);

        is.skip(len);
        break;
      }
    }

    return false;
  }

  private String readString(ReadStream is)
    throws IOException
  {
    int len = 256 * (is.read() & 0xff) + is.read();

    char []buf = new char[len];

    is.readAll(buf, 0, len);

    return new String(buf);
  }

  /**
   * Returns the bytes still available.
   */
  public int getAvailable() throws IOException
  {
    if (! _didGet)
      getConnInput();

    return _rs.getAvailable();
  }

  /**
   * Close the connection.
   */
  public void close() throws IOException
  {
    if (_isKeepalive) {
      // If recycling, read any unread data
      if (! _didGet)
        getConnInput();

      if (! _isRequestDone) {
        if (_tempBuffer == null)
          _tempBuffer = new byte[256];

        try {
          while (read(_tempBuffer, 0, _tempBuffer.length) > 0) {
          }
        } catch (IOException e) {
          _isKeepalive = false;
        }
      }
    }

    if (com.caucho.server.util.CauchoSystem.isTesting())
      _isKeepalive = false; // XXX:
    
    if (_isKeepalive) {
      HmuxStream oldSaved;
      long now = CurrentTime.getCurrentTime();
      synchronized (LOCK) {
        oldSaved = _savedStream;
        _savedStream = this;
        _saveTime = now;
      }

      if (oldSaved != null && oldSaved != this) {
        oldSaved._isKeepalive = false;
        oldSaved.close();
      }

      return;
    }

    try {
      try {
        if (_ws != null)
          _ws.close();
      } catch (Throwable e) {
      }
      _ws = null;

      try {
        if (_rs != null)
          _rs.close();
      } catch (Throwable e) {
      }
      _rs = null;

      try {
        if (_os != null)
          _os.close();
      } catch (Throwable e) {
      }
      _os = null;

      try {
        if (_is != null)
          _is.close();
      } catch (Throwable e) {
      }
      _is = null;
    } finally {
      if (_s != null)
        _s.close();
      _s = null;
    }
  }

  static {
    _reserved = new HashMap<String,String>();
    _reserved.put("user-agent", "");
    _reserved.put("content-length", "");
    _reserved.put("content-encoding", "");
    _reserved.put("connection", "");
    _reserved.put("host", "");
  }
}
