// Copyright 2018 The Chromium Embedded Framework 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 "cef/libcef/browser/gpu/external_texture_manager.h"

#include "gpu/command_buffer/service/service_utils.h"
#include "third_party/khronos/EGL/egl.h"
#include "third_party/khronos/EGL/eglext.h"
#include "ui/gl/gl_bindings.h"
#include "ui/gl/gl_context_egl.h"
#include "ui/gl/gl_image.h"
#include "ui/gl/gl_surface_egl.h"
#include "ui/gl/init/gl_factory.h"

#if defined(OS_WIN)
#include <d3d11_1.h>
#include "ui/gl/gl_angle_util_win.h"
#include "ui/gl/gl_image_dxgi.h"
#endif

#ifndef EGL_ANGLE_d3d_texture_client_buffer
#define EGL_ANGLE_d3d_texture_client_buffer 1
#define EGL_D3D_TEXTURE_ANGLE 0x33A3
#endif

namespace gpu {
namespace gles2 {

namespace {

#if defined(OS_WIN)

class GLImageDXGISharedHandle : public gl::GLImageDXGI {
 public:
  GLImageDXGISharedHandle(const gfx::Size& size)
      : GLImageDXGI(size, nullptr),
        handle_((HANDLE)0),
        surface_(EGL_NO_SURFACE),
        texture_id_(0) {}

  void* share_handle() const { return handle_; }

  bool Initialize() {
    Microsoft::WRL::ComPtr<ID3D11Device> d3d11_device =
        gl::QueryD3D11DeviceObjectFromANGLE();
    if (!d3d11_device) {
      return false;
    }

    Microsoft::WRL::ComPtr<ID3D11Device1> d3d11_device1;
    HRESULT hr = d3d11_device.As(&d3d11_device1);
    if (FAILED(hr)) {
      return false;
    }

    D3D11_TEXTURE2D_DESC td = {0};
    td.ArraySize = 1;
    td.CPUAccessFlags = 0;
    td.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
    td.Width = GetSize().width();
    td.Height = GetSize().height();
    td.MipLevels = 1;
    td.SampleDesc.Count = 1;
    td.SampleDesc.Quality = 0;
    td.Usage = D3D11_USAGE_DEFAULT;
    td.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
    td.MiscFlags = 0;

    hr = d3d11_device1->CreateTexture2D(&td, nullptr, texture_.GetAddressOf());
    if (FAILED(hr)) {
      return false;
    }

    // Create a staging texture that will not be a render-target, but will be
    // shared.  We could make the render target directly shareable, but the
    // staged copy is safer for synchronization and less problematic
    td.BindFlags = D3D11_BIND_SHADER_RESOURCE;
    td.MiscFlags = D3D11_RESOURCE_MISC_SHARED;
    hr = d3d11_device1->CreateTexture2D(&td, nullptr,
                                        staging_texture_.GetAddressOf());
    if (FAILED(hr)) {
      return false;
    }

    // If using a staging texture ... then we need the shared handle for that
    Microsoft::WRL::ComPtr<IDXGIResource> dxgi_res;
    if (staging_texture_.Get()) {
      hr = staging_texture_.As(&dxgi_res);
    } else {
      hr = texture_.As(&dxgi_res);
    }
    if (SUCCEEDED(hr)) {
      dxgi_res->GetSharedHandle(&handle_);
    }

    return true;
  }

  void Lock() {
    // In the future a keyed mutex could be utilized here.
  }

  void Unlock() {
    if (staging_texture_.Get() && texture_.Get()) {
      Microsoft::WRL::ComPtr<ID3D11Device> d3d11_device;
      staging_texture_->GetDevice(&d3d11_device);
      if (d3d11_device.Get()) {
        Microsoft::WRL::ComPtr<ID3D11DeviceContext> d3d11_ctx;
        d3d11_device->GetImmediateContext(&d3d11_ctx);
        if (d3d11_ctx.Get()) {
          d3d11_ctx->CopyResource(staging_texture_.Get(), texture_.Get());
        }
      }
    }
  }

