/* Copyright (C) 2002-2005 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.
 */

#include <windows.h>
#include <commctrl.h>
#include <rfb/Configuration.h>
#include <rfb/LogWriter.h>
#include <rfb_win32/WMShatter.h>
#include <rfb_win32/LowLevelKeyEvents.h>
#include <rfb_win32/MonitorInfo.h>
#include <rfb_win32/DeviceContext.h>
#include <rfb_win32/Win32Util.h>
#include <rfb_win32/MsgBox.h>
#include <vncviewer/DesktopWindow.h>
#include <vncviewer/resource.h>

using namespace rfb;
using namespace rfb::win32;


// - Statics & consts

static LogWriter vlog("DesktopWindow");

const int TIMER_BUMPSCROLL = 1;
const int TIMER_POINTER_INTERVAL = 2;
const int TIMER_POINTER_3BUTTON = 3;


//
// -=- DesktopWindowClass

//
// Window class used as the basis for all DesktopWindow instances
//

class DesktopWindowClass {
public:
  DesktopWindowClass();
  ~DesktopWindowClass();
  ATOM classAtom;
  HINSTANCE instance;
};

LRESULT CALLBACK DesktopWindowProc(HWND wnd, UINT msg, WPARAM wParam, LPARAM lParam) {
  LRESULT result;
  if (msg == WM_CREATE)
    SetWindowLong(wnd, GWL_USERDATA, (long)((CREATESTRUCT*)lParam)->lpCreateParams);
  else if (msg == WM_DESTROY)
    SetWindowLong(wnd, GWL_USERDATA, 0);
  DesktopWindow* _this = (DesktopWindow*) GetWindowLong(wnd, GWL_USERDATA);
  if (!_this) {
    vlog.info("null _this in %x, message %u", wnd, msg);
    return rfb::win32::SafeDefWindowProc(wnd, msg, wParam, lParam);
  }

  try {
    result = _this->processMessage(msg, wParam, lParam);
  } catch (rfb::UnsupportedPixelFormatException &e) {
    MsgBox(0, e.str(), MB_OK);
    _this->getCallback()->closeWindow();
  } catch (rdr::Exception& e) {
    vlog.error("untrapped: %s", e.str());
  }

  return result;
};

static HCURSOR dotCursor = (HCURSOR)LoadImage(GetModuleHandle(0), MAKEINTRESOURCE(IDC_DOT_CURSOR), IMAGE_CURSOR, 0, 0, LR_SHARED);
static HCURSOR arrowCursor = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED); 

DesktopWindowClass::DesktopWindowClass() : classAtom(0) {
  WNDCLASS wndClass;
  wndClass.style = 0;
  wndClass.lpfnWndProc = DesktopWindowProc;
  wndClass.cbClsExtra = 0;
  wndClass.cbWndExtra = 0;
  wndClass.hInstance = instance = GetModuleHandle(0);
  wndClass.hIcon = (HICON)LoadImage(GetModuleHandle(0), MAKEINTRESOURCE(IDI_ICON), IMAGE_ICON, 0, 0, LR_SHARED);
  if (!wndClass.hIcon)
    printf("unable to load icon:%ld", GetLastError());
  wndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
  wndClass.hbrBackground = NULL;
  wndClass.lpszMenuName = 0;
  wndClass.lpszClassName = _T("rfb::win32::DesktopWindowClass");
  classAtom = RegisterClass(&wndClass);
  if (!classAtom) {
    throw rdr::SystemException("unable to register DesktopWindow window class", GetLastError());
  }
}

DesktopWindowClass::~DesktopWindowClass() {
  if (classAtom) {
    UnregisterClass((const TCHAR*)classAtom, instance);
  }
}

DesktopWindowClass baseClass;

//
// -=- FrameClass

//
// Window class used for child windows that display pixel data
//

class FrameClass {
public:
  FrameClass();
  ~FrameClass();
  ATOM classAtom;
  HINSTANCE instance;
};

LRESULT CALLBACK FrameProc(HWND wnd, UINT msg, WPARAM wParam, LPARAM lParam) {
  LRESULT result;
  if (msg == WM_CREATE)
    SetWindowLong(wnd, GWL_USERDATA, (long)((CREATESTRUCT*)lParam)->lpCreateParams);
  else if (msg == WM_DESTROY)
    SetWindowLong(wnd, GWL_USERDATA, 0);
  DesktopWindow* _this = (DesktopWindow*) GetWindowLong(wnd, GWL_USERDATA);
  if (!_this) {
    vlog.info("null _this in %x, message %u", wnd, msg);
    return rfb::win32::SafeDefWindowProc(wnd, msg, wParam, lParam);
  }

  try {
    result = _this->processFrameMessage(msg, wParam, lParam);
  } catch (rdr::Exception& e) {
    vlog.error("untrapped: %s", e.str());
  }

  return result;
}

FrameClass::FrameClass() : classAtom(0) {
  WNDCLASS wndClass;
  wndClass.style = 0;
  wndClass.lpfnWndProc = FrameProc;
  wndClass.cbClsExtra = 0;
  wndClass.cbWndExtra = 0;
  wndClass.hInstance = instance = GetModuleHandle(0);
  wndClass.hIcon = 0;
  wndClass.hCursor = NULL;
  wndClass.hbrBackground = NULL;
  wndClass.lpszMenuName = 0;
  wndClass.lpszClassName = _T("rfb::win32::FrameClass");
  classAtom = RegisterClass(&wndClass);
  if (!classAtom) {
    throw rdr::SystemException("unable to register Frame window class", GetLastError());
  }
}

