// 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.

#import <Cocoa/Cocoa.h>

#include "content/browser/renderer_host/backing_store_mac.h"

#include <cmath>

#include "base/logging.h"
#include "base/mac/mac_util.h"
#include "base/mac/scoped_cftyperef.h"
#include "content/browser/renderer_host/render_process_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/public/browser/render_widget_host_view.h"
#include "skia/ext/platform_canvas.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "ui/gfx/rect.h"
#include "ui/gfx/rect_conversions.h"
#include "ui/gfx/size_conversions.h"
#include "ui/gfx/scoped_cg_context_save_gstate_mac.h"
#include "ui/surface/transport_dib.h"

namespace content {

// Mac Backing Stores:
//
// Since backing stores are only ever written to or drawn into windows, we keep
// our backing store in a CGLayer that can get cached in GPU memory.  This
// allows acclerated drawing into the layer and lets scrolling and such happen
// all or mostly on the GPU, which is good for performance.

BackingStoreMac::BackingStoreMac(RenderWidgetHost* widget,
                                 const gfx::Size& size,
                                 float device_scale_factor)
    : BackingStore(widget, size), device_scale_factor_(device_scale_factor) {
  cg_layer_.reset(CreateCGLayer());
  if (!cg_layer_) {
    // The view isn't in a window yet.  Use a CGBitmapContext for now.
    cg_bitmap_.reset(CreateCGBitmapContext());
    CGContextScaleCTM(cg_bitmap_, device_scale_factor_, device_scale_factor_);
  }
}

BackingStoreMac::~BackingStoreMac() {
}

void BackingStoreMac::ScaleFactorChanged(float device_scale_factor) {
  if (device_scale_factor == device_scale_factor_)
    return;

  device_scale_factor_ = device_scale_factor;

  base::ScopedCFTypeRef<CGLayerRef> new_layer(CreateCGLayer());
  // If we have a layer, copy the old contents. A pixelated flash is better
  // than a white flash.
  if (new_layer && cg_layer_) {
    CGContextRef layer = CGLayerGetContext(new_layer);
    CGContextDrawLayerAtPoint(layer, CGPointMake(0, 0), cg_layer_);
  }

  cg_layer_.swap(new_layer);
  if (!cg_layer_) {
    // The view isn't in a window yet.  Use a CGBitmapContext for now.
    cg_bitmap_.reset(CreateCGBitmapContext());
    CGContextScaleCTM(cg_bitmap_, device_scale_factor_, device_scale_factor_);
  }
}

size_t BackingStoreMac::MemorySize() {
  return gfx::ToFlooredSize(
      gfx::ScaleSize(size(), device_scale_factor_)).GetArea() * 4;
}

void BackingStoreMac::PaintToBackingStore(
    RenderProcessHost* process,
    TransportDIB::Id bitmap,
    const gfx::Rect& bitmap_rect,
    const std::vector<gfx::Rect>& copy_rects,
    float scale_factor,
    const base::Closure& completion_callback,
    bool* scheduled_completion_callback) {
  *scheduled_completion_callback = false;
  DCHECK_NE(static_cast<bool>(cg_layer()), static_cast<bool>(cg_bitmap()));

  TransportDIB* dib = process->GetTransportDIB(bitmap);
  if (!dib)
    return;

  gfx::Size pixel_size = gfx::ToFlooredSize(
      gfx::ScaleSize(size(), device_scale_factor_));
  gfx::Rect pixel_bitmap_rect = ToFlooredRectDeprecated(
      gfx::ScaleRect(bitmap_rect, scale_factor));

  size_t bitmap_byte_count =
      pixel_bitmap_rect.width() * pixel_bitmap_rect.height() * 4;
  DCHECK_GE(dib->size(), bitmap_byte_count);

  base::ScopedCFTypeRef<CGDataProviderRef> data_provider(
      CGDataProviderCreateWithData(
          NULL, dib->memory(), bitmap_byte_count, NULL));

  base::ScopedCFTypeRef<CGImageRef> bitmap_image(
      CGImageCreate(pixel_bitmap_rect.width(),
                    pixel_bitmap_rect.height(),
                    8,
                    32,
                    4 * pixel_bitmap_rect.width(),
                    base::mac::GetSystemColorSpace(),
                    kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host,
                    data_provider,
                    NULL,
                    false,
                    kCGRenderingIntentDefault));

  for (size_t i = 0; i < copy_rects.size(); i++) {
    const gfx::Rect& copy_rect = copy_rects[i];
    gfx::Rect pixel_copy_rect = ToFlooredRectDeprecated(
        gfx::ScaleRect(copy_rect, scale_factor));

    // Only the subpixels given by copy_rect have pixels to copy.
    base::ScopedCFTypeRef<CGImageRef> image(CGImageCreateWithImageInRect(
        bitmap_image,
        CGRectMake(pixel_copy_rect.x() - pixel_bitmap_rect.x(),
                   pixel_copy_rect.y() - pixel_bitmap_rect.y(),
                   pixel_copy_rect.width(),
                   pixel_copy_rect.height())));

    if (!cg_layer()) {
      // The view may have moved to a window.  Try to get a CGLayer.
      cg_layer_.reset(CreateCGLayer());
      if (cg_layer()) {
        // Now that we have a layer, copy the cached image into it.
        base::ScopedCFTypeRef<CGImageRef> bitmap_image(
            CGBitmapContextCreateImage(cg_bitmap_));
        CGContextDrawImage(CGLayerGetContext(cg_layer()),
                           CGRectMake(0, 0, size().width(), size().height()),
                           bitmap_image);
        // Discard the cache bitmap, since we no longer need it.
        cg_bitmap_.reset(NULL);
      }
    }

    DCHECK_NE(static_cast<bool>(cg_layer()), static_cast<bool>(cg_bitmap()));

    if (cg_layer()) {
      // The CGLayer's origin is in the lower left, but flipping the CTM would
      // cause the image to get drawn upside down.  So we move the rectangle
      // to the right position before drawing the image.
      CGContextRef layer = CGLayerGetContext(cg_layer());
      gfx::Rect paint_rect = copy_rect;
      paint_rect.set_y(size().height() - copy_rect.bottom());
      CGContextDrawImage(layer, paint_rect.ToCGRect(), image);
    } else {
      // The layer hasn't been created yet, so draw into the cache bitmap.
      gfx::Rect paint_rect = copy_rect;
      paint_rect.set_y(size().height() - copy_rect.bottom());
      CGContextDrawImage(cg_bitmap_, paint_rect.ToCGRect(), image);
    }
  }
}

bool BackingStoreMac::CopyFromBackingStore(const gfx::Rect& rect,
                                           skia::PlatformBitmap* output) {
  // TODO(thakis): Make sure this works with HiDPI backing stores.
  if (!output->Allocate(rect.width(), rect.height(), true))
    return false;

  CGContextRef temp_context = output->GetSurface();
  gfx::ScopedCGContextSaveGState save_gstate(temp_context);
  CGContextTranslateCTM(temp_context, 0.0, size().height());
  CGContextScaleCTM(temp_context, 1.0, -1.0);
  if (cg_layer()) {
    CGContextDrawLayerAtPoint(temp_context, CGPointMake(-rect.x(), -rect.y()),
                              cg_layer());
  } else {
    base::ScopedCFTypeRef<CGImageRef> bitmap_image(
        CGBitmapContextCreateImage(cg_bitmap_));
    CGContextDrawImage(
        temp_context,
        CGRectMake(-rect.x(), -rect.y(), rect.width(), rect.height()),
        bitmap_image);
  }

  return true;
}

// Scroll the contents of our CGLayer
void BackingStoreMac::ScrollBackingStore(const gfx::Vector2d& delta,
                                         const gfx::Rect& clip_rect,
                                         const gfx::Size& view_size) {
  DCHECK_NE(static_cast<bool>(cg_layer()), static_cast<bool>(cg_bitmap()));

  // "Scroll" the contents of the layer by creating a new CGLayer,
  // copying the contents of the old one into the new one offset by the scroll
  // amount, swapping in the new CGLayer, and then painting in the new data.
  //
  // The Windows code always sets the whole backing store as the source of the
  // scroll. Thus, we only have to worry about pixels which will end up inside
  // the clipping rectangle. (Note that the clipping rectangle is not
  // translated by the scroll.)

  // We assume |clip_rect| is contained within the backing store.
  DCHECK(clip_rect.bottom() <= size().height());
  DCHECK(clip_rect.right() <= size().width());

  if ((delta.x() || delta.y()) &&
       abs(delta.x()) < size().width() && abs(delta.y()) < size().height()) {
    if (cg_layer()) {
      CGContextRef layer = CGLayerGetContext(cg_layer());
      gfx::ScopedCGContextSaveGState save_gstate(layer);
      CGContextClipToRect(layer,
                          CGRectMake(clip_rect.x(),
                                     size().height() - clip_rect.bottom(),
                                     clip_rect.width(),
                                     clip_rect.height()));
      CGContextDrawLayerAtPoint(layer,
                                CGPointMake(delta.x(), -delta.y()), cg_layer());
    } else {
      // We don't have a layer, so scroll the contents of the CGBitmapContext.
      base::ScopedCFTypeRef<CGImageRef> bitmap_image(
          CGBitmapContextCreateImage(cg_bitmap_));
      gfx::ScopedCGContextSaveGState save_gstate(cg_bitmap_);
      CGContextClipToRect(cg_bitmap_,
                          CGRectMake(clip_rect.x(),
                                     size().height() - clip_rect.bottom(),
                                     clip_rect.width(),
                                     clip_rect.height()));
      CGContextDrawImage(cg_bitmap_,
                         CGRectMake(delta.x(), -delta.y(),
                                    size().width(), size().height()),
                         bitmap_image);
    }
  }
}

void BackingStoreMac::CopyFromBackingStoreToCGContext(const CGRect& dest_rect,
                                                      CGContextRef context) {
  gfx::ScopedCGContextSaveGState save_gstate(context);
  CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
  if (cg_layer_) {
    CGContextDrawLayerInRect(context, dest_rect, cg_layer_);
  } else {
    base::ScopedCFTypeRef<CGImageRef> image(
        CGBitmapContextCreateImage(cg_bitmap_));
    CGContextDrawImage(context, dest_rect, image);
  }
}

CGLayerRef BackingStoreMac::CreateCGLayer() {
  // The CGLayer should be optimized for drawing into the containing window,
  // so extract a CGContext corresponding to the window to be passed to
  // CGLayerCreateWithContext.
  NSWindow* window = [render_widget_host()->GetView()->GetNativeView() window];
  if ([window windowNumber] <= 0) {
    // This catches a nil |window|, as well as windows that exist but that
    // aren't yet connected to WindowServer.
    return NULL;
  }

  NSGraphicsContext* ns_context = [window graphicsContext];
  DCHECK(ns_context);

  CGContextRef cg_context = static_cast<CGContextRef>(
      [ns_context graphicsPort]);
  DCHECK(cg_context);

  // Note: This takes the backingScaleFactor of cg_context into account. The
  // bitmap backing |layer| will be size() * 2 in HiDPI mode automatically.
  CGLayerRef layer = CGLayerCreateWithContext(cg_context,
                                              size().ToCGSize(),
                                              NULL);
  DCHECK(layer);

  return layer;
}

CGContextRef BackingStoreMac::CreateCGBitmapContext() {
  gfx::Size pixel_size = gfx::ToFlooredSize(
      gfx::ScaleSize(size(), device_scale_factor_));
  // A CGBitmapContext serves as a stand-in for the layer before the view is
  // in a containing window.
  CGContextRef context = CGBitmapContextCreate(NULL,
                                               pixel_size.width(),
                                               pixel_size.height(),
                                               8, pixel_size.width() * 4,
                                               base::mac::GetSystemColorSpace(),
                                               kCGImageAlphaPremultipliedFirst |
                                                   kCGBitmapByteOrder32Host);
  DCHECK(context);

  return context;
}

}  // namespace content