/*
 * 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 java.io.IOException;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.caucho.VersionFactory;
import com.caucho.cloud.network.ClusterServer;
import com.caucho.cloud.topology.CloudCluster;
import com.caucho.cloud.topology.CloudPod;
import com.caucho.cloud.topology.CloudServer;
import com.caucho.server.cluster.ServletService;
import com.caucho.server.host.Host;
import com.caucho.server.host.HostController;
import com.caucho.server.webapp.WebApp;
import com.caucho.server.webapp.WebAppContainer;
import com.caucho.server.webapp.WebAppController;
import com.caucho.util.Base64;
import com.caucho.util.CharBuffer;
import com.caucho.util.Crc64;
import com.caucho.util.CurrentTime;
import com.caucho.vfs.ReadStream;
import com.caucho.vfs.WriteStream;

/**
 * Handles the filter mapping (config) requests from a remote dispatcher.
 */
public class HmuxDispatchRequest {
  private static final Logger log
    = Logger.getLogger(HmuxDispatchRequest.class.getName());

  // other, specialized protocols
  public static final int HMUX_HOST =      'h';
  public static final int HMUX_QUERY_ALL = 'q';
  public static final int HMUX_QUERY_URL = 'r';
  public static final int HMUX_QUERY_SERVER = 's';
  public static final int HMUX_WEB_APP =   'a';
  public static final int HMUX_MATCH =     'm';
  public static final int HMUX_IGNORE =    'i';
  public static final int HMUX_ETAG =      'e';
  public static final int HMUX_NO_CHANGE = 'n';
  public static final int HMUX_CLUSTER =   'c';
  public static final int HMUX_SRUN    =   's';
  public static final int HMUX_SRUN_BACKUP = 'b';
  public static final int HMUX_SRUN_SSL = 'e';
  public static final int HMUX_UNAVAILABLE = 'u';
  public static final int HMUX_WEB_APP_UNAVAILABLE = 'U';

  private CharBuffer _cb = new CharBuffer();

  private HmuxRequest _request;
  private ServletService _server;

  public HmuxDispatchRequest(HmuxRequest request)
  {
    _request = request;

    _server = request.getServer();
  }

  /**
   * Handles a new request.  Initializes the protocol handler and
   * the request streams.
   *
   * <p>Note: ClientDisconnectException must be rethrown to
   * the caller.
   */
  public boolean handleRequest(ReadStream is, WriteStream os)
    throws IOException
  {
    boolean isLoggable = log.isLoggable(Level.FINE);
    int code;
    int len;
    String host = "";
    String etag = null;

    while (true) {
      os.flush();
      
      code = is.read();

      switch (code) {
      case -1:
        if (isLoggable)
          log.fine(dbgId() + "end of file");
        return false;

      case HmuxRequest.HMUX_QUIT:
        if (isLoggable)
          log.fine(dbgId() + (char) code + ": end of request");
        return true;

      case HmuxRequest.HMUX_EXIT:
        if (isLoggable)
          log.fine(dbgId() + (char) code + ": end of socket");

        return false;

      case HMUX_ETAG:
        len = (is.read() << 8) + is.read();
        _cb.clear();
        is.readAll(_cb, len);
        etag = _cb.toString();

        if (isLoggable)
          log.fine(dbgId() + "etag: " + etag);
        break;

      case HMUX_HOST:
        len = (is.read() << 8) + is.read();
        _cb.clear();
        is.readAll(_cb, len);
        host = _cb.toString();

        if (isLoggable)
          log.fine(dbgId() + "host: " + host);
        break;

      case HMUX_QUERY_ALL:
        len = (is.read() << 8) + is.read();
        _cb.clear();
        is.readAll(_cb, len);

        if (isLoggable)
          log.fine(dbgId() + "query: " + _cb);

        queryAll(os, host, _cb.toString(), etag);
        break;

        /*
      case HMUX_QUERY_SERVER:
        len = (is.read() << 8) + is.read();
        _cb.clear();
        is.readAll(_cb, len);

        if (isLoggable)
          log.fine(dbgId() + "query-server: " + _cb);

        queryCluster(os, host, _cb.toString());
        break;
        */
        
      default:
        len = (is.read() << 8) + is.read();

        if (isLoggable)
          log.fine(dbgId() + (char) code + " " + len + " (dispatch)");
        is.skip(len);
        break;
      }
    }

    // _filter.setClientClosed(true);

    // return false;
  }
  