FrameClass::~FrameClass() {
  if (classAtom) {
    UnregisterClass((const TCHAR*)classAtom, instance);
  }
}

FrameClass frameClass;


//
// -=- DesktopWindow instance implementation
//

DesktopWindow::DesktopWindow(Callback* cb) 
  : buffer(0),
    showToolbar(false), autoScaling(false),
    client_size(0, 0, 16, 16), window_size(0, 0, 32, 32),
    cursorVisible(false), cursorAvailable(false), cursorInBuffer(false),
    systemCursorVisible(true), trackingMouseLeave(false),
    handle(0), frameHandle(0), has_focus(false), palette_changed(false),
    fullscreenActive(false), fullscreenRestore(false),
    bumpScroll(false), callback(cb) {

  // Create the window
  const char* name = "DesktopWindow";
  handle = CreateWindow((const TCHAR*)baseClass.classAtom, TStr(name),
    WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
    0, 0, 10, 10, 0, 0, baseClass.instance, this);
  if (!handle)
    throw rdr::SystemException("unable to create WMNotifier window instance", GetLastError());
  vlog.debug("created window \"%s\" (%x)", name, handle);

  // Create the toolbar
  tb.create(handle);
  vlog.debug("created toolbar window \"%s\" (%x)", "ViewerToolBar", tb.getHandle());

  // Create the frame window
  frameHandle = CreateWindowEx(WS_EX_CLIENTEDGE, (const TCHAR*)frameClass.classAtom,
    0, WS_CHILD | WS_CLIPSIBLINGS | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT,
    CW_USEDEFAULT, CW_USEDEFAULT, handle, 0, frameClass.instance, this);
  if (!frameHandle) {
    throw rdr::SystemException("unable to create rfb frame window instance", GetLastError());
  }
  vlog.debug("created window \"%s\" (%x)", "Frame Window", frameHandle);

  // Initialise the CPointer pointer handler
  ptr.setHWND(frameHandle);
  ptr.setIntervalTimerId(TIMER_POINTER_INTERVAL);
  ptr.set3ButtonTimerId(TIMER_POINTER_3BUTTON);

  // Initialise the bumpscroll timer
  bumpScrollTimer.setHWND(handle);
  bumpScrollTimer.setId(TIMER_BUMPSCROLL);

  // Hook the clipboard
  clipboard.setNotifier(this);

  // Create the backing buffer
  buffer = new win32::ScaledDIBSectionBuffer(frameHandle);

  // Show the window
  centerWindow(handle, 0);
  ShowWindow(handle, SW_SHOW);
}

DesktopWindow::~DesktopWindow() {
  vlog.debug("~DesktopWindow");
  showSystemCursor();
  if (handle) {
    disableLowLevelKeyEvents(handle);
    DestroyWindow(handle);
    handle = 0;
  }
  delete buffer;
  vlog.debug("~DesktopWindow done");
}


void DesktopWindow::setFullscreen(bool fs) {
  if (fs && !fullscreenActive) {
    fullscreenActive = bumpScroll = true;

    // Un-minimize the window if required
    if (GetWindowLong(handle, GWL_STYLE) & WS_MINIMIZE)
      ShowWindow(handle, SW_RESTORE);

    // Save the current window position
    GetWindowRect(handle, &fullscreenOldRect);

    // Find the size of the display the window is on
    MonitorInfo mi(handle);

    // Hide the toolbar
    if (tb.isVisible())
      tb.hide();
    SetWindowLong(frameHandle, GWL_EXSTYLE, 0);

    // Set the window full-screen
    DWORD flags = GetWindowLong(handle, GWL_STYLE);
    fullscreenOldFlags = flags;
    flags = flags & ~(WS_CAPTION | WS_THICKFRAME | WS_MAXIMIZE | WS_MINIMIZE);
    vlog.debug("flags=%x", flags);

    SetWindowLong(handle, GWL_STYLE, flags);
    SetWindowPos(handle, HWND_TOP, mi.rcMonitor.left, mi.rcMonitor.top,
      mi.rcMonitor.right-mi.rcMonitor.left,
      mi.rcMonitor.bottom-mi.rcMonitor.top,
      SWP_FRAMECHANGED);
  } else if (!fs && fullscreenActive) {
    fullscreenActive = bumpScroll = false;

    // Show the toolbar
    if (showToolbar)
      tb.show();
    SetWindowLong(frameHandle, GWL_EXSTYLE, WS_EX_CLIENTEDGE);

    // Set the window non-fullscreen
    SetWindowLong(handle, GWL_STYLE, fullscreenOldFlags);

    // Set the window position
    SetWindowPos(handle, HWND_NOTOPMOST,
      fullscreenOldRect.left, fullscreenOldRect.top,
      fullscreenOldRect.right - fullscreenOldRect.left, 
      fullscreenOldRect.bottom - fullscreenOldRect.top,
      SWP_FRAMECHANGED);
  }

  // Adjust the viewport offset to cope with change in size between FS
  // and previous window state.
  setViewportOffset(scrolloffset);
}

