/*
 *  Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

#include "webrtc/modules/desktop_capture/cropping_window_capturer.h"

#include "webrtc/base/win32.h"
#include "webrtc/modules/desktop_capture/win/scoped_gdi_object.h"
#include "webrtc/modules/desktop_capture/win/screen_capture_utils.h"
#include "webrtc/modules/desktop_capture/win/window_capture_utils.h"
#include "webrtc/system_wrappers/interface/logging.h"

namespace webrtc {

namespace {

// Used to pass input/output data during the EnumWindow call for verifying if
// the selected window is on top.
struct TopWindowVerifierContext {
  TopWindowVerifierContext(HWND selected_window, HWND excluded_window)
      : selected_window(selected_window),
        excluded_window(excluded_window),
        is_top_window(false),
        selected_window_process_id(0) {}

  HWND selected_window;
  HWND excluded_window;
  bool is_top_window;
  DWORD selected_window_process_id;
  DesktopRect selected_window_rect;
};

// The function is called during EnumWindow for every window enumerated and is
// responsible for verifying if the selected window is on top.
BOOL CALLBACK TopWindowVerifier(HWND hwnd, LPARAM param) {
  TopWindowVerifierContext* context =
      reinterpret_cast<TopWindowVerifierContext*>(param);

  if (hwnd == context->selected_window) {
    context->is_top_window = true;
    return FALSE;
  }

  // Ignore the excluded window.
  if (hwnd == context->excluded_window) {
    return TRUE;
  }

  // Ignore hidden or minimized window.
  if (IsIconic(hwnd) || !IsWindowVisible(hwnd)) {
    return TRUE;
  }

  // Ignore descendant/owned windows since we want to capture them.
  // This check does not work for tooltips and context menus. Drop down menus
  // and popup windows are fine.
  if (GetAncestor(hwnd, GA_ROOTOWNER) == context->selected_window) {
    return TRUE;
  }

  // If |hwnd| has no title and belongs to the same process, assume it's a
  // tooltip or context menu from the selected window and ignore it.
  const size_t kTitleLength = 32;
  WCHAR window_title[kTitleLength];
  GetWindowText(hwnd, window_title, kTitleLength);
  if (wcsnlen_s(window_title, kTitleLength) == 0) {
    DWORD enumerated_process;
    GetWindowThreadProcessId(hwnd, &enumerated_process);
    if (!context->selected_window_process_id) {
      GetWindowThreadProcessId(context->selected_window,
                               &context->selected_window_process_id);
    }
    if (context->selected_window_process_id == enumerated_process) {
      return TRUE;
    }
  }

  // Check if the enumerated window intersects with the selected window.
  RECT enumerated_rect;
  if (!GetWindowRect(hwnd, &enumerated_rect)) {
    // Bail out if failed to get the window area.
    context->is_top_window = false;
    return FALSE;
  }

  DesktopRect intersect_rect = context->selected_window_rect;
  DesktopRect enumerated_desktop_rect =
      DesktopRect::MakeLTRB(enumerated_rect.left,
                            enumerated_rect.top,
                            enumerated_rect.right,
                            enumerated_rect.bottom);
  intersect_rect.IntersectWith(enumerated_desktop_rect);

  // If intersection is not empty, the selected window is not on top.
  if (!intersect_rect.is_empty()) {
    context->is_top_window = false;
    return FALSE;
  }
  // Otherwise, keep enumerating.
  return TRUE;
}

class CroppingWindowCapturerWin : public CroppingWindowCapturer {
 public:
  CroppingWindowCapturerWin(
      const DesktopCaptureOptions& options)
      : CroppingWindowCapturer(options) {}

 private:
  bool ShouldUseScreenCapturer() override;
  DesktopRect GetWindowRectInVirtualScreen() override;

  // The region from GetWindowRgn in the desktop coordinate if the region is
  // rectangular, or the rect from GetWindowRect if the region is not set.
  DesktopRect window_region_rect_;
};

bool CroppingWindowCapturerWin::ShouldUseScreenCapturer() {
  if (!rtc::IsWindows8OrLater())
    return false;

  // Check if the window is a translucent layered window.
  HWND selected = reinterpret_cast<HWND>(selected_window());
  LONG window_ex_style = GetWindowLong(selected, GWL_EXSTYLE);
  if (window_ex_style & WS_EX_LAYERED) {
    COLORREF color_ref_key = 0;
    BYTE alpha = 0;
    DWORD flags = 0;

    // GetLayeredWindowAttributes fails if the window was setup with
    // UpdateLayeredWindow. We have no way to know the opacity of the window in
    // that case. This happens for Stiky Note (crbug/412726).
    if (!GetLayeredWindowAttributes(selected, &color_ref_key, &alpha, &flags))
      return false;

    // UpdateLayeredWindow is the only way to set per-pixel alpha and will cause
    // the previous GetLayeredWindowAttributes to fail. So we only need to check
    // the window wide color key or alpha.
    if ((flags & LWA_COLORKEY) || ((flags & LWA_ALPHA) && (alpha < 255)))
      return false;
  }

  TopWindowVerifierContext context(
      selected, reinterpret_cast<HWND>(excluded_window()));

  RECT selected_window_rect;
  if (!GetWindowRect(selected, &selected_window_rect)) {
    return false;
  }
  context.selected_window_rect = DesktopRect::MakeLTRB(
      selected_window_rect.left,
      selected_window_rect.top,
      selected_window_rect.right,
      selected_window_rect.bottom);

  // Get the window region and check if it is rectangular.
  win::ScopedGDIObject<HRGN, win::DeleteObjectTraits<HRGN> >
      scoped_hrgn(CreateRectRgn(0, 0, 0, 0));
  int region_type = GetWindowRgn(selected, scoped_hrgn.Get());

  // Do not use the screen capturer if the region is empty or not rectangular.
  if (region_type == COMPLEXREGION || region_type == NULLREGION)
    return false;

  if (region_type == SIMPLEREGION) {
    RECT region_rect;
    GetRgnBox(scoped_hrgn.Get(), &region_rect);
    DesktopRect rgn_rect =
        DesktopRect::MakeLTRB(region_rect.left,
                              region_rect.top,
                              region_rect.right,
                              region_rect.bottom);
    rgn_rect.Translate(context.selected_window_rect.left(),
                       context.selected_window_rect.top());
    context.selected_window_rect.IntersectWith(rgn_rect);
  }
  window_region_rect_ = context.selected_window_rect;

  // Check if the window is occluded by any other window, excluding the child
  // windows, context menus, and |excluded_window_|.
  EnumWindows(&TopWindowVerifier, reinterpret_cast<LPARAM>(&context));
  return context.is_top_window;
}

DesktopRect CroppingWindowCapturerWin::GetWindowRectInVirtualScreen() {
  DesktopRect original_rect;
  DesktopRect window_rect;
  HWND hwnd = reinterpret_cast<HWND>(selected_window());
  if (!GetCroppedWindowRect(hwnd, &window_rect, &original_rect)) {
    LOG(LS_WARNING) << "Failed to get window info: " << GetLastError();
    return window_rect;
  }
  window_rect.IntersectWith(window_region_rect_);

  // Convert |window_rect| to be relative to the top-left of the virtual screen.
  DesktopRect screen_rect(GetScreenRect(kFullDesktopScreenId, L""));
  window_rect.IntersectWith(screen_rect);
  window_rect.Translate(-screen_rect.left(), -screen_rect.top());
  return window_rect;
}

}  // namespace

// static
WindowCapturer*
CroppingWindowCapturer::Create(const DesktopCaptureOptions& options) {
  return new CroppingWindowCapturerWin(options);
}

}  // namespace webrtc
