/* Copyright (C) 2002-2004 RealVNC Ltd.  All Rights Reserved.
 *    
 * This 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.
 * 
 * This software 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.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this software; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
 * USA.
 */

// -=- WinVNC Version 4.0 Main Routine

#include <winvnc/VNCServerWin32.h>
#include <winvnc/resource.h>
#include <winvnc/STrayIcon.h>

#include <rfb_win32/Win32Util.h>
#include <rfb_win32/Service.h>
#include <rfb/SSecurityFactoryStandard.h>
#include <rfb/Hostname.h>
#include <rfb/LogWriter.h>

using namespace rfb;
using namespace win32;
using namespace winvnc;
using namespace network;

static LogWriter vlog("VNCServerWin32");


const TCHAR* winvnc::VNCServerWin32::RegConfigPath = _T("Software\\TightVNC\\WinVNC4");

const UINT VNCM_REG_CHANGED = WM_USER;
const UINT VNCM_COMMAND = WM_USER + 1;


static IntParameter http_port("HTTPPortNumber",
  "TCP/IP port on which the server will serve the Java applet VNC Viewer ", 5800);
static IntParameter port_number("PortNumber",
  "TCP/IP port on which the server will accept connections", 5900);
static StringParameter hosts("Hosts",
  "Filter describing which hosts are allowed access to this server", "+");
static VncAuthPasswdConfigParameter vncAuthPasswd;
static BoolParameter localHost("LocalHost",
  "Only accept connections from via the local loop-back network interface", false);


// -=- ManagedListener
//     Wrapper class which simplifies the management of a listening socket
//     on a specified port, attached to a SocketManager and SocketServer.
//     Ensures that socket and filter are deleted and updated appropriately.

class ManagedListener {
public:
  ManagedListener(win32::SocketManager* mgr, SocketServer* svr)
    : sock(0), filter(0), port(0), manager(mgr),
      server(svr), localOnly(0) {}
  ~ManagedListener() {setPort(0);}
  void setPort(int port, bool localOnly=false);
  void setFilter(const char* filter);
  TcpListener* sock;
protected:
  TcpFilter* filter;
  win32::SocketManager* manager;
  SocketServer* server;
  int port;
  bool localOnly;
};

// - If the port number/localHost setting has changed then tell the
//   SocketManager to shutdown and delete it.  Also remove &
//   delete the filter.  Then try to open a socket on the new port.
void ManagedListener::setPort(int newPort, bool newLocalOnly) {
  if ((port == newPort) && (localOnly == newLocalOnly) && sock) return;
  if (sock) {
    vlog.info("Closed TcpListener on port %d", port);
    sock->setFilter(0);
    delete filter;
    manager->remListener(sock);
    sock = 0;
    filter = 0;
  }
  port = newPort;
  localOnly = newLocalOnly;
  if (port != 0) {
    try {
      sock = new TcpListener(port, localOnly);
      vlog.info("Created TcpListener on port %d%s", port,
                localOnly ? "(localhost)" : "(any)");
    } catch (rdr::Exception& e) {
      vlog.error("TcpListener on port %d failed (%s)", port, e.str());
    }
  }
  if (sock)
    manager->addListener(sock, server);
}

void ManagedListener::setFilter(const char* newFilter) {
  if (!sock) return;
  vlog.info("Updating TcpListener filter");
  sock->setFilter(0);
  delete filter;
  filter = new TcpFilter(newFilter);
  sock->setFilter(filter);
}


VNCServerWin32::VNCServerWin32()
  : vncServer(CStr(ComputerName().buf), &desktop),
    httpServer(0), runServer(false),
    isDesktopStarted(false),
    command(NoCommand), commandSig(commandLock),
    queryConnectDialog(0) {
  // Create the Java-viewer HTTP server
  httpServer = new JavaViewerServer(&vncServer);

  // Initialise the desktop
  desktop.setStatusLocation(&isDesktopStarted);

  // Initialise the VNC server
  vncServer.setQueryConnectionHandler(this);

  // Register the desktop's event to be handled
  sockMgr.addEvent(desktop.getUpdateEvent(), &desktop);
}

VNCServerWin32::~VNCServerWin32() {
  // Stop the SDisplay from updating our state
  desktop.setStatusLocation(0);

  // Destroy the HTTP server
  delete httpServer;
}


