/*
 * Copyright 2024 Joel Winarske
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "waypp/window/egl.h"

#include <wayland-egl.h>
#include <cstring>
#include <stdexcept>

#include "logging/logging.h"

/**
 * @brief The Egl class represents an EGL object used for OpenGL rendering.
 *
 * This class provides functionality for initializing EGL, choosing an EGL
 * configuration, creating an EGL context, and managing various EGL extensions.
 */
Egl::Egl(wl_display* display,
         wl_surface* wl_surface,
         const int width,
         const int height,
         config* config)
    : dpy_(eglGetDisplay(reinterpret_cast<EGLNativeDisplayType>(display))),
      context_attribs_(config->context_attribs,
                       config->context_attribs + config->context_attribs_size),
      config_attribs_(config->config_attribs,
                      config->config_attribs + config->config_attribs_size),
      buffer_bpp_(config->buffer_bpp),
      wl_surface_(wl_surface),
      width_(width),
      height_(height) {
  DLOG_TRACE("++Egl::Egl()");
  EGLBoolean ret = eglInitialize(dpy_, &major_, &minor_);
  if (ret == EGL_FALSE) {
    throw std::runtime_error("eglInitialize failed.");
  }

  ret = eglBindAPI(config->type);
  if (ret != EGL_TRUE) {
    throw std::runtime_error("eglBindAPI failed.");
  }

  EGLint count;
  eglGetConfigs(dpy_, nullptr, 0, &count);
  DLOG_DEBUG("EGL has {} configs", count);

  auto* configs = reinterpret_cast<EGLConfig*>(
      calloc(static_cast<size_t>(count), sizeof(EGLConfig)));

  EGLint n;
  ret = eglChooseConfig(dpy_, config_attribs_.data(), configs, count, &n);
  if (n == 0) {
    DLOG_DEBUG("EGL Config: Check Config Attributes");
    exit(EXIT_FAILURE);
  }

  EGLint red_size;
  for (EGLint i = 0; i < n; i++) {
    eglGetConfigAttrib(dpy_, configs[i], EGL_BUFFER_SIZE, &config->buffer_bpp);
    eglGetConfigAttrib(dpy_, configs[i], EGL_RED_SIZE, &red_size);
    DLOG_DEBUG("EGL_BUFFER_SIZE: {}", config->buffer_bpp);
    DLOG_DEBUG("EGL_RED_SIZE: {}", red_size);
    if ((buffer_bpp_ == 0 || buffer_bpp_ == config->buffer_bpp) &&
        red_size < 10) {
      config_ = configs[i];
      break;
    }
  }
  free(configs);
  if (config_ == nullptr) {
    LOG_CRITICAL("did not find config with buffer size {}", buffer_bpp_);
    exit(EXIT_FAILURE);
  }

  context_ =
      eglCreateContext(dpy_, config_, EGL_NO_CONTEXT, context_attribs_.data());

#if !defined(NDEBUG)
  egl_khr_debug_init();
#endif

  const auto extensions = eglQueryString(dpy_, EGL_EXTENSIONS);

  // setup for Damage Region Management
  if (has_egl_extension(extensions, "EGL_EXT_swap_buffers_with_damage")) {
    pfSwapBufferWithDamage_ =
        reinterpret_cast<PFNEGLSWAPBUFFERSWITHDAMAGEEXTPROC>(
            eglGetProcAddress("eglSwapBuffersWithDamageEXT"));
  } else if (has_egl_extension(extensions,
                               "EGL_KHR_swap_buffers_with_damage")) {
    pfSwapBufferWithDamage_ =
        reinterpret_cast<PFNEGLSWAPBUFFERSWITHDAMAGEEXTPROC>(
            eglGetProcAddress("eglSwapBuffersWithDamageKHR"));
  }

  if (has_egl_extension(extensions, "EGL_EXT_partial_update")) {
    pfSetDamageRegion_ = reinterpret_cast<PFNEGLSETDAMAGEREGIONKHRPROC>(
        eglGetProcAddress("eglSetDamageRegionEXT"));

  } else if (has_egl_extension(extensions, "EGL_KHR_partial_update")) {
    pfSetDamageRegion_ = reinterpret_cast<PFNEGLSETDAMAGEREGIONKHRPROC>(
        eglGetProcAddress("eglSetDamageRegionKHR"));
  }

  wl_egl_window_ = wl_egl_window_create(wl_surface_, width_, height_);
  eglMakeCurrent(dpy_, egl_surface_, egl_surface_, context_);
  egl_surface_ = eglCreateWindowSurface(
      dpy_, config_, reinterpret_cast<EGLNativeWindowType>(wl_egl_window_),
      nullptr);
  eglMakeCurrent(dpy_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
  DLOG_TRACE("--Egl::Egl()");
}

/**
 * @brief Destructor for the Egl class.
 *
 * This destructor terminates the EGL display and releases resources associated
 * with the EGL thread.
 */
Egl::~Egl() {
  DLOG_TRACE("++Egl::~Egl()");
  eglTerminate(dpy_);
  eglReleaseThread();
  DLOG_TRACE("--Egl::~Egl()");
}

/**
 * \brief Make the EGL context current.
 *
 * This function checks if the current EGL context is different from the stored
 * context_. If it is different, it calls eglMakeCurrent to make the stored
 * context_ the current context.
 *
 * \return True if the context was made current successfully, false otherwise.
 */
void Egl::make_current() {
  DLOG_TRACE("++Egl::make_current()");
  if (eglGetCurrentContext() != context_) {
    eglMakeCurrent(dpy_, egl_surface_, egl_surface_, context_);
  }
  DLOG_TRACE("--Egl::make_current()");
}

/**
 * @brief Clears the current EGL context.
 *
 * This function checks if there is a current EGL context using
 * `eglGetCurrentContext()`. If a current context exists, it calls
 * `eglMakeCurrent()` with `EGL_NO_SURFACE`, `EGL_NO_SURFACE`, and
 * `EGL_NO_CONTEXT` as parameters to clear the current context.
 *
 * @return true if the current context was cleared successfully, false
 * otherwise.
 */
void Egl::clear_current() {
  DLOG_TRACE("++Egl::clear_current()");
  if (eglGetCurrentContext() != EGL_NO_CONTEXT) {
    eglMakeCurrent(dpy_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
  }
  DLOG_TRACE("--Egl::clear_current()");
}

/**
 * @brief Swaps the front and back buffers of the EGL surface.
 *
 * This function swaps the front and back buffers of the EGL surface associated
 * with the Egl object. It uses the EGL function eglSwapBuffers() internally.
 *
 * @return True if the swap was successful, false otherwise.
 */
void Egl::swap_buffers() const {
  DLOG_TRACE("++Egl::swap_buffers()");
  eglSwapBuffers(dpy_, egl_surface_);
  DLOG_TRACE("--Egl::swap_buffers()");
}

void Egl::get_buffer_age(EGLint& buffer_age) const {
  if (pfSwapBufferWithDamage_) {
    eglQuerySurface(dpy_, egl_surface_, EGL_BUFFER_AGE_EXT, &buffer_age);
    return;
  }
  buffer_age = 0;
}

void Egl::swap_buffers_with_damage(EGLint* rects, const EGLint n_rects) const {
  if (pfSwapBufferWithDamage_) {
    pfSwapBufferWithDamage_(dpy_, egl_surface_, rects, n_rects);
  }
}

/**
 * @brief Checks if a given EGL extension is supported.
 *
 * This function searches for the specified extension name within the provided
 * extensions string. The extensions string is expected to be in a
 * space-separated format, with each extension name terminated by a space
 * character or a null terminator.
 *
 * @param extensions The extensions string to search within.
 * @param name The name of the extension to check for.
 * @return true if the extension is found, false otherwise.
 */
bool Egl::has_egl_extension(const char* extensions, const char* name) {
  const char* r = strstr(extensions, name);
  const auto len = strlen(name);
  // check that the extension name is terminated by space or null terminator
  return r != nullptr && (r[len] == ' ' || r[len] == 0);
}

/**
 * @brief Debug callback for EGL errors.
 *
 * This function is called when an EGL error occurs. It prints the error details
 * and additional information to the standard error stream (std::cerr).
 *
 * @param error The EGL error code.
 * @param command The EGL command associated with the error.
 * @param messageType The EGL message type.
 * @param threadLabel The EGL thread label.
 * @param objectLabel The EGL object label.
 * @param message The error message.
 *
 * @return None.
 */
void Egl::debug_callback(EGLenum error,
                         const char* command,
                         EGLint messageType,
                         EGLLabelKHR threadLabel,
                         EGLLabelKHR objectLabel,
                         const char* message) {
  LOG_ERROR("**** EGL Error");
  LOG_ERROR("\terror: {}", error);
  LOG_ERROR("\tcommand: {}", command);
  switch (error) {
    case EGL_BAD_ACCESS:
      LOG_ERROR("\terror: EGL_BAD_ACCESS");
      break;
    case EGL_BAD_ALLOC:
      LOG_ERROR("\terror: EGL_BAD_ALLOC");
      break;
    case EGL_BAD_ATTRIBUTE:
      LOG_ERROR("\terror: EGL_BAD_ATTRIBUTE");
      break;
    case EGL_BAD_CONFIG:
      LOG_ERROR("\terror: EGL_BAD_CONFIG");
      break;
    case EGL_BAD_CONTEXT:
      LOG_ERROR("\terror: EGL_BAD_CONTEXT");
      break;
    case EGL_BAD_CURRENT_SURFACE:
      LOG_ERROR("\terror: EGL_BAD_CURRENT_SURFACE");
      break;
    case EGL_BAD_DISPLAY:
      LOG_ERROR("\terror: EGL_BAD_DISPLAY");
      break;
    case EGL_BAD_MATCH:
      LOG_ERROR("\terror: EGL_BAD_MATCH");
      break;
    case EGL_BAD_NATIVE_PIXMAP:
      LOG_ERROR("\terror: EGL_BAD_NATIVE_PIXMAP");
      break;
    case EGL_BAD_NATIVE_WINDOW:
      LOG_ERROR("\terror: EGL_BAD_NATIVE_WINDOW");
      break;
    case EGL_BAD_PARAMETER:
      LOG_ERROR("\terror: EGL_BAD_PARAMETER");
      break;
    case EGL_BAD_SURFACE:
      LOG_ERROR("\terror: EGL_BAD_SURFACE");
      break;
    default:
      LOG_ERROR("\terror: {}", error);
      break;
  }
  LOG_ERROR("\tmessageType: {}", messageType);
  LOG_ERROR("\tthreadLabel: {}", threadLabel);
  LOG_ERROR("\tobjectLabel: {}", objectLabel);
  LOG_ERROR("\tmessage: {}", ((message == nullptr) ? "" : message));
}

/**
 * @brief Initialize the EGL debugging functionality.
 *
 * This function initializes the EGL debugging functionality by calling the
 * eglDebugMessageControlKHR function if it is available. The debug_callback
 * function is set as the callback function for EGL debug messages.
 *
 * @note This function requires that the EGL extension EGL_KHR_debug is
 * supported.
 */
void Egl::egl_khr_debug_init() {
  auto pfDebugMessageControl =
      reinterpret_cast<PFNEGLDEBUGMESSAGECONTROLKHRPROC>(
          eglGetProcAddress("eglDebugMessageControlKHR"));

  if (pfDebugMessageControl) {
    const EGLAttrib sDebugAttribList[] = {EGL_DEBUG_MSG_CRITICAL_KHR,
                                          EGL_TRUE,
                                          EGL_DEBUG_MSG_ERROR_KHR,
                                          EGL_TRUE,
                                          EGL_DEBUG_MSG_WARN_KHR,
                                          EGL_TRUE,
                                          EGL_DEBUG_MSG_INFO_KHR,
                                          EGL_TRUE,
                                          EGL_NONE,
                                          0};

    pfDebugMessageControl(debug_callback, sDebugAttribList);
  }
}

void Egl::set_swap_interval(int interval) {
  make_current();

  EGLBoolean ret = eglSwapInterval(dpy_, interval);
  if (ret == EGL_FALSE) {
    throw std::runtime_error("eglSwapInterval failed");
  }
  clear_current();
}

void Egl::resize(int width, int height, int dx, int dy) {
  width_ = width;
  height_ = height;
  wl_egl_window_resize(wl_egl_window_, width, height, dx, dy);
}
