package nxm.webvnc.prim;

import java.io.File;
import java.util.logging.Level;

import nxm.sys.inc.Chainable;
import nxm.sys.lib.LogUtilities;
import nxm.sys.lib.Midas;
import nxm.sys.lib.Primitive;
import nxm.sys.lib.SSLUtilities;
import nxm.sys.lib.Shell;
import nxm.sys.lib.Table;
import nxm.sys.lib.Time;
import nxm.sys.net.HPage;
import nxm.sys.net.HServer;
import nxm.sys.net.HSource;
import nxm.webvnc.lib.CloseInstanceHandler;
import nxm.webvnc.lib.NewInstanceHandler;
import nxm.webvnc.lib.RestSource;
import nxm.webvnc.lib.RootHFiles;
import nxm.webvnc.lib.VNCSessionManager;

/**
 * Stands up a web accessible vnc server using websockify and novnc
 *
 * @author adw
 * @since NeXtMidas 4.1.0
 */
public class webvncserver extends Primitive implements Chainable {
  /** The HServer object for the instance. */
  protected HServer server;

  /** The thread that the server executes in. */
  protected Thread srvThread;

  /** Manages vncserver displays available to users */
  private VNCSessionManager sessionManager = null;

  private boolean debug = false;
  private boolean sslDebug = false;
  private boolean manual = false;
  private boolean ansiColors = false; // default to Shell ANSI Color output

  /** default constructor */
  public webvncserver () {}

