// Copyright 2013 The Flutter 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 "flutter/testing/test_gl_surface.h"

#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <EGL/eglplatform.h>
#include <GLES2/gl2.h>

#include <sstream>
#include <string>

#include "flutter/fml/build_config.h"
#include "flutter/fml/logging.h"
#include "third_party/skia/include/core/SkColorSpace.h"
#include "third_party/skia/include/core/SkColorType.h"
#include "third_party/skia/include/core/SkSurface.h"
#include "third_party/skia/include/gpu/GrBackendSurface.h"
#include "third_party/skia/include/gpu/ganesh/SkSurfaceGanesh.h"
#include "third_party/skia/include/gpu/ganesh/gl/GrGLBackendSurface.h"
#include "third_party/skia/include/gpu/gl/GrGLAssembleInterface.h"
#include "third_party/skia/include/gpu/gl/GrGLTypes.h"

namespace flutter {
namespace testing {

static std::string GetEGLError() {
  std::stringstream stream;

  auto error = ::eglGetError();

  stream << "EGL Result: '";

  switch (error) {
    case EGL_SUCCESS:
      stream << "EGL_SUCCESS";
      break;
    case EGL_NOT_INITIALIZED:
      stream << "EGL_NOT_INITIALIZED";
      break;
    case EGL_BAD_ACCESS:
      stream << "EGL_BAD_ACCESS";
      break;
    case EGL_BAD_ALLOC:
      stream << "EGL_BAD_ALLOC";
      break;
    case EGL_BAD_ATTRIBUTE:
      stream << "EGL_BAD_ATTRIBUTE";
      break;
    case EGL_BAD_CONTEXT:
      stream << "EGL_BAD_CONTEXT";
      break;
    case EGL_BAD_CONFIG:
      stream << "EGL_BAD_CONFIG";
      break;
    case EGL_BAD_CURRENT_SURFACE:
      stream << "EGL_BAD_CURRENT_SURFACE";
      break;
    case EGL_BAD_DISPLAY:
      stream << "EGL_BAD_DISPLAY";
      break;
    case EGL_BAD_SURFACE:
      stream << "EGL_BAD_SURFACE";
      break;
    case EGL_BAD_MATCH:
      stream << "EGL_BAD_MATCH";
      break;
    case EGL_BAD_PARAMETER:
      stream << "EGL_BAD_PARAMETER";
      break;
    case EGL_BAD_NATIVE_PIXMAP:
      stream << "EGL_BAD_NATIVE_PIXMAP";
      break;
    case EGL_BAD_NATIVE_WINDOW:
      stream << "EGL_BAD_NATIVE_WINDOW";
      break;
    case EGL_CONTEXT_LOST:
      stream << "EGL_CONTEXT_LOST";
      break;
    default:
      stream << "Unknown";
  }

  stream << "' (0x" << std::hex << error << std::dec << ").";
  return stream.str();
}

static bool HasExtension(const char* extensions, const char* name) {
  const char* r = strstr(extensions, name);
  auto len = strlen(name);
  // check that the extension name is terminated by space or null terminator
  return r != nullptr && (r[len] == ' ' || r[len] == 0);
}

static void CheckSwanglekExtensions() {
  const char* extensions = ::eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS);
  FML_CHECK(HasExtension(extensions, "EGL_EXT_platform_base")) << extensions;
  FML_CHECK(HasExtension(extensions, "EGL_ANGLE_platform_angle_vulkan"))
      << extensions;
  FML_CHECK(HasExtension(extensions,
                         "EGL_ANGLE_platform_angle_device_type_swiftshader"))
      << extensions;
}

static EGLDisplay CreateSwangleDisplay() {
  CheckSwanglekExtensions();

  PFNEGLGETPLATFORMDISPLAYEXTPROC egl_get_platform_display_EXT =
      reinterpret_cast<PFNEGLGETPLATFORMDISPLAYEXTPROC>(
          eglGetProcAddress("eglGetPlatformDisplayEXT"));
  FML_CHECK(egl_get_platform_display_EXT)
      << "eglGetPlatformDisplayEXT not available.";

  const EGLint display_config[] = {
      EGL_PLATFORM_ANGLE_TYPE_ANGLE,
      EGL_PLATFORM_ANGLE_TYPE_VULKAN_ANGLE,
      EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE,
      EGL_PLATFORM_ANGLE_DEVICE_TYPE_SWIFTSHADER_ANGLE,
      EGL_PLATFORM_ANGLE_NATIVE_PLATFORM_TYPE_ANGLE,
      EGL_PLATFORM_VULKAN_DISPLAY_MODE_HEADLESS_ANGLE,
      EGL_NONE,
  };

  return egl_get_platform_display_EXT(
      EGL_PLATFORM_ANGLE_ANGLE,
      reinterpret_cast<EGLNativeDisplayType*>(EGL_DEFAULT_DISPLAY),
      display_config);
}

TestGLSurface::TestGLSurface(SkISize surface_size)
    : surface_size_(surface_size) {
  display_ = CreateSwangleDisplay();
  FML_CHECK(display_ != EGL_NO_DISPLAY);

  auto result = ::eglInitialize(display_, nullptr, nullptr);
  FML_CHECK(result == EGL_TRUE) << GetEGLError();

  EGLConfig config = {0};

  EGLint num_config = 0;
  const EGLint attribute_list[] = {EGL_RED_SIZE,
                                   8,
                                   EGL_GREEN_SIZE,
                                   8,
                                   EGL_BLUE_SIZE,
                                   8,
                                   EGL_ALPHA_SIZE,
                                   8,
                                   EGL_SURFACE_TYPE,
                                   EGL_PBUFFER_BIT,
                                   EGL_CONFORMANT,
                                   EGL_OPENGL_ES2_BIT,
                                   EGL_RENDERABLE_TYPE,
                                   EGL_OPENGL_ES2_BIT,
                                   EGL_NONE};

  result = ::eglChooseConfig(display_, attribute_list, &config, 1, &num_config);
  FML_CHECK(result == EGL_TRUE) << GetEGLError();
  FML_CHECK(num_config == 1) << GetEGLError();

  {
    const EGLint onscreen_surface_attributes[] = {
        EGL_WIDTH,  surface_size_.width(),   //
        EGL_HEIGHT, surface_size_.height(),  //
        EGL_NONE,
    };

    onscreen_surface_ = ::eglCreatePbufferSurface(
        display_,                    // display connection
        config,                      // config
        onscreen_surface_attributes  // surface attributes
    );
    FML_CHECK(onscreen_surface_ != EGL_NO_SURFACE) << GetEGLError();
  }

  {
    const EGLint offscreen_surface_attributes[] = {
        EGL_WIDTH,  1,  //
        EGL_HEIGHT, 1,  //
        EGL_NONE,
    };
    offscreen_surface_ = ::eglCreatePbufferSurface(
        display_,                     // display connection
        config,                       // config
        offscreen_surface_attributes  // surface attributes
    );
    FML_CHECK(offscreen_surface_ != EGL_NO_SURFACE) << GetEGLError();
  }

  {
    const EGLint context_attributes[] = {
        EGL_CONTEXT_CLIENT_VERSION,  //
        2,                           //
        EGL_NONE                     //
    };

    onscreen_context_ =
        ::eglCreateContext(display_,           // display connection
                           config,             // config
                           EGL_NO_CONTEXT,     // sharegroup
                           context_attributes  // context attributes
        );
    FML_CHECK(onscreen_context_ != EGL_NO_CONTEXT) << GetEGLError();

    offscreen_context_ =
        ::eglCreateContext(display_,           // display connection
                           config,             // config
                           onscreen_context_,  // sharegroup
                           context_attributes  // context attributes
        );
    FML_CHECK(offscreen_context_ != EGL_NO_CONTEXT) << GetEGLError();
  }
}

TestGLSurface::~TestGLSurface() {
  context_ = nullptr;

  auto result = ::eglDestroyContext(display_, onscreen_context_);
  FML_CHECK(result == EGL_TRUE) << GetEGLError();

  result = ::eglDestroyContext(display_, offscreen_context_);
  FML_CHECK(result == EGL_TRUE) << GetEGLError();

  result = ::eglDestroySurface(display_, onscreen_surface_);
  FML_CHECK(result == EGL_TRUE) << GetEGLError();

  result = ::eglDestroySurface(display_, offscreen_surface_);
  FML_CHECK(result == EGL_TRUE) << GetEGLError();

  result = ::eglTerminate(display_);
  FML_CHECK(result == EGL_TRUE);
}

const SkISize& TestGLSurface::GetSurfaceSize() const {
  return surface_size_;
}

bool TestGLSurface::MakeCurrent() {
  auto result = ::eglMakeCurrent(display_, onscreen_surface_, onscreen_surface_,
                                 onscreen_context_);

  if (result == EGL_FALSE) {
    FML_LOG(ERROR) << "Could not make the context current. " << GetEGLError();
  }

  return result == EGL_TRUE;
}

bool TestGLSurface::ClearCurrent() {
  auto result = ::eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE,
                                 EGL_NO_CONTEXT);