void DesktopWindow::setShowToolbar(bool st)
{
  showToolbar = st;
  if (fullscreenActive) return;

  RECT r;
  GetWindowRect(handle, &r);
  bool maximized = GetWindowLong(handle, GWL_STYLE) & WS_MAXIMIZE;

  if (showToolbar && !tb.isVisible()) {
    refreshToolbarButtons();
    tb.show();
    if (!maximized) r.bottom += tb.getHeight();
  } else if (!showToolbar && tb.isVisible()) {
    tb.hide();
    if (!maximized) r.bottom -= tb.getHeight();
  }
  // Resize the chiled windows even if the parent window size 
  // has not been changed (the main window is maximized)
  if (maximized) SendMessage(handle, WM_SIZE, 0, 0);
  else SetWindowPos(handle, NULL, 0, 0, r.right-r.left, r.bottom-r.top, SWP_NOMOVE | SWP_NOZORDER);
}

void DesktopWindow::refreshToolbarButtons() {
  int scale = getDesktopScale();
  if (scale == 100) tb.enableButton(ID_ACTUAL_SIZE, false);
  else tb.enableButton(ID_ACTUAL_SIZE, true);
  if (scale <= 10) {
    tb.enableButton(ID_ZOOM_IN, true);
    tb.enableButton(ID_ZOOM_OUT, false);
  } else if (scale >= 200) {
    tb.enableButton(ID_ZOOM_IN, false);
    tb.enableButton(ID_ZOOM_OUT, true);
  } else {
    tb.enableButton(ID_ZOOM_IN, true);
    tb.enableButton(ID_ZOOM_OUT, true);
  }
  if (isAutoScaling()) tb.pressButton(ID_AUTO_SIZE, true);
  else tb.pressButton(ID_AUTO_SIZE, false);
}

void DesktopWindow::setDisableWinKeys(bool dwk) {
  // Enable low-level event hooking, so we get special keys directly
  if (dwk)
    enableLowLevelKeyEvents(handle);
  else
    disableLowLevelKeyEvents(handle);
}


void DesktopWindow::setMonitor(const char* monitor) {
  MonitorInfo mi(monitor);
  mi.moveTo(handle);
}

char* DesktopWindow::getMonitor() const {
  MonitorInfo mi(handle);
  return strDup(mi.szDevice);
}


bool DesktopWindow::setViewportOffset(const Point& tl) {
  Point np = Point(max(0, min(tl.x, buffer->width()-client_size.width())),
    max(0, min(tl.y, buffer->height()-client_size.height())));
  Point delta = np.translate(scrolloffset.negate());
  if (!np.equals(scrolloffset)) {
    scrolloffset = np;
    ScrollWindowEx(frameHandle, -delta.x, -delta.y, 0, 0, 0, 0, SW_INVALIDATE);
    UpdateWindow(frameHandle);
    return true;
  }
  return false;
}


bool DesktopWindow::processBumpScroll(const Point& pos)
{
  if (!bumpScroll) return false;
  int bumpScrollPixels = 20;
  bumpScrollDelta = Point();

  if (pos.x == client_size.width()-1)
    bumpScrollDelta.x = bumpScrollPixels;
  else if (pos.x == 0)
    bumpScrollDelta.x = -bumpScrollPixels;
  if (pos.y == client_size.height()-1)
    bumpScrollDelta.y = bumpScrollPixels;
  else if (pos.y == 0)
    bumpScrollDelta.y = -bumpScrollPixels;

  if (bumpScrollDelta.x || bumpScrollDelta.y) {
    if (bumpScrollTimer.isActive()) return true;
    if (setViewportOffset(scrolloffset.translate(bumpScrollDelta))) {
      bumpScrollTimer.start(25);
      return true;
    }
  }

  bumpScrollTimer.stop();
  return false;
}