  void SetSurface(EGLSurface surface, GLuint texture_id) {
    surface_ = surface;
    texture_id_ = texture_id;
  }

  EGLSurface surface() const { return surface_; }

  GLuint texture_id() const { return texture_id_; }

 protected:
  ~GLImageDXGISharedHandle() override {}

 private:
  HANDLE handle_;
  Microsoft::WRL::ComPtr<ID3D11Texture2D> staging_texture_;
  EGLSurface surface_;
  GLuint texture_id_;
};

#endif  // defined(OS_WIN)

}  // namespace

ExternalTextureManager::ExternalTextureManager() {}

ExternalTextureManager::~ExternalTextureManager() {}

void* ExternalTextureManager::CreateTexture(GLuint texture_id,
                                            uint32_t width,
                                            uint32_t height,
                                            TextureManager* tex_man) {
  void* share_handle = nullptr;

#if defined(OS_WIN)
  EGLDisplay egl_display = gl::GLSurfaceEGL::GetHardwareDisplay();
  if (egl_display == EGL_NO_DISPLAY) {
    return nullptr;
  }

  EGLContext curContext = eglGetCurrentContext();
  if (curContext == EGL_NO_CONTEXT) {
    return nullptr;
  }

  gfx::Size size(width, height);
  scoped_refptr<gl::GLImage> image;
  void* texture = nullptr;

  GLImageDXGISharedHandle* dxgi_image = new GLImageDXGISharedHandle(size);
  if (!dxgi_image->Initialize()) {
    return nullptr;
  }
  image = dxgi_image;
  share_handle = dxgi_image->share_handle();
  texture = dxgi_image->texture().Get();

  if (!image) {  // this check seems unnecessary
    return nullptr;
  }

  EGLint numConfigs = 0;
  EGLint configAttrs[] = {
      EGL_RENDERABLE_TYPE,
      EGL_OPENGL_ES3_BIT,  // must remain in this position for ES2 fallback
      EGL_SURFACE_TYPE,
      EGL_PBUFFER_BIT,
      EGL_BUFFER_SIZE,
      32,
      EGL_RED_SIZE,
      8,
      EGL_GREEN_SIZE,
      8,
      EGL_BLUE_SIZE,
      8,
      EGL_ALPHA_SIZE,
      8,
      EGL_DEPTH_SIZE,
      0,
      EGL_STENCIL_SIZE,
      0,
      EGL_SAMPLE_BUFFERS,
      0,
      EGL_NONE};

  EGLConfig config = nullptr;
  if (eglChooseConfig(egl_display, configAttrs, &config, 1, &numConfigs) !=
      EGL_TRUE) {
    return nullptr;
  }

  EGLSurface surface = EGL_NO_SURFACE;
  EGLint surfAttrs[] = {EGL_WIDTH,
                        width,
                        EGL_HEIGHT,
                        height,
                        EGL_TEXTURE_TARGET,
                        EGL_TEXTURE_2D,
                        EGL_TEXTURE_FORMAT,
                        EGL_TEXTURE_RGBA,
                        EGL_NONE};

  surface = eglCreatePbufferFromClientBuffer(egl_display, EGL_D3D_TEXTURE_ANGLE,
                                             texture, config, surfAttrs);
  if (surface == EGL_NO_SURFACE) {
    // fallback to ES2 - it could be that we're running on older hardware
    // and ES3 isn't available

    // EGL_RENDERABLE_TYPE is the bit at configAttrs[0]
    configAttrs[1] = EGL_OPENGL_ES2_BIT;
    config = nullptr;
    if (eglChooseConfig(egl_display, configAttrs, &config, 1, &numConfigs) ==
        EGL_TRUE) {
      surface = eglCreatePbufferFromClientBuffer(
          egl_display, EGL_D3D_TEXTURE_ANGLE, texture, config, surfAttrs);
    }

    // still no surface? we're done
    if (surface == EGL_NO_SURFACE) {
      return nullptr;
    }
  }

  dxgi_image->SetSurface(surface, texture_id);

  surfaceMap_[share_handle] = image;

  EGLSurface drawSurface = eglGetCurrentSurface(EGL_DRAW);
  EGLSurface readSurface = eglGetCurrentSurface(EGL_READ);

  eglMakeCurrent(egl_display, surface, surface, curContext);

  if (eglBindTexImage(egl_display, surface, EGL_BACK_BUFFER)) {
    if (tex_man) {
      TextureRef* texture_ref = tex_man->GetTexture(texture_id);
      tex_man->SetLevelInfo(texture_ref, GL_TEXTURE_2D, 0, GL_BGRA_EXT, width,
                            height, 1, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE,
                            gfx::Rect(size));
      tex_man->SetLevelImage(texture_ref, GL_TEXTURE_2D, 0, image.get(),
                             Texture::BOUND);
    }
  }

  eglMakeCurrent(egl_display, drawSurface, readSurface, curContext);

#endif  // defined(OS_WIN)

  return share_handle;
}

void ExternalTextureManager::LockTexture(void* handle) {
#if defined(OS_WIN)
  auto const img = surfaceMap_.find(handle);
  if (img != surfaceMap_.end()) {
    GLImageDXGISharedHandle* dxgi_image =
        reinterpret_cast<GLImageDXGISharedHandle*>(img->second.get());
    dxgi_image->Lock();
  }
#endif  // defined(OS_WIN)
}

void ExternalTextureManager::UnlockTexture(void* handle) {
#if defined(OS_WIN)
  auto const img = surfaceMap_.find(handle);
  if (img != surfaceMap_.end()) {
    GLImageDXGISharedHandle* dxgi_image =
        reinterpret_cast<GLImageDXGISharedHandle*>(img->second.get());
    dxgi_image->Unlock();
  }
#endif  // defined(OS_WIN)
}

void ExternalTextureManager::DeleteTexture(void* handle,
                                           TextureManager* tex_man) {
#if defined(OS_WIN)
  EGLDisplay egl_display = gl::GLSurfaceEGL::GetHardwareDisplay();
  if (egl_display == EGL_NO_DISPLAY) {
    return;
  }
  auto const img = surfaceMap_.find(handle);
  if (img == surfaceMap_.end()) {
    return;
  }

  EGLSurface surface = EGL_NO_SURFACE;
  GLuint texture_id = 0;

  GLImageDXGISharedHandle* dxgi_image =
      reinterpret_cast<GLImageDXGISharedHandle*>(img->second.get());
  surface = dxgi_image->surface();
  texture_id = dxgi_image->texture_id();

  if (surface != EGL_NO_SURFACE) {
    EGLContext curContext = eglGetCurrentContext();
    if (curContext != EGL_NO_CONTEXT) {
      EGLSurface drawSurface = eglGetCurrentSurface(EGL_DRAW);
      EGLSurface readSurface = eglGetCurrentSurface(EGL_READ);

      eglMakeCurrent(egl_display, surface, surface, curContext);

      TextureRef* texture_ref = nullptr;
      if (tex_man) {
        texture_ref = tex_man->GetTexture(texture_id);
      }

      eglReleaseTexImage(egl_display, surface, EGL_BACK_BUFFER);

      if (tex_man && texture_ref) {
        tex_man->SetLevelInfo(texture_ref, GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 1,
                              0, GL_RGBA, GL_UNSIGNED_BYTE, gfx::Rect());
        tex_man->SetLevelImage(texture_ref, GL_TEXTURE_2D, 0, nullptr,
                               Texture::UNBOUND);
      }

      eglMakeCurrent(egl_display, drawSurface, readSurface, curContext);

      eglDestroySurface(egl_display, surface);
    }
  }
  surfaceMap_.erase(img);
#endif  // defined(OS_WIN)
}

}  // namespace gles2
}  // namespace gpu