  if (result == EGL_FALSE) {
    FML_LOG(ERROR) << "Could not clear the current context. " << GetEGLError();
  }

  return result == EGL_TRUE;
}

bool TestGLSurface::Present() {
  auto result = ::eglSwapBuffers(display_, onscreen_surface_);

  if (result == EGL_FALSE) {
    FML_LOG(ERROR) << "Could not swap buffers. " << GetEGLError();
  }

  return result == EGL_TRUE;
}

uint32_t TestGLSurface::GetFramebuffer(uint32_t width, uint32_t height) const {
  return GetWindowFBOId();
}

bool TestGLSurface::MakeResourceCurrent() {
  auto result = ::eglMakeCurrent(display_, offscreen_surface_,
                                 offscreen_surface_, offscreen_context_);

  if (result == EGL_FALSE) {
    FML_LOG(ERROR) << "Could not make the resource context current. "
                   << GetEGLError();
  }

  return result == EGL_TRUE;
}

void* TestGLSurface::GetProcAddress(const char* name) const {
  if (name == nullptr) {
    return nullptr;
  }
  auto symbol = ::eglGetProcAddress(name);
  if (symbol == NULL) {
    FML_LOG(ERROR) << "Could not fetch symbol for name: " << name;
  }
  return reinterpret_cast<void*>(symbol);
}

