// Copyright 2013 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 "media/gpu/rendering_helper.h"

#include <string.h>

#include <algorithm>
#include <memory>
#include <numeric>
#include <vector>

#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/command_line.h"
#include "base/mac/scoped_nsautorelease_pool.h"
#include "base/macros.h"
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/stringize_macros.h"
#include "base/synchronization/waitable_event.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "ui/gl/gl_context.h"
#include "ui/gl/gl_implementation.h"
#include "ui/gl/gl_surface.h"
#include "ui/gl/init/gl_factory.h"

#if defined(OS_WIN)
#include <windows.h>
#endif

#if defined(USE_X11)
#include "ui/gfx/x/x11_types.h"  // nogncheck
#endif

#if defined(ARCH_CPU_X86_FAMILY) && defined(USE_X11)
#include "ui/gl/gl_surface_glx.h"
#define GL_VARIANT_GLX 1
#else
#include "ui/gl/gl_surface_egl.h"
#define GL_VARIANT_EGL 1
#endif

#if defined(USE_OZONE)
#if defined(OS_CHROMEOS)
#include "ui/display/chromeos/display_configurator.h"
#include "ui/display/types/native_display_delegate.h"
#endif  // defined(OS_CHROMEOS)
#include "ui/ozone/public/ozone_platform.h"
#include "ui/platform_window/platform_window.h"
#include "ui/platform_window/platform_window_delegate.h"
#endif  // defined(USE_OZONE)

// Helper for Shader creation.
static void CreateShader(GLuint program,
                         GLenum type,
                         const char* source,
                         int size) {
  GLuint shader = glCreateShader(type);
  glShaderSource(shader, 1, &source, &size);
  glCompileShader(shader);
  int result = GL_FALSE;
  glGetShaderiv(shader, GL_COMPILE_STATUS, &result);
  if (!result) {
    char log[4096];
    glGetShaderInfoLog(shader, arraysize(log), NULL, log);
    LOG(FATAL) << log;
  }
  glAttachShader(program, shader);
  glDeleteShader(shader);
  CHECK_EQ(static_cast<int>(glGetError()), GL_NO_ERROR);
}

namespace media {
namespace {

void WaitForSwapAck(const base::Closure& callback, gfx::SwapResult result) {
  callback.Run();
}

}  // namespace

#if defined(USE_OZONE)

class DisplayConfiguratorObserver : public ui::DisplayConfigurator::Observer {
 public:
  explicit DisplayConfiguratorObserver(base::RunLoop* loop) : loop_(loop) {}
  ~DisplayConfiguratorObserver() override {}

 private:
  // ui::DisplayConfigurator::Observer overrides:
  void OnDisplayModeChanged(
      const ui::DisplayConfigurator::DisplayStateList& outputs) override {
    if (!loop_)
      return;
    loop_->Quit();
    loop_ = nullptr;
  }
  void OnDisplayModeChangeFailed(
      const ui::DisplayConfigurator::DisplayStateList& outputs,
      ui::MultipleDisplayState failed_new_state) override {
    LOG(FATAL) << "Could not configure display";
  }

  base::RunLoop* loop_;

  DISALLOW_COPY_AND_ASSIGN(DisplayConfiguratorObserver);
};

class RenderingHelper::StubOzoneDelegate : public ui::PlatformWindowDelegate {
 public:
  StubOzoneDelegate() : accelerated_widget_(gfx::kNullAcceleratedWidget) {
    platform_window_ = ui::OzonePlatform::GetInstance()->CreatePlatformWindow(
        this, gfx::Rect());
  }
  ~StubOzoneDelegate() override {}

  void OnBoundsChanged(const gfx::Rect& new_bounds) override {}

  void OnDamageRect(const gfx::Rect& damaged_region) override {}

  void DispatchEvent(ui::Event* event) override {}

  void OnCloseRequest() override {}
  void OnClosed() override {}

  void OnWindowStateChanged(ui::PlatformWindowState new_state) override {}

  void OnLostCapture() override{};

  void OnAcceleratedWidgetAvailable(gfx::AcceleratedWidget widget,
                                    float device_pixel_ratio) override {
    accelerated_widget_ = widget;
  }

  void OnAcceleratedWidgetDestroyed() override { NOTREACHED(); }

  void OnActivationChanged(bool active) override{};

