// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <windows.h>

#include "base/compiler_specific.h"
#include "base/logging.h"
#include "base/process/memory.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/scoped_gdi_object.h"
#include "base/win/scoped_hdc.h"
#include "base/win/scoped_select_object.h"
#include "remoting/host/client_session_control.h"
#include "remoting/host/host_window.h"
#include "remoting/host/win/core_resource.h"

namespace remoting {

namespace {

const int DISCONNECT_HOTKEY_ID = 1000;

// Maximum length of "Your desktop is shared with ..." message in UTF-16
// characters.
const size_t kMaxSharingWithTextLength = 100;

const wchar_t kShellTrayWindowName[] = L"Shell_TrayWnd";
const int kWindowBorderRadius = 14;

// Margin between dialog controls (in dialog units).
const int kWindowTextMargin = 8;

class DisconnectWindowWin : public HostWindow {
 public:
  DisconnectWindowWin();
  virtual ~DisconnectWindowWin();

  // HostWindow overrides.
  virtual void Start(
      const base::WeakPtr<ClientSessionControl>& client_session_control)
      OVERRIDE;

 protected:
  static INT_PTR CALLBACK DialogProc(HWND hwnd, UINT message, WPARAM wparam,
                                     LPARAM lparam);

  BOOL OnDialogMessage(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);

  // Creates the dialog window and registers the disconnect hot key.
  bool BeginDialog();

  // Closes the dialog, unregisters the hot key and invokes the disconnect
  // callback, if set.
  void EndDialog();

  // Returns |control| rectangle in the dialog coordinates.
  bool GetControlRect(HWND control, RECT* rect);

  // Trys to position the dialog window above the taskbar.
  void SetDialogPosition();

  // Applies localization string and resizes the dialog.
  bool SetStrings();

  // Used to disconnect the client session.
  base::WeakPtr<ClientSessionControl> client_session_control_;

  // Specifies the remote user name.
  std::string username_;

  HWND hwnd_;
  bool has_hotkey_;
  base::win::ScopedGDIObject<HPEN> border_pen_;

