// 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 "chrome/gpu/gpu_video_layer_glx.h"

#include <GL/glew.h>

#include "chrome/common/gpu_messages.h"
#include "chrome/gpu/gpu_thread.h"
#include "chrome/gpu/gpu_view_x.h"

// Handy constants for addressing YV12 data.
static const int kYUVPlanes = 3;
static const int kYPlane = 0;
static const int kUPlane = 1;
static const int kVPlane = 2;

// Buffer size for shader compile errors.
static const unsigned int kErrorSize = 4096;

// Matrix used for the YUV to RGB conversion.
static const float kYUV2RGB[9] = {
  1.f, 0.f, 1.403f,
  1.f, -.344f, -.714f,
  1.f, 1.772f, 0.f,
};

// Texture coordinates mapping the entire texture.
static const float kTextureCoords[8] = {
  0, 0,
  0, 1,
  1, 0,
  1, 1,
};

// Pass-through vertex shader.
static const char kVertexShader[] =
    "varying vec2 interp_tc;\n"
    "\n"
    "attribute vec4 in_pos;\n"
    "attribute vec2 in_tc;\n"
    "\n"
    "void main() {\n"
    "  interp_tc = in_tc;\n"
    "  gl_Position = in_pos;\n"
    "}\n";

// YUV to RGB pixel shader. Loads a pixel from each plane and pass through the
// matrix.
static const char kFragmentShader[] =
    "varying vec2 interp_tc;\n"
    "\n"
    "uniform sampler2D y_tex;\n"
    "uniform sampler2D u_tex;\n"
    "uniform sampler2D v_tex;\n"
    "uniform mat3 yuv2rgb;\n"
    "\n"
    "void main() {\n"
    "  float y = texture2D(y_tex, interp_tc).x;\n"
    "  float u = texture2D(u_tex, interp_tc).r - .5;\n"
    "  float v = texture2D(v_tex, interp_tc).r - .5;\n"
    "  vec3 rgb = yuv2rgb * vec3(y, u, v);\n"
    "  gl_FragColor = vec4(rgb, 1);\n"
    "}\n";

GpuVideoLayerGLX::GpuVideoLayerGLX(GpuViewX* view,
                                   GpuThread* gpu_thread,
                                   int32 routing_id,
                                   const gfx::Size& size)
    : view_(view),
      gpu_thread_(gpu_thread),
      routing_id_(routing_id),
      native_size_(size),
      program_(0) {
  memset(textures_, 0, sizeof(textures_));

  // Load identity vertices.
  gfx::Rect identity(0, 0, 1, 1);
  CalculateVertices(identity.size(), identity, target_vertices_);

  gpu_thread_->AddRoute(routing_id_, this);

  view_->BindContext();  // Must do this before issuing OpenGl.

  glMatrixMode(GL_MODELVIEW);

  // Create 3 textures, one for each plane, and bind them to different
  // texture units.
  glGenTextures(kYUVPlanes, textures_);

  glBindTexture(GL_TEXTURE_2D, textures_[kYPlane]);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

  glBindTexture(GL_TEXTURE_2D, textures_[kUPlane]);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

  glBindTexture(GL_TEXTURE_2D, textures_[kVPlane]);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

  // Create our YUV->RGB shader.
  program_ = glCreateProgram();
  GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER);
  const char* vs_source = kVertexShader;
  int vs_size = sizeof(kVertexShader);
  glShaderSource(vertex_shader, 1, &vs_source, &vs_size);
  glCompileShader(vertex_shader);
  int result = GL_FALSE;
  glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &result);
  if (!result) {
    char log[kErrorSize];
    int len;
    glGetShaderInfoLog(vertex_shader, kErrorSize - 1, &len, log);
    log[kErrorSize - 1] = 0;
    LOG(FATAL) << log;
  }
  glAttachShader(program_, vertex_shader);
  glDeleteShader(vertex_shader);

  GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
  const char* ps_source = kFragmentShader;
  int ps_size = sizeof(kFragmentShader);
  glShaderSource(fragment_shader, 1, &ps_source, &ps_size);
  glCompileShader(fragment_shader);
  result = GL_FALSE;
  glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &result);
  if (!result) {
    char log[kErrorSize];
    int len;
    glGetShaderInfoLog(fragment_shader, kErrorSize - 1, &len, log);
    log[kErrorSize - 1] = 0;
    LOG(FATAL) << log;
  }
  glAttachShader(program_, fragment_shader);
  glDeleteShader(fragment_shader);

  glLinkProgram(program_);
  result = GL_FALSE;
  glGetProgramiv(program_, GL_LINK_STATUS, &result);
  if (!result) {
    char log[kErrorSize];
    int len;
    glGetProgramInfoLog(program_, kErrorSize - 1, &len, log);
    log[kErrorSize - 1] = 0;
    LOG(FATAL) << log;
  }
}

GpuVideoLayerGLX::~GpuVideoLayerGLX() {
  // TODO(scherkus): this seems like a bad idea.. we might be better off with
  // separate Initialize()/Teardown() calls instead.
  view_->BindContext();
  if (program_) {
    glDeleteProgram(program_);
  }

  gpu_thread_->RemoveRoute(routing_id_);
}

