/* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
 * Copyright 2011-2019 Pierre Ossman for Cendio AB
 * 
 * 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.
 */

// -=- SDisplay.cxx
//
// The SDisplay class encapsulates a particular system display.

#include <assert.h>

#include <rfb_win32/SDisplay.h>
#include <rfb_win32/Service.h>
#include <rfb_win32/TsSessions.h>
#include <rfb_win32/CleanDesktop.h>
#include <rfb_win32/CurrentUser.h>
#include <rfb_win32/MonitorInfo.h>
#include <rfb_win32/SDisplayCorePolling.h>
#include <rfb_win32/SDisplayCoreWMHooks.h>
#include <rfb/Exception.h>
#include <rfb/LogWriter.h>
#include <rfb/ledStates.h>


using namespace rdr;
using namespace rfb;
using namespace rfb::win32;

static LogWriter vlog("SDisplay");

// - SDisplay-specific configuration options

IntParameter rfb::win32::SDisplay::updateMethod("UpdateMethod",
  "How to discover desktop updates; 0 - Polling, 1 - Application hooking, 2 - Driver hooking.", 0);
BoolParameter rfb::win32::SDisplay::disableLocalInputs("DisableLocalInputs",
  "Disable local keyboard and pointer input while the server is in use", false);
StringParameter rfb::win32::SDisplay::disconnectAction("DisconnectAction",
  "Action to perform when all clients have disconnected.  (None, Lock, Logoff)", "None");
StringParameter displayDevice("DisplayDevice",
  "Display device name of the monitor to be remoted, or empty to export the whole desktop.", "");
BoolParameter rfb::win32::SDisplay::removeWallpaper("RemoveWallpaper",
  "Remove the desktop wallpaper when the server is in use.", false);
BoolParameter rfb::win32::SDisplay::disableEffects("DisableEffects",
  "Disable desktop user interface effects when the server is in use.", false);


//////////////////////////////////////////////////////////////////////////////
//
// SDisplay
//

// -=- Constructor/Destructor

SDisplay::SDisplay()
  : server(0), pb(0), device(0),
    core(0), ptr(0), kbd(0), clipboard(0),
    inputs(0), monitor(0), cleanDesktop(0), cursor(0),
    statusLocation(0), queryConnectionHandler(0), ledState(0)
{
  updateEvent.h = CreateEvent(0, TRUE, FALSE, 0);
  terminateEvent.h = CreateEvent(0, TRUE, FALSE, 0);
}

SDisplay::~SDisplay()
{
  // XXX when the VNCServer has been deleted with clients active, stop()
  // doesn't get called - this ought to be fixed in VNCServerST.  In any event,
  // we should never call any methods on VNCServer once we're being deleted.
  // This is because it is supposed to be guaranteed that the SDesktop exists
  // throughout the lifetime of the VNCServer.  So if we're being deleted, then
  // the VNCServer ought not to exist and therefore we shouldn't invoke any
  // methods on it.  Setting server to zero here ensures that stop() doesn't
  // call setPixelBuffer(0) on the server.
  server = 0;
  if (core) stop();
}


// -=- SDesktop interface

void SDisplay::start(VNCServer* vs)
{
  vlog.debug("starting");

  // Try to make session zero the console session
  if (!inConsoleSession())
    setConsoleSession();

  // Start the SDisplay core
  server = vs;
  startCore();

  vlog.debug("started");

  if (statusLocation) *statusLocation = true;
}

void SDisplay::stop()
{
  vlog.debug("stopping");

  // If we successfully start()ed then perform the DisconnectAction
  if (core) {
    CurrentUserToken cut;
    CharArray action(disconnectAction.getData());
    if (stricmp(action.buf, "Logoff") == 0) {
      if (!cut.h)
        vlog.info("ignoring DisconnectAction=Logoff - no current user");
      else
        ExitWindowsEx(EWX_LOGOFF, 0);
    } else if (stricmp(action.buf, "Lock") == 0) {
      if (!cut.h) {
        vlog.info("ignoring DisconnectAction=Lock - no current user");
      } else {
        LockWorkStation();
      }
    }
  }

  // Stop the SDisplayCore
  if (server)
    server->setPixelBuffer(0);
  stopCore();
  server = 0;

  vlog.debug("stopped");

  if (statusLocation) *statusLocation = false;
}

void SDisplay::terminate()
{
  SetEvent(terminateEvent);
}


void SDisplay::queryConnection(network::Socket* sock,
                               const char* userName)
{
  assert(server != NULL);

  if (queryConnectionHandler) {
    queryConnectionHandler->queryConnection(sock, userName);
    return;
  }

  server->approveConnection(sock, true);
}