LRESULT
DesktopWindow::processMessage(UINT msg, WPARAM wParam, LPARAM lParam) {
  switch (msg) {

    // -=- Process standard window messages

  case WM_NOTIFY:
    if (wParam == ID_TOOLBAR)
      tb.processWM_NOTIFY(wParam, lParam);
    break;

  case WM_DISPLAYCHANGE:
    // Display format has changed - notify callback
    callback->displayChanged();
    break;

    // -=- Window position

    // Prevent the window from being resized to be too large if in normal mode.
    // If maximized or fullscreen the allow oversized windows.

  case WM_WINDOWPOSCHANGING:
    {
      WINDOWPOS* wpos = (WINDOWPOS*)lParam;
      if ((wpos->flags & SWP_NOSIZE) || isAutoScaling())
        break;

      // Work out how big the window should ideally be
      DWORD current_style = GetWindowLong(frameHandle, GWL_STYLE);
      DWORD style = current_style & ~(WS_VSCROLL | WS_HSCROLL);
      DWORD style_ex = GetWindowLong(frameHandle, GWL_EXSTYLE);

      RECT r;
      SetRect(&r, 0, 0, buffer->width(), buffer->height());
      AdjustWindowRectEx(&r, style, FALSE, style_ex);
      Rect reqd_size = Rect(r.left, r.top, r.right, r.bottom);
      if (current_style & WS_VSCROLL)
        reqd_size.br.x += GetSystemMetrics(SM_CXVSCROLL);
      if (current_style & WS_HSCROLL)
        reqd_size.br.y += GetSystemMetrics(SM_CXHSCROLL);

      SetRect(&r, reqd_size.tl.x, reqd_size.tl.y, reqd_size.br.x, reqd_size.br.y);
      if (isToolbarEnabled())
        r.bottom += tb.getHeight();
      AdjustWindowRect(&r, GetWindowLong(handle, GWL_STYLE), FALSE);
      reqd_size = Rect(r.left, r.top, r.right, r.bottom);

      RECT current;
      GetWindowRect(handle, &current);

      if (!(GetWindowLong(handle, GWL_STYLE) & WS_MAXIMIZE) && !fullscreenActive) {
        // Ensure that the window isn't resized too large
        if (wpos->cx > reqd_size.width()) {
          wpos->cx = reqd_size.width();
          wpos->x = current.left;
        }
        if (wpos->cy > reqd_size.height()) {
          wpos->cy = reqd_size.height();
          wpos->y = current.top;
        }
      }
    }
    break;

    // Resize child windows and update window size info we have cached.

  case WM_SIZE:
    {
      Point old_offset = desktopToClient(Point(0, 0));
      RECT r;

      // Resize child windows
      GetClientRect(handle, &r);
      if (tb.isVisible()) {
        MoveWindow(frameHandle, 0, tb.getHeight(),
                   r.right, r.bottom - tb.getHeight(), TRUE);
      } else {
        MoveWindow(frameHandle, 0, 0, r.right, r.bottom, TRUE);
      }
      tb.autoSize();
 
      // Update the cached sizing information
      GetWindowRect(frameHandle, &r);
      window_size = Rect(r.left, r.top, r.right, r.bottom);
      GetClientRect(frameHandle, &r);
      client_size = Rect(r.left, r.top, r.right, r.bottom);

      // Perform the AutoScaling operation
      if (isAutoScaling()) {
        fitBufferToWindow(false);
      } else {
        // Determine whether scrollbars are required
        calculateScrollBars();
      }

      // Redraw if required
      if ((!old_offset.equals(desktopToClient(Point(0, 0)))) || isAutoScaling())
        InvalidateRect(frameHandle, 0, TRUE);
    }
    break;

    // -=- Bump-scrolling

  case WM_TIMER:
    switch (wParam) {
    case TIMER_BUMPSCROLL:
      if (!setViewportOffset(scrolloffset.translate(bumpScrollDelta)))
        bumpScrollTimer.stop();
      break;
    case TIMER_POINTER_INTERVAL:
    case TIMER_POINTER_3BUTTON:
      ptr.handleTimer(callback, wParam);
      break;
    }
    break;

    // -=- Track whether or not the window has focus

  case WM_SETFOCUS:
    has_focus = true;
    break;
  case WM_KILLFOCUS:
    has_focus = false;
    cursorOutsideBuffer();
    // Restore the keyboard to a consistent state
    kbd.releaseAllKeys(callback);
    break;

    // -=- If the menu is about to be shown, make sure it's up to date

  case WM_INITMENU:
    callback->refreshMenu(true);
    break;

    // -=- Handle the extra window menu items

    // Pass system menu messages to the callback and only attempt
    // to process them ourselves if the callback returns false.
  case WM_SYSCOMMAND:
    // Call the supplied callback
    if (callback->sysCommand(wParam, lParam))
      break;

    // - Not processed by the callback, so process it as a system message
    switch (wParam & 0xfff0) {

      // When restored, ensure that full-screen mode is re-enabled if required.
    case SC_RESTORE:
      {
      if (GetWindowLong(handle, GWL_STYLE) & WS_MINIMIZE) {
        rfb::win32::SafeDefWindowProc(handle, msg, wParam, lParam);
        setFullscreen(fullscreenRestore);
      }
      else if (fullscreenActive)
        setFullscreen(false);
      else
        rfb::win32::SafeDefWindowProc(handle, msg, wParam, lParam);

      return 0;
      }

      // If we are maximized or minimized then that cancels full-screen mode.
    case SC_MINIMIZE:
    case SC_MAXIMIZE:
      fullscreenRestore = fullscreenActive;
      setFullscreen(false);
      break;

    }
    break;

    // Treat all menu commands as system menu commands
  case WM_COMMAND:
    SendMessage(handle, WM_SYSCOMMAND, wParam, lParam);
    return 0;

    // -=- Handle keyboard input

  case WM_KEYUP:
  case WM_KEYDOWN:
    // Hook the MenuKey to pop-up the window menu
    if (menuKey && (wParam == menuKey)) {

      bool ctrlDown = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
      bool altDown = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0;
      bool shiftDown = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
      if (!(ctrlDown || altDown || shiftDown)) {

        // If MenuKey is being released then pop-up the menu
        if ((msg == WM_KEYDOWN)) {
          // Make sure it's up to date
          //
          // NOTE: Here we call refreshMenu only to grey out Move and Size
          //       menu items. Other things will be refreshed once again
          //       while processing the WM_INITMENU message.
          //
          callback->refreshMenu(false);

          // Show it under the pointer
          POINT pt;
          GetCursorPos(&pt);
          cursorInBuffer = false;
          TrackPopupMenu(GetSystemMenu(handle, FALSE),
            TPM_CENTERALIGN | TPM_VCENTERALIGN, pt.x, pt.y, 0, handle, 0);
        }

        // Ignore the MenuKey keypress for both press & release events
        return 0;
      }
    }
	case WM_SYSKEYDOWN:
	case WM_SYSKEYUP:
    kbd.keyEvent(callback, wParam, lParam, (msg == WM_KEYDOWN) || (msg == WM_SYSKEYDOWN));
    return 0;

    // -=- Handle mouse wheel scroll events

#ifdef WM_MOUSEWHEEL
  case WM_MOUSEWHEEL:
    processMouseMessage(msg, wParam, lParam);
    break;
#endif

    // -=- Handle the window closing

  case WM_CLOSE:
    vlog.debug("WM_CLOSE %x", handle);
    callback->closeWindow();
    break;

  }

  return rfb::win32::SafeDefWindowProc(handle, msg, wParam, lParam);
}

