/*
 *  Copyright (c) 2013 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 "modules/desktop_capture/mouse_cursor_monitor.h"


#include <memory>

#include <ApplicationServices/ApplicationServices.h>
#include <Cocoa/Cocoa.h>
#include <CoreFoundation/CoreFoundation.h>

#include "api/scoped_refptr.h"
#include "modules/desktop_capture/desktop_capture_options.h"
#include "modules/desktop_capture/desktop_capture_types.h"
#include "modules/desktop_capture/desktop_frame.h"
#include "modules/desktop_capture/mac/desktop_configuration.h"
#include "modules/desktop_capture/mac/desktop_configuration_monitor.h"
#include "modules/desktop_capture/mac/window_list_utils.h"
#include "modules/desktop_capture/mouse_cursor.h"
#include "rtc_base/checks.h"

namespace webrtc {

namespace {
CGImageRef CreateScaledCGImage(CGImageRef image, int width, int height) {
  // Create context, keeping original image properties.
  CGColorSpaceRef colorspace = CGImageGetColorSpace(image);
  CGContextRef context = CGBitmapContextCreate(nullptr,
                                               width,
                                               height,
                                               CGImageGetBitsPerComponent(image),
                                               width * DesktopFrame::kBytesPerPixel,
                                               colorspace,
                                               CGImageGetBitmapInfo(image));

  if (!context) return nil;

  // Draw image to context, resizing it.
  CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
  // Extract resulting image from context.
  CGImageRef imgRef = CGBitmapContextCreateImage(context);
  CGContextRelease(context);

  return imgRef;
}
}  // namespace

class MouseCursorMonitorMac : public MouseCursorMonitor {
 public:
  MouseCursorMonitorMac(const DesktopCaptureOptions& options,
                        CGWindowID window_id,
                        ScreenId screen_id);
  ~MouseCursorMonitorMac() override;

  void Init(Callback* callback, Mode mode) override;
  void Capture() override;

 private:
  static void DisplaysReconfiguredCallback(CGDirectDisplayID display,
                                           CGDisplayChangeSummaryFlags flags,
                                           void *user_parameter);
  void DisplaysReconfigured(CGDirectDisplayID display,
                            CGDisplayChangeSummaryFlags flags);

  void CaptureImage(float scale);

  rtc::scoped_refptr<DesktopConfigurationMonitor> configuration_monitor_;
  CGWindowID window_id_;
  ScreenId screen_id_;
  Callback* callback_ = NULL;
  Mode mode_;
  __strong NSImage* last_cursor_ = NULL;
};

MouseCursorMonitorMac::MouseCursorMonitorMac(const DesktopCaptureOptions& options,
                                             CGWindowID window_id,
                                             ScreenId screen_id)
    : configuration_monitor_(options.configuration_monitor()),
      window_id_(window_id),
      screen_id_(screen_id),
      mode_(SHAPE_AND_POSITION) {
  RTC_DCHECK(window_id == kCGNullWindowID || screen_id == kInvalidScreenId);
}

MouseCursorMonitorMac::~MouseCursorMonitorMac() {}

void MouseCursorMonitorMac::Init(Callback* callback, Mode mode) {
  RTC_DCHECK(!callback_);
  RTC_DCHECK(callback);

  callback_ = callback;
  mode_ = mode;
}

void MouseCursorMonitorMac::Capture() {
  RTC_DCHECK(callback_);

  CGEventRef event = CGEventCreate(NULL);
  CGPoint gc_position = CGEventGetLocation(event);
  CFRelease(event);

  DesktopVector position(gc_position.x, gc_position.y);

  MacDesktopConfiguration configuration =
      configuration_monitor_->desktop_configuration();
  float scale = GetScaleFactorAtPosition(configuration, position);

  CaptureImage(scale);

  if (mode_ != SHAPE_AND_POSITION)
    return;

  // Always report cursor position in DIP pixel.
  callback_->OnMouseCursorPosition(
      position.subtract(configuration.bounds.top_left()));
}

void MouseCursorMonitorMac::CaptureImage(float scale) {
  NSCursor* nscursor = [NSCursor currentSystemCursor];

  NSImage* nsimage = [nscursor image];
  if (nsimage == nil || !nsimage.isValid) {
    return;
  }
  NSSize nssize = [nsimage size];  // DIP size

  // No need to caputre cursor image if it's unchanged since last capture.
  if (last_cursor_ && [[nsimage TIFFRepresentation] isEqual:[last_cursor_ TIFFRepresentation]]) return;
  last_cursor_ = nsimage;

  DesktopSize size(round(nssize.width * scale),
                   round(nssize.height * scale));  // Pixel size
  NSPoint nshotspot = [nscursor hotSpot];
  DesktopVector hotspot(
      std::max(0,
               std::min(size.width(), static_cast<int>(nshotspot.x * scale))),
      std::max(0,
               std::min(size.height(), static_cast<int>(nshotspot.y * scale))));
  CGImageRef cg_image =
      [nsimage CGImageForProposedRect:NULL context:nil hints:nil];
  if (!cg_image)
    return;

  // Before 10.12, OSX may report 1X cursor on Retina screen. (See
  // crbug.com/632995.) After 10.12, OSX may report 2X cursor on non-Retina
  // screen. (See crbug.com/671436.) So scaling the cursor if needed.
  CGImageRef scaled_cg_image = nil;
  if (CGImageGetWidth(cg_image) != static_cast<size_t>(size.width())) {
    scaled_cg_image = CreateScaledCGImage(cg_image, size.width(), size.height());
    if (scaled_cg_image != nil) {
      cg_image = scaled_cg_image;
    }
  }
  if (CGImageGetBitsPerPixel(cg_image) != DesktopFrame::kBytesPerPixel * 8 ||
      CGImageGetWidth(cg_image) != static_cast<size_t>(size.width()) ||
      CGImageGetBitsPerComponent(cg_image) != 8) {
    if (scaled_cg_image != nil) CGImageRelease(scaled_cg_image);
    return;
  }

  CGDataProviderRef provider = CGImageGetDataProvider(cg_image);
  CFDataRef image_data_ref = CGDataProviderCopyData(provider);
  if (image_data_ref == NULL) {
    if (scaled_cg_image != nil) CGImageRelease(scaled_cg_image);
    return;
  }

  const uint8_t* src_data =
      reinterpret_cast<const uint8_t*>(CFDataGetBytePtr(image_data_ref));

  // Create a MouseCursor that describes the cursor and pass it to
  // the client.
  std::unique_ptr<DesktopFrame> image(
      new BasicDesktopFrame(DesktopSize(size.width(), size.height())));

  int src_stride = CGImageGetBytesPerRow(cg_image);
  image->CopyPixelsFrom(src_data, src_stride, DesktopRect::MakeSize(size));

  CFRelease(image_data_ref);
  if (scaled_cg_image != nil) CGImageRelease(scaled_cg_image);

  std::unique_ptr<MouseCursor> cursor(
      new MouseCursor(image.release(), hotspot));

  callback_->OnMouseCursor(cursor.release());
}

MouseCursorMonitor* MouseCursorMonitor::CreateForWindow(
    const DesktopCaptureOptions& options, WindowId window) {
  return new MouseCursorMonitorMac(options, window, kInvalidScreenId);
}

MouseCursorMonitor* MouseCursorMonitor::CreateForScreen(
    const DesktopCaptureOptions& options,
    ScreenId screen) {
  return new MouseCursorMonitorMac(options, kCGNullWindowID, screen);
}

std::unique_ptr<MouseCursorMonitor> MouseCursorMonitor::Create(
    const DesktopCaptureOptions& options) {
  return std::unique_ptr<MouseCursorMonitor>(
      CreateForScreen(options, kFullDesktopScreenId));
}

}  // namespace webrtc