  DISALLOW_COPY_AND_ASSIGN(DisconnectWindowWin);
};

// Returns the text for the given dialog control window.
bool GetControlText(HWND control, base::string16* text) {
  // GetWindowText truncates the text if it is longer than can fit into
  // the buffer.
  WCHAR buffer[256];
  int result = GetWindowText(control, buffer, arraysize(buffer));
  if (!result)
    return false;

  text->assign(buffer);
  return true;
}

// Returns width |text| rendered in |control| window.
bool GetControlTextWidth(HWND control,
                         const base::string16& text,
                         LONG* width) {
  RECT rect = {0, 0, 0, 0};
  base::win::ScopedGetDC dc(control);
  base::win::ScopedSelectObject font(
      dc, (HFONT)SendMessage(control, WM_GETFONT, 0, 0));
  if (!DrawText(dc, text.c_str(), -1, &rect, DT_CALCRECT | DT_SINGLELINE))
    return false;

  *width = rect.right;
  return true;
}

DisconnectWindowWin::DisconnectWindowWin()
    : hwnd_(NULL),
      has_hotkey_(false),
      border_pen_(CreatePen(PS_SOLID, 5,
                            RGB(0.13 * 255, 0.69 * 255, 0.11 * 255))) {
}

DisconnectWindowWin::~DisconnectWindowWin() {
  EndDialog();
}

void DisconnectWindowWin::Start(
    const base::WeakPtr<ClientSessionControl>& client_session_control) {
  DCHECK(CalledOnValidThread());
  DCHECK(!client_session_control_);
  DCHECK(client_session_control);

  client_session_control_ = client_session_control;

  std::string client_jid = client_session_control_->client_jid();
  username_ = client_jid.substr(0, client_jid.find('/'));
  if (!BeginDialog())
    EndDialog();
}

INT_PTR CALLBACK DisconnectWindowWin::DialogProc(HWND hwnd,
                                                 UINT message,
                                                 WPARAM wparam,
                                                 LPARAM lparam) {
  LONG_PTR self = NULL;
  if (message == WM_INITDIALOG) {
    self = lparam;

    // Store |this| to the window's user data.
    SetLastError(ERROR_SUCCESS);
    LONG_PTR result = SetWindowLongPtr(hwnd, DWLP_USER, self);
    if (result == 0 && GetLastError() != ERROR_SUCCESS)
      reinterpret_cast<DisconnectWindowWin*>(self)->EndDialog();
  } else {
    self = GetWindowLongPtr(hwnd, DWLP_USER);
  }

  if (self) {
    return reinterpret_cast<DisconnectWindowWin*>(self)->OnDialogMessage(
        hwnd, message, wparam, lparam);
  }
  return FALSE;
}

BOOL DisconnectWindowWin::OnDialogMessage(HWND hwnd,
                                          UINT message,
                                          WPARAM wparam,
                                          LPARAM lparam) {
  DCHECK(CalledOnValidThread());

  switch (message) {
    // Ignore close messages.
    case WM_CLOSE:
      return TRUE;

    // Handle the Disconnect button.
    case WM_COMMAND:
      switch (LOWORD(wparam)) {
        case IDC_DISCONNECT:
          EndDialog();
          return TRUE;
      }
      return FALSE;

    // Ensure we don't try to use the HWND anymore.
    case WM_DESTROY:
      hwnd_ = NULL;

      // Ensure that the disconnect callback is invoked even if somehow our
      // window gets destroyed.
      EndDialog();

      return TRUE;

    // Ensure the dialog stays visible if the work area dimensions change.
    case WM_SETTINGCHANGE:
      if (wparam == SPI_SETWORKAREA)
        SetDialogPosition();
      return TRUE;

    // Ensure the dialog stays visible if the display dimensions change.
    case WM_DISPLAYCHANGE:
      SetDialogPosition();
      return TRUE;

    // Handle the disconnect hot-key.
    case WM_HOTKEY:
      EndDialog();
      return TRUE;

    // Let the window be draggable by its client area by responding
    // that the entire window is the title bar.
    case WM_NCHITTEST:
      SetWindowLongPtr(hwnd, DWLP_MSGRESULT, HTCAPTION);
      return TRUE;

    case WM_PAINT: {
      PAINTSTRUCT ps;
      HDC hdc = BeginPaint(hwnd_, &ps);
      RECT rect;
      GetClientRect(hwnd_, &rect);
      {
        base::win::ScopedSelectObject border(hdc, border_pen_);
        base::win::ScopedSelectObject brush(hdc, GetStockObject(NULL_BRUSH));
        RoundRect(hdc, rect.left, rect.top, rect.right - 1, rect.bottom - 1,
                  kWindowBorderRadius, kWindowBorderRadius);
      }
      EndPaint(hwnd_, &ps);
      return TRUE;
    }
  }
  return FALSE;
}

bool DisconnectWindowWin::BeginDialog() {
  DCHECK(CalledOnValidThread());
  DCHECK(!hwnd_);

  HMODULE module = base::GetModuleFromAddress(&DialogProc);
  hwnd_ = CreateDialogParam(module, MAKEINTRESOURCE(IDD_DISCONNECT), NULL,
                            DialogProc, reinterpret_cast<LPARAM>(this));
  if (!hwnd_)
    return false;

  // Set up handler for Ctrl-Alt-Esc shortcut.
  if (!has_hotkey_ && RegisterHotKey(hwnd_, DISCONNECT_HOTKEY_ID,
                                     MOD_ALT | MOD_CONTROL, VK_ESCAPE)) {
    has_hotkey_ = true;
  }

  if (!SetStrings())
    return false;

  SetDialogPosition();
  ShowWindow(hwnd_, SW_SHOW);
  return IsWindowVisible(hwnd_) != FALSE;
}

void DisconnectWindowWin::EndDialog() {
  DCHECK(CalledOnValidThread());

  if (has_hotkey_) {
    UnregisterHotKey(hwnd_, DISCONNECT_HOTKEY_ID);
    has_hotkey_ = false;
  }

  if (hwnd_) {
    DestroyWindow(hwnd_);
    hwnd_ = NULL;
  }

  if (client_session_control_)
    client_session_control_->DisconnectSession();
}

// Returns |control| rectangle in the dialog coordinates.
bool DisconnectWindowWin::GetControlRect(HWND control, RECT* rect) {
  if (!GetWindowRect(control, rect))
    return false;
  SetLastError(ERROR_SUCCESS);
  int result = MapWindowPoints(HWND_DESKTOP, hwnd_,
                               reinterpret_cast<LPPOINT>(rect), 2);
  if (!result && GetLastError() != ERROR_SUCCESS)
    return false;

  return true;
}

void DisconnectWindowWin::SetDialogPosition() {
  DCHECK(CalledOnValidThread());

  // Try to center the window above the task-bar. If that fails, use the
  // primary monitor. If that fails (very unlikely), use the default position.
  HWND taskbar = FindWindow(kShellTrayWindowName, NULL);
  HMONITOR monitor = MonitorFromWindow(taskbar, MONITOR_DEFAULTTOPRIMARY);
  MONITORINFO monitor_info = {sizeof(monitor_info)};
  RECT window_rect;
  if (GetMonitorInfo(monitor, &monitor_info) &&
      GetWindowRect(hwnd_, &window_rect)) {
    int window_width = window_rect.right - window_rect.left;
    int window_height = window_rect.bottom - window_rect.top;
    int top = monitor_info.rcWork.bottom - window_height;
    int left = (monitor_info.rcWork.right + monitor_info.rcWork.left -
        window_width) / 2;
    SetWindowPos(hwnd_, NULL, left, top, 0, 0, SWP_NOSIZE | SWP_NOZORDER);
  }
}

bool DisconnectWindowWin::SetStrings() {
  DCHECK(CalledOnValidThread());

  // Localize the disconnect button text and measure length of the old and new
  // labels.
  HWND hwnd_button = GetDlgItem(hwnd_, IDC_DISCONNECT);
  HWND hwnd_message = GetDlgItem(hwnd_, IDC_DISCONNECT_SHARINGWITH);
  if (!hwnd_button || !hwnd_message)
    return false;

  base::string16 button_text;
  base::string16 message_text;
  if (!GetControlText(hwnd_button, &button_text) ||
      !GetControlText(hwnd_message, &message_text)) {
    return false;
  }

  // Format and truncate "Your desktop is shared with ..." message.
  message_text = ReplaceStringPlaceholders(message_text,
                                           base::UTF8ToUTF16(username_),
                                           NULL);
  if (message_text.length() > kMaxSharingWithTextLength)
    message_text.erase(kMaxSharingWithTextLength);

  if (!SetWindowText(hwnd_message, message_text.c_str()))
    return false;

  // Calculate the margin between controls in pixels.
  RECT rect = {0};
  rect.right = kWindowTextMargin;
  if (!MapDialogRect(hwnd_, &rect))
    return false;
  int margin = rect.right;

  // Resize |hwnd_message| so that the text is not clipped.
  RECT message_rect;
  if (!GetControlRect(hwnd_message, &message_rect))
    return false;

  LONG control_width;
  if (!GetControlTextWidth(hwnd_message, message_text, &control_width))
    return false;
  message_rect.right = message_rect.left + control_width + margin;

  if (!SetWindowPos(hwnd_message, NULL,
                    message_rect.left, message_rect.top,
                    message_rect.right - message_rect.left,
                    message_rect.bottom - message_rect.top,
                    SWP_NOZORDER)) {
    return false;
  }

  // Reposition and resize |hwnd_button| as well.
  RECT button_rect;
  if (!GetControlRect(hwnd_button, &button_rect))
    return false;

  if (!GetControlTextWidth(hwnd_button, button_text, &control_width))
    return false;

  button_rect.left = message_rect.right;
  button_rect.right = button_rect.left + control_width + margin * 2;
  if (!SetWindowPos(hwnd_button, NULL,
                    button_rect.left, button_rect.top,
                    button_rect.right - button_rect.left,
                    button_rect.bottom - button_rect.top,
                    SWP_NOZORDER)) {
    return false;
  }

  // Resize the whole window to fit the resized controls.
  RECT window_rect;
  if (!GetWindowRect(hwnd_, &window_rect))
    return false;
  int width = button_rect.right + margin;
  int height = window_rect.bottom - window_rect.top;
  if (!SetWindowPos(hwnd_, NULL, 0, 0, width, height,
                    SWP_NOMOVE | SWP_NOZORDER)) {
    return false;
  }

  // Make the corners of the disconnect window rounded.
  HRGN rgn = CreateRoundRectRgn(0, 0, width, height, kWindowBorderRadius,
                                kWindowBorderRadius);
  if (!rgn)
    return false;
  if (!SetWindowRgn(hwnd_, rgn, TRUE))
    return false;

  return true;
}

} // namespace

// static
scoped_ptr<HostWindow> HostWindow::CreateDisconnectWindow() {
  return scoped_ptr<HostWindow>(new DisconnectWindowWin());
}

}  // namespace remoting