LRESULT
DesktopWindow::processFrameMessage(UINT msg, WPARAM wParam, LPARAM lParam) {
  switch (msg) {

    // -=- Paint the remote frame buffer

  case WM_PAINT:
    {
      PAINTSTRUCT ps;
      HDC paintDC = BeginPaint(frameHandle, &ps);
      if (!paintDC)
        throw rdr::SystemException("unable to BeginPaint", GetLastError());
      Rect pr = Rect(ps.rcPaint.left, ps.rcPaint.top, ps.rcPaint.right, ps.rcPaint.bottom);

      if (!pr.is_empty()) {

        // Draw using the correct palette
        PaletteSelector pSel(paintDC, windowPalette.getHandle());

        if (buffer->bitmap) {
          // Update the bitmap's palette
          if (palette_changed) {
            palette_changed = false;
            buffer->refreshPalette();
          }

          // Get device context
          BitmapDC bitmapDC(paintDC, buffer->bitmap);

          // Blit the border if required
          Rect bufpos = desktopToClient(buffer->getRect());
          if (!pr.enclosed_by(bufpos)) {
            vlog.debug("draw border");
            HBRUSH black = (HBRUSH) GetStockObject(BLACK_BRUSH);
            RECT r;
            SetRect(&r, 0, 0, bufpos.tl.x, client_size.height()); FillRect(paintDC, &r, black);
            SetRect(&r, bufpos.tl.x, 0, bufpos.br.x, bufpos.tl.y); FillRect(paintDC, &r, black);
            SetRect(&r, bufpos.br.x, 0, client_size.width(), client_size.height()); FillRect(paintDC, &r, black);
            SetRect(&r, bufpos.tl.x, bufpos.br.y, bufpos.br.x, client_size.height()); FillRect(paintDC, &r, black);
          }

          // Do the blit
          Point buf_pos = clientToDesktop(pr.tl);

          if (!BitBlt(paintDC, pr.tl.x, pr.tl.y, pr.width(), pr.height(),
                      bitmapDC, buf_pos.x, buf_pos.y, SRCCOPY))
            throw rdr::SystemException("unable to BitBlt to window", GetLastError());
        }
      }

      EndPaint(frameHandle, &ps);

      // - Notify the callback that a paint message has finished processing
      callback->paintCompleted();
    }
    return 0;

    // -=- Palette management

  case WM_PALETTECHANGED:
    vlog.debug("WM_PALETTECHANGED");
    if ((HWND)wParam == frameHandle) {
      vlog.debug("ignoring");
      break;
    }
  case WM_QUERYNEWPALETTE:
    vlog.debug("re-selecting palette");
    {
      WindowDC wdc(frameHandle);
      PaletteSelector pSel(wdc, windowPalette.getHandle());
      if (pSel.isRedrawRequired()) {
        InvalidateRect(frameHandle, 0, FALSE);
        UpdateWindow(frameHandle);
      }
    }
    return TRUE;

  case WM_VSCROLL:
  case WM_HSCROLL: 
    {
      Point delta;
      int newpos = (msg == WM_VSCROLL) ? scrolloffset.y : scrolloffset.x;

      switch (LOWORD(wParam)) {
      case SB_PAGEUP: newpos -= 50; break;
      case SB_PAGEDOWN: newpos += 50; break;
      case SB_LINEUP: newpos -= 5; break;
      case SB_LINEDOWN: newpos += 5; break;
      case SB_THUMBTRACK:
      case SB_THUMBPOSITION: newpos = HIWORD(wParam); break;
      default: vlog.info("received unknown scroll message");
      };

      if (msg == WM_HSCROLL)
        setViewportOffset(Point(newpos, scrolloffset.y));
      else
        setViewportOffset(Point(scrolloffset.x, newpos));
  
      SCROLLINFO si;
      si.cbSize = sizeof(si); 
      si.fMask  = SIF_POS; 
      si.nPos   = newpos; 
      SetScrollInfo(frameHandle, (msg == WM_VSCROLL) ? SB_VERT : SB_HORZ, &si, TRUE); 
    }
    break;

    // -=- Cursor shape/visibility handling

  case WM_SETCURSOR:
    if (LOWORD(lParam) != HTCLIENT)
      break;
    SetCursor(cursorInBuffer ? dotCursor : arrowCursor);
    return TRUE;

  case WM_MOUSELEAVE:
    trackingMouseLeave = false;
    cursorOutsideBuffer();
    return 0;

    // -=- Mouse input handling

  case WM_MOUSEMOVE:
  case WM_LBUTTONUP:
  case WM_MBUTTONUP:
  case WM_RBUTTONUP:
  case WM_LBUTTONDOWN:
  case WM_MBUTTONDOWN:
  case WM_RBUTTONDOWN:
    processMouseMessage(msg, wParam, lParam);
    break;
  }

  return rfb::win32::SafeDefWindowProc(frameHandle, msg, wParam, lParam);
}

