// Copyright 2018 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 "ui/gl/gl_image_io_surface_egl.h"

#include "base/bind.h"
#include "base/callback_helpers.h"
#include "ui/gl/gl_context.h"
#include "ui/gl/gl_surface_egl.h"
#include "ui/gl/scoped_binders.h"
#include "ui/gl/yuv_to_rgb_converter.h"

// Enums for the EGL_ANGLE_iosurface_client_buffer extension
#define EGL_IOSURFACE_ANGLE 0x3454
#define EGL_IOSURFACE_PLANE_ANGLE 0x345A
#define EGL_TEXTURE_RECTANGLE_ANGLE 0x345B
#define EGL_TEXTURE_TYPE_ANGLE 0x345C
#define EGL_TEXTURE_INTERNAL_FORMAT_ANGLE 0x345D

namespace gl {

namespace {

struct InternalFormatType {
  InternalFormatType(GLenum format, GLenum type) : format(format), type(type) {}

  GLenum format;
  GLenum type;
};

// Convert a gfx::BufferFormat to a (internal format, type) combination from the
// EGL_ANGLE_iosurface_client_buffer extension spec.
InternalFormatType BufferFormatToInternalFormatType(gfx::BufferFormat format) {
  switch (format) {
    case gfx::BufferFormat::R_8:
      return {GL_RED, GL_UNSIGNED_BYTE};
    case gfx::BufferFormat::R_16:
      return {GL_RED_INTEGER, GL_UNSIGNED_SHORT};
    case gfx::BufferFormat::RG_88:
      return {GL_RG, GL_UNSIGNED_BYTE};
    case gfx::BufferFormat::BGRA_8888:
    case gfx::BufferFormat::BGRX_8888:  // See https://crbug.com/595948.
    case gfx::BufferFormat::RGBA_8888:
      return {GL_BGRA_EXT, GL_UNSIGNED_BYTE};
    case gfx::BufferFormat::RGBA_F16:
      return {GL_RGBA, GL_HALF_FLOAT};
    case gfx::BufferFormat::UYVY_422:
    case gfx::BufferFormat::YUV_420_BIPLANAR:
    case gfx::BufferFormat::BGRX_1010102:
      NOTIMPLEMENTED();
      return {GL_NONE, GL_NONE};
    case gfx::BufferFormat::ATC:
    case gfx::BufferFormat::ATCIA:
    case gfx::BufferFormat::DXT1:
    case gfx::BufferFormat::DXT5:
    case gfx::BufferFormat::ETC1:
    case gfx::BufferFormat::BGR_565:
    case gfx::BufferFormat::RGBA_4444:
    case gfx::BufferFormat::RGBX_8888:
    case gfx::BufferFormat::RGBX_1010102:
    case gfx::BufferFormat::YVU_420:
      NOTREACHED();
      return {GL_NONE, GL_NONE};
  }

  NOTREACHED();
  return {GL_NONE, GL_NONE};
}

}  // anonymous namespace

GLImageIOSurfaceEGL::GLImageIOSurfaceEGL(const gfx::Size& size,
                                         unsigned internalformat)
    : GLImageIOSurface(size, internalformat),
      display_(GLSurfaceEGL::GetHardwareDisplay()),
      pbuffer_(EGL_NO_SURFACE),
      dummy_config_(nullptr),
      texture_bound_(false) {
  DCHECK(display_ != EGL_NO_DISPLAY);

  // When creating a pbuffer we need to supply an EGLConfig. On ANGLE and
  // Swiftshader on Mac, there's only ever one config. Query it from EGL.
  EGLint numConfigs = 0;
  EGLBoolean result =
      eglChooseConfig(display_, nullptr, &dummy_config_, 1, &numConfigs);
  DCHECK(result == EGL_TRUE);
  DCHECK(numConfigs = 1);
  DCHECK(dummy_config_ != nullptr);
}

GLImageIOSurfaceEGL::~GLImageIOSurfaceEGL() {
  if (pbuffer_ != EGL_NO_SURFACE) {
    EGLBoolean result = eglDestroySurface(display_, pbuffer_);
    DCHECK(result == EGL_TRUE);
  }
}

void GLImageIOSurfaceEGL::ReleaseTexImage(unsigned target) {
  DCHECK(target == GL_TEXTURE_RECTANGLE_ARB);
  DCHECK(pbuffer_ != EGL_NO_SURFACE);
  DCHECK(texture_bound_);

  EGLBoolean result = eglReleaseTexImage(display_, pbuffer_, EGL_BACK_BUFFER);
  DCHECK(result == EGL_TRUE);
  texture_bound_ = false;
}

bool GLImageIOSurfaceEGL::BindTexImageImpl(unsigned internalformat) {
  // TODO(cwallez@chromium.org): internalformat is used by Blink's
  // DrawingBuffer::SetupRGBEmulationForBlitFramebuffer to bind an RGBA
  // IOSurface as RGB. We should support this.
  if (internalformat != 0) {
    LOG(ERROR) << "GLImageIOSurfaceEGL doesn't support binding with a custom "
                  "internal format yet.";
    return false;
  }

  // Create the pbuffer representing this IOSurface lazily because we don't know
  // in the constructor if we're going to be used to bind plane 0 to a texture,
  // or to transform YUV to RGB.
  if (pbuffer_ == EGL_NO_SURFACE) {
    InternalFormatType formatType = BufferFormatToInternalFormatType(format_);

    // clang-format off
    const EGLint attribs[] = {
      EGL_WIDTH,                         size_.width(),
      EGL_HEIGHT,                        size_.height(),
      EGL_IOSURFACE_PLANE_ANGLE,         0,
      EGL_TEXTURE_TARGET,                EGL_TEXTURE_RECTANGLE_ANGLE,
      EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, formatType.format,
      EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
      EGL_TEXTURE_TYPE_ANGLE,            formatType.type,
      EGL_NONE,                          EGL_NONE,
    };
    // clang-format on

    pbuffer_ = eglCreatePbufferFromClientBuffer(display_, EGL_IOSURFACE_ANGLE,
        io_surface_.get(), dummy_config_, attribs);
    if (pbuffer_ == EGL_NO_SURFACE) {
      LOG(ERROR) << "eglCreatePbufferFromClientBuffer failed, EGL error is "
                 << eglGetError();
      return false;
    }
  }

  DCHECK(!texture_bound_);
  EGLBoolean result = eglBindTexImage(display_, pbuffer_, EGL_BACK_BUFFER);

  if (result != EGL_TRUE) {
    LOG(ERROR) << "eglBindTexImage failed, EGL error is "
               << eglGetError();
    return false;
  }

  texture_bound_ = true;
  return true;
}

bool GLImageIOSurfaceEGL::CopyTexImage(unsigned target) {
  DCHECK(thread_checker_.CalledOnValidThread());

  if (format_ != gfx::BufferFormat::YUV_420_BIPLANAR)
    return false;

  GLContext* gl_context = GLContext::GetCurrent();
  DCHECK(gl_context);

  YUVToRGBConverter* yuv_to_rgb_converter =
      gl_context->GetYUVToRGBConverter(color_space_for_yuv_to_rgb_);
  DCHECK(yuv_to_rgb_converter);

  // Note that state restoration is done explicitly instead of scoped binders to
  // avoid https://crbug.com/601729.
  GLint rgb_texture = 0;
  GLenum target_getter = 0;
  switch (target) {
    case GL_TEXTURE_2D:
      target_getter = GL_TEXTURE_BINDING_2D;
      break;
    case GL_TEXTURE_CUBE_MAP:
      target_getter = GL_TEXTURE_BINDING_CUBE_MAP;
      break;
    case GL_TEXTURE_EXTERNAL_OES:
      target_getter = GL_TEXTURE_BINDING_EXTERNAL_OES;
      break;
    case GL_TEXTURE_RECTANGLE_ARB:
      target_getter = GL_TEXTURE_BINDING_RECTANGLE_ARB;
      break;
    default:
      NOTIMPLEMENTED() << " Target not supported.";
      return false;
  }

  EGLSurface y_surface = EGL_NO_SURFACE;
  EGLSurface uv_surface = EGL_NO_SURFACE;

  EGLSurface* y_surface_ptr = &y_surface;
  EGLSurface* uv_surface_ptr = &uv_surface;
  glGetIntegerv(target_getter, &rgb_texture);
  base::ScopedClosureRunner destroy_resources_runner(
      base::BindOnce(base::RetainBlock(^{
        if (*y_surface_ptr != EGL_NO_SURFACE) {
          EGLBoolean result =
              eglReleaseTexImage(display_, *y_surface_ptr, EGL_BACK_BUFFER);
          DCHECK(result == EGL_TRUE);
          result = eglDestroySurface(display_, *y_surface_ptr);
          DCHECK(result == EGL_TRUE);
        }
        if (*uv_surface_ptr != EGL_NO_SURFACE) {
          EGLBoolean result =
              eglReleaseTexImage(display_, *uv_surface_ptr, EGL_BACK_BUFFER);
          DCHECK(result == EGL_TRUE);
          result = eglDestroySurface(display_, *uv_surface_ptr);
          DCHECK(result == EGL_TRUE);
        }
        glBindTexture(target, rgb_texture);
      })));

  glBindTexture(GL_TEXTURE_RECTANGLE_ARB, yuv_to_rgb_converter->y_texture());
  if (glGetError() != GL_NO_ERROR) {
    LOG(ERROR) << "Can't bind Y texture";
    return false;
  }

  // clang-format off
  const EGLint yAttribs[] = {
    EGL_WIDTH,                         size_.width(),
    EGL_HEIGHT,                        size_.height(),
    EGL_IOSURFACE_PLANE_ANGLE,         0,
    EGL_TEXTURE_TARGET,                EGL_TEXTURE_RECTANGLE_ANGLE,
    EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_RED,
    EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
    EGL_TEXTURE_TYPE_ANGLE,            GL_UNSIGNED_BYTE,
    EGL_NONE,                          EGL_NONE,
  };
  // clang-format on

  y_surface = eglCreatePbufferFromClientBuffer(display_, EGL_IOSURFACE_ANGLE,
                                               io_surface_.get(), dummy_config_,
                                               yAttribs);
  if (y_surface == EGL_NO_SURFACE) {
    LOG(ERROR) << "eglCreatePbufferFromClientBuffer failed, EGL error is "
               << eglGetError();
    return false;
  }

  EGLBoolean result = eglBindTexImage(display_, y_surface, EGL_BACK_BUFFER);
  if (result != EGL_TRUE) {
    LOG(ERROR) << "eglBindTexImage failed, EGL error is " << eglGetError();
    return false;
  }

  glBindTexture(GL_TEXTURE_RECTANGLE_ARB, yuv_to_rgb_converter->uv_texture());
  if (glGetError() != GL_NO_ERROR) {
    LOG(ERROR) << "Can't bind UV texture";
    return false;
  }

  // clang-format off
  const EGLint uvAttribs[] = {
    EGL_WIDTH,                         size_.width() / 2,
    EGL_HEIGHT,                        size_.height() / 2,
    EGL_IOSURFACE_PLANE_ANGLE,         1,
    EGL_TEXTURE_TARGET,                EGL_TEXTURE_RECTANGLE_ANGLE,
    EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_RG,
    EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
    EGL_TEXTURE_TYPE_ANGLE,            GL_UNSIGNED_BYTE,
    EGL_NONE,                          EGL_NONE,
  };
  // clang-format on

  uv_surface = eglCreatePbufferFromClientBuffer(display_, EGL_IOSURFACE_ANGLE,
                                                io_surface_.get(),
                                                dummy_config_, uvAttribs);
  if (uv_surface == EGL_NO_SURFACE) {
    LOG(ERROR) << "eglCreatePbufferFromClientBuffer failed, EGL error is "
               << eglGetError();
    return false;
  }

  result = eglBindTexImage(display_, uv_surface, EGL_BACK_BUFFER);
  if (result != EGL_TRUE) {
    LOG(ERROR) << "eglBindTexImage failed, EGL error is " << eglGetError();
    return false;
  }

  yuv_to_rgb_converter->CopyYUV420ToRGB(target, size_, rgb_texture);
  if (glGetError() != GL_NO_ERROR) {
    LOG(ERROR) << "Failed converting from YUV to RGB";
    return false;
  }

  return true;
}

}  // namespace gl
