// 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/shell/platform/android/android_context_gl.h"

#include <EGL/eglext.h>

#include <utility>

#include "flutter/fml/trace_event.h"

namespace flutter {

template <class T>
using EGLResult = std::pair<bool, T>;

static void LogLastEGLError() {
  struct EGLNameErrorPair {
    const char* name;
    EGLint code;
  };

#define _EGL_ERROR_DESC(a) \
  { #a, a }

  const EGLNameErrorPair pairs[] = {
      _EGL_ERROR_DESC(EGL_SUCCESS),
      _EGL_ERROR_DESC(EGL_NOT_INITIALIZED),
      _EGL_ERROR_DESC(EGL_BAD_ACCESS),
      _EGL_ERROR_DESC(EGL_BAD_ALLOC),
      _EGL_ERROR_DESC(EGL_BAD_ATTRIBUTE),
      _EGL_ERROR_DESC(EGL_BAD_CONTEXT),
      _EGL_ERROR_DESC(EGL_BAD_CONFIG),
      _EGL_ERROR_DESC(EGL_BAD_CURRENT_SURFACE),
      _EGL_ERROR_DESC(EGL_BAD_DISPLAY),
      _EGL_ERROR_DESC(EGL_BAD_SURFACE),
      _EGL_ERROR_DESC(EGL_BAD_MATCH),
      _EGL_ERROR_DESC(EGL_BAD_PARAMETER),
      _EGL_ERROR_DESC(EGL_BAD_NATIVE_PIXMAP),
      _EGL_ERROR_DESC(EGL_BAD_NATIVE_WINDOW),
      _EGL_ERROR_DESC(EGL_CONTEXT_LOST),
  };

#undef _EGL_ERROR_DESC

  const auto count = sizeof(pairs) / sizeof(EGLNameErrorPair);

  EGLint last_error = eglGetError();

  for (size_t i = 0; i < count; i++) {
    if (last_error == pairs[i].code) {
      FML_LOG(ERROR) << "EGL Error: " << pairs[i].name << " (" << pairs[i].code
                     << ")";
      return;
    }
  }

  FML_LOG(ERROR) << "Unknown EGL Error";
}

static EGLResult<EGLSurface> CreateContext(EGLDisplay display,
                                           EGLConfig config,
                                           EGLContext share = EGL_NO_CONTEXT) {
  EGLint attributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};

  EGLContext context = eglCreateContext(display, config, share, attributes);

  return {context != EGL_NO_CONTEXT, context};
}

static EGLResult<EGLConfig> ChooseEGLConfiguration(EGLDisplay display) {
  EGLint attributes[] = {
      // clang-format off
      EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
      EGL_SURFACE_TYPE,    EGL_WINDOW_BIT,
      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_NONE,            // termination sentinel
      // clang-format on
  };

  EGLint config_count = 0;
  EGLConfig egl_config = nullptr;

  if (eglChooseConfig(display, attributes, &egl_config, 1, &config_count) !=
      EGL_TRUE) {
    return {false, nullptr};
  }

  bool success = config_count > 0 && egl_config != nullptr;

  return {success, success ? egl_config : nullptr};
}

static bool TeardownContext(EGLDisplay display, EGLContext context) {
  if (context != EGL_NO_CONTEXT) {
    return eglDestroyContext(display, context) == EGL_TRUE;
  }

  return true;
}

static bool TeardownSurface(EGLDisplay display, EGLSurface surface) {
  if (surface != EGL_NO_SURFACE) {
    return eglDestroySurface(display, surface) == EGL_TRUE;
  }

  return true;
}

// For onscreen rendering.
bool AndroidContextGL::CreateWindowSurface(
    fml::RefPtr<AndroidNativeWindow> window) {
  // The configurations are only required when dealing with extensions or VG.
  // We do neither.

  window_ = std::move(window);
  EGLDisplay display = environment_->Display();

  const EGLint attribs[] = {EGL_NONE};

  // If surface not empty, create a new one will lead mem-leak in mali driver.
  // So should teardown the old surface before create a new one.
  TeardownSurface(display, surface_);
  surface_ = eglCreateWindowSurface(
      display, config_,
      reinterpret_cast<EGLNativeWindowType>(window_->handle()), attribs);
  return surface_ != EGL_NO_SURFACE;
}

// For offscreen rendering.
bool AndroidContextGL::CreatePBufferSurface() {
  // We only ever create pbuffer surfaces for background resource loading
  // contexts. We never bind the pbuffer to anything.

  EGLDisplay display = environment_->Display();

  const EGLint attribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE};

