// Copyright (c) 2010 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 "app/surface/accelerated_surface_mac.h"

#include "app/surface/io_surface_support_mac.h"
#include "base/logging.h"
#include "gfx/rect.h"

AcceleratedSurface::AcceleratedSurface()
    : gl_context_(NULL),
      pbuffer_(NULL),
      allocate_fbo_(false),
      texture_(0),
      fbo_(0),
      depth_stencil_renderbuffer_(0) {
}

bool AcceleratedSurface::Initialize(CGLContextObj share_context,
                                    bool allocate_fbo) {
  allocate_fbo_ = allocate_fbo;

  // TODO(kbr): we should reuse the code for PbufferGLContext here instead
  // of duplicating it. However, in order to do so, we need to move the
  // GLContext classes out of gpu/ and into gfx/.

  // Create a 1x1 pbuffer and associated context to bootstrap things
  static const CGLPixelFormatAttribute attribs[] = {
    (CGLPixelFormatAttribute) kCGLPFAPBuffer,
    (CGLPixelFormatAttribute) 0
  };
  CGLPixelFormatObj pixel_format;
  GLint num_pixel_formats;
  if (CGLChoosePixelFormat(attribs,
                           &pixel_format,
                           &num_pixel_formats) != kCGLNoError) {
    DLOG(ERROR) << "Error choosing pixel format.";
    return false;
  }
  if (!pixel_format) {
    return false;
  }
  CGLContextObj context;
  CGLError res = CGLCreateContext(pixel_format, share_context, &context);
  CGLDestroyPixelFormat(pixel_format);
  if (res != kCGLNoError) {
    DLOG(ERROR) << "Error creating context.";
    return false;
  }
  CGLPBufferObj pbuffer;
  if (CGLCreatePBuffer(1, 1,
                       GL_TEXTURE_2D, GL_RGBA,
                       0, &pbuffer) != kCGLNoError) {
    CGLDestroyContext(context);
    DLOG(ERROR) << "Error creating pbuffer.";
    return false;
  }
  if (CGLSetPBuffer(context, pbuffer, 0, 0, 0) != kCGLNoError) {
    CGLDestroyContext(context);
    CGLDestroyPBuffer(pbuffer);
    DLOG(ERROR) << "Error attaching pbuffer to context.";
    return false;
  }
  gl_context_ = context;
  pbuffer_ = pbuffer;
  // Now we're ready to handle SetWindowSize calls, which will
  // allocate and/or reallocate the IOSurface and associated offscreen
  // OpenGL structures for rendering.
  return true;
}

void AcceleratedSurface::Destroy() {
  // The FBO and texture objects will be destroyed when the OpenGL context,
  // and any other contexts sharing resources with it, is. We don't want to
  // make the context current one last time here just in order to delete
  // these objects.

  // Release the old TransportDIB in the browser.
  if (dib_free_callback_.get() && transport_dib_.get()) {
    dib_free_callback_->Run(transport_dib_->id());
  }
  transport_dib_.reset();
  if (gl_context_)
    CGLDestroyContext(gl_context_);
  if (pbuffer_)
    CGLDestroyPBuffer(pbuffer_);
}

// Call after making changes to the surface which require a visual update.
// Makes the rendering show up in other processes.
void AcceleratedSurface::SwapBuffers() {
  if (io_surface_.get() != NULL) {
    if (allocate_fbo_) {
      // Bind and unbind the framebuffer to make changes to the
      // IOSurface show up in the other process.
      glFlush();
      glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
      glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo_);
    } else {
      // Copy the current framebuffer's contents into our "live" texture.
      // Note that the current GL context might not be ours at this point!
      // This is deliberate, so that surrounding code using GL can produce
      // rendering results consumed by the AcceleratedSurface.
      // Need to save and restore OpenGL state around this call.
      GLint current_texture = 0;
      GLenum target_binding = GL_TEXTURE_BINDING_RECTANGLE_ARB;
      GLenum target = GL_TEXTURE_RECTANGLE_ARB;
      glGetIntegerv(target_binding, &current_texture);
      glBindTexture(target, texture_);
      glCopyTexSubImage2D(target, 0,
                          0, 0,
                          0, 0,
                          surface_size_.width(), surface_size_.height());
      glBindTexture(target, current_texture);
    }
  } else if (transport_dib_.get() != NULL) {
    // Pre-Mac OS X 10.6, fetch the rendered image from the current frame
    // buffer and copy it into the TransportDIB.
    // TODO(dspringer): There are a couple of options that can speed this up.
    // First is to use async reads into a PBO, second is to use SPI that
    // allows many tasks to access the same CGSSurface.
    void* pixel_memory = transport_dib_->memory();
    if (pixel_memory) {
      // Note that glReadPixels does an implicit glFlush().
      glReadPixels(0,
                   0,
                   surface_size_.width(),
                   surface_size_.height(),
                   GL_BGRA,  // This pixel format should have no conversion.
                   GL_UNSIGNED_INT_8_8_8_8_REV,
                   pixel_memory);
    }
  }
}