  gfx::AcceleratedWidget accelerated_widget() const {
    return accelerated_widget_;
  }

  gfx::Size GetSize() { return platform_window_->GetBounds().size(); }

  ui::PlatformWindow* platform_window() const { return platform_window_.get(); }

 private:
  std::unique_ptr<ui::PlatformWindow> platform_window_;
  gfx::AcceleratedWidget accelerated_widget_;

  DISALLOW_COPY_AND_ASSIGN(StubOzoneDelegate);
};

#endif  // defined(USE_OZONE)

RenderingHelperParams::RenderingHelperParams()
    : rendering_fps(0), warm_up_iterations(0), render_as_thumbnails(false) {}

RenderingHelperParams::RenderingHelperParams(
    const RenderingHelperParams& other) = default;

RenderingHelperParams::~RenderingHelperParams() {}

VideoFrameTexture::VideoFrameTexture(uint32_t texture_target,
                                     uint32_t texture_id,
                                     const base::Closure& no_longer_needed_cb)
    : texture_target_(texture_target),
      texture_id_(texture_id),
      no_longer_needed_cb_(no_longer_needed_cb) {
  DCHECK(!no_longer_needed_cb_.is_null());
}

VideoFrameTexture::~VideoFrameTexture() {
  base::ResetAndReturn(&no_longer_needed_cb_).Run();
}

RenderingHelper::RenderedVideo::RenderedVideo()
    : is_flushing(false), frames_to_drop(0) {}

RenderingHelper::RenderedVideo::RenderedVideo(const RenderedVideo& other) =
    default;

RenderingHelper::RenderedVideo::~RenderedVideo() {}

// static
void RenderingHelper::InitializeOneOff(base::WaitableEvent* done) {
  base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
#if GL_VARIANT_GLX
  cmd_line->AppendSwitchASCII(switches::kUseGL,
                              gl::kGLImplementationDesktopName);
#else
  cmd_line->AppendSwitchASCII(switches::kUseGL, gl::kGLImplementationEGLName);
#endif

  if (!gl::init::InitializeGLOneOff())
    LOG(FATAL) << "Could not initialize GL";
  done->Signal();
}

RenderingHelper::RenderingHelper() : ignore_vsync_(false) {
  window_ = gfx::kNullAcceleratedWidget;
  Clear();
}

RenderingHelper::~RenderingHelper() {
  CHECK_EQ(videos_.size(), 0U) << "Must call UnInitialize before dtor.";
  Clear();
}

void RenderingHelper::Setup() {
#if defined(USE_X11)
  Display* display = gfx::GetXDisplay();
  Screen* screen = DefaultScreenOfDisplay(display);

  CHECK(display);

  XSetWindowAttributes window_attributes;
  memset(&window_attributes, 0, sizeof(window_attributes));
  window_attributes.background_pixel =
      BlackPixel(display, DefaultScreen(display));
  window_attributes.override_redirect = true;
  int depth = DefaultDepth(display, DefaultScreen(display));

  window_ = XCreateWindow(display,
                          DefaultRootWindow(display),
                          0, 0,
                          XWidthOfScreen(screen),
                          XHeightOfScreen(screen),
                          0 /* border width */,
                          depth,
                          CopyFromParent /* class */,
                          CopyFromParent /* visual */,
                          (CWBackPixel | CWOverrideRedirect),
                          &window_attributes);
  XStoreName(display, window_, "VideoDecodeAcceleratorTest");
  XSelectInput(display, window_, ExposureMask);
  XMapWindow(display, window_);
#elif defined(USE_OZONE)
  base::MessageLoop::ScopedNestableTaskAllower nest_loop(
      base::MessageLoop::current());
  base::RunLoop wait_window_resize;

  platform_window_delegate_.reset(new RenderingHelper::StubOzoneDelegate());
  window_ = platform_window_delegate_->accelerated_widget();
  gfx::Size window_size(800, 600);
  // Ignore the vsync provider by default. On ChromeOS this will be set
  // accordingly based on the display configuration.
  ignore_vsync_ = true;
#if defined(OS_CHROMEOS)
  // We hold onto the main loop here to wait for the DisplayController
  // to give us the size of the display so we can create a window of
  // the same size.
  base::RunLoop wait_display_setup;
  DisplayConfiguratorObserver display_setup_observer(&wait_display_setup);
  display_configurator_.reset(new ui::DisplayConfigurator());
  display_configurator_->SetDelegateForTesting(0);
  display_configurator_->AddObserver(&display_setup_observer);
  display_configurator_->Init(
      ui::OzonePlatform::GetInstance()->CreateNativeDisplayDelegate(), true);
  display_configurator_->ForceInitialConfigure(0);
  // Make sure all the display configuration is applied.
  wait_display_setup.Run();
  display_configurator_->RemoveObserver(&display_setup_observer);

  gfx::Size framebuffer_size = display_configurator_->framebuffer_size();
  if (!framebuffer_size.IsEmpty()) {
    window_size = framebuffer_size;
    ignore_vsync_ = false;
  }
#endif
  if (ignore_vsync_)
    DVLOG(1) << "Ignoring vsync provider";

  platform_window_delegate_->platform_window()->SetBounds(
      gfx::Rect(window_size));

  // On Ozone/DRI, platform windows are associated with the physical
  // outputs. Association is achieved by matching the bounds of the
  // window with the origin & modeset of the display output. Until a
  // window is associated with a display output, we cannot get vsync
  // events, because there is no hardware to get events from. Here we
  // wait for the window to resized and therefore associated with
  // display output to be sure that we will get such events.
  wait_window_resize.RunUntilIdle();
#elif !defined(OS_WIN)
#error unknown platform
#endif
}

void RenderingHelper::TearDown() {
#if defined(USE_X11)
  // Destroy resources acquired in Initialize, in reverse-acquisition order.
  if (window_) {
    CHECK(XUnmapWindow(gfx::GetXDisplay(), window_));
    CHECK(XDestroyWindow(gfx::GetXDisplay(), window_));
  }
#elif defined(USE_OZONE)
  platform_window_delegate_.reset();
#if defined(OS_CHROMEOS)
  display_configurator_->PrepareForExit();
  display_configurator_.reset();
#endif
#endif
  window_ = gfx::kNullAcceleratedWidget;
}

void RenderingHelper::Initialize(const RenderingHelperParams& params,
                                 base::WaitableEvent* done) {
#if defined(OS_WIN)
  window_ = CreateWindowEx(
      0, L"Static", L"VideoDecodeAcceleratorTest",
      WS_OVERLAPPEDWINDOW | WS_VISIBLE, 0, 0, GetSystemMetrics(SM_CXSCREEN),
      GetSystemMetrics(SM_CYSCREEN), NULL, NULL, NULL, NULL);
#endif
  CHECK(window_ != gfx::kNullAcceleratedWidget);
  // Use videos_.size() != 0 as a proxy for the class having already been
  // Initialize()'d, and UnInitialize() before continuing.
  if (videos_.size()) {
    base::WaitableEvent done(base::WaitableEvent::ResetPolicy::AUTOMATIC,
                             base::WaitableEvent::InitialState::NOT_SIGNALED);
    UnInitialize(&done);
    done.Wait();
  }

  render_task_.Reset(
      base::Bind(&RenderingHelper::RenderContent, base::Unretained(this)));

  frame_duration_ = params.rendering_fps > 0
                        ? base::TimeDelta::FromSeconds(1) / params.rendering_fps
                        : base::TimeDelta();

  render_as_thumbnails_ = params.render_as_thumbnails;
  task_runner_ = base::ThreadTaskRunnerHandle::Get();

  gl_surface_ = gl::init::CreateViewGLSurface(window_);
#if defined(USE_OZONE)
  gl_surface_->Resize(platform_window_delegate_->GetSize(), 1.f, true);
#endif  // defined(USE_OZONE)
  screen_size_ = gl_surface_->GetSize();

  gl_context_ = gl::init::CreateGLContext(nullptr, gl_surface_.get(),
                                          gl::GLContextAttribs());
  CHECK(gl_context_->MakeCurrent(gl_surface_.get()));

  CHECK_GT(params.window_sizes.size(), 0U);
  videos_.resize(params.window_sizes.size());
  LayoutRenderingAreas(params.window_sizes);

  if (render_as_thumbnails_) {
    CHECK_EQ(videos_.size(), 1U);

    GLint max_texture_size;
    glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_texture_size);
    CHECK_GE(max_texture_size, params.thumbnails_page_size.width());
    CHECK_GE(max_texture_size, params.thumbnails_page_size.height());

    thumbnails_fbo_size_ = params.thumbnails_page_size;
    thumbnail_size_ = params.thumbnail_size;

    glGenFramebuffersEXT(1, &thumbnails_fbo_id_);
    glGenTextures(1, &thumbnails_texture_id_);
    glBindTexture(GL_TEXTURE_2D, thumbnails_texture_id_);
    glTexImage2D(GL_TEXTURE_2D,
                 0,
                 GL_RGB,
                 thumbnails_fbo_size_.width(), thumbnails_fbo_size_.height(),
                 0,
                 GL_RGB,
                 GL_UNSIGNED_SHORT_5_6_5,
                 NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glBindTexture(GL_TEXTURE_2D, 0);

    glBindFramebufferEXT(GL_FRAMEBUFFER, thumbnails_fbo_id_);
    glFramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                              GL_TEXTURE_2D, thumbnails_texture_id_, 0);

    GLenum fb_status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER);
    CHECK(fb_status == GL_FRAMEBUFFER_COMPLETE) << fb_status;
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glBindFramebufferEXT(GL_FRAMEBUFFER,
                         gl_surface_->GetBackingFramebufferObject());
  }

  // These vertices and texture coords. map (0,0) in the texture to the
  // bottom left of the viewport.  Since we get the video frames with the
  // the top left at (0,0) we need to flip the texture y coordinate
  // in the vertex shader for this to be rendered the right way up.
  // In the case of thumbnail rendering we use the same vertex shader
  // to render the FBO the screen, where we do not want this flipping.
  static const float kVertices[] = {
      -1.f, 1.f, -1.f, -1.f, 1.f, 1.f, 1.f, -1.f,
  };
  static const float kTextureCoords[] = {
      0, 1, 0, 0, 1, 1, 1, 0,
  };
  static const char kVertexShader[] =
      STRINGIZE(varying vec2 interp_tc; attribute vec4 in_pos;
                attribute vec2 in_tc; uniform bool tex_flip; void main() {
                  if (tex_flip)
                    interp_tc = vec2(in_tc.x, 1.0 - in_tc.y);
                  else
                    interp_tc = in_tc;
                  gl_Position = in_pos;
                });