void
DesktopWindow::processMouseMessage(UINT msg, WPARAM wParam, LPARAM lParam)
{
  if (!has_focus) {
    cursorOutsideBuffer();
    return;
  }

  if (!trackingMouseLeave) {
    TRACKMOUSEEVENT tme;
    tme.cbSize = sizeof(TRACKMOUSEEVENT);
    tme.dwFlags = TME_LEAVE;
    tme.hwndTrack = frameHandle;
    _TrackMouseEvent(&tme);
    trackingMouseLeave = true;
  }
  int mask = 0;
  if (LOWORD(wParam) & MK_LBUTTON) mask |= 1;
  if (LOWORD(wParam) & MK_MBUTTON) mask |= 2;
  if (LOWORD(wParam) & MK_RBUTTON) mask |= 4;

#ifdef WM_MOUSEWHEEL
  if (msg == WM_MOUSEWHEEL) {
    int delta = (short)HIWORD(wParam);
    int repeats = (abs(delta)+119) / 120;
    int wheelMask = (delta > 0) ? 8 : 16;
    vlog.debug("repeats %d, mask %d\n",repeats,wheelMask);
    for (int i=0; i<repeats; i++) {
      ptr.pointerEvent(callback, oldpos, mask | wheelMask);
      ptr.pointerEvent(callback, oldpos, mask);
    }
  } else {
#endif
    Point clientPos = Point(LOWORD(lParam), HIWORD(lParam));
    Point p = clientToDesktop(clientPos);

    // If the mouse is not within the server buffer area, do nothing
    cursorInBuffer = buffer->getRect().contains(p);
    if (!cursorInBuffer) {
      cursorOutsideBuffer();
      return;
    }

    // If we're locally rendering the cursor then redraw it
    if (cursorAvailable) {
      // - Render the cursor!
      if (!p.equals(cursorPos)) {
        hideLocalCursor();
        cursorPos = p;
        showLocalCursor();
        if (cursorVisible)
          hideSystemCursor();
      }
    }

    // If we are doing bump-scrolling then try that first...
    if (processBumpScroll(clientPos))
      return;

    // Send a pointer event to the server
    oldpos = p;
    if (buffer->isScaling()) {
      p.x /= buffer->getScaleRatio();
      p.y /= buffer->getScaleRatio();
    }
    ptr.pointerEvent(callback, p, mask);
#ifdef WM_MOUSEWHEEL
  }
#endif
}

void
DesktopWindow::hideLocalCursor() {
  // - Blit the cursor backing store over the cursor
  // *** ALWAYS call this BEFORE changing buffer PF!!!
  if (cursorVisible) {
    cursorVisible = false;
    buffer->DIBSectionBuffer::imageRect(cursorBackingRect, cursorBacking.data);
    invalidateDesktopRect(cursorBackingRect, false);
  }
}

void
DesktopWindow::showLocalCursor() {
  if (cursorAvailable && !cursorVisible && cursorInBuffer) {
    if (!buffer->getScaledPixelFormat().equal(cursor.getPF()) ||
      cursor.getRect().is_empty()) {
      vlog.info("attempting to render invalid local cursor");
      cursorAvailable = false;
      showSystemCursor();
      return;
    }
    cursorVisible = true;
    
    cursorBackingRect = cursor.getRect().translate(cursorPos).translate(cursor.hotspot.negate());
    cursorBackingRect = cursorBackingRect.intersect(buffer->getRect());
    buffer->getImage(cursorBacking.data, cursorBackingRect);

    renderLocalCursor();

    invalidateDesktopRect(cursorBackingRect, false);
  }
}

void DesktopWindow::cursorOutsideBuffer()
{
  cursorInBuffer = false;
  hideLocalCursor();
  showSystemCursor();
}

void
DesktopWindow::renderLocalCursor()
{
  Rect r = cursor.getRect();
  r = r.translate(cursorPos).translate(cursor.hotspot.negate());
  buffer->DIBSectionBuffer::maskRect(r, cursor.data, cursor.mask.buf);
}

void
DesktopWindow::hideSystemCursor() {
  if (systemCursorVisible) {
    vlog.debug("hide system cursor");
    systemCursorVisible = false;
    ShowCursor(FALSE);
  }
}

void
DesktopWindow::showSystemCursor() {
  if (!systemCursorVisible) {
    vlog.debug("show system cursor");
    systemCursorVisible = true;
    ShowCursor(TRUE);
  }
}


bool
DesktopWindow::invalidateDesktopRect(const Rect& crect, bool scaling) {
  Rect rect;
  if (buffer->isScaling() && scaling) {
    rect = desktopToClient(buffer->calculateScaleBoundary(crect));
  } else rect = desktopToClient(crect);
  if (rect.intersect(client_size).is_empty()) return false;
  RECT invalid = {rect.tl.x, rect.tl.y, rect.br.x, rect.br.y};
  InvalidateRect(frameHandle, &invalid, FALSE);
  return true;
}