  /**
   * Returns the url.
   */
  private void queryAll(WriteStream os, String hostName,
                        String url, String etag)
    throws IOException
  {
    int channel = 2;
    boolean isLoggable = log.isLoggable(Level.FINE);
    
    os.write(HmuxRequest.HMUX_CHANNEL);
    os.write(channel >> 8);
    os.write(channel);

    Host host = _server.getHost(hostName, 80);
    if (host == null) {
      HostController controller = _server.getHostController(hostName, 80);
      
      if (controller != null) {
        writeString(os, HMUX_UNAVAILABLE, "");
      }
      else {
        writeString(os, HmuxRequest.HMUX_HEADER, "check-interval");
        writeString(os, HmuxRequest.HMUX_STRING,
                    String.valueOf(_server.getDependencyCheckInterval() / 1000));
      }

      if (isLoggable)
        log.fine(dbgId() + "host '" + hostName + "' not configured");
      return;
    }
    else if (! host.getState().isActive()) {
      writeString(os, HMUX_UNAVAILABLE, "");

      if (isLoggable)
        log.fine(dbgId() + "host '" + host + "' not active");
      return;
    }

    if (host.getConfigETag() == null)
      sendQuery(null, host, hostName, url);

    if (etag == null) {
    }
    else if (etag.equals(host.getConfigETag())) {
      if (isLoggable)
        log.fine(dbgId() + "host '" + host + "' no change");
      
      writeString(os, HMUX_NO_CHANGE, "");
      return;
    }
    else if (etag.equals("h-" + host.getName())) {
      if (isLoggable) {
        log.fine(dbgId() + "host alias '" + hostName + " -> '"
                 + host + "' no change");
      }
      
      writeString(os, HMUX_NO_CHANGE, "");
      return;
    }
    else {
      if (isLoggable)
        log.fine(dbgId() + "host '" + host + "' changed");
    }
    
    sendQuery(os, host, hostName, url);
  }

  /**
   * Writes the host data, returning the crc
   */
  private void sendQuery(WriteStream os, Host host,
                         String hostName, String url)
    throws IOException
  {
    boolean isLoggable = log.isLoggable(Level.FINE);
    
    long crc64 = 0;

    if (! CurrentTime.isTest())
      crc64 = Crc64.generate(crc64, VersionFactory.getFullVersion());
    
    queryServer(os);

    String canonicalHostName = host.getHostName();

    if (canonicalHostName.equals("default"))
      canonicalHostName = "";
    
    writeString(os, HMUX_HOST, canonicalHostName);

    if (hostName.equals(canonicalHostName)) {
      crc64 = queryCluster(os, host, crc64);

      WebAppContainer webAppContainer = host.getWebAppContainer();
      
      WebAppController controller = webAppContainer.findByURI(url);
      if (controller != null) {
        try {
          controller.request();
        } catch (Throwable e) {
          log.log(Level.WARNING, e.toString(), e);
        }
      }

      for (WebAppController appEntry : webAppContainer.getWebAppList()) {
        if (appEntry.getParent() != null
            && appEntry.getParent().isDynamicDeploy()) {
          continue;
        }

        writeString(os, HMUX_WEB_APP, appEntry.getContextPath());
        if (isLoggable)
          log.fine(dbgId() + "web-app '" + appEntry.getContextPath() + "'");

        crc64 = Crc64.generate(crc64, appEntry.getContextPath());

        WebApp app = appEntry.getWebApp();

        if (appEntry.isDynamicDeploy()) {
          writeString(os, HMUX_MATCH, "/*");

          crc64 = Crc64.generate(crc64, "/*");

          if (isLoggable)
            log.fine(dbgId() + "dynamic '" + appEntry.getContextPath() + "'");
        }
        else if (app == null || ! app.isActive()) {
          if (isLoggable)
            log.fine(dbgId() + "not active '" + appEntry.getContextPath() + "'");

          writeString(os, HMUX_WEB_APP_UNAVAILABLE, "");
        }
        else {
          if (isLoggable)
            log.fine(dbgId() + "active '" + appEntry.getContextPath() + "'");
          ArrayList<String> patternList = app.getServletMappingPatterns();

          for (int j = 0; patternList != null && j < patternList.size(); j++) {
            String pattern = patternList.get(j);

            writeString(os, HMUX_MATCH, pattern);

            crc64 = Crc64.generate(crc64, pattern);
          }

          patternList = app.getServletIgnoreMappingPatterns();

          for (int j = 0; patternList != null && j < patternList.size(); j++) {
            String pattern = patternList.get(j);

            writeString(os, HMUX_IGNORE, pattern);

            crc64 = Crc64.generate(crc64, "i");
            crc64 = Crc64.generate(crc64, pattern);
          }
        }
      }

      CharBuffer cb = new CharBuffer();
      Base64.encode(cb, crc64);
      String newETag = cb.close();
      host.setConfigETag(newETag);
    
      writeString(os, HMUX_ETAG, host.getConfigETag());
    }
    else {
      // aliased hosts use the host name as the etag
      writeString(os, HMUX_ETAG, "h-" + host.getName());
    }
  }
  