void SDisplay::startCore() {

  // Currently, we just check whether we're in the console session, and
  //   fail if not
  if (!inConsoleSession())
    throw rdr::Exception("Console is not session zero - oreconnect to restore Console sessin");
  
  // Switch to the current input desktop
  if (rfb::win32::desktopChangeRequired()) {
    if (!rfb::win32::changeDesktop())
      throw rdr::Exception("unable to switch into input desktop");
  }

  // Initialise the change tracker and clipper
  updates.clear();
  clipper.setUpdateTracker(server);

  // Create the framebuffer object
  recreatePixelBuffer(true);

  // Create the SDisplayCore
  updateMethod_ = updateMethod;
  int tryMethod = updateMethod_;
  while (!core) {
    try {
      if (tryMethod == 1)
        core = new SDisplayCoreWMHooks(this, &updates);
      else
        core = new SDisplayCorePolling(this, &updates);
      core->setScreenRect(screenRect);
    } catch (rdr::Exception& e) {
      delete core; core = 0;
      if (tryMethod == 0)
        throw rdr::Exception("unable to access desktop");
      tryMethod--;
      vlog.error("%s", e.str());
    }
  }
  vlog.info("Started %s", core->methodName());

  // Start display monitor, clipboard handler and input handlers
  monitor = new WMMonitor;
  monitor->setNotifier(this);
  clipboard = new Clipboard;
  clipboard->setNotifier(this);
  ptr = new SPointer;
  kbd = new SKeyboard;
  inputs = new WMBlockInput;
  cursor = new WMCursor;

  // Apply desktop optimisations
  cleanDesktop = new CleanDesktop;
  if (removeWallpaper)
    cleanDesktop->disableWallpaper();
  if (disableEffects)
    cleanDesktop->disableEffects();
  isWallpaperRemoved = removeWallpaper;
  areEffectsDisabled = disableEffects;

  checkLedState();
  if (server)
    server->setLEDState(ledState);
}

void SDisplay::stopCore() {
  if (core)
    vlog.info("Stopping %s", core->methodName());
  delete core; core = 0;
  delete pb; pb = 0;
  delete device; device = 0;
  delete monitor; monitor = 0;
  delete clipboard; clipboard = 0;
  delete inputs; inputs = 0;
  delete ptr; ptr = 0;
  delete kbd; kbd = 0;
  delete cleanDesktop; cleanDesktop = 0;
  delete cursor; cursor = 0;
  ResetEvent(updateEvent);
}


bool SDisplay::isRestartRequired() {
  // - We must restart the SDesktop if:
  // 1. We are no longer in the input desktop.
  // 2. The any setting has changed.

  // - Check that our session is the Console
  if (!inConsoleSession())
    return true;

  // - Check that we are in the input desktop
  if (rfb::win32::desktopChangeRequired())
    return true;

  // - Check that the update method setting hasn't changed
  //   NB: updateMethod reflects the *selected* update method, not
  //   necessarily the one in use, since we fall back to simpler
  //   methods if more advanced ones fail!
  if (updateMethod_ != updateMethod)
    return true;

  // - Check that the desktop optimisation settings haven't changed
  //   This isn't very efficient, but it shouldn't change very often!
  if ((isWallpaperRemoved != removeWallpaper) ||
      (areEffectsDisabled != disableEffects))
    return true;

  return false;
}


void SDisplay::restartCore() {
  vlog.info("restarting");

  // Stop the existing Core  related resources
  stopCore();
  try {
    // Start a new Core if possible
    startCore();
    vlog.info("restarted");
  } catch (rdr::Exception& e) {
    // If startCore() fails then we MUST disconnect all clients,
    // to cause the server to stop() the desktop.
    // Otherwise, the SDesktop is in an inconsistent state
    // and the server will crash.
    server->closeClients(e.str());
  }
}


void SDisplay::handleClipboardRequest() {
  CharArray data(clipboard->getClipText());
  server->sendClipboardData(data.buf);
}

void SDisplay::handleClipboardAnnounce(bool available) {
  // FIXME: Wait for an application to actually request it
  if (available)
    server->requestClipboard();
}

void SDisplay::handleClipboardData(const char* data) {
  clipboard->setClipText(data);
}


void SDisplay::pointerEvent(const Point& pos, int buttonmask) {
  if (pb->getRect().contains(pos)) {
    Point screenPos = pos.translate(screenRect.tl);
    // - Check that the SDesktop doesn't need restarting
    if (isRestartRequired())
      restartCore();
    if (ptr)
      ptr->pointerEvent(screenPos, buttonmask);
  }
}

void SDisplay::keyEvent(rdr::U32 keysym, rdr::U32 keycode, bool down) {
  // - Check that the SDesktop doesn't need restarting
  if (isRestartRequired())
    restartCore();
  if (kbd)
    kbd->keyEvent(keysym, keycode, down);
}

bool SDisplay::checkLedState() {
  unsigned state = 0;

  if (GetKeyState(VK_SCROLL) & 0x0001)
    state |= ledScrollLock;
  if (GetKeyState(VK_NUMLOCK) & 0x0001)
    state |= ledNumLock;
  if (GetKeyState(VK_CAPITAL) & 0x0001)
    state |= ledCapsLock;

  if (ledState != state) {
    ledState = state;
    return true;
  }

  return false;
}