void
DesktopWindow::notifyClipboardChanged(const char* text, int len) {
  callback->clientCutText(text, len);
}


void
DesktopWindow::setPF(const PixelFormat& pf) {
  // If the cursor is the wrong format then clear it
  if (!pf.equal(buffer->getScaledPixelFormat()))
    setCursor(0, 0, Point(), 0, 0);

  // Update the desktop buffer
  buffer->setPF(pf);
  
  // Redraw the window
  InvalidateRect(frameHandle, 0, FALSE);
}

void
DesktopWindow::setSize(int w, int h) {
  vlog.debug("setSize %dx%d", w, h);

  // If the locally-rendered cursor is visible then remove it
  hideLocalCursor();

  // Resize the backing buffer
  buffer->setSize(w, h);

  // Calculate the pixel buffer aspect correlation. It's used
  // for the autoScaling operation.
  aspect_corr = (double)w / h;

  // If the window is not maximised or full-screen then resize it
  if (!(GetWindowLong(handle, GWL_STYLE) & WS_MAXIMIZE) && !fullscreenActive) {
    // Resize the window to the required size
    RECT r = {0, 0, w, h};
    AdjustWindowRectEx(&r, GetWindowLong(frameHandle, GWL_STYLE), FALSE,
                       GetWindowLong(frameHandle, GWL_EXSTYLE));
    if (isToolbarEnabled())
      r.bottom += tb.getHeight();
    AdjustWindowRect(&r, GetWindowLong(handle, GWL_STYLE), FALSE);

    // Resize about the center of the window, and clip to current monitor
    MonitorInfo mi(handle);
    resizeWindow(handle, r.right-r.left, r.bottom-r.top);
    mi.clipTo(handle);
  } else {
    // Ensure the screen contents are consistent
    InvalidateRect(frameHandle, 0, FALSE);
  }

  // Enable/disable scrollbars as appropriate
  calculateScrollBars();
}

void DesktopWindow::setAutoScaling(bool as) { 
  autoScaling = as;
  if (as) fitBufferToWindow();
}

void DesktopWindow::setDesktopScaleRatio(double scale_ratio) {
  buffer->setScaleRatio(scale_ratio);
  InvalidateRect(frameHandle, 0, FALSE);
  if (!isAutoScaling()) calculateScrollBars();
  if (isToolbarEnabled()) refreshToolbarButtons();
  char *newTitle = new char[strlen(desktopName)+20];
  sprintf(newTitle, "%s @ %i%%", desktopName, getDesktopScale());
  SetWindowText(handle, TStr(newTitle));
  delete [] newTitle;
}