static void AddBooleanValue(CFMutableDictionaryRef dictionary,
                            const CFStringRef key,
                            bool value) {
  CFDictionaryAddValue(dictionary, key,
                       (value ? kCFBooleanTrue : kCFBooleanFalse));
}

static void AddIntegerValue(CFMutableDictionaryRef dictionary,
                            const CFStringRef key,
                            int32 value) {
  CFNumberRef number = CFNumberCreate(NULL, kCFNumberSInt32Type, &value);
  CFDictionaryAddValue(dictionary, key, number);
}

void AcceleratedSurface::AllocateRenderBuffers(GLenum target,
                                               const gfx::Size& size) {
  if (!texture_) {
    // Generate the texture object.
    glGenTextures(1, &texture_);
    glBindTexture(target, texture_);
    glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    // Generate and bind the framebuffer object.
    glGenFramebuffersEXT(1, &fbo_);
    glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo_);
    // Generate (but don't bind) the depth buffer -- we don't need
    // this bound in order to do offscreen rendering.
    glGenRenderbuffersEXT(1, &depth_stencil_renderbuffer_);
  }

  // Reallocate the depth buffer.
  glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, depth_stencil_renderbuffer_);
  glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT,
                           GL_DEPTH24_STENCIL8_EXT,
                           size.width(),
                           size.height());

  // Unbind the renderbuffers.
  glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, 0);

  // Make sure that subsequent set-up code affects the render texture.
  glBindTexture(target, texture_);
}

bool AcceleratedSurface::SetupFrameBufferObject(GLenum target) {
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo_);
  GLenum fbo_status;
  glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,
                            GL_COLOR_ATTACHMENT0_EXT,
                            target,
                            texture_,
                            0);
  fbo_status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
  if (fbo_status == GL_FRAMEBUFFER_COMPLETE_EXT) {
    glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT,
                                 GL_DEPTH_ATTACHMENT_EXT,
                                 GL_RENDERBUFFER_EXT,
                                 depth_stencil_renderbuffer_);
    fbo_status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
  }
  // Attach the depth and stencil buffer.
  if (fbo_status == GL_FRAMEBUFFER_COMPLETE_EXT) {
    glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT,
                                 0x8D20, // GL_STENCIL_ATTACHMENT,
                                 GL_RENDERBUFFER_EXT,
                                 depth_stencil_renderbuffer_);
    fbo_status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
  }
  return fbo_status == GL_FRAMEBUFFER_COMPLETE_EXT;
}

bool AcceleratedSurface::MakeCurrent() {
  if (CGLGetCurrentContext() != gl_context_) {
    if (CGLSetCurrentContext(gl_context_) != kCGLNoError) {
      DLOG(ERROR) << "Unable to make gl context current.";
      return false;
    }
  }
  return true;
}

void AcceleratedSurface::Clear(const gfx::Rect& rect) {
  glClearColor(1.0, 1.0, 1.0, 1.0);
  glViewport(0, 0, rect.width(), rect.height());
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(0, rect.width(), 0, rect.height(), -1, 1);
  glClear(GL_COLOR_BUFFER_BIT);
}