#if GL_VARIANT_EGL && !defined(OS_WIN)
  static const char kFragmentShader[] =
      "#extension GL_OES_EGL_image_external : enable\n"
      "precision mediump float;\n"
      "varying vec2 interp_tc;\n"
      "uniform sampler2D tex;\n"
      "#ifdef GL_OES_EGL_image_external\n"
      "uniform samplerExternalOES tex_external;\n"
      "#endif\n"
      "void main() {\n"
      "  vec4 color = texture2D(tex, interp_tc);\n"
      "#ifdef GL_OES_EGL_image_external\n"
      "  color += texture2D(tex_external, interp_tc);\n"
      "#endif\n"
      "  gl_FragColor = color;\n"
      "}\n";
#else
  static const char kFragmentShader[] =
      "#ifdef GL_ES\n"
      "precision mediump float;\n"
      "#endif\n"
      "varying vec2 interp_tc;\n"
      "uniform sampler2D tex;\n"
      "void main() {\n"
      "  gl_FragColor = texture2D(tex, interp_tc);\n"
      "}\n";
#endif
  program_ = glCreateProgram();
  CreateShader(program_, GL_VERTEX_SHADER, kVertexShader,
               arraysize(kVertexShader));
  CreateShader(program_, GL_FRAGMENT_SHADER, kFragmentShader,
               arraysize(kFragmentShader));
  glLinkProgram(program_);
  int result = GL_FALSE;
  glGetProgramiv(program_, GL_LINK_STATUS, &result);
  if (!result) {
    char log[4096];
    glGetShaderInfoLog(program_, arraysize(log), NULL, log);
    LOG(FATAL) << log;
  }
  glUseProgram(program_);
  glDeleteProgram(program_);

  glUniform1i(glGetUniformLocation(program_, "tex_flip"), 0);
  glUniform1i(glGetUniformLocation(program_, "tex"), 0);
  GLint tex_external = glGetUniformLocation(program_, "tex_external");
  if (tex_external != -1) {
    glUniform1i(tex_external, 1);
  }
  int pos_location = glGetAttribLocation(program_, "in_pos");
  glEnableVertexAttribArray(pos_location);
  glVertexAttribPointer(pos_location, 2, GL_FLOAT, GL_FALSE, 0, kVertices);
  int tc_location = glGetAttribLocation(program_, "in_tc");
  glEnableVertexAttribArray(tc_location);
  glVertexAttribPointer(tc_location, 2, GL_FLOAT, GL_FALSE, 0, kTextureCoords);

  if (!frame_duration_.is_zero()) {
    int warm_up_iterations = params.warm_up_iterations;
#if defined(USE_OZONE)
    // On Ozone the VSyncProvider can't provide a vsync interval until
    // we render at least a frame, so we warm up with at least one
    // frame.
    // On top of this, we want to make sure all the buffers backing
    // the GL surface are cleared, otherwise we can see the previous
    // test's last frames, so we set warm up iterations to 2, to clear
    // the front and back buffers.
    warm_up_iterations = std::max(2, warm_up_iterations);
#endif
    WarmUpRendering(warm_up_iterations);
  }

  // It's safe to use Unretained here since |rendering_thread_| will be stopped
  // in VideoDecodeAcceleratorTest.TearDown(), while the |rendering_helper_| is
  // a member of that class. (See video_decode_accelerator_unittest.cc.)
  gfx::VSyncProvider* vsync_provider = gl_surface_->GetVSyncProvider();

  // VSync providers rely on the underlying CRTC to get the timing. In headless
  // mode the surface isn't associated with a CRTC so the vsync provider can not
  // get the timing, meaning it will not call UpdateVsyncParameters() ever.
  if (!ignore_vsync_ && vsync_provider && !frame_duration_.is_zero()) {
    vsync_provider->GetVSyncParameters(base::Bind(
        &RenderingHelper::UpdateVSyncParameters, base::Unretained(this), done));
  } else {
    done->Signal();
  }
}