void DesktopWindow::fitBufferToWindow(bool repaint) {
  double scale_ratio;
  double resized_aspect_corr = double(client_size.width()) / client_size.height();
  DWORD style = GetWindowLong(frameHandle, GWL_STYLE);
  if (style & (WS_VSCROLL | WS_HSCROLL)) {
    style &= ~(WS_VSCROLL | WS_HSCROLL);
    SetWindowLong(frameHandle, GWL_STYLE, style);
    SetWindowPos(frameHandle, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
    // Update the cached client size
    RECT r;
    GetClientRect(frameHandle, &r);
    client_size = Rect(r.left, r.top, r.right, r.bottom);
  }
  if (resized_aspect_corr > aspect_corr) {
    scale_ratio = double(client_size.height()) / buffer->getSrcHeight();
  } else { 
    scale_ratio = double(client_size.width()) / buffer->getSrcWidth();
  }
  setDesktopScaleRatio(scale_ratio);
}

void
DesktopWindow::setCursor(int w, int h, const Point& hotspot, void* data, void* mask) {
  hideLocalCursor();

  cursor.hotspot = hotspot;

  cursor.setSize(w, h);
  cursor.setPF(buffer->getScaledPixelFormat());
  cursor.imageRect(cursor.getRect(), data);
  memcpy(cursor.mask.buf, mask, cursor.maskLen());
  cursor.crop();

  cursorBacking.setSize(w, h);
  cursorBacking.setPF(buffer->getScaledPixelFormat());

  cursorAvailable = true;

  showLocalCursor();
}

PixelFormat
DesktopWindow::getNativePF() const {
  vlog.debug("getNativePF()");
  return WindowDC(handle).getPF();
}


void
DesktopWindow::refreshWindowPalette(int start, int count) {
  vlog.debug("refreshWindowPalette(%d, %d)", start, count);

  Colour colours[256];
  if (count > 256) {
    vlog.debug("%d palette entries", count);
    throw rdr::Exception("too many palette entries");
  }

  // Copy the palette from the DIBSectionBuffer
  ColourMap* cm = buffer->getColourMap();
  if (!cm) return;
  for (int i=0; i<count; i++) {
    int r, g, b;
    cm->lookup(i, &r, &g, &b);
    colours[i].r = r;
    colours[i].g = g;
    colours[i].b = b;
  }

  // Set the window palette
  windowPalette.setEntries(start, count, colours);

  // Cause the window to be redrawn
  palette_changed = true;
  InvalidateRect(handle, 0, FALSE);
}


void DesktopWindow::calculateScrollBars() {
  // Calculate the required size of window
  DWORD current_style = GetWindowLong(frameHandle, GWL_STYLE);
  DWORD style = current_style & ~(WS_VSCROLL | WS_HSCROLL);
  DWORD style_ex = GetWindowLong(frameHandle, GWL_EXSTYLE);
  DWORD old_style;
  RECT r;
  SetRect(&r, 0, 0, buffer->width(), buffer->height());
  AdjustWindowRectEx(&r, style, FALSE, style_ex);
  Rect reqd_size = Rect(r.left, r.top, r.right, r.bottom);

  if (!bumpScroll) {
    // We only enable scrollbars if bump-scrolling is not active.
    // Effectively, this means if full-screen is not active,
    // but I think it's better to make these things explicit.
    
    // Work out whether scroll bars are required
    do {
      old_style = style;

      if (!(style & WS_HSCROLL) && (reqd_size.width() > window_size.width())) {
        style |= WS_HSCROLL;
        reqd_size.br.y += GetSystemMetrics(SM_CXHSCROLL);
      }
      if (!(style & WS_VSCROLL) && (reqd_size.height() > window_size.height())) {
        style |= WS_VSCROLL;
        reqd_size.br.x += GetSystemMetrics(SM_CXVSCROLL);
      }
    } while (style != old_style);
  }

  // Tell Windows to update the window style & cached settings
  if (style != current_style) {
    SetWindowLong(frameHandle, GWL_STYLE, style);
    SetWindowPos(frameHandle, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
  }

  // Update the scroll settings
  SCROLLINFO si;
  if (style & WS_VSCROLL) {
    si.cbSize = sizeof(si); 
    si.fMask  = SIF_RANGE | SIF_PAGE | SIF_POS; 
    si.nMin   = 0; 
    si.nMax   = buffer->height(); 
    si.nPage  = buffer->height() - (reqd_size.height() - window_size.height()); 
    maxscrolloffset.y = max(0, si.nMax-si.nPage);
    scrolloffset.y = min(maxscrolloffset.y, scrolloffset.y);
    si.nPos   = scrolloffset.y;
    SetScrollInfo(frameHandle, SB_VERT, &si, TRUE);
  }
  if (style & WS_HSCROLL) {
    si.cbSize = sizeof(si); 
    si.fMask  = SIF_RANGE | SIF_PAGE | SIF_POS; 
    si.nMin   = 0;
    si.nMax   = buffer->width(); 
    si.nPage  = buffer->width() - (reqd_size.width() - window_size.width()); 
    maxscrolloffset.x = max(0, si.nMax-si.nPage);
    scrolloffset.x = min(maxscrolloffset.x, scrolloffset.x);
    si.nPos   = scrolloffset.x;
    SetScrollInfo(frameHandle, SB_HORZ, &si, TRUE);
  }

  // Update the cached client size
  GetClientRect(frameHandle, &r);
  client_size = Rect(r.left, r.top, r.right, r.bottom);
}


void
DesktopWindow::setName(const char* name) {
  SetWindowText(handle, TStr(name));
  strCopy(desktopName, name, sizeof(desktopName));
}


void
DesktopWindow::serverCutText(const char* str, int len) {
  CharArray t(len+1);
  memcpy(t.buf, str, len);
  t.buf[len] = 0;
  clipboard.setClipText(t.buf);
}


void DesktopWindow::fillRect(const Rect& r, Pixel pix) {
  Rect img_rect = buffer->isScaling() ? buffer->calculateScaleBoundary(r) : r;
  if (cursorBackingRect.overlaps(img_rect)) hideLocalCursor();
  buffer->fillRect(r, pix);
  invalidateDesktopRect(r);
}
void DesktopWindow::imageRect(const Rect& r, void* pixels) {
  Rect img_rect = buffer->isScaling() ? buffer->calculateScaleBoundary(r) : r;
  if (cursorBackingRect.overlaps(img_rect)) hideLocalCursor();
  buffer->imageRect(r, pixels);
  invalidateDesktopRect(r);
}
void DesktopWindow::copyRect(const Rect& r, int srcX, int srcY) {
  Rect img_rect = buffer->isScaling() ? buffer->calculateScaleBoundary(r) : r;
  if (cursorBackingRect.overlaps(img_rect) ||
      cursorBackingRect.overlaps(Rect(srcX, srcY, srcX+img_rect.width(), srcY+img_rect.height())))
    hideLocalCursor();
  buffer->copyRect(r, Point(r.tl.x-srcX, r.tl.y-srcY));
  invalidateDesktopRect(r);
}

void DesktopWindow::invertRect(const Rect& r) {
  int stride;
  rdr::U8* p = buffer->isScaling() ? buffer->getPixelsRW(buffer->calculateScaleBoundary(r), &stride) 
   : buffer->getPixelsRW(r, &stride);
  for (int y = 0; y < r.height(); y++) {
    for (int x = 0; x < r.width(); x++) {
      switch (buffer->getPF().bpp) {
      case 8:  ((rdr::U8* )p)[x+y*stride] ^= 0xff;       break;
      case 16: ((rdr::U16*)p)[x+y*stride] ^= 0xffff;     break;
      case 32: ((rdr::U32*)p)[x+y*stride] ^= 0xffffffff; break;
      }
    }
  }
  invalidateDesktopRect(r);
}