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

#include "ppapi/proxy/video_decoder_resource.h"

#include <utility>

#include "base/bind.h"
#include "gpu/command_buffer/client/gles2_cmd_helper.h"
#include "gpu/command_buffer/client/gles2_implementation.h"
#include "gpu/command_buffer/common/mailbox.h"
#include "ipc/ipc_message.h"
#include "ppapi/c/pp_errors.h"
#include "ppapi/c/ppb_opengles2.h"
#include "ppapi/proxy/plugin_dispatcher.h"
#include "ppapi/proxy/ppapi_messages.h"
#include "ppapi/proxy/ppb_graphics_3d_proxy.h"
#include "ppapi/proxy/serialized_handle.h"
#include "ppapi/proxy/video_decoder_constants.h"
#include "ppapi/shared_impl/ppapi_globals.h"
#include "ppapi/shared_impl/ppb_graphics_3d_shared.h"
#include "ppapi/shared_impl/proxy_lock.h"
#include "ppapi/shared_impl/resource_tracker.h"
#include "ppapi/thunk/enter.h"

using ppapi::thunk::EnterResourceNoLock;
using ppapi::thunk::PPB_Graphics3D_API;
using ppapi::thunk::PPB_VideoDecoder_API;

namespace ppapi {
namespace proxy {

VideoDecoderResource::ShmBuffer::ShmBuffer(
    std::unique_ptr<base::SharedMemory> shm_ptr,
    uint32_t size,
    uint32_t shm_id)
    : shm(std::move(shm_ptr)), addr(NULL), shm_id(shm_id) {
  if (shm->Map(size))
    addr = shm->memory();
}

VideoDecoderResource::ShmBuffer::~ShmBuffer() {
}

VideoDecoderResource::Texture::Texture(uint32_t texture_target,
                                       const PP_Size& size)
    : texture_target(texture_target), size(size) {
}

VideoDecoderResource::Texture::~Texture() {
}

VideoDecoderResource::Picture::Picture(int32_t decode_id,
                                       uint32_t texture_id,
                                       const PP_Rect& visible_rect)
    : decode_id(decode_id), texture_id(texture_id), visible_rect(visible_rect) {
}

VideoDecoderResource::Picture::~Picture() {
}

VideoDecoderResource::VideoDecoderResource(Connection connection,
                                           PP_Instance instance)
    : PluginResource(connection, instance),
      num_decodes_(0),
      min_picture_count_(0),
      get_picture_(NULL),
      get_picture_0_1_(NULL),
      gles2_impl_(NULL),
      initialized_(false),
      testing_(false),
      // Set |decoder_last_error_| to PP_OK after successful initialization.
      // This makes error checking a little more concise, since we can check
      // that the decoder has been initialized and hasn't returned an error by
      // just testing |decoder_last_error_|.
      decoder_last_error_(PP_ERROR_FAILED) {
  // Clear the decode_ids_ array.
  memset(decode_ids_, 0, sizeof(decode_ids_));
  SendCreate(RENDERER, PpapiHostMsg_VideoDecoder_Create());
}

VideoDecoderResource::~VideoDecoderResource() {
  // Destroy any textures which haven't been dismissed.
  TextureMap::iterator it = textures_.begin();
  for (; it != textures_.end(); ++it)
    DeleteGLTexture(it->first);
}

PPB_VideoDecoder_API* VideoDecoderResource::AsPPB_VideoDecoder_API() {
  return this;
}

int32_t VideoDecoderResource::Initialize0_1(
    PP_Resource graphics_context,
    PP_VideoProfile profile,
    PP_Bool allow_software_fallback,
    scoped_refptr<TrackedCallback> callback) {
  return Initialize(graphics_context,
                    profile,
                    allow_software_fallback
                        ? PP_HARDWAREACCELERATION_WITHFALLBACK
                        : PP_HARDWAREACCELERATION_ONLY,
                    0,
                    callback);
}

int32_t VideoDecoderResource::Initialize0_2(
    PP_Resource graphics_context,
    PP_VideoProfile profile,
    PP_HardwareAcceleration acceleration,
    scoped_refptr<TrackedCallback> callback) {
  return Initialize(graphics_context,
                    profile,
                    acceleration,
                    0,
                    callback);
}

int32_t VideoDecoderResource::Initialize(
    PP_Resource graphics_context,
    PP_VideoProfile profile,
    PP_HardwareAcceleration acceleration,
    uint32_t min_picture_count,
    scoped_refptr<TrackedCallback> callback) {
  if (initialized_)
    return PP_ERROR_FAILED;
  if (profile < 0 || profile > PP_VIDEOPROFILE_MAX)
    return PP_ERROR_BADARGUMENT;
  if (min_picture_count > kMaximumPictureCount)
    return PP_ERROR_BADARGUMENT;
  if (initialize_callback_.get())
    return PP_ERROR_INPROGRESS;
  if (!graphics_context)
    return PP_ERROR_BADRESOURCE;

  min_picture_count_ = min_picture_count;

  HostResource host_resource;
  if (!testing_) {
    // Create a new Graphics3D resource that can create texture resources to
    // share with the plugin. We can't use the plugin's Graphics3D, since we
    // create textures on a proxy thread, and would interfere with the plugin.
    thunk::EnterResourceCreationNoLock enter_create(pp_instance());
    if (enter_create.failed())
      return PP_ERROR_FAILED;
    int32_t attrib_list[] = {PP_GRAPHICS3DATTRIB_NONE};
    graphics3d_ =
        ScopedPPResource(ScopedPPResource::PassRef(),
                         enter_create.functions()->CreateGraphics3D(
                             pp_instance(), graphics_context, attrib_list));
    EnterResourceNoLock<PPB_Graphics3D_API> enter_graphics(graphics3d_.get(),
                                                           false);
    if (enter_graphics.failed())
      return PP_ERROR_BADRESOURCE;

    PPB_Graphics3D_Shared* ppb_graphics3d_shared =
        static_cast<PPB_Graphics3D_Shared*>(enter_graphics.object());
    gles2_impl_ = ppb_graphics3d_shared->gles2_impl();
    host_resource = ppb_graphics3d_shared->host_resource();
  }

  initialize_callback_ = callback;

  Call<PpapiPluginMsg_VideoDecoder_InitializeReply>(
      RENDERER,
      PpapiHostMsg_VideoDecoder_Initialize(
          host_resource, profile, acceleration, min_picture_count),
      base::Bind(&VideoDecoderResource::OnPluginMsgInitializeComplete, this));

  return PP_OK_COMPLETIONPENDING;
}

int32_t VideoDecoderResource::Decode(uint32_t decode_id,
                                     uint32_t size,
                                     const void* buffer,
                                     scoped_refptr<TrackedCallback> callback) {
  if (decoder_last_error_)
    return decoder_last_error_;
  if (flush_callback_.get() || reset_callback_.get())
    return PP_ERROR_FAILED;
  if (decode_callback_.get())
    return PP_ERROR_INPROGRESS;
  if (size > kMaximumBitstreamBufferSize)
    return PP_ERROR_NOMEMORY;

  // If we allow the plugin to call Decode again, we must have somewhere to
  // copy their buffer.
  DCHECK(!available_shm_buffers_.empty() ||
         shm_buffers_.size() < kMaximumPendingDecodes);

  // Count up, wrapping back to 0 before overflowing.
  int32_t uid = ++num_decodes_;
  if (uid == std::numeric_limits<int32_t>::max())
    num_decodes_ = 0;

  // Save decode_id in a ring buffer. The ring buffer is sized to store
  // decode_id for the maximum picture delay.
  decode_ids_[uid % kMaximumPictureDelay] = decode_id;

  if (available_shm_buffers_.empty() ||
      available_shm_buffers_.back()->shm->mapped_size() < size) {
    uint32_t shm_id;
    if (shm_buffers_.size() < kMaximumPendingDecodes) {
      // Signal the host to create a new shm buffer by passing an index outside
      // the legal range.
      shm_id = static_cast<uint32_t>(shm_buffers_.size());
    } else {
      // Signal the host to grow a buffer by passing a legal index. Choose the
      // last available shm buffer for simplicity.
      shm_id = available_shm_buffers_.back()->shm_id;
      available_shm_buffers_.pop_back();
    }

    // Synchronously get shared memory. Use GenericSyncCall so we can get the
    // reply params, which contain the handle.
    uint32_t shm_size = 0;
    IPC::Message reply;
    ResourceMessageReplyParams reply_params;
    int32_t result =
        GenericSyncCall(RENDERER,
                        PpapiHostMsg_VideoDecoder_GetShm(shm_id, size),
                        &reply,
                        &reply_params);
    if (result != PP_OK)
      return PP_ERROR_FAILED;
    if (!UnpackMessage<PpapiPluginMsg_VideoDecoder_GetShmReply>(reply,
                                                                &shm_size))
      return PP_ERROR_FAILED;
    base::SharedMemoryHandle shm_handle;
    if (!reply_params.TakeSharedMemoryHandleAtIndex(0, &shm_handle))
      return PP_ERROR_NOMEMORY;
    std::unique_ptr<base::SharedMemory> shm(
        new base::SharedMemory(shm_handle, false /* read_only */));
    std::unique_ptr<ShmBuffer> shm_buffer(
        new ShmBuffer(std::move(shm), shm_size, shm_id));
    if (!shm_buffer->addr)
      return PP_ERROR_NOMEMORY;

    available_shm_buffers_.push_back(shm_buffer.get());
    if (shm_buffers_.size() < kMaximumPendingDecodes)
      shm_buffers_.push_back(std::move(shm_buffer));
    else
      shm_buffers_[shm_id] = std::move(shm_buffer);
  }

  // At this point we should have shared memory to hold the plugin's buffer.
  DCHECK(!available_shm_buffers_.empty() &&
         available_shm_buffers_.back()->shm->mapped_size() >= size);

  ShmBuffer* shm_buffer = available_shm_buffers_.back();
  available_shm_buffers_.pop_back();
  memcpy(shm_buffer->addr, buffer, size);

  Call<PpapiPluginMsg_VideoDecoder_DecodeReply>(
      RENDERER,
      PpapiHostMsg_VideoDecoder_Decode(shm_buffer->shm_id, size, uid),
      base::Bind(&VideoDecoderResource::OnPluginMsgDecodeComplete, this));

  // If we have another free buffer, or we can still create new buffers, let
  // the plugin call Decode again.
  if (!available_shm_buffers_.empty() ||
      shm_buffers_.size() < kMaximumPendingDecodes)
    return PP_OK;

  // All buffers are busy and we can't create more. Delay completion until a
  // buffer is available.
  decode_callback_ = callback;
  return PP_OK_COMPLETIONPENDING;
}

int32_t VideoDecoderResource::GetPicture0_1(
    PP_VideoPicture_0_1* picture,
    scoped_refptr<TrackedCallback> callback) {
  get_picture_0_1_ = picture;
  return GetPicture(NULL, callback);
}

int32_t VideoDecoderResource::GetPicture(
    PP_VideoPicture* picture,
    scoped_refptr<TrackedCallback> callback) {
  if (decoder_last_error_)
    return decoder_last_error_;
  if (reset_callback_.get())
    return PP_ERROR_FAILED;
  if (get_picture_callback_.get())
    return PP_ERROR_INPROGRESS;

  get_picture_ = picture;

  // If the next picture is ready, return it synchronously.
  if (!received_pictures_.empty()) {
    WriteNextPicture();
    return PP_OK;
  }

  get_picture_callback_ = callback;

  return PP_OK_COMPLETIONPENDING;
}

void VideoDecoderResource::RecyclePicture(const PP_VideoPicture* picture) {
  if (decoder_last_error_)
    return;

  Post(RENDERER, PpapiHostMsg_VideoDecoder_RecyclePicture(picture->texture_id));
}

int32_t VideoDecoderResource::Flush(scoped_refptr<TrackedCallback> callback) {
  if (decoder_last_error_)
    return decoder_last_error_;
  if (reset_callback_.get())
    return PP_ERROR_FAILED;
  if (flush_callback_.get())
    return PP_ERROR_INPROGRESS;
  flush_callback_ = callback;

  Call<PpapiPluginMsg_VideoDecoder_FlushReply>(
      RENDERER,
      PpapiHostMsg_VideoDecoder_Flush(),
      base::Bind(&VideoDecoderResource::OnPluginMsgFlushComplete, this));

  return PP_OK_COMPLETIONPENDING;
}

int32_t VideoDecoderResource::Reset(scoped_refptr<TrackedCallback> callback) {
  if (decoder_last_error_)
    return decoder_last_error_;
  if (flush_callback_.get())
    return PP_ERROR_FAILED;
  if (reset_callback_.get())
    return PP_ERROR_INPROGRESS;
  reset_callback_ = callback;

  // Cause any pending Decode or GetPicture callbacks to abort after we return,
  // to avoid reentering the plugin.
  if (TrackedCallback::IsPending(decode_callback_))
    decode_callback_->PostAbort();
  decode_callback_ = NULL;
  if (TrackedCallback::IsPending(get_picture_callback_))
    get_picture_callback_->PostAbort();
  get_picture_callback_ = NULL;
  Call<PpapiPluginMsg_VideoDecoder_ResetReply>(
      RENDERER,
      PpapiHostMsg_VideoDecoder_Reset(),
      base::Bind(&VideoDecoderResource::OnPluginMsgResetComplete, this));

  return PP_OK_COMPLETIONPENDING;
}

void VideoDecoderResource::OnReplyReceived(
    const ResourceMessageReplyParams& params,
    const IPC::Message& msg) {
  PPAPI_BEGIN_MESSAGE_MAP(VideoDecoderResource, msg)
    PPAPI_DISPATCH_PLUGIN_RESOURCE_CALL(
        PpapiPluginMsg_VideoDecoder_RequestTextures, OnPluginMsgRequestTextures)
    PPAPI_DISPATCH_PLUGIN_RESOURCE_CALL(
        PpapiPluginMsg_VideoDecoder_PictureReady, OnPluginMsgPictureReady)
    PPAPI_DISPATCH_PLUGIN_RESOURCE_CALL(
        PpapiPluginMsg_VideoDecoder_DismissPicture, OnPluginMsgDismissPicture)
    PPAPI_DISPATCH_PLUGIN_RESOURCE_CALL(
        PpapiPluginMsg_VideoDecoder_NotifyError, OnPluginMsgNotifyError)
    PPAPI_DISPATCH_PLUGIN_RESOURCE_CALL_UNHANDLED(
        PluginResource::OnReplyReceived(params, msg))
  PPAPI_END_MESSAGE_MAP()
}

void VideoDecoderResource::SetForTest() {
  testing_ = true;
}

void VideoDecoderResource::OnPluginMsgRequestTextures(
    const ResourceMessageReplyParams& params,
    uint32_t num_textures,
    const PP_Size& size,
    uint32_t texture_target) {
  DCHECK(num_textures);
  DCHECK(num_textures >= min_picture_count_);
  std::vector<uint32_t> texture_ids(num_textures);
  std::vector<gpu::Mailbox> mailboxes(num_textures);
  if (gles2_impl_) {
    gles2_impl_->GenTextures(num_textures, texture_ids.data());
    for (uint32_t i = 0; i < num_textures; ++i) {
      gles2_impl_->ActiveTexture(GL_TEXTURE0);
      gles2_impl_->BindTexture(texture_target, texture_ids[i]);
      gles2_impl_->TexParameteri(
          texture_target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
      gles2_impl_->TexParameteri(
          texture_target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
      gles2_impl_->TexParameterf(
          texture_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
      gles2_impl_->TexParameterf(
          texture_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

      if (texture_target == GL_TEXTURE_2D) {
        gles2_impl_->TexImage2D(texture_target,
                                0,
                                GL_RGBA,
                                size.width,
                                size.height,
                                0,
                                GL_RGBA,
                                GL_UNSIGNED_BYTE,
                                NULL);
      }
      gles2_impl_->ProduceTextureDirectCHROMIUM(texture_ids[i],
                                                mailboxes[i].name);

      textures_.insert(
          std::make_pair(texture_ids[i], Texture(texture_target, size)));
    }
    gles2_impl_->Flush();
  } else {
    DCHECK(testing_);
    // Create some fake texture ids so we can test picture handling.
    for (uint32_t i = 0; i < num_textures; ++i) {
      texture_ids[i] = i + 1;
      textures_.insert(
          std::make_pair(texture_ids[i], Texture(texture_target, size)));
    }
  }

  Post(RENDERER, PpapiHostMsg_VideoDecoder_AssignTextures(
                     size, std::move(texture_ids), std::move(mailboxes)));
}

void VideoDecoderResource::OnPluginMsgPictureReady(
    const ResourceMessageReplyParams& params,
    int32_t decode_id,
    uint32_t texture_id,
    const PP_Rect& visible_rect) {
  received_pictures_.push(Picture(decode_id, texture_id, visible_rect));

  if (TrackedCallback::IsPending(get_picture_callback_)) {
    // The plugin may call GetPicture in its callback.
    scoped_refptr<TrackedCallback> callback;
    callback.swap(get_picture_callback_);
    WriteNextPicture();
    callback->Run(PP_OK);
  }
}

void VideoDecoderResource::OnPluginMsgDismissPicture(
    const ResourceMessageReplyParams& params,
    uint32_t texture_id) {
  DeleteGLTexture(texture_id);
  textures_.erase(texture_id);
}

void VideoDecoderResource::OnPluginMsgNotifyError(
    const ResourceMessageReplyParams& params,
    int32_t error) {
  decoder_last_error_ = error;
  // Cause any pending callbacks to run immediately. Reentrancy isn't a problem,
  // since the plugin wasn't calling us.
  RunCallbackWithError(&initialize_callback_);
  RunCallbackWithError(&decode_callback_);
  RunCallbackWithError(&get_picture_callback_);
  RunCallbackWithError(&flush_callback_);
  RunCallbackWithError(&reset_callback_);
}

void VideoDecoderResource::OnPluginMsgInitializeComplete(
    const ResourceMessageReplyParams& params) {
  decoder_last_error_ = params.result();
  if (decoder_last_error_ == PP_OK)
    initialized_ = true;

  // Let the plugin call Initialize again from its callback in case of failure.
  scoped_refptr<TrackedCallback> callback;
  callback.swap(initialize_callback_);
  callback->Run(decoder_last_error_);
}

void VideoDecoderResource::OnPluginMsgDecodeComplete(
    const ResourceMessageReplyParams& params,
    uint32_t shm_id) {
  if (shm_id >= shm_buffers_.size()) {
    NOTREACHED();
    return;
  }
  // Make the shm buffer available.
  available_shm_buffers_.push_back(shm_buffers_[shm_id].get());
  // If the plugin is waiting, let it call Decode again.
  if (decode_callback_.get()) {
    scoped_refptr<TrackedCallback> callback;
    callback.swap(decode_callback_);
    callback->Run(PP_OK);
  }
}

void VideoDecoderResource::OnPluginMsgFlushComplete(
    const ResourceMessageReplyParams& params) {
  // All shm buffers should have been made available by now.
  DCHECK_EQ(shm_buffers_.size(), available_shm_buffers_.size());

  if (get_picture_callback_.get()) {
    scoped_refptr<TrackedCallback> callback;
    callback.swap(get_picture_callback_);
    callback->Abort();
  }

  scoped_refptr<TrackedCallback> callback;
  callback.swap(flush_callback_);
  callback->Run(params.result());
}

void VideoDecoderResource::OnPluginMsgResetComplete(
    const ResourceMessageReplyParams& params) {
  // All shm buffers should have been made available by now.
  DCHECK_EQ(shm_buffers_.size(), available_shm_buffers_.size());
  // Recycle any pictures which haven't been passed to the plugin.
  while (!received_pictures_.empty()) {
    Post(RENDERER, PpapiHostMsg_VideoDecoder_RecyclePicture(
        received_pictures_.front().texture_id));
    received_pictures_.pop();
  }

  scoped_refptr<TrackedCallback> callback;
  callback.swap(reset_callback_);
  callback->Run(params.result());
}

void VideoDecoderResource::RunCallbackWithError(
    scoped_refptr<TrackedCallback>* callback) {
  if (TrackedCallback::IsPending(*callback)) {
    scoped_refptr<TrackedCallback> temp;
    callback->swap(temp);
    temp->Run(decoder_last_error_);
  }
}

void VideoDecoderResource::DeleteGLTexture(uint32_t id) {
  if (gles2_impl_) {
    gles2_impl_->DeleteTextures(1, &id);
    gles2_impl_->Flush();
  }
}

void VideoDecoderResource::WriteNextPicture() {
  DCHECK(!received_pictures_.empty());
  Picture& picture = received_pictures_.front();

  // Internally, we identify decodes by a unique id, which the host returns
  // to us in the picture. Use this to get the plugin's decode_id.
  uint32_t decode_id = decode_ids_[picture.decode_id % kMaximumPictureDelay];
  uint32_t texture_id = picture.texture_id;
  uint32_t texture_target = 0;
  PP_Size texture_size = PP_MakeSize(0, 0);
  TextureMap::iterator it = textures_.find(picture.texture_id);
  if (it != textures_.end()) {
    texture_target = it->second.texture_target;
    texture_size = it->second.size;
  } else {
    NOTREACHED();
  }

  if (get_picture_) {
    DCHECK(!get_picture_0_1_);
    get_picture_->decode_id = decode_id;
    get_picture_->texture_id = texture_id;
    get_picture_->texture_target = texture_target;
    get_picture_->texture_size = texture_size;
    get_picture_->visible_rect = picture.visible_rect;
    get_picture_ = NULL;
  } else {
    DCHECK(get_picture_0_1_);
    get_picture_0_1_->decode_id = decode_id;
    get_picture_0_1_->texture_id = texture_id;
    get_picture_0_1_->texture_target = texture_target;
    get_picture_0_1_->texture_size = texture_size;
    get_picture_0_1_ = NULL;
  }

  received_pictures_.pop();
}

}  // namespace proxy
}  // namespace ppapi