uint64 AcceleratedSurface::SetSurfaceSize(const gfx::Size& size) {
  if (surface_size_ == size) {
    // Return 0 to indicate to the caller that no new backing store
    // allocation occurred.
    return 0;
  }

  IOSurfaceSupport* io_surface_support = IOSurfaceSupport::Initialize();
  if (!io_surface_support)
    return 0;  // Caller can try using SetWindowSizeForTransportDIB().

  if (!MakeCurrent())
    return 0;

  // GL_TEXTURE_RECTANGLE_ARB is the best supported render target on
  // Mac OS X and is required for IOSurface interoperability.
  GLenum target = GL_TEXTURE_RECTANGLE_ARB;
  if (allocate_fbo_) {
    AllocateRenderBuffers(target, size);
  } else if (!texture_) {
    // Generate the texture object.
    glGenTextures(1, &texture_);
    glBindTexture(target, texture_);
    glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  }

  // Allocate a new IOSurface, which is the GPU resource that can be
  // shared across processes.
  scoped_cftyperef<CFMutableDictionaryRef> properties;
  properties.reset(CFDictionaryCreateMutable(kCFAllocatorDefault,
                                             0,
                                             &kCFTypeDictionaryKeyCallBacks,
                                             &kCFTypeDictionaryValueCallBacks));
  AddIntegerValue(properties,
                  io_surface_support->GetKIOSurfaceWidth(), size.width());
  AddIntegerValue(properties,
                  io_surface_support->GetKIOSurfaceHeight(), size.height());
  AddIntegerValue(properties,
                  io_surface_support->GetKIOSurfaceBytesPerElement(), 4);
  AddBooleanValue(properties,
                  io_surface_support->GetKIOSurfaceIsGlobal(), true);
  // I believe we should be able to unreference the IOSurfaces without
  // synchronizing with the browser process because they are
  // ultimately reference counted by the operating system.
  io_surface_.reset(io_surface_support->IOSurfaceCreate(properties));

  // Don't think we need to identify a plane.
  GLuint plane = 0;
  io_surface_support->CGLTexImageIOSurface2D(gl_context_,
                                             target,
                                             GL_RGBA,
                                             size.width(),
                                             size.height(),
                                             GL_BGRA,
                                             GL_UNSIGNED_INT_8_8_8_8_REV,
                                             io_surface_.get(),
                                             plane);
  if (allocate_fbo_) {
    // Set up the frame buffer object.
    SetupFrameBufferObject(target);
  }
  surface_size_ = size;

  // Now send back an identifier for the IOSurface. We originally
  // intended to send back a mach port from IOSurfaceCreateMachPort
  // but it looks like Chrome IPC would need to be modified to
  // properly send mach ports between processes. For the time being we
  // make our IOSurfaces global and send back their identifiers. On
  // the browser process side the identifier is reconstituted into an
  // IOSurface for on-screen rendering.
  return io_surface_support->IOSurfaceGetID(io_surface_);
}

TransportDIB::Handle AcceleratedSurface::SetTransportDIBSize(
    const gfx::Size& size) {
  if (surface_size_ == size) {
    // Return an invalid handle to indicate to the caller that no new backing
    // store allocation occurred.
    return TransportDIB::DefaultHandleValue();
  }
  surface_size_ = size;

  // Release the old TransportDIB in the browser.
  if (dib_free_callback_.get() && transport_dib_.get()) {
    dib_free_callback_->Run(transport_dib_->id());
  }
  transport_dib_.reset();

  // Ask the renderer to create a TransportDIB.
  size_t dib_size = size.width() * 4 * size.height();  // 4 bytes per pixel.
  TransportDIB::Handle dib_handle;
  if (dib_alloc_callback_.get()) {
    dib_alloc_callback_->Run(dib_size, &dib_handle);
  }
  if (!TransportDIB::is_valid(dib_handle)) {
    // If the allocator fails, it means the DIB was not created in the browser,
    // so there is no need to run the deallocator here.
    return TransportDIB::DefaultHandleValue();
  }
  transport_dib_.reset(TransportDIB::Map(dib_handle));
  if (transport_dib_.get() == NULL) {
    // TODO(dspringer): if the Map() fails, should the deallocator be run so
    // that the DIB is deallocated in the browser?
    return TransportDIB::DefaultHandleValue();
  }

  if (allocate_fbo_) {
    // Set up the render buffers and reserve enough space on the card for the
    // framebuffer texture.
    GLenum target = GL_TEXTURE_RECTANGLE_ARB;
    AllocateRenderBuffers(target, size);
    glTexImage2D(target,
                 0,  // mipmap level 0
                 GL_RGBA8,  // internal pixel format
                 size.width(),
                 size.height(),
                 0,  // 0 border
                 GL_BGRA,  // Used for consistency
                 GL_UNSIGNED_INT_8_8_8_8_REV,
                 NULL);  // No data, just reserve room on the card.
    SetupFrameBufferObject(target);
  }
  return transport_dib_->handle();
}

void AcceleratedSurface::SetTransportDIBAllocAndFree(
    Callback2<size_t, TransportDIB::Handle*>::Type* allocator,
    Callback1<TransportDIB::Id>::Type* deallocator) {
  dib_alloc_callback_.reset(allocator);
  dib_free_callback_.reset(deallocator);
}