  public int open () {
    int port                = MA.getL("PORT", 8080); // 0 for any free port
    String dir              = MA.getCS("DIR", Shell.getOptPath("webvnc", "web"));
    int maxSessions         = MA.getL("MAXSESSIONS", 4); // max number of simultaneous vnc sessions
    String geometry         = MA.getCS("GEOMETRY", null); // max number of simultaneous vnc sessions
    String xstartup         = MA.getCS("XSTARTUP", "~/.vnc/xstartup");
    String VNCSecurityTypes = MA.getCS("VNCSECURITYTYPES", null);
    String sslConfigFile    = MA.getCS("SSLCONFIG", null);
    String certPEM          = MA.getCS("CERTPEM", null);
    String keyPEM           = MA.getCS("KEYPEM", null);

    int portRange          = MA.getL("/PRANGE", 1); // range of port numbers above port to use if already allocated
    boolean https          = MA.getState("/HTTPS", MA.find("/HTTPS"));
    boolean httpsOneWay    = MA.getState("/ONEWAY", MA.find("/ONEWAY"));
    boolean anyHostName    = MA.getState("/ANYHOSTNAME", MA.find("/ANYHOSTNAME"));
    boolean disableDefault = MA.getState("/NODEFAULT", MA.find("/NODEFAULT"));
    String mName           = MA.getCS("/MNAME", null);
    debug                  = MA.getState("/DEBUG", MA.find("/DEBUG"));
    sslDebug               = MA.getState("/SSLDEBUG", MA.find("/SSLDEBUG"));
    manual                 = MA.getState("/MANUAL", MA.find("/MANUAL"));
    ansiColors             = MA.getState("/ANSICOLORS", MA.find("/ANSICOLORS"));

    if (sslDebug) System.setProperty("javax.net.debug", "ssl,handshake");
    HServer.setDebug(debug);
    Midas.setUseANSIColors(ansiColors);

    if (debug) {
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("PORT             = {}", port), null);
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("DIR              = {}", dir), null);
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("MAXSESSIONS      = {}", maxSessions), null);
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("GEOMETRY         = {}", geometry), null);
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("XSTARTUP         = {}", xstartup), null);
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("VNCSECURITYTYPES = {}", VNCSecurityTypes), null);
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("SSLCONFIG        = {}", sslConfigFile), null);
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("CERTPEM          = {}", certPEM), null);
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("KEYPEM           = {}", keyPEM), null);

      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("PRANGE           = {}", portRange), null);
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("MNAME            = {}", mName), null);
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("NODEFAULT        = {}", disableDefault), null);
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("HTTPS            = {}", https), null);
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("ONEWAY           = {}", httpsOneWay), null);
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("ANYHOSTNAME      = {}", anyHostName), null);
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("DEBUG            = {}", debug), null);
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("SSLDEBUG         = {}", sslDebug), null);
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("MANUAL           = {}", manual), null);
    }


    HServer.setHttps(https); // Is it HTTPS, set it in HServer since 3.9.0
    HServer.setOneWay(httpsOneWay); // Is it insecure (one way auth). set it in HServer since 3.9.0

    boolean isWin = Shell.isWindows();
    if (isWin) {
      // TODO
      String msg = LogUtilities.buildMessage("Windows not yet supported");
      if (ansiColors) LogUtilities.midasLog(null, Level.WARNING, msg, null);
      else LogUtilities.log(Level.WARNING, msg);
      return ABORT;

      // windows fully qualified path will start with C:, not a File.separator
//      if(dir.startsWith(File.separator)) {
//        dir = "C:"+ dir;
//      }
      // If it doesn't start with C:, we need to prepend nmroot to get full path.
//      else if(!dir.startsWith("C:")) {
//        dir = Shell.getNmRoot() + dir;
//      }
    }
    // Linux full path will start with a File.separator
    else if (!dir.startsWith(File.separator)) {
      dir = Shell.getNmRoot() + dir;
    }

    if (https) {
      String msg = LogUtilities.buildMessage("HTTPS Enabled. Initializing Context");
      if (ansiColors) LogUtilities.midasLog(null, Level.INFO, msg, null);
      else LogUtilities.log(Level.INFO, msg);
      if (SSLUtilities.isInitDone()) {
        msg = LogUtilities.buildMessage("SSLUtilities init already complete! Settings May not have taken effect!");
        if (ansiColors) LogUtilities.midasLog(null, Level.WARNING, msg, null);
        else LogUtilities.log(Level.WARNING, msg);
      } else {
        if (anyHostName) {
          msg = LogUtilities.buildMessage("/ANYHOSTNAME is set. Hostname verification will not be performed!");
          if (ansiColors) LogUtilities.midasLog(null, Level.WARNING, msg, null);
          else LogUtilities.log(Level.WARNING, msg);
          SSLUtilities.initAnyHostNameSSL(null, null, sslConfigFile, null, null, null, null, null);
        } else {
          SSLUtilities.initSSLContext(null, sslConfigFile);
        }
      }
    }

    if (https && debug) {
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("[DEBUG] SSLUtilities.properties System Property = {}", System.getProperty("SSLUtilities.properties")), null);
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("[DEBUG] Using Cert                              = {}", SSLUtilities.getUserDN()), null);
      LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("[DEBUG] Keystore type                           = {}", SSLUtilities.getCurrentTrustStore().getType()), null);
      try {
        LogUtilities.midasLog(null, Level.INFO, LogUtilities.buildMessage("[DEBUG] Keystore key count                      = {}", SSLUtilities.getCurrentTrustStore().size()), null );
      } catch (Exception e) {
        LogUtilities.midasLog(null, Level.SEVERE, LogUtilities.buildMessage("[DEBUG] Keystore Not Initialized"), null);
      }
    }

    // Create the server
    server = HServer.launch(null, "HTTP Server", port, portRange);
    // Remove the default sources
    server.removeSource("");
    server.removeSource("nmroot");
    server.setParent(this);
    // Add the root directory
    server.addSource(new RootHFiles(dir), true);

    if ((mName != null) && (mName.length() > 0)) {
      if (getMessageHandler() == null) {
        M.warning("HTTPSERV: Found /MNAME= but missing /MSGID=.");
      } else {
        boolean isRoot = mName.equals("/"); // /MNAME is the root on this server and should override all
        server.addSource(new MacroSource(mName), isRoot);
      }
    }

    if (!disableDefault) {
      sessionManager = new VNCSessionManager(ansiColors)
          .setMaxSessions(maxSessions)
          .setManual(manual)
          .setGeometry(geometry)
          .setSecurityTypes(VNCSecurityTypes);
      if (https) {
        sessionManager.enableSSL(certPEM, keyPEM);
      }
      if (xstartup != null) {
        sessionManager.setXStartup(xstartup);
      }
      sessionManager.setDebug(debug);
      server.addSource(new RestSource("rest/session/new", new NewInstanceHandler(sessionManager)));
      server.addSource(new RestSource("rest/session/close", new CloseInstanceHandler(sessionManager)));
    }

    return NORMAL;
  }

  public int process () {
    if (server.isRunning())
      return NOOP;
    else
      return FINISH;
  }

  public int close () {
    server.stopThread();
    if (sessionManager != null) {
      sessionManager.closeAllVNCSessions();
    }
    return NORMAL;
  }

  /** Listens for requests, and forwards them to the calling macro via
   *  sendMessage()
   */
  private class MacroSource extends HSource {
    public MacroSource (String name) {
      super(name);
    }

    /** Invoked to write the data from this source into the specified HPage.
     *
     *  @param uri The Request string containing the request parameters.
     *  @param hp  The HPage to write the data into.
     */
    public void handleRequest (String uri, HPage hp) {
      Table tbl = new Table();
      tbl.put("PARAMETERS", hp.getParameters());
      tbl.put("SOCKET", hp);
      sendMessage("HTTP" + hp.getMethod(), 0, tbl);
      while (!hp.isClosed()) {
        Time.sleep(0.5); // Wait for the request to be handled.
      }
    }
  }

  public Object getNextLink () {
    return server;
  }

  public Object getPrevLink () {
    return null;
  }
}
