/*
 * 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.http;

import java.io.IOException;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;

import com.caucho.network.listen.TcpSocketLink;
import com.caucho.server.cluster.ServletService;
import com.caucho.server.webapp.WebApp;
import com.caucho.util.CharBuffer;
import com.caucho.util.CurrentTime;
import com.caucho.vfs.WriteStream;

public class HttpResponse extends AbstractHttpResponse
{
  private static final Logger log
    = Logger.getLogger(HttpResponse.class.getName());
  
  private static final byte []_http10ok = "HTTP/1.0 200 OK".getBytes();
  private static final byte []_http11ok = "HTTP/1.1 200 OK".getBytes();
  private static final byte []_contentLengthBytes = "\r\nContent-Length: ".getBytes();
  private static final byte []_contentTypeBytes = "\r\nContent-Type: ".getBytes();
  private static final byte []_textHtmlBytes = "\r\nContent-Type: text/html".getBytes();
  private static final byte []_charsetBytes = "; charset=".getBytes();
  private static final byte []_textHtmlLatin1Bytes = "\r\nContent-Type: text/html; charset=iso-8859-1".getBytes();

  private static final byte []_connectionCloseBytes = "\r\nConnection: close".getBytes();

  private static final char []_connectionCb = "Connection".toCharArray();
  private static final CharBuffer _closeCb = new CharBuffer("Close");

  private final byte []_resinServerBytes;

  private final HttpRequest _request;
  private final CharBuffer _cb = new CharBuffer();
  
  private boolean _isChunked;

  private WriteStream _rawWrite;

  /**
   * Creates a new HTTP-protocol response.
   *
   * @param request the matching request object.
   */
  HttpResponse(HttpRequest request, WriteStream rawWrite)
  {
    super(request);

    _request = request;
    _rawWrite = rawWrite;

    ServletService server = request.getServer();

    _resinServerBytes = ("\r\nServer: " + server.getServerHeader()).getBytes();
  }

  @Override
  protected AbstractResponseStream createResponseStream()
  {
    HttpRequest request = (HttpRequest) getRequest();

    return new HttpResponseStream(this, request.getRawWrite());
  }

  boolean isChunkedEncoding()
  {
    return _isChunked;
  }

  protected WriteStream getRawWrite()
  {
    return _rawWrite;
  }

  /**
   * Upgrade protocol
   */
  /*
  @Override
  public TcpDuplexController upgradeProtocol(TcpDuplexHandler handler)
  {
    TcpConnection conn
      = (TcpConnection) ((TcpServerRequest) getRequest()).getConnection();

    TcpDuplexController controller = conn.toDuplex(handler);

    HttpServletResponseImpl response = _request.getResponseFacade();

    response.setStatus(101);
    setContentLength(0);

    if (log.isLoggable(Level.FINE))
      log.fine(this + " upgrade HTTP to " + handler);

    return controller;
  }
  */

  /**
   * Writes the 100 continue response.
   */
  @Override
    protected void writeContinueInt()
    throws IOException
  {
    // #2938, server/0558
    /*
    os.print("HTTP/1.1 100 Continue");

    if (! containsHeader("Server"))
      os.write(_resinServerBytes, 0, _resinServerBytes.length);

    os.print("\r\nContent-Length: 0");

    long now = CurrentTime.getCurrentTime();
    if (_lastDate + 1000 < now) {
      fillDate(now);
    }

    os.write(_dateBuffer, 0, _dateBufferLength);
    os.flush();
    */

    // server/269n
    WriteStream os = getRawWrite();
    os.print("HTTP/1.1 100 Continue\r\n\r\n");
    os.flush();
  }

  /**
   * Implementation to write the HTTP headers.  If the length is positive,
   * it's a small request where the buffer contains the entire request,
   * so the length is already known.
   *
   * @param os the output stream to write the headers to.
   * @param length if non-negative, the length of the entire request.
   *
   * @return true if the data in the request should use chunked encoding.
   */
  @Override
  protected boolean writeHeadersInt(int length,
                                    boolean isHead)
    throws IOException
  {
    HttpServletRequestImpl request = _request.getRequestFacade();
    HttpServletResponseImpl response = _request.getResponseFacade();

    if (request == null)
      return false;

    _isChunked = false;

    int version = _request.getVersion();
    boolean debug = log.isLoggable(Level.FINE);

    if (version < HttpRequest.HTTP_1_0) {
      _request.killKeepalive("http client version " + version);
      return false;
    }

    TcpSocketLink tcpConn = null;

    if (_request.getConnection() instanceof TcpSocketLink)
      tcpConn = (TcpSocketLink) _request.getConnection();

    WebApp webApp = request.getWebApp();

    String contentType = response.getContentTypeImpl();
    String charEncoding = response.getCharacterEncodingImpl();

    WriteStream os = getRawWrite();
    
    long contentLength = getContentLengthHeader();

    int statusCode = response.getStatus();
    if (statusCode == 200) {
      if (version < HttpRequest.HTTP_1_1)
        os.write(_http10ok, 0, _http10ok.length);
      else
        os.write(_http11ok, 0, _http11ok.length);
    } else {
      if (version < HttpRequest.HTTP_1_1)
        os.printLatin1("HTTP/1.0 ");
      else
        os.printLatin1("HTTP/1.1 ");

      os.write((statusCode / 100) % 10 + '0');
      os.write((statusCode / 10) % 10 + '0');
      os.write(statusCode % 10 + '0');
      os.write(' ');
      os.printLatin1(response.getStatusMessage());
    }

    if (debug) {
      log.fine(_request.dbgId() + "HTTP/1.1 " +
               statusCode + " " + response.getStatusMessage());
    }

    boolean isUpgrade = false;

    if (tcpConn != null && tcpConn.isDuplex()) {
      isUpgrade = true;

      String upgrade = getHeader("Upgrade");

      if (upgrade != null) {
        os.printLatin1("\r\nUpgrade: ");
        os.printLatin1NoLf(upgrade);
      }

      os.printLatin1("\r\nConnection: Upgrade");
      _request.killKeepalive("duplex/upgrade");

      if (debug)
        log.fine(_request.dbgId() + "Connection: Upgrade");
    }

    String serverHeader = getServerHeader();
    if (serverHeader == null) {
      os.write(_resinServerBytes, 0, _resinServerBytes.length);
    }
    else {
      os.printLatin1("\r\nServer: ");
      os.printLatin1NoLf(serverHeader);
    }

    if (statusCode >= 400) {
      removeHeader("ETag");
      removeHeader("Last-Modified");
    }
    else if (statusCode == HttpServletResponse.SC_NOT_MODIFIED
             || statusCode == HttpServletResponse.SC_NO_CONTENT) {
      // php/1b0k

      contentType = null;
    }
    else if (response.isCacheControl()) {
      // application manages cache control
    }
    else if (response.isNoCache()) {
      // server/1b15
      removeHeader("ETag");
      removeHeader("Last-Modified");

      // even in case of 302, this may be needed for filters which
      // automatically set cache headers
      setHeaderImpl("Expires", "Thu, 01 Dec 1994 16:00:00 GMT");

      os.printLatin1("\r\nCache-Control: no-cache");

      if (debug) {
        log.fine(_request.dbgId() + "" +
                 "Cache-Control: no-cache");
      }
    }
    else if (response.isNoCacheUnlessVary()
             && ! containsHeader("Vary")) {
      os.printLatin1("\r\nCache-Control: private");

      if (debug) {
        log.fine(_request.dbgId() + "Cache-Control: private");
      }
    }
    else if (response.isPrivateCache()) {
      if (HttpRequest.HTTP_1_1 <= version) {
        // technically, this could be private="Set-Cookie,Set-Cookie2"
        // but caches don't recognize it, so there's no real extra value
        os.printLatin1("\r\nCache-Control: private");

        if (debug)
          log.fine(_request.dbgId() + "Cache-Control: private");
      }
      else {
        setHeaderImpl("Expires", "Thu, 01 Dec 1994 16:00:00 GMT");
        os.printLatin1("\r\nCache-Control: no-cache");

        if (debug) {
          log.fine(_request.dbgId() + "CacheControl: no-cache");
        }
      }
    }
    
    ArrayList<String> headerKeys = getHeaderKeys();
    ArrayList<String> headerValues = getHeaderValues();

    int size = headerKeys.size();
    for (int i = 0; i < size; i++) {
      String key = (String) headerKeys.get(i);

      if (isUpgrade && "Upgrade".equalsIgnoreCase(key))
        continue;

      os.write('\r');
      os.write('\n');
      os.printLatin1NoLf(key);
      os.write(':');
      os.write(' ');
      os.printLatin1NoLf((String) headerValues.get(i));

      if (debug) {
        log.fine(_request.dbgId() + "" +
                 key + ": " + headerValues.get(i));
      }
    }

    long now = CurrentTime.getCurrentTime();
    ArrayList<Cookie> cookiesOut = response.getCookies();

    if (cookiesOut != null) {
      for (int i = 0; i < cookiesOut.size(); i++) {
        Cookie cookie = cookiesOut.get(i);
        int cookieVersion = cookie.getVersion();

        CharBuffer cb = _cb;
        // XXX:
        cb.clear();
        fillCookie(cb, cookie, now, cookieVersion, false);
        os.printLatin1("\r\nSet-Cookie: ");
        os.printLatin1(cb.getBuffer(), 0, cb.getLength());
        if (cookieVersion > 0) {
          cb.clear();
          fillCookie(cb, cookie, now, cookieVersion, true);
          os.printLatin1("\r\nSet-Cookie2: ");
          os.printLatin1(cb.getBuffer(), 0, cb.getLength());
        }

        if (debug)
          log.fine(_request.dbgId() + "Set-Cookie: " + cb);
      }
    }

    if (contentType != null) {
      // server/1b5a
      if (charEncoding == null
          && (contentType.startsWith("text/")
              || contentType.startsWith("multipart/"))) {
        if (webApp != null)
          charEncoding = webApp.getCharacterEncoding();

        // always use a character encoding to avoid XSS attacks (?)
        if (charEncoding == null)
          charEncoding = "utf-8";
      }

      os.write(_contentTypeBytes, 0, _contentTypeBytes.length);
      os.printLatin1(contentType);
      
      if (charEncoding != null) {
        os.write(_charsetBytes, 0, _charsetBytes.length);
        os.printLatin1(charEncoding);
      }

      if (debug) {
        log.fine(_request.dbgId() + "Content-Type: " + contentType
                 + "; charset=" + charEncoding);
      }
    }

    if (hasFooter()) {
      contentLength = -1;
      length = -1;
    }

    boolean hasContentLength = false;
    /*
    if (isHead()) {
      // server/269t, server/0560
      hasContentLength = true;
      os.write(_contentLengthBytes, 0, _contentLengthBytes.length);
      os.print(0);
    }
    else
    */
    if (contentLength >= 0) {
      os.write(_contentLengthBytes, 0, _contentLengthBytes.length);
      os.print(contentLength);
      hasContentLength = true;

      if (debug)
        log.fine(_request.dbgId() + "Content-Length: " + contentLength);
    }
    else if (statusCode == HttpServletResponse.SC_NOT_MODIFIED) {
      // #3089
      // In the HTTP spec, a 304 has no message body so the content-length
      // is not needed.  The content-length is not explicitly forbidden,
      // but does cause problems with certain clients.
      hasContentLength = true;
      setHead();
    }
    else if (statusCode == HttpServletResponse.SC_NO_CONTENT) {
      hasContentLength = true;
      os.write(_contentLengthBytes, 0, _contentLengthBytes.length);
      os.print(0);
      setHead();

      if (debug)
        log.fine(_request.dbgId() + "Content-Length: 0");
    }
    else if (length >= 0) {
      os.write(_contentLengthBytes, 0, _contentLengthBytes.length);
      os.print(length);
      hasContentLength = true;

      if (debug)
        log.fine(_request.dbgId() + "Content-Length: " + length);
    }

    if (version < HttpRequest.HTTP_1_1) {
      _request.killKeepalive("http response version: " + version);
    }
    else {
      /* XXX: the request processing already processed this header
      CharSegment conn = _request.getHeaderBuffer(_connectionCb,
                                                  _connectionCb.length);
      if (conn != null && conn.equalsIgnoreCase(_closeCb)) {
        _request.killKeepalive();
      }
      else
      */

      if (_request.isKeepalive()) {
      }
      else if (isUpgrade) {
        _request.killKeepalive("http response upgrade");
      }
      else {
        os.write(_connectionCloseBytes, 0, _connectionCloseBytes.length);

        if (debug)
          log.fine(_request.dbgId() + "Connection: close");
      }
    }

    if (HttpRequest.HTTP_1_1 <= version
        && ! hasContentLength
        && ! isHead) {
      os.printLatin1("\r\nTransfer-Encoding: chunked");
      _isChunked = true;

      if (debug)
        log.fine(_request.dbgId() + "Transfer-Encoding: chunked");
    }
    
    byte []dateBuffer = fillDateBuffer(now);
    int dateBufferLength = getDateBufferLength();
    
    if (_isChunked)
      os.write(dateBuffer, 0, dateBufferLength - 2);
    else
      os.write(dateBuffer, 0, dateBufferLength);

    /*
    if (_lastDate / 1000 != now / 1000) {
      fillDate(now);
    }

    if (_isChunked)
      os.write(_dateBuffer, 0, _dateBufferLength - 2);
    else
      os.write(_dateBuffer, 0, _dateBufferLength);
      */

    return _isChunked;
  }
  
  /*
  public byte []fillDateBuffer(long now)
  {
    if (_lastDate / 1000 != now / 1000) {
      fillDate(now);
    }
    
    return _dateBuffer;
  }
  
  public int getDateBufferLength()
  {
    return _dateBufferLength;
  }

  private void fillDate(long now)
  {
    if (_lastDate / 60000 == now / 60000) {
      int min = (int) (now / 60000 % 60);
      int sec = (int) (now / 1000 % 60);

      int m2 = '0' + (min / 10);
      int m1 = '0' + (min % 10);

      int s2 = '0' + (sec / 10);
      int s1 = '0' + (sec % 10);

      _dateBuffer[28] = (byte) m2;
      _dateBuffer[29] = (byte) m1;

      _dateBuffer[31] = (byte) s2;
      _dateBuffer[32] = (byte) s1;

      _lastDate = now;

      return;
    }

    _lastDate = now;
    _calendar.setGMTTime(now);
    _dateCharBuffer.clear();
    _dateCharBuffer.append("\r\nDate: ");
    _calendar.printDate(_dateCharBuffer);

    char []cb = _dateCharBuffer.getBuffer();
    int len = _dateCharBuffer.getLength();

    for (int i = len - 1; i >= 0; i--)
      _dateBuffer[i] = (byte) cb[i];

    _dateBuffer[len] = (byte) '\r';
    _dateBuffer[len + 1] = (byte) '\n';
    _dateBuffer[len + 2] = (byte) '\r';
    _dateBuffer[len + 3] = (byte) '\n';

    _dateBufferLength = len + 4;
  }
  */

  @Override
  public String toString()
  {
    return "HttpResponse" + _request.dbgId();
  }
}