  /**
   * Queries the cluster.
   */
  private long queryCluster(WriteStream os, Host host, long crc64)
    throws IOException
  {
    /*
    int channel = 2;
    
    os.write(HmuxRequest.HMUX_CHANNEL);
    os.write(channel >> 8);
    os.write(channel);
    */

    CloudCluster cluster = host.getCluster();

    if (cluster == null)
      return 0;

    writeString(os, HMUX_CLUSTER, cluster.getId());

    crc64 = Crc64.generate(crc64, cluster.getId());

    CloudPod []pods = cluster.getPodList();
    
    int serverLength = (pods.length > 0 ? pods[0].getServerLength() : 0);
    CloudServer []servers = (pods.length > 0
                             ? pods[0].getServerList()
                             : null);
    
    if (serverLength > 0) {
      CloudServer cloudServer = servers[0];
      ClusterServer server = cloudServer.getData(ClusterServer.class);

      crc64 = writeTime(os, crc64, "live-time", 
                        server.getLoadBalanceIdleTime());

      crc64 = writeTime(os, crc64, "dead-time", 
                        server.getLoadBalanceRecoverTime());

      crc64 = writeTime(os, crc64, "read-timeout", 
                        server.getLoadBalanceSocketTimeout());

      crc64 = writeTime(os, crc64, "connect-timeout", 
                        server.getLoadBalanceConnectTimeout());
    }

    for (int i = 0; i < serverLength; i++) {
      CloudServer cloudServer = servers[i];
      
      if (cloudServer == null)
        continue;
      
      ClusterServer server = cloudServer.getData(ClusterServer.class);

      if (server != null) {
        String srunHost = server.getIp() + ":" + server.getPort();

        boolean isSSL = false; // server.isSSL();
        
        if (server.isBackup())
          writeString(os, HMUX_SRUN_BACKUP, srunHost);
        else if (isSSL)
          writeString(os, HMUX_SRUN_SSL, srunHost);
        else
          writeString(os, HMUX_SRUN, srunHost);
      
        crc64 = Crc64.generate(crc64, srunHost);
      }
    }

    return crc64;
  }
  
  private long writeTime(WriteStream os, long crc, String header, long time)
    throws IOException
  {
    crc = Crc64.generate(crc, time);

    writeTime(os, header, time);
    
    return crc;
  }
  
  private void writeTime(WriteStream os, String header, long time)
    throws IOException
  {
    writeString(os, HmuxRequest.HMUX_HEADER, header);
    writeString(os, HmuxRequest.HMUX_STRING, "" + ((time + 999) / 1000));
  }
  
  /**
   * Queries the cluster.
   */
  private void queryServer(WriteStream os)
    throws IOException
  {
    writeTime(os, "check-interval", _server.getDependencyCheckInterval());
    
    writeString(os, HmuxRequest.HMUX_HEADER, "cookie");
    writeString(os, HmuxRequest.HMUX_STRING,
                _server.getSessionCookie());
    writeString(os, HmuxRequest.HMUX_HEADER, "ssl-cookie");
    writeString(os, HmuxRequest.HMUX_STRING,
                _server.getSSLSessionCookie());
    writeString(os, HmuxRequest.HMUX_HEADER, "session-url-prefix");
    writeString(os, HmuxRequest.HMUX_STRING,
                _server.getSessionURLPrefix());
    writeString(os, HmuxRequest.HMUX_HEADER, "alt-session-url-prefix");
    writeString(os, HmuxRequest.HMUX_STRING,
                _server.getAlternateSessionURLPrefix());

    if (_server.getConnectionErrorPage() != null) {
      writeString(os, HmuxRequest.HMUX_HEADER, "connection-error-page");
      writeString(os, HmuxRequest.HMUX_STRING,
                  _server.getConnectionErrorPage());
    }
  }

  void writeString(WriteStream os, int code, String value)
    throws IOException
  {
    if (os == null)
      return;
    
    if (value == null)
      value = "";
    
    int len = value.length();

    os.write(code);
    os.write(len >> 8);
    os.write(len);
    os.print(value);
    
    if (log.isLoggable(Level.FINE))
      log.fine(dbgId() + (char)code + " " + value);
  }

  void writeString(WriteStream os, int code, CharBuffer value)
    throws IOException
  {
    if (os == null)
      return;
    
    int len = value.length();

    os.write(code);
    os.write(len >> 8);
    os.write(len);
    os.print(value);
    
    if (log.isLoggable(Level.FINE))
      log.fine(dbgId() + (char)code + " " + value);
  }

  private String dbgId()
  {
    return _request.dbgId();
  }
}
