// 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 <stdlib.h>
#include <string.h>

#include <map>
#include <vector>

#include "ppapi/c/dev/ppb_video_capture_dev.h"
#include "ppapi/c/pp_errors.h"
#include "ppapi/c/ppb_opengles2.h"
#include "ppapi/cpp/dev/buffer_dev.h"
#include "ppapi/cpp/dev/device_ref_dev.h"
#include "ppapi/cpp/dev/video_capture_dev.h"
#include "ppapi/cpp/dev/video_capture_client_dev.h"
#include "ppapi/cpp/completion_callback.h"
#include "ppapi/cpp/graphics_3d_client.h"
#include "ppapi/cpp/graphics_3d.h"
#include "ppapi/cpp/instance.h"
#include "ppapi/cpp/module.h"
#include "ppapi/cpp/rect.h"
#include "ppapi/cpp/var.h"
#include "ppapi/lib/gl/include/GLES2/gl2.h"
#include "ppapi/utility/completion_callback_factory.h"

// When compiling natively on Windows, PostMessage can be #define-d to
// something else.
#ifdef PostMessage
#undef PostMessage
#endif

// Assert |context_| isn't holding any GL Errors.  Done as a macro instead of a
// function to preserve line number information in the failure message.
#define AssertNoGLError() \
  PP_DCHECK(!gles2_if_->GetError(context_->pp_resource()));

namespace {

const char* const kDelimiter = "#__#";

// This object is the global object representing this plugin library as long
// as it is loaded.
class VCDemoModule : public pp::Module {
 public:
  VCDemoModule() : pp::Module() {}
  virtual ~VCDemoModule() {}