  surface_ = eglCreatePbufferSurface(display, config_, attribs);
  return surface_ != EGL_NO_SURFACE;
}

AndroidContextGL::AndroidContextGL(fml::RefPtr<AndroidEnvironmentGL> env,
                                   const AndroidContextGL* share_context)
    : environment_(env),
      window_(nullptr),
      config_(nullptr),
      surface_(EGL_NO_SURFACE),
      context_(EGL_NO_CONTEXT),
      valid_(false) {
  if (!environment_->IsValid()) {
    return;
  }

  bool success = false;

  // Choose a valid configuration.

  std::tie(success, config_) = ChooseEGLConfiguration(environment_->Display());

  if (!success) {
    FML_LOG(ERROR) << "Could not choose an EGL configuration.";
    LogLastEGLError();
    return;
  }

  // Create a context for the configuration.

  std::tie(success, context_) = CreateContext(
      environment_->Display(), config_,
      share_context != nullptr ? share_context->context_ : EGL_NO_CONTEXT);

  if (!success) {
    FML_LOG(ERROR) << "Could not create an EGL context";
    LogLastEGLError();
    return;
  }

  if (!this->CreatePBufferSurface()) {
    FML_LOG(ERROR) << "Could not create the EGL surface.";
    LogLastEGLError();
    return;
  }

  // All done!
  valid_ = true;
}

AndroidContextGL::~AndroidContextGL() {
  if (!TeardownContext(environment_->Display(), context_)) {
    FML_LOG(ERROR)
        << "Could not tear down the EGL context. Possible resource leak.";
    LogLastEGLError();
  }

  if (!TeardownSurface(environment_->Display(), surface_)) {
    FML_LOG(ERROR)
        << "Could not tear down the EGL surface. Possible resource leak.";
    LogLastEGLError();
  }
}

fml::RefPtr<AndroidEnvironmentGL> AndroidContextGL::Environment() const {
  return environment_;
}

bool AndroidContextGL::IsValid() const {
  return valid_;
}

bool AndroidContextGL::MakeCurrent() {
  // If same context just return, avoid unused egl call.
  if (eglGetCurrentContext() == context_) {
    return true;
  }
  if (eglMakeCurrent(environment_->Display(), surface_, surface_, context_) !=
      EGL_TRUE) {
    FML_LOG(ERROR) << "Could not make the context current";
    LogLastEGLError();
    return false;
  }
  return true;
}

bool AndroidContextGL::ClearCurrent() {
  if (eglGetCurrentContext() != context_) {
    return true;
  }
  if (eglMakeCurrent(environment_->Display(), EGL_NO_SURFACE, EGL_NO_SURFACE,
                     EGL_NO_CONTEXT) != EGL_TRUE) {
    FML_LOG(ERROR) << "Could not clear the current context";
    LogLastEGLError();
    return false;
  }
  return true;
}

bool AndroidContextGL::SwapBuffers() {
  TRACE_EVENT0("flutter", "AndroidContextGL::SwapBuffers");
  return eglSwapBuffers(environment_->Display(), surface_);
}

SkISize AndroidContextGL::GetSize() {
  EGLint width = 0;
  EGLint height = 0;

  if (!eglQuerySurface(environment_->Display(), surface_, EGL_WIDTH, &width) ||
      !eglQuerySurface(environment_->Display(), surface_, EGL_HEIGHT,
                       &height)) {
    FML_LOG(ERROR) << "Unable to query EGL surface size";
    LogLastEGLError();
    return SkISize::Make(0, 0);
  }
  return SkISize::Make(width, height);
}

bool AndroidContextGL::Resize(const SkISize& size) {
  if (size == GetSize()) {
    return true;
  }

  ClearCurrent();

  TeardownSurface(environment_->Display(), surface_);

  if (!this->CreateWindowSurface(window_)) {
    FML_LOG(ERROR) << "Unable to create EGL window surface on resize.";
    return false;
  }

  MakeCurrent();

  return true;
}

}  // namespace flutter