// The rendering for the first few frames is slow (e.g., 100ms on Peach Pit).
// This affects the numbers measured in the performance test. We try to render
// several frames here to warm up the rendering.
void RenderingHelper::WarmUpRendering(int warm_up_iterations) {
  unsigned int texture_id;
  std::unique_ptr<GLubyte[]> emptyData(
      new GLubyte[screen_size_.GetArea() * 2]());
  glGenTextures(1, &texture_id);
  glBindTexture(GL_TEXTURE_2D, texture_id);
  glTexImage2D(GL_TEXTURE_2D,
               0,
               GL_RGB,
               screen_size_.width(), screen_size_.height(),
               0,
               GL_RGB,
               GL_UNSIGNED_SHORT_5_6_5,
               emptyData.get());
  for (int i = 0; i < warm_up_iterations; ++i) {
    RenderTexture(GL_TEXTURE_2D, texture_id);

    // Need to allow nestable tasks since WarmUpRendering() is called from
    // within another task on the renderer thread.
    base::MessageLoop::ScopedNestableTaskAllower allow(
        base::MessageLoop::current());
    base::RunLoop wait_for_swap_ack;
    if (gl_surface_->SupportsAsyncSwap()) {
      gl_surface_->SwapBuffersAsync(
          base::Bind(&WaitForSwapAck, wait_for_swap_ack.QuitClosure()));
      wait_for_swap_ack.Run();
    } else {
      gl_surface_->SwapBuffers();
    }
  }
  glDeleteTextures(1, &texture_id);
}