  virtual pp::Instance* CreateInstance(PP_Instance instance);
};

class VCDemoInstance : public pp::Instance,
                       public pp::Graphics3DClient,
                       public pp::VideoCaptureClient_Dev {
 public:
  VCDemoInstance(PP_Instance instance, pp::Module* module);
  virtual ~VCDemoInstance();

  // pp::Instance implementation (see PPP_Instance).
  virtual void DidChangeView(const pp::Rect& position,
                             const pp::Rect& clip_ignored);
  virtual void HandleMessage(const pp::Var& message_data);

  // pp::Graphics3DClient implementation.
  virtual void Graphics3DContextLost() {
    InitGL();
    CreateYUVTextures();
    Render();
  }

  virtual void OnDeviceInfo(PP_Resource resource,
                            const PP_VideoCaptureDeviceInfo_Dev& info,
                            const std::vector<pp::Buffer_Dev>& buffers) {
    capture_info_ = info;
    buffers_ = buffers;
    CreateYUVTextures();
  }

  virtual void OnStatus(PP_Resource resource, uint32_t status) {
  }

  virtual void OnError(PP_Resource resource, uint32_t error) {
  }

  virtual void OnBufferReady(PP_Resource resource, uint32_t buffer) {
    const char* data = static_cast<const char*>(buffers_[buffer].data());
    int32_t width = capture_info_.width;
    int32_t height = capture_info_.height;
    gles2_if_->ActiveTexture(context_->pp_resource(), GL_TEXTURE0);
    gles2_if_->TexSubImage2D(
        context_->pp_resource(), GL_TEXTURE_2D, 0, 0, 0, width, height,
        GL_LUMINANCE, GL_UNSIGNED_BYTE, data);

    data += width * height;
    width /= 2;
    height /= 2;

    gles2_if_->ActiveTexture(context_->pp_resource(), GL_TEXTURE1);
    gles2_if_->TexSubImage2D(
        context_->pp_resource(), GL_TEXTURE_2D, 0, 0, 0, width, height,
        GL_LUMINANCE, GL_UNSIGNED_BYTE, data);

    data += width * height;
    gles2_if_->ActiveTexture(context_->pp_resource(), GL_TEXTURE2);
    gles2_if_->TexSubImage2D(
        context_->pp_resource(), GL_TEXTURE_2D, 0, 0, 0, width, height,
        GL_LUMINANCE, GL_UNSIGNED_BYTE, data);

    video_capture_.ReuseBuffer(buffer);
    if (is_painting_)
      needs_paint_ = true;
    else
      Render();
  }

 private:
  void Render();

  // GL-related functions.
  void InitGL();
  GLuint CreateTexture(int32_t width, int32_t height, int unit);
  void CreateGLObjects();
  void CreateShader(GLuint program, GLenum type, const char* source, int size);
  void PaintFinished(int32_t result);
  void CreateYUVTextures();

  void Open(const pp::DeviceRef_Dev& device);
  void Stop();
  void Start();
  void EnumerateDevicesFinished(int32_t result,
                                std::vector<pp::DeviceRef_Dev>& devices);
  void OpenFinished(int32_t result);

  static void MonitorDeviceChangeCallback(void* user_data,
                                          uint32_t device_count,
                                          const PP_Resource devices[]);

  pp::Size position_size_;
  bool is_painting_;
  bool needs_paint_;
  GLuint texture_y_;
  GLuint texture_u_;
  GLuint texture_v_;
  pp::VideoCapture_Dev video_capture_;
  PP_VideoCaptureDeviceInfo_Dev capture_info_;
  std::vector<pp::Buffer_Dev> buffers_;
  pp::CompletionCallbackFactory<VCDemoInstance> callback_factory_;

  // Unowned pointers.
  const struct PPB_OpenGLES2* gles2_if_;

  // Owned data.
  pp::Graphics3D* context_;

  std::vector<pp::DeviceRef_Dev> enumerate_devices_;
  std::vector<pp::DeviceRef_Dev> monitor_devices_;
};

VCDemoInstance::VCDemoInstance(PP_Instance instance, pp::Module* module)
    : pp::Instance(instance),
      pp::Graphics3DClient(this),
      pp::VideoCaptureClient_Dev(this),
      is_painting_(false),
      needs_paint_(false),
      texture_y_(0),
      texture_u_(0),
      texture_v_(0),
      video_capture_(this),
      callback_factory_(this),
      context_(NULL) {
  gles2_if_ = static_cast<const struct PPB_OpenGLES2*>(
      module->GetBrowserInterface(PPB_OPENGLES2_INTERFACE));
  PP_DCHECK(gles2_if_);

  capture_info_.width = 320;
  capture_info_.height = 240;
  capture_info_.frames_per_second = 30;
}

VCDemoInstance::~VCDemoInstance() {
  video_capture_.MonitorDeviceChange(NULL, NULL);
  delete context_;
}

void VCDemoInstance::DidChangeView(
    const pp::Rect& position, const pp::Rect& clip_ignored) {
  if (position.width() == 0 || position.height() == 0)
    return;
  if (position.size() == position_size_)
    return;

  position_size_ = position.size();

  // Initialize graphics.
  InitGL();

  Render();
}

void VCDemoInstance::HandleMessage(const pp::Var& message_data) {
  if (message_data.is_string()) {
    std::string event = message_data.AsString();
    if (event == "PageInitialized") {
      int32_t result = video_capture_.MonitorDeviceChange(
          &VCDemoInstance::MonitorDeviceChangeCallback, this);
      if (result != PP_OK)
        PostMessage(pp::Var("MonitorDeviceChangeFailed"));

      pp::CompletionCallbackWithOutput<std::vector<pp::DeviceRef_Dev> >
          callback = callback_factory_.NewCallbackWithOutput(
              &VCDemoInstance::EnumerateDevicesFinished);
      result = video_capture_.EnumerateDevices(callback);
      if (result != PP_OK_COMPLETIONPENDING)
        PostMessage(pp::Var("EnumerationFailed"));
    } else if (event == "UseDefault") {
      Open(pp::DeviceRef_Dev());
    } else if (event == "Stop") {
      Stop();
    } else if (event == "Start") {
      Start();
    } else if (event.find("Monitor:") == 0) {
      std::string index_str = event.substr(strlen("Monitor:"));
      int index = atoi(index_str.c_str());
      if (index >= 0 && index < static_cast<int>(monitor_devices_.size()))
        Open(monitor_devices_[index]);
      else
        PP_NOTREACHED();
    } else if (event.find("Enumerate:") == 0) {
      std::string index_str = event.substr(strlen("Enumerate:"));
      int index = atoi(index_str.c_str());
      if (index >= 0 && index < static_cast<int>(enumerate_devices_.size()))
        Open(enumerate_devices_[index]);
      else
        PP_NOTREACHED();
    }
  }
}

void VCDemoInstance::InitGL() {
  PP_DCHECK(position_size_.width() && position_size_.height());
  is_painting_ = false;

  delete context_;
  int32_t attributes[] = {
    PP_GRAPHICS3DATTRIB_ALPHA_SIZE, 0,
    PP_GRAPHICS3DATTRIB_BLUE_SIZE, 8,
    PP_GRAPHICS3DATTRIB_GREEN_SIZE, 8,
    PP_GRAPHICS3DATTRIB_RED_SIZE, 8,
    PP_GRAPHICS3DATTRIB_DEPTH_SIZE, 0,
    PP_GRAPHICS3DATTRIB_STENCIL_SIZE, 0,
    PP_GRAPHICS3DATTRIB_SAMPLES, 0,
    PP_GRAPHICS3DATTRIB_SAMPLE_BUFFERS, 0,
    PP_GRAPHICS3DATTRIB_WIDTH, position_size_.width(),
    PP_GRAPHICS3DATTRIB_HEIGHT, position_size_.height(),
    PP_GRAPHICS3DATTRIB_NONE,
  };
  context_ = new pp::Graphics3D(this, attributes);
  PP_DCHECK(!context_->is_null());

  // Set viewport window size and clear color bit.
  gles2_if_->ClearColor(context_->pp_resource(), 1, 0, 0, 1);
  gles2_if_->Clear(context_->pp_resource(), GL_COLOR_BUFFER_BIT);
  gles2_if_->Viewport(context_->pp_resource(), 0, 0,
                      position_size_.width(), position_size_.height());

  BindGraphics(*context_);
  AssertNoGLError();

  CreateGLObjects();
}

void VCDemoInstance::Render() {
  PP_DCHECK(!is_painting_);
  is_painting_ = true;
  needs_paint_ = false;
  if (texture_y_) {
    gles2_if_->DrawArrays(context_->pp_resource(), GL_TRIANGLE_STRIP, 0, 4);
  } else {
    gles2_if_->Clear(context_->pp_resource(), GL_COLOR_BUFFER_BIT);
  }
  pp::CompletionCallback cb = callback_factory_.NewCallback(
      &VCDemoInstance::PaintFinished);
  context_->SwapBuffers(cb);
}

void VCDemoInstance::PaintFinished(int32_t result) {
  is_painting_ = false;
  if (needs_paint_)
    Render();
}

GLuint VCDemoInstance::CreateTexture(int32_t width, int32_t height, int unit) {
  GLuint texture_id;
  gles2_if_->GenTextures(context_->pp_resource(), 1, &texture_id);
  AssertNoGLError();
  // Assign parameters.
  gles2_if_->ActiveTexture(context_->pp_resource(), GL_TEXTURE0 + unit);
  gles2_if_->BindTexture(context_->pp_resource(), GL_TEXTURE_2D, texture_id);
  gles2_if_->TexParameteri(
      context_->pp_resource(), GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
      GL_NEAREST);
  gles2_if_->TexParameteri(
      context_->pp_resource(), GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
      GL_NEAREST);
  gles2_if_->TexParameterf(
      context_->pp_resource(), GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,
      GL_CLAMP_TO_EDGE);
  gles2_if_->TexParameterf(
      context_->pp_resource(), GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,
      GL_CLAMP_TO_EDGE);

  // Allocate texture.
  gles2_if_->TexImage2D(
      context_->pp_resource(), GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0,
      GL_LUMINANCE, GL_UNSIGNED_BYTE, NULL);
  AssertNoGLError();
  return texture_id;
}

void VCDemoInstance::CreateGLObjects() {
  // Code and constants for shader.
  static const char kVertexShader[] =
      "varying vec2 v_texCoord;            \n"
      "attribute vec4 a_position;          \n"
      "attribute vec2 a_texCoord;          \n"
      "void main()                         \n"
      "{                                   \n"
      "    v_texCoord = a_texCoord;        \n"
      "    gl_Position = a_position;       \n"
      "}";

  static const char kFragmentShader[] =
      "precision mediump float;                                   \n"
      "varying vec2 v_texCoord;                                   \n"
      "uniform sampler2D y_texture;                               \n"
      "uniform sampler2D u_texture;                               \n"
      "uniform sampler2D v_texture;                               \n"
      "uniform mat3 color_matrix;                                 \n"
      "void main()                                                \n"
      "{                                                          \n"
      "  vec3 yuv;                                                \n"
      "  yuv.x = texture2D(y_texture, v_texCoord).r;              \n"
      "  yuv.y = texture2D(u_texture, v_texCoord).r;              \n"
      "  yuv.z = texture2D(v_texture, v_texCoord).r;              \n"
      "  vec3 rgb = color_matrix * (yuv - vec3(0.0625, 0.5, 0.5));\n"
      "  gl_FragColor = vec4(rgb, 1.0);                           \n"
      "}";

  static const float kColorMatrix[9] = {
    1.1643828125f, 1.1643828125f, 1.1643828125f,
    0.0f, -0.39176171875f, 2.017234375f,
    1.59602734375f, -0.81296875f, 0.0f
  };

  PP_Resource context = context_->pp_resource();

  // Create shader program.
  GLuint program = gles2_if_->CreateProgram(context);
  CreateShader(program, GL_VERTEX_SHADER, kVertexShader, sizeof(kVertexShader));
  CreateShader(
      program, GL_FRAGMENT_SHADER, kFragmentShader, sizeof(kFragmentShader));
  gles2_if_->LinkProgram(context, program);
  gles2_if_->UseProgram(context, program);
  gles2_if_->DeleteProgram(context, program);
  gles2_if_->Uniform1i(
      context, gles2_if_->GetUniformLocation(context, program, "y_texture"), 0);
  gles2_if_->Uniform1i(
      context, gles2_if_->GetUniformLocation(context, program, "u_texture"), 1);
  gles2_if_->Uniform1i(
      context, gles2_if_->GetUniformLocation(context, program, "v_texture"), 2);
  gles2_if_->UniformMatrix3fv(
      context,
      gles2_if_->GetUniformLocation(context, program, "color_matrix"),
      1, GL_FALSE, kColorMatrix);
  AssertNoGLError();

  // Assign vertex positions and texture coordinates to buffers for use in
  // shader program.
  static const float kVertices[] = {
    -1, 1, -1, -1, 1, 1, 1, -1,  // Position coordinates.
    0, 0, 0, 1, 1, 0, 1, 1,  // Texture coordinates.
  };

  GLuint buffer;
  gles2_if_->GenBuffers(context, 1, &buffer);
  gles2_if_->BindBuffer(context, GL_ARRAY_BUFFER, buffer);
  gles2_if_->BufferData(context, GL_ARRAY_BUFFER,
                        sizeof(kVertices), kVertices, GL_STATIC_DRAW);
  AssertNoGLError();
  GLint pos_location = gles2_if_->GetAttribLocation(
      context, program, "a_position");
  GLint tc_location = gles2_if_->GetAttribLocation(
      context, program, "a_texCoord");
  AssertNoGLError();
  gles2_if_->EnableVertexAttribArray(context, pos_location);
  gles2_if_->VertexAttribPointer(context, pos_location, 2,
                                 GL_FLOAT, GL_FALSE, 0, 0);
  gles2_if_->EnableVertexAttribArray(context, tc_location);
  gles2_if_->VertexAttribPointer(
      context, tc_location, 2, GL_FLOAT, GL_FALSE, 0,
      static_cast<float*>(0) + 8);  // Skip position coordinates.
  AssertNoGLError();
}

void VCDemoInstance::CreateShader(
    GLuint program, GLenum type, const char* source, int size) {
  PP_Resource context = context_->pp_resource();
  GLuint shader = gles2_if_->CreateShader(context, type);
  gles2_if_->ShaderSource(context, shader, 1, &source, &size);
  gles2_if_->CompileShader(context, shader);
  gles2_if_->AttachShader(context, program, shader);
  gles2_if_->DeleteShader(context, shader);
}

void VCDemoInstance::CreateYUVTextures() {
  int32_t width = capture_info_.width;
  int32_t height = capture_info_.height;
  texture_y_ = CreateTexture(width, height, 0);

  width /= 2;
  height /= 2;
  texture_u_ = CreateTexture(width, height, 1);
  texture_v_ = CreateTexture(width, height, 2);
}

void VCDemoInstance::Open(const pp::DeviceRef_Dev& device) {
  pp::CompletionCallback callback = callback_factory_.NewCallback(
      &VCDemoInstance::OpenFinished);
  int32_t result = video_capture_.Open(device, capture_info_, 4, callback);
  if (result != PP_OK_COMPLETIONPENDING)
    PostMessage(pp::Var("OpenFailed"));
}

void VCDemoInstance::Stop() {
  if (video_capture_.StopCapture() != PP_OK)
    PostMessage(pp::Var("StopFailed"));
}

void VCDemoInstance::Start() {
  if (video_capture_.StartCapture() != PP_OK)
    PostMessage(pp::Var("StartFailed"));
}

void VCDemoInstance::EnumerateDevicesFinished(
    int32_t result,
  std::vector<pp::DeviceRef_Dev>& devices) {
  if (result == PP_OK) {
    enumerate_devices_.swap(devices);
    std::string device_names = "Enumerate:";
    for (size_t index = 0; index < enumerate_devices_.size(); ++index) {
      pp::Var name = enumerate_devices_[index].GetName();
      PP_DCHECK(name.is_string());

      if (index != 0)
        device_names += kDelimiter;
      device_names += name.AsString();
    }
    PostMessage(pp::Var(device_names));
  } else {
    PostMessage(pp::Var("EnumerationFailed"));
  }
}

void VCDemoInstance::OpenFinished(int32_t result) {
  if (result == PP_OK)
    Start();
  else
    PostMessage(pp::Var("OpenFailed"));
}

// static
void VCDemoInstance::MonitorDeviceChangeCallback(void* user_data,
                                                 uint32_t device_count,
                                                 const PP_Resource devices[]) {
  VCDemoInstance* thiz = static_cast<VCDemoInstance*>(user_data);

  std::string device_names = "Monitor:";
  thiz->monitor_devices_.clear();
  thiz->monitor_devices_.reserve(device_count);
  for (size_t index = 0; index < device_count; ++index) {
    thiz->monitor_devices_.push_back(pp::DeviceRef_Dev(devices[index]));
    pp::Var name = thiz->monitor_devices_.back().GetName();
    PP_DCHECK(name.is_string());

    if (index != 0)
      device_names += kDelimiter;
    device_names += name.AsString();
  }
  thiz->PostMessage(pp::Var(device_names));
}

pp::Instance* VCDemoModule::CreateInstance(PP_Instance instance) {
  return new VCDemoInstance(instance, this);
}

}  // anonymous namespace

namespace pp {
// Factory function for your specialization of the Module object.
Module* CreateModule() {
  return new VCDemoModule();
}
}  // namespace pp