// Copyright 2014 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 "content/browser/compositor/software_output_device_mac.h"

#import <Cocoa/Cocoa.h>
#include <stddef.h>
#include <stdint.h>

#include "base/mac/foundation_util.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "ui/accelerated_widget_mac/accelerated_widget_mac.h"
#include "ui/compositor/compositor.h"
#include "ui/gfx/skia_util.h"

namespace content {

SoftwareOutputDeviceMac::SoftwareOutputDeviceMac(ui::Compositor* compositor)
    : compositor_(compositor), scale_factor_(1), current_index_(0) {}

SoftwareOutputDeviceMac::~SoftwareOutputDeviceMac() {
}

void SoftwareOutputDeviceMac::Resize(const gfx::Size& pixel_size,
                                     float scale_factor) {
  if (pixel_size_ == pixel_size && scale_factor_ == scale_factor)
    return;

  pixel_size_ = pixel_size;
  scale_factor_ = scale_factor;

  DiscardBackbuffer();
}

void SoftwareOutputDeviceMac::CopyPreviousBufferDamage(
    const SkRegion& new_damage_region) {
  TRACE_EVENT0("browser", "CopyPreviousBufferDamage");

  // The region to copy is the region drawn last frame, minus the region that
  // is drawn this frame.
  SkRegion copy_region = previous_buffer_damage_region_;
  bool copy_region_nonempty =
      copy_region.op(new_damage_region, SkRegion::kDifference_Op);
  previous_buffer_damage_region_ = new_damage_region;

  if (!copy_region_nonempty)
    return;

  IOSurfaceRef previous_io_surface = io_surfaces_[!current_index_].get();

  {
    TRACE_EVENT0("browser", "IOSurfaceLock for software copy");
    IOReturn io_result = IOSurfaceLock(
        previous_io_surface, kIOSurfaceLockReadOnly | kIOSurfaceLockAvoidSync,
        nullptr);
    if (io_result) {
      DLOG(ERROR) << "Failed to lock previous IOSurface " << io_result;
      return;
    }
  }

  uint8_t* pixels =
      static_cast<uint8_t*>(IOSurfaceGetBaseAddress(previous_io_surface));
  size_t stride = IOSurfaceGetBytesPerRow(previous_io_surface);
  size_t bytes_per_element = 4;
  for (SkRegion::Iterator it(copy_region); !it.done(); it.next()) {
    const SkIRect& rect = it.rect();
    canvas_->writePixels(
        SkImageInfo::MakeN32Premul(rect.width(), rect.height()),
        pixels + bytes_per_element * rect.x() + stride * rect.y(), stride,
        rect.x(), rect.y());
  }

  {
    TRACE_EVENT0("browser", "IOSurfaceUnlock");
    IOReturn io_result = IOSurfaceUnlock(
        previous_io_surface, kIOSurfaceLockReadOnly | kIOSurfaceLockAvoidSync,
        nullptr);
    if (io_result)
      DLOG(ERROR) << "Failed to unlock previous IOSurface " << io_result;
  }
}

bool SoftwareOutputDeviceMac::EnsureBuffersExist() {
  for (int i = 0; i < 2; ++i) {
    if (!io_surfaces_[i]) {
      TRACE_EVENT0("browser", "IOSurfaceCreate");
      unsigned pixel_format = 'BGRA';
      unsigned bytes_per_element = 4;
      NSDictionary* options = @{
        static_cast<id>(kIOSurfaceWidth) : @(pixel_size_.width()),
        static_cast<id>(kIOSurfaceHeight) : @(pixel_size_.height()),
        static_cast<id>(kIOSurfacePixelFormat) : @(pixel_format),
        static_cast<id>(kIOSurfaceBytesPerElement) : @(bytes_per_element),
      };
      io_surfaces_[i].reset(IOSurfaceCreate(base::mac::NSToCFCast(options)));
    }
    if (!io_surfaces_[i]) {
      DLOG(ERROR) << "Failed to allocate IOSurface";
      return false;
    }
  }
  return true;
}

SkCanvas* SoftwareOutputDeviceMac::BeginPaint(
    const gfx::Rect& new_damage_rect) {
  if (!EnsureBuffersExist())
    return nullptr;

  {
    TRACE_EVENT0("browser", "IOSurfaceLock for software paint");
    IOReturn io_result = IOSurfaceLock(io_surfaces_[current_index_],
                                       kIOSurfaceLockAvoidSync, nullptr);
    if (io_result) {
      DLOG(ERROR) << "Failed to lock IOSurface " << io_result;
      return nullptr;
    }
  }

  SkPMColor* pixels = static_cast<SkPMColor*>(
      IOSurfaceGetBaseAddress(io_surfaces_[current_index_]));
  size_t stride = IOSurfaceGetBytesPerRow(io_surfaces_[current_index_]);

  canvas_ = skia::AdoptRef(SkCanvas::NewRasterDirectN32(
      pixel_size_.width(), pixel_size_.height(), pixels, stride));

  CopyPreviousBufferDamage(SkRegion(gfx::RectToSkIRect(new_damage_rect)));

  return canvas_.get();
}

void SoftwareOutputDeviceMac::EndPaint() {
  SoftwareOutputDevice::EndPaint();
  {
    TRACE_EVENT0("browser", "IOSurfaceUnlock");
    IOReturn io_result = IOSurfaceUnlock(io_surfaces_[current_index_],
                                         kIOSurfaceLockAvoidSync, nullptr);
    if (io_result)
      DLOG(ERROR) << "Failed to unlock IOSurface " << io_result;
  }

  canvas_ = nullptr;
  base::TimeTicks vsync_timebase;
  base::TimeDelta vsync_interval;
  ui::AcceleratedWidgetMacGotFrame(
      compositor_->widget(), 0, io_surfaces_[current_index_], pixel_size_,
      scale_factor_, &vsync_timebase, &vsync_interval);
  if (!update_vsync_callback_.is_null())
    update_vsync_callback_.Run(vsync_timebase, vsync_interval);

  current_index_ = !current_index_;
}

void SoftwareOutputDeviceMac::DiscardBackbuffer() {
  for (int i = 0; i < 2; ++i)
    io_surfaces_[i].reset();
}

void SoftwareOutputDeviceMac::EnsureBackbuffer() {}

gfx::VSyncProvider* SoftwareOutputDeviceMac::GetVSyncProvider() {
  return this;
}

void SoftwareOutputDeviceMac::GetVSyncParameters(
    const gfx::VSyncProvider::UpdateVSyncCallback& callback) {
  update_vsync_callback_ = callback;
}

}  // namespace content