int VNCServerWin32::run() {
  { Lock l(runLock);
    hostThread = Thread::self();
    runServer = true;
  }

  // - Register for notification of configuration changes
  if (isServiceProcess())
    config.setKey(HKEY_LOCAL_MACHINE, RegConfigPath);
  else
    config.setKey(HKEY_CURRENT_USER, RegConfigPath);
  config.setNotifyThread(Thread::self(), VNCM_REG_CHANGED);

  // - Create the tray icon if possible
  STrayIconThread trayIcon(*this, IDI_ICON, IDI_CONNECTED, IDR_TRAY);

  DWORD result = 0;
  try {
    // - Create some managed listening sockets
    ManagedListener rfb(&sockMgr, &vncServer);
    ManagedListener http(&sockMgr, httpServer);

    // - Continue to operate until WM_QUIT is processed
    MSG msg;
    do {
      // -=- Make sure we're listening on the right ports.
      rfb.setPort(port_number, localHost);
      http.setPort(http_port, localHost);

      // -=- Update the Java viewer's web page port number.
      httpServer->setRFBport(rfb.sock ? port_number : 0);

      // -=- Update the TCP address filter for both ports, if open.
      CharArray pattern;
      pattern.buf = hosts.getData();
      if (!localHost) {
        rfb.setFilter(pattern.buf);
        http.setFilter(pattern.buf);
      }

      // - If there is a listening port then add the address to the
      //   tray icon's tool-tip text.
      {
        const TCHAR* prefix = isServiceProcess() ?
          _T("VNC Server (Service):") : _T("VNC Server (User):");

        std::list<char*> addrs;
        if (rfb.sock)
          rfb.sock->getMyAddresses(&addrs);
        else
          addrs.push_front(strDup("Not accepting connections"));

        std::list<char*>::iterator i, next_i;
        int length = _tcslen(prefix)+1;
        for (i=addrs.begin(); i!= addrs.end(); i++)
          length += strlen(*i) + 1;

        TCharArray toolTip(length);
        _tcscpy(toolTip.buf, prefix);
        for (i=addrs.begin(); i!= addrs.end(); i=next_i) {
          next_i = i; next_i ++;
          TCharArray addr = *i;    // Assumes ownership of string
          _tcscat(toolTip.buf, addr.buf);
          if (next_i != addrs.end())
            _tcscat(toolTip.buf, _T(","));
        }
        trayIcon.setToolTip(toolTip.buf);
      }

      vlog.debug("Entering message loop");

      // - Run the server until the registry changes, or we're told to quit
      while (sockMgr.getMessage(&msg, NULL, 0, 0)) {
        if (msg.hwnd == 0) {
          if (msg.message == VNCM_REG_CHANGED)
            break;
          if (msg.message == VNCM_COMMAND)
            doCommand();
        }
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }

    } while ((msg.message != WM_QUIT) || runServer);

    vlog.debug("Server exited cleanly");
  } catch (rdr::SystemException &s) {
    vlog.error(s.str());
    result = s.err;
  } catch (rdr::Exception &e) {
    vlog.error(e.str());
  }

  { Lock l(runLock);
    runServer = false;
    hostThread = 0;
  }

  return result;
}

void VNCServerWin32::stop() {
  Lock l(runLock);
  runServer = false;
  PostThreadMessage(hostThread->getThreadId(), WM_QUIT, 0, 0);
}


bool VNCServerWin32::disconnectClients(const char* reason) {
  return queueCommand(DisconnectClients, reason, 0);
}

bool VNCServerWin32::addNewClient(const char* client) {
  TcpSocket* sock = 0;
  try {
    CharArray hostname;
    int port;
    getHostAndPort(client, &hostname.buf, &port, 5500);
    vlog.error("port=%d", port);
    sock = new TcpSocket(hostname.buf, port);
    if (queueCommand(AddClient, sock, 0))
      return true;
    delete sock;
  } catch (...) {
    delete sock;
  }
  return false;
}


VNCServerST::queryResult VNCServerWin32::queryConnection(network::Socket* sock,
                                            const char* userName,
                                            char** reason)
{
  if (queryConnectDialog) {
    *reason = rfb::strDup("Another connection is currently being queried.");
    return VNCServerST::REJECT;
  }
  queryConnectDialog = new QueryConnectDialog(sock, userName, this);
  queryConnectDialog->startDialog();
  return VNCServerST::PENDING;
}

void VNCServerWin32::queryConnectionComplete() {
  Thread* qcd = queryConnectDialog;
  queueCommand(QueryConnectionComplete, 0, 0);
  delete qcd->join();
}


bool VNCServerWin32::queueCommand(Command cmd, const void* data, int len) {
  Lock l(commandLock);
  while (command != NoCommand) commandSig.wait();
  command = cmd;
  commandData = data;
  commandDataLen = len;
  if (PostThreadMessage(hostThread->getThreadId(), VNCM_COMMAND, 0, 0))
    while (command != NoCommand) commandSig.wait();
  else
    return false;
  return true;
}

void VNCServerWin32::doCommand() {
  Lock l(commandLock);
  if (command == NoCommand) return;

  // Perform the required command
  switch (command) {

  case DisconnectClients:
    // Disconnect all currently active VNC Viewers
    vncServer.closeClients((const char*)commandData);
    break;

  case AddClient:
    // Make a reverse connection to a VNC Viewer
    vncServer.addClient((network::Socket*)commandData, true);
    sockMgr.addSocket((network::Socket*)commandData, &vncServer);
    break;

  case QueryConnectionComplete:
    // The Accept/Reject dialog has completed
    // Get the result, then clean it up
    vncServer.approveConnection(queryConnectDialog->getSock(),
                                queryConnectDialog->isAccepted(),
                                "Connection rejected by user");
    queryConnectDialog = 0;
    break;

  default:
    vlog.error("unknown command %d queued", command);
  };

  // Clear the command and signal completion
  command = NoCommand;
  commandSig.signal();
}