void GpuVideoLayerGLX::Render(const gfx::Size& viewport_size) {
  // Nothing to do if we're not visible or have no YUV data.
  if (target_rect_.IsEmpty()) {
    return;
  }

  // Calculate the position of our quad.
  CalculateVertices(viewport_size, target_rect_, target_vertices_);

  // Bind Y, U and V textures to texture units.
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, textures_[kYPlane]);
  glActiveTexture(GL_TEXTURE1);
  glBindTexture(GL_TEXTURE_2D, textures_[kUPlane]);
  glActiveTexture(GL_TEXTURE2);
  glBindTexture(GL_TEXTURE_2D, textures_[kVPlane]);

  // Bind vertex/fragment shader program.
  glUseProgram(program_);

  // Bind parameters.
  glUniform1i(glGetUniformLocation(program_, "y_tex"), 0);
  glUniform1i(glGetUniformLocation(program_, "u_tex"), 1);
  glUniform1i(glGetUniformLocation(program_, "v_tex"), 2);

  int yuv2rgb_location = glGetUniformLocation(program_, "yuv2rgb");
  glUniformMatrix3fv(yuv2rgb_location, 1, GL_TRUE, kYUV2RGB);

  // TODO(scherkus): instead of calculating and loading a geometry each time,
  // we should store a constant geometry in a VBO and use a vertex shader.
  int pos_location = glGetAttribLocation(program_, "in_pos");
  glEnableVertexAttribArray(pos_location);
  glVertexAttribPointer(pos_location, 2, GL_FLOAT, GL_FALSE, 0,
                        target_vertices_);

  int tc_location = glGetAttribLocation(program_, "in_tc");
  glEnableVertexAttribArray(tc_location);
  glVertexAttribPointer(tc_location, 2, GL_FLOAT, GL_FALSE, 0,
                        kTextureCoords);

  // Render!
  glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

  // Reset back to original state.
  glDisableVertexAttribArray(pos_location);
  glDisableVertexAttribArray(tc_location);
  glActiveTexture(GL_TEXTURE0);
  glUseProgram(0);
}

void GpuVideoLayerGLX::OnMessageReceived(const IPC::Message& msg) {
  IPC_BEGIN_MESSAGE_MAP(GpuVideoLayerGLX, msg)
    IPC_MESSAGE_HANDLER(GpuMsg_PaintToVideoLayer, OnPaintToVideoLayer)
  IPC_END_MESSAGE_MAP_EX()
}

void GpuVideoLayerGLX::OnChannelConnected(int32 peer_pid) {
}

void GpuVideoLayerGLX::OnChannelError() {
  // FIXME(brettw) does this mean we aren't getting any more messages and we
  // should delete outselves?
  NOTIMPLEMENTED();
}

void GpuVideoLayerGLX::OnPaintToVideoLayer(base::ProcessId source_process_id,
                                           TransportDIB::Id id,
                                           const gfx::Rect& bitmap_rect) {
  // Assume that somewhere along the line, someone will do width * height * 4
  // with signed numbers. If the maximum value is 2**31, then 2**31 / 4 =
  // 2**29 and floor(sqrt(2**29)) = 23170.
  //
  // TODO(scherkus): |native_size_| is set in constructor, so perhaps this check
  // should be a DCHECK().
  const int width = native_size_.width();
  const int height = native_size_.height();
  const int stride = width;
  if (width > 23170 || height > 23170)
    return;

  TransportDIB* dib = TransportDIB::Map(id);
  if (!dib)
    return;

  // Everything looks good, update our target position and size.
  target_rect_ = bitmap_rect;

  // Perform colour space conversion.
  uint8* planes[kYUVPlanes];
  planes[kYPlane] = reinterpret_cast<uint8*>(dib->memory());
  planes[kUPlane] = planes[kYPlane] + width * height;
  planes[kVPlane] = planes[kUPlane] + ((width * height) >> 2);

  view_->BindContext();  // Must do this before issuing OpenGl.

  // Assume YV12 format.
  for (int i = 0; i < kYUVPlanes; ++i) {
    int plane_width = (i == kYPlane ? width : width / 2);
    int plane_height = (i == kYPlane ? height : height / 2);
    int plane_stride = (i == kYPlane ? stride : stride / 2);

    glActiveTexture(GL_TEXTURE0 + i);
    glBindTexture(GL_TEXTURE_2D, textures_[i]);
    glPixelStorei(GL_UNPACK_ROW_LENGTH, plane_stride);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, plane_width, plane_height, 0,
                 GL_LUMINANCE, GL_UNSIGNED_BYTE, planes[i]);
  }

  // Reset back to original state.
  glActiveTexture(GL_TEXTURE0);
  glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
  glFlush();

  // TODO(scherkus): we may not need to ACK video layer updates at all.
  gpu_thread_->Send(new GpuHostMsg_PaintToVideoLayer_ACK(routing_id_));
}

// static
void GpuVideoLayerGLX::CalculateVertices(const gfx::Size& world,
                                         const gfx::Rect& object,
                                         float* vertices) {
  // Don't forget GL has a flipped Y-axis!
  float width = world.width();
  float height = world.height();

  // Top left.
  vertices[0] = 2.0f * (object.x() / width) - 1.0f;
  vertices[1] = -2.0f * (object.y() / height) + 1.0f;

  // Bottom left.
  vertices[2] = 2.0f * (object.x() / width) - 1.0f;
  vertices[3] = -2.0f * (object.bottom() / height) + 1.0f;

  // Top right.
  vertices[4] = 2.0f * (object.right() / width) - 1.0f;
  vertices[5] = -2.0f * (object.y() / height) + 1.0f;

  // Bottom right.
  vertices[6] = 2.0f * (object.right() / width) - 1.0f;
  vertices[7] = -2.0f * (object.bottom() / height) + 1.0f;
}