void
SDisplay::notifyClipboardChanged(bool available) {
  vlog.debug("clipboard text changed");
  if (server)
    server->announceClipboard(available);
}


void
SDisplay::notifyDisplayEvent(WMMonitor::Notifier::DisplayEventType evt) {
  switch (evt) {
  case WMMonitor::Notifier::DisplaySizeChanged:
    vlog.debug("desktop size changed");
    recreatePixelBuffer();
    break;
  case WMMonitor::Notifier::DisplayPixelFormatChanged:
    vlog.debug("desktop format changed");
    recreatePixelBuffer();
    break;
  default:
    vlog.error("unknown display event received");
  }
}

void
SDisplay::processEvent(HANDLE event) {
  if (event == updateEvent) {
    vlog.write(120, "processEvent");
    ResetEvent(updateEvent);

    // - If the SDisplay isn't even started then quit now
    if (!core) {
      vlog.error("not start()ed");
      return;
    }

    // - Ensure that the disableLocalInputs flag is respected
    inputs->blockInputs(disableLocalInputs);

    // - Only process updates if the server is ready
    if (server) {
      // - Check that the SDesktop doesn't need restarting
      if (isRestartRequired()) {
        restartCore();
        return;
      }

      // - Flush any updates from the core
      try {
        core->flushUpdates();
      } catch (rdr::Exception& e) {
        vlog.error("%s", e.str());
        restartCore();
        return;
      }

      // Ensure the cursor is up to date
      WMCursor::Info info = cursor->getCursorInfo();
      if (old_cursor != info) {
        // Update the cursor shape if the visibility has changed
        bool set_cursor = info.visible != old_cursor.visible;
        // OR if the cursor is visible and the shape has changed.
        set_cursor |= info.visible && (old_cursor.cursor != info.cursor);

        // Update the cursor shape
        if (set_cursor)
          pb->setCursor(info.visible ? info.cursor : 0, server);

        // Update the cursor position
        // NB: First translate from Screen coordinates to Desktop
        Point desktopPos = info.position.translate(screenRect.tl.negate());
        server->setCursorPos(desktopPos);

        old_cursor = info;
      }

      // Flush any changes to the server
      flushChangeTracker();

      // Forward current LED state to the server
      if (checkLedState())
        server->setLEDState(ledState);
    }
    return;
  }
  throw rdr::Exception("No such event");
}


// -=- Protected methods

void
SDisplay::recreatePixelBuffer(bool force) {
  // Open the specified display device
  //   If no device is specified, open entire screen using GetDC().
  //   Opening the whole display with CreateDC doesn't work on multi-monitor
  //   systems for some reason.
  DeviceContext* new_device = 0;
  TCharArray deviceName(displayDevice.getData());
  if (deviceName.buf[0]) {
    vlog.info("Attaching to device %s", (const char*)CStr(deviceName.buf));
    new_device = new DeviceDC(deviceName.buf);
  }
  if (!new_device) {
    vlog.info("Attaching to virtual desktop");
    new_device = new WindowDC(0);
  }

  // Get the coordinates of the specified dispay device
  Rect newScreenRect;
  if (deviceName.buf[0]) {
    MonitorInfo info(CStr(deviceName.buf));
    newScreenRect = Rect(info.rcMonitor.left, info.rcMonitor.top,
                         info.rcMonitor.right, info.rcMonitor.bottom);
  } else {
    newScreenRect = new_device->getClipBox();
  }

  // If nothing has changed & a recreate has not been forced, delete
  // the new device context and return
  if (pb && !force &&
    newScreenRect.equals(screenRect) &&
    new_device->getPF().equal(pb->getPF())) {
    delete new_device;
    return;
  }

  // Flush any existing changes to the server
  flushChangeTracker();

  // Delete the old pixelbuffer and device context
  vlog.debug("deleting old pixel buffer & device");
  if (pb)
    delete pb;
  if (device)
    delete device;

  // Create a DeviceFrameBuffer attached to the new device
  vlog.debug("creating pixel buffer");
  DeviceFrameBuffer* new_buffer = new DeviceFrameBuffer(*new_device);

  // Replace the old PixelBuffer
  screenRect = newScreenRect;
  pb = new_buffer;
  device = new_device;

  // Initialise the pixels
  pb->grabRegion(pb->getRect());

  // Prevent future grabRect operations from throwing exceptions
  pb->setIgnoreGrabErrors(true);

  // Update the clipping update tracker
  clipper.setClipRect(pb->getRect());

  // Inform the core of the changes
  if (core)
    core->setScreenRect(screenRect);

  // Inform the server of the changes
  if (server)
    server->setPixelBuffer(pb);
}

bool SDisplay::flushChangeTracker() {
  if (updates.is_empty())
    return false;

  vlog.write(120, "flushChangeTracker");

  // Translate the update coordinates from Screen coords to Desktop
  updates.translate(screenRect.tl.negate());

  // Clip the updates & flush them to the server
  updates.copyTo(&clipper);
  updates.clear();
  return true;
}