void RenderingHelper::UnInitialize(base::WaitableEvent* done) {
  CHECK(task_runner_->BelongsToCurrentThread());

  render_task_.Cancel();

  if (render_as_thumbnails_) {
    glDeleteTextures(1, &thumbnails_texture_id_);
    glDeleteFramebuffersEXT(1, &thumbnails_fbo_id_);
  }

  gl_context_->ReleaseCurrent(gl_surface_.get());
  gl_context_ = NULL;
  gl_surface_ = NULL;

  Clear();

#if defined(OS_WIN)
  if (window_)
    DestroyWindow(window_);
  window_ = gfx::kNullAcceleratedWidget;
#endif

  done->Signal();
}

void RenderingHelper::CreateTexture(uint32_t texture_target,
                                    uint32_t* texture_id,
                                    const gfx::Size& size,
                                    base::WaitableEvent* done) {
  if (!task_runner_->BelongsToCurrentThread()) {
    task_runner_->PostTask(
        FROM_HERE,
        base::Bind(&RenderingHelper::CreateTexture, base::Unretained(this),
                   texture_target, texture_id, size, done));
    return;
  }
  glGenTextures(1, texture_id);
  glBindTexture(texture_target, *texture_id);
  if (texture_target == GL_TEXTURE_2D) {
    glTexImage2D(GL_TEXTURE_2D,
                 0,
                 GL_RGBA,
                 size.width(), size.height(),
                 0,
                 GL_RGBA,
                 GL_UNSIGNED_BYTE,
                 NULL);
  }
  glTexParameteri(texture_target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(texture_target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  // OpenGLES2.0.25 section 3.8.2 requires CLAMP_TO_EDGE for NPOT textures.
  glTexParameteri(texture_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(texture_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  CHECK_EQ(static_cast<int>(glGetError()), GL_NO_ERROR);
  done->Signal();
}

// Helper function to set GL viewport.
static inline void GLSetViewPort(const gfx::Rect& area) {
  glViewport(area.x(), area.y(), area.width(), area.height());
  glScissor(area.x(), area.y(), area.width(), area.height());
}

void RenderingHelper::RenderThumbnail(uint32_t texture_target,
                                      uint32_t texture_id) {
  CHECK(task_runner_->BelongsToCurrentThread());
  const int width = thumbnail_size_.width();
  const int height = thumbnail_size_.height();
  const int thumbnails_in_row = thumbnails_fbo_size_.width() / width;
  const int thumbnails_in_column = thumbnails_fbo_size_.height() / height;
  const int row = (frame_count_ / thumbnails_in_row) % thumbnails_in_column;
  const int col = frame_count_ % thumbnails_in_row;

  gfx::Rect area(col * width, row * height, width, height);

  glUniform1i(glGetUniformLocation(program_, "tex_flip"), 0);
  glBindFramebufferEXT(GL_FRAMEBUFFER, thumbnails_fbo_id_);
  GLSetViewPort(area);
  RenderTexture(texture_target, texture_id);
  glBindFramebufferEXT(GL_FRAMEBUFFER,
                       gl_surface_->GetBackingFramebufferObject());

  // Need to flush the GL commands before we return the tnumbnail texture to
  // the decoder.
  glFlush();
  ++frame_count_;
}

void RenderingHelper::QueueVideoFrame(
    size_t window_id,
    scoped_refptr<VideoFrameTexture> video_frame) {
  CHECK(task_runner_->BelongsToCurrentThread());
  RenderedVideo* video = &videos_[window_id];
  DCHECK(!video->is_flushing);

  video->pending_frames.push(video_frame);

  if (video->frames_to_drop > 0 && video->pending_frames.size() > 1) {
    --video->frames_to_drop;
    video->pending_frames.pop();
  }

  // Schedules the first RenderContent() if need.
  if (scheduled_render_time_.is_null()) {
    scheduled_render_time_ = base::TimeTicks::Now();
    task_runner_->PostTask(FROM_HERE, render_task_.callback());
  }
}

void RenderingHelper::RenderTexture(uint32_t texture_target,
                                    uint32_t texture_id) {
  // The ExternalOES sampler is bound to GL_TEXTURE1 and the Texture2D sampler
  // is bound to GL_TEXTURE0.
  if (texture_target == GL_TEXTURE_2D) {
    glActiveTexture(GL_TEXTURE0 + 0);
  } else if (texture_target == GL_TEXTURE_EXTERNAL_OES) {
    glActiveTexture(GL_TEXTURE0 + 1);
  }
  glBindTexture(texture_target, texture_id);
  glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
  glBindTexture(texture_target, 0);
  CHECK_EQ(static_cast<int>(glGetError()), GL_NO_ERROR);
}

void RenderingHelper::DeleteTexture(uint32_t texture_id) {
  CHECK(task_runner_->BelongsToCurrentThread());
  glDeleteTextures(1, &texture_id);
  CHECK_EQ(static_cast<int>(glGetError()), GL_NO_ERROR);
}

gl::GLContext* RenderingHelper::GetGLContext() {
  return gl_context_.get();
}

void* RenderingHelper::GetGLDisplay() {
  return gl_surface_->GetDisplay();
}

void RenderingHelper::Clear() {
  videos_.clear();
  task_runner_ = nullptr;
  gl_context_ = NULL;
  gl_surface_ = NULL;

  render_as_thumbnails_ = false;
  frame_count_ = 0;
  thumbnails_fbo_id_ = 0;
  thumbnails_texture_id_ = 0;
}

void RenderingHelper::GetThumbnailsAsRGB(std::vector<unsigned char>* rgb,
                                         bool* alpha_solid,
                                         base::WaitableEvent* done) {
  CHECK(render_as_thumbnails_);

  const size_t num_pixels = thumbnails_fbo_size_.GetArea();
  std::vector<unsigned char> rgba;
  rgba.resize(num_pixels * 4);
  glBindFramebufferEXT(GL_FRAMEBUFFER, thumbnails_fbo_id_);
  glPixelStorei(GL_PACK_ALIGNMENT, 1);
  // We can only count on GL_RGBA/GL_UNSIGNED_BYTE support.
  glReadPixels(0, 0,
               thumbnails_fbo_size_.width(), thumbnails_fbo_size_.height(),
               GL_RGBA,
               GL_UNSIGNED_BYTE,
               &rgba[0]);
  glBindFramebufferEXT(GL_FRAMEBUFFER,
                       gl_surface_->GetBackingFramebufferObject());
  rgb->resize(num_pixels * 3);
  // Drop the alpha channel, but check as we go that it is all 0xff.
  bool solid = true;
  unsigned char* rgb_ptr = &((*rgb)[0]);
  unsigned char* rgba_ptr = &rgba[0];
  for (size_t i = 0; i < num_pixels; ++i) {
    *rgb_ptr++ = *rgba_ptr++;
    *rgb_ptr++ = *rgba_ptr++;
    *rgb_ptr++ = *rgba_ptr++;
    solid = solid && (*rgba_ptr == 0xff);
    rgba_ptr++;
  }
  *alpha_solid = solid;

  done->Signal();
}

void RenderingHelper::Flush(size_t window_id) {
  videos_[window_id].is_flushing = true;
}

void RenderingHelper::RenderContent() {
  CHECK(task_runner_->BelongsToCurrentThread());

  // Update the VSync params.
  //
  // It's safe to use Unretained here since |rendering_thread_| will be stopped
  // in VideoDecodeAcceleratorTest.TearDown(), while the |rendering_helper_| is
  // a member of that class. (See video_decode_accelerator_unittest.cc.)
  gfx::VSyncProvider* vsync_provider = gl_surface_->GetVSyncProvider();
  if (vsync_provider && !ignore_vsync_) {
    vsync_provider->GetVSyncParameters(base::Bind(
        &RenderingHelper::UpdateVSyncParameters, base::Unretained(this),
        static_cast<base::WaitableEvent*>(NULL)));
  }

  int tex_flip = 1;
#if defined(USE_OZONE)
  // Ozone surfaceless renders flipped from normal GL, so there's no need to
  // do an extra flip.
  tex_flip = 0;
#endif  // defined(USE_OZONE)
  glUniform1i(glGetUniformLocation(program_, "tex_flip"), tex_flip);

  // Frames that will be returned to the client (via the no_longer_needed_cb)
  // after this vector falls out of scope at the end of this method. We need
  // to keep references to them until after SwapBuffers() call below.
  std::vector<scoped_refptr<VideoFrameTexture>> frames_to_be_returned;
  bool need_swap_buffer = false;
  if (render_as_thumbnails_) {
    // In render_as_thumbnails_ mode, we render the FBO content on the
    // screen instead of the decoded textures.
    GLSetViewPort(videos_[0].render_area);
    RenderTexture(GL_TEXTURE_2D, thumbnails_texture_id_);
    need_swap_buffer = true;
  } else {
    for (RenderedVideo& video : videos_) {
      if (video.pending_frames.empty())
        continue;
      need_swap_buffer = true;
      scoped_refptr<VideoFrameTexture> frame = video.pending_frames.front();
      GLSetViewPort(video.render_area);
      RenderTexture(frame->texture_target(), frame->texture_id());

      if (video.pending_frames.size() > 1 || video.is_flushing) {
        frames_to_be_returned.push_back(video.pending_frames.front());
        video.pending_frames.pop();
      } else {
        ++video.frames_to_drop;
      }
    }
  }

  base::Closure schedule_frame = base::Bind(
      &RenderingHelper::ScheduleNextRenderContent, base::Unretained(this));
  if (!need_swap_buffer) {
    schedule_frame.Run();
    return;
  }

  if (gl_surface_->SupportsAsyncSwap()) {
    gl_surface_->SwapBuffersAsync(base::Bind(&WaitForSwapAck, schedule_frame));
  } else {
    gl_surface_->SwapBuffers();
    ScheduleNextRenderContent();
  }
}

// Helper function for the LayoutRenderingAreas(). The |lengths| are the
// heights(widths) of the rows(columns). It scales the elements in
// |lengths| proportionally so that the sum of them equal to |total_length|.
// It also outputs the coordinates of the rows(columns) to |offsets|.
static void ScaleAndCalculateOffsets(std::vector<int>* lengths,
                                     std::vector<int>* offsets,
                                     int total_length) {
  int sum = std::accumulate(lengths->begin(), lengths->end(), 0);
  for (size_t i = 0; i < lengths->size(); ++i) {
    lengths->at(i) = lengths->at(i) * total_length / sum;
    offsets->at(i) = (i == 0) ? 0 : offsets->at(i - 1) + lengths->at(i - 1);
  }
}

void RenderingHelper::LayoutRenderingAreas(
    const std::vector<gfx::Size>& window_sizes) {
  // Find the number of colums and rows.
  // The smallest n * n or n * (n + 1) > number of windows.
  size_t cols = sqrt(videos_.size() - 1) + 1;
  size_t rows = (videos_.size() + cols - 1) / cols;

  // Find the widths and heights of the grid.
  std::vector<int> widths(cols);
  std::vector<int> heights(rows);
  std::vector<int> offset_x(cols);
  std::vector<int> offset_y(rows);

  for (size_t i = 0; i < window_sizes.size(); ++i) {
    const gfx::Size& size = window_sizes[i];
    widths[i % cols] = std::max(widths[i % cols], size.width());
    heights[i / cols] = std::max(heights[i / cols], size.height());
  }

  ScaleAndCalculateOffsets(&widths, &offset_x, screen_size_.width());
  ScaleAndCalculateOffsets(&heights, &offset_y, screen_size_.height());

  // Put each render_area_ in the center of each cell.
  for (size_t i = 0; i < window_sizes.size(); ++i) {
    const gfx::Size& size = window_sizes[i];
    float scale =
        std::min(static_cast<float>(widths[i % cols]) / size.width(),
                 static_cast<float>(heights[i / cols]) / size.height());

    // Don't scale up the texture.
    scale = std::min(1.0f, scale);

    size_t w = scale * size.width();
    size_t h = scale * size.height();
    size_t x = offset_x[i % cols] + (widths[i % cols] - w) / 2;
    size_t y = offset_y[i / cols] + (heights[i / cols] - h) / 2;
    videos_[i].render_area = gfx::Rect(x, y, w, h);
  }
}

void RenderingHelper::UpdateVSyncParameters(base::WaitableEvent* done,
                                            const base::TimeTicks timebase,
                                            const base::TimeDelta interval) {
  vsync_timebase_ = timebase;
  vsync_interval_ = interval;

  if (done)
    done->Signal();
}

void RenderingHelper::DropOneFrameForAllVideos() {
  for (RenderedVideo& video : videos_) {
    if (video.pending_frames.empty())
      continue;

    if (video.pending_frames.size() > 1 || video.is_flushing) {
      video.pending_frames.pop();
    } else {
      ++video.frames_to_drop;
    }
  }
}

void RenderingHelper::ScheduleNextRenderContent() {
  scheduled_render_time_ += frame_duration_;
  base::TimeTicks now = base::TimeTicks::Now();
  base::TimeTicks target;

  if (vsync_interval_.is_zero()) {
    target = std::max(now, scheduled_render_time_);
  } else {
    // Schedules the next RenderContent() at latest VSYNC before the
    // |scheduled_render_time_|.
    target = std::max(now + vsync_interval_, scheduled_render_time_);

    int64_t intervals = (target - vsync_timebase_) / vsync_interval_;
    target = vsync_timebase_ + intervals * vsync_interval_;
  }

  // When the rendering falls behind, drops frames.
  while (scheduled_render_time_ < target) {
    scheduled_render_time_ += frame_duration_;
    DropOneFrameForAllVideos();
  }

  task_runner_->PostDelayedTask(FROM_HERE, render_task_.callback(),
                                target - now);
}
}  // namespace media