sk_sp<GrDirectContext> TestGLSurface::GetGrContext() {
  if (context_) {
    return context_;
  }

  return CreateGrContext();
}

sk_sp<GrDirectContext> TestGLSurface::CreateGrContext() {
  if (!MakeCurrent()) {
    return nullptr;
  }

  auto get_string =
      reinterpret_cast<PFNGLGETSTRINGPROC>(GetProcAddress("glGetString"));

  if (!get_string) {
    return nullptr;
  }

  auto c_version = reinterpret_cast<const char*>(get_string(GL_VERSION));

  if (c_version == NULL) {
    return nullptr;
  }

  GrGLGetProc get_proc = [](void* context, const char name[]) -> GrGLFuncPtr {
    return reinterpret_cast<GrGLFuncPtr>(
        reinterpret_cast<TestGLSurface*>(context)->GetProcAddress(name));
  };

  std::string version(c_version);
  auto interface = version.find("OpenGL ES") == std::string::npos
                       ? GrGLMakeAssembledGLInterface(this, get_proc)
                       : GrGLMakeAssembledGLESInterface(this, get_proc);

  if (!interface) {
    return nullptr;
  }

  context_ = GrDirectContext::MakeGL(interface);
  return context_;
}

sk_sp<SkSurface> TestGLSurface::GetOnscreenSurface() {
  FML_CHECK(::eglGetCurrentContext() != EGL_NO_CONTEXT);

  GrGLFramebufferInfo framebuffer_info = {};
  const uint32_t width = surface_size_.width();
  const uint32_t height = surface_size_.height();
  framebuffer_info.fFBOID = GetFramebuffer(width, height);
#if FML_OS_MACOSX
  framebuffer_info.fFormat = 0x8058;  // GL_RGBA8
#else
  framebuffer_info.fFormat = 0x93A1;  // GL_BGRA8;
#endif

  auto backend_render_target =
      GrBackendRenderTargets::MakeGL(width,            // width
                                     height,           // height
                                     1,                // sample count
                                     8,                // stencil bits
                                     framebuffer_info  // framebuffer info
      );

  SkSurfaceProps surface_properties(0, kUnknown_SkPixelGeometry);

  auto surface = SkSurfaces::WrapBackendRenderTarget(
      GetGrContext().get(),         // context
      backend_render_target,        // backend render target
      kBottomLeft_GrSurfaceOrigin,  // surface origin
      kN32_SkColorType,             // color type
      SkColorSpace::MakeSRGB(),     // color space
      &surface_properties,          // surface properties
      nullptr,                      // release proc
      nullptr                       // release context
  );

  if (!surface) {
    FML_LOG(ERROR) << "Could not wrap the surface while attempting to "
                      "snapshot the GL surface.";
    return nullptr;
  }

  return surface;
}

sk_sp<SkImage> TestGLSurface::GetRasterSurfaceSnapshot() {
  auto surface = GetOnscreenSurface();

  if (!surface) {
    FML_LOG(ERROR) << "Aborting snapshot because of on-screen surface "
                      "acquisition failure.";
    return nullptr;
  }

  auto device_snapshot = surface->makeImageSnapshot();

  if (!device_snapshot) {
    FML_LOG(ERROR) << "Could not create the device snapshot while attempting "
                      "to snapshot the GL surface.";
    return nullptr;
  }

  auto host_snapshot = device_snapshot->makeRasterImage();

  if (!host_snapshot) {
    FML_LOG(ERROR) << "Could not create the host snapshot while attempting to "
                      "snapshot the GL surface.";
    return nullptr;
  }

  return host_snapshot;
}

uint32_t TestGLSurface::GetWindowFBOId() const {
  return 0u;
}

}  // namespace testing
}  // namespace flutter
