// SPDX-FileCopyrightText: 2016 Gaetan Allaert
// SPDX-License-Identifier: GPL-2.0-or-later

#include <assert.h>
#include <iostream>
#include <string.h>
#include <sys/time.h>

#ifdef _RPI
#include <bcm_host.h>
#else
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>
#endif

#include "GLES_utils.hxx"

/// esCreateWindow flag - RGB color buffer
#define GLES_UTILS_WINDOW_RGB           0
/// esCreateWindow flag - ALPHA color buffer
#define GLES_UTILS_WINDOW_ALPHA         1
/// esCreateWindow flag - depth buffer
#define GLES_UTILS_WINDOW_DEPTH         2
/// esCreateWindow flag - stencil buffer
#define GLES_UTILS_WINDOW_STENCIL       4
/// esCreateWindow flat - multi-sample buffer
#define GLES_UTILS_WINDOW_MULTISAMPLE   8

GLES_utils::GLES_utils () :
  m_State (),
  display_func (NULL),
  idle_func (NULL),
  keyboard_func (NULL),
  reshape_func (NULL) {}

GLES_utils::~GLES_utils () {}

GLES_utils&
GLES_utils::instance () {
  static GLES_utils* const The_Instance (new GLES_utils);
  return *The_Instance;
}

void
GLES_utils::init (const string &title) {
#ifdef _RPI
  bcm_host_init ();
  init_dispmanx (m_State.native_window);
#else
  init_display (m_State, title);
#endif
  init_egl (m_State, GLES_UTILS_WINDOW_RGB);
}

void
GLES_utils::print_config_info (const int n, const EGLDisplay &display, EGLConfig &config) {
  int size;

  cout << "EGL Configuration " << n << " is" << endl;

  eglGetConfigAttrib (display, config, EGL_RED_SIZE, &size);
  cout << "EGL  Red size is " << size << endl;

  eglGetConfigAttrib (display, config, EGL_BLUE_SIZE, &size);
  cout << "EGL  Blue size is " << size << endl;

  eglGetConfigAttrib (display, config, EGL_GREEN_SIZE, &size);
  cout << "EGL  Green size is " << size << endl;

  eglGetConfigAttrib (display, config, EGL_BUFFER_SIZE, &size);
  cout << "EGL  Buffer size is " << size << endl;

  eglGetConfigAttrib (display, config,  EGL_BIND_TO_TEXTURE_RGB , &size);
  if (size == EGL_TRUE) {
    cout << "EGL  Can be bound to RGB texture" << endl;
  } else {
    cout << "EGL  Can't be bound to RGB texture" << endl;
  }

  eglGetConfigAttrib (display, config,  EGL_BIND_TO_TEXTURE_RGBA , &size);
  if (size == EGL_TRUE) {
    cout << "EGL  Can be bound to RGBA texture" << endl;
  } else {
    cout << "EGL  Can't be bound to RGBA texture" << endl;
  }
}

void
GLES_utils::init_egl (EGL_STATE_T &state, const GLuint flags) {
  EGLBoolean result;

  static const EGLint attribute_list[] = {
    EGL_RED_SIZE,       5,
    EGL_GREEN_SIZE,     6,
    EGL_BLUE_SIZE,      5,
    EGL_ALPHA_SIZE,     (flags & GLES_UTILS_WINDOW_ALPHA) ? 8 : EGL_DONT_CARE,
    EGL_DEPTH_SIZE,     (flags & GLES_UTILS_WINDOW_DEPTH) ? 8 : EGL_DONT_CARE,
    EGL_STENCIL_SIZE,   (flags & GLES_UTILS_WINDOW_STENCIL) ? 8 : EGL_DONT_CARE,
    EGL_SAMPLE_BUFFERS, (flags & GLES_UTILS_WINDOW_MULTISAMPLE) ? 1 : 0,
    EGL_NONE
  };

  static const EGLint context_attributes[] = {
    EGL_CONTEXT_CLIENT_VERSION, 2,
    EGL_NONE
#ifndef _RPI
    , EGL_NONE
#endif
  };

  EGLConfig *configs;

  // get an EGL display connection
#ifdef _RPI
  state.display = eglGetDisplay (EGL_DEFAULT_DISPLAY);
#else
  state.display = eglGetDisplay ((EGLNativeDisplayType) state.x_display);
#endif
  assert (state.display != EGL_NO_DISPLAY);

  // initialize the EGL display connection
  result = eglInitialize (state.display, &(state.major_version), &(state.minor_version));
  assert (result != EGL_FALSE);

  result = eglGetConfigs (state.display, NULL, 0, &(state.num_configs));
  assert (result != EGL_FALSE);

  configs = (EGLConfig *) calloc (state.num_configs, sizeof (*configs));
  result = eglGetConfigs (state.display, configs, state.num_configs, &(state.num_configs));
  assert (result != EGL_FALSE);

  cout << "EGL version = " << state.major_version << "." << state.minor_version << endl;
  cout << "EGL has " << state.num_configs << " configs" << endl;
  for (int i = 0; i < state.num_configs; ++i) {
    print_config_info (i, state.display, configs[i]);
  }

  // get an appropriate EGL frame buffer configuration
  result = eglChooseConfig (state.display, attribute_list, &(state.config), 1, &(state.num_configs));
  assert (result != EGL_FALSE);

  // Choose the OpenGL ES API
  result = eglBindAPI (EGL_OPENGL_ES_API);
  assert (result != EGL_FALSE);

#ifdef _RPI
  state.surface = eglCreateWindowSurface (state.display,
                                          state.config,
                                          (EGLNativeWindowType) &(state.native_window),
                                          NULL);
#else
  state.surface = eglCreateWindowSurface (state.display,
                                          state.config,
                                          state.native_window,
                                          NULL);
#endif
  assert (state.surface != EGL_NO_SURFACE);

  // create an EGL rendering context
  state.context = eglCreateContext (state.display, state.config, EGL_NO_CONTEXT, context_attributes);
  assert (state.context != EGL_NO_CONTEXT);

  // connect the context to the surface
  result = eglMakeCurrent (state.display, state.surface, state.surface, state.context);
  assert (result != EGL_FALSE);
}

#ifdef _RPI
void
GLES_utils::init_dispmanx (EGL_DISPMANX_WINDOW_T &native_window) {
  int32_t success = 0;
  uint32_t screen_width;
  uint32_t screen_height;

  DISPMANX_ELEMENT_HANDLE_T dispman_element;
  DISPMANX_DISPLAY_HANDLE_T dispman_display;
  DISPMANX_UPDATE_HANDLE_T dispman_update;
  VC_RECT_T dst_rect;
  VC_RECT_T src_rect;

  // create an EGL window surface
  success = graphics_get_display_size (0 /* LCD */,
                                       &screen_width,
                                       &screen_height);
  assert (success >= 0);

  dst_rect.x = 0;
  dst_rect.y = 0;
  dst_rect.width = screen_width;
  dst_rect.height = screen_height;

  src_rect.x = 0;
  src_rect.y = 0;
  src_rect.width = screen_width << 16;
  src_rect.height = screen_height << 16;

  dispman_display = vc_dispmanx_display_open (0 /* LCD */);
  dispman_update = vc_dispmanx_update_start (0);

  dispman_element = vc_dispmanx_element_add (dispman_update,
                                             dispman_display,
                                             0 /*layer*/,
                                             &dst_rect,
                                             0 /*src*/,
                                             &src_rect,
                                             DISPMANX_PROTECTION_NONE,
                                             0 /*alpha*/,
                                             0 /*clamp*/,
                                             DISPMANX_TRANSFORM_T (0) /*transform*/);

  // Build an EGL_DISPMANX_WINDOW_T from the Dispmanx window
  native_window.element = dispman_element;
  native_window.width = screen_width;
  native_window.height = screen_height;
  vc_dispmanx_update_submit_sync (dispman_update);

  cout << "Got a Dispmanx window" << endl;
}

GLboolean
GLES_utils::user_interrupt () {
  return GL_FALSE;
}
#else
void
GLES_utils::init_display (EGL_STATE_T &state, const string &title) {
    Window root;
    XSetWindowAttributes swa;
    XSetWindowAttributes xattr;
    Atom wm_state;
    XWMHints hints;
    XEvent xev;
    Window win;

    state.width = 1024;
    state.height = 768;

    /*
     * X11 native display initialization
     */

    state.x_display = XOpenDisplay (NULL);
    assert (state.x_display != NULL);

    root = DefaultRootWindow (state.x_display);

    swa.event_mask = ExposureMask | PointerMotionMask | KeyPressMask;
    win = XCreateWindow (state.x_display,
                         root,
                         0,
                         0,
                         state.width,
                         state.height,
                         0,
                         CopyFromParent,
                         InputOutput,
                         CopyFromParent,
                         CWEventMask,
                         &swa);

    xattr.override_redirect = false;
    XChangeWindowAttributes (state.x_display, win, CWOverrideRedirect, &xattr);

    hints.input = true;
    hints.flags = InputHint;
    XSetWMHints (state.x_display, win, &hints);

    // make the window visible on the screen
    XMapWindow (state.x_display, win);
    XStoreName (state.x_display, win, title.c_str ());

    // get identifiers for the provided atom name strings
    wm_state = XInternAtom (state.x_display, "_NET_WM_STATE", false);

    memset (&xev, 0, sizeof (xev));
    xev.type                 = ClientMessage;
    xev.xclient.window       = win;
    xev.xclient.message_type = wm_state;
    xev.xclient.format       = 32;
    xev.xclient.data.l[0]    = 1;
    xev.xclient.data.l[1]    = false;
    XSendEvent (state.x_display, DefaultRootWindow (state.x_display), false, SubstructureNotifyMask, &xev);

    state.native_window = (EGLNativeWindowType) win;
}

GLboolean
GLES_utils::user_interrupt () {
  XEvent xev;
  KeySym key;
  GLboolean user_interrupt = GL_FALSE;
  char text;

  // Pump all messages from X server. Keypresses are directed to keyfunc (if defined)
  while (XPending (m_State.x_display)) {
    XNextEvent (m_State.x_display, &xev);
    if (xev.type == KeyPress) {
      if (XLookupString (&xev.xkey, &text, 1, &key, 0) == 1) {
        if (keyboard_func != NULL) {
          keyboard_func (text, 0, 0);
        }
      }
    }
    if (xev.type == DestroyNotify) {
      user_interrupt = GL_TRUE;
    }
  }
  return user_interrupt;
}
#endif

void
GLES_utils::register_display_func (void (*display_func) ()) {
  this->display_func = display_func;
}

void
GLES_utils::register_idle_func (void (*idle_func) ()) {
  this->idle_func = idle_func;
}

void
GLES_utils::register_keyboard_func (void (*keyboard_func) (unsigned char, int, int)) {
  this->keyboard_func = keyboard_func;
}

void
GLES_utils::register_reshape_func (void (*reshape_func) (int, int)) {
  this->reshape_func = reshape_func;
}

void
GLES_utils::main_loop () {
  struct timeval t1, t2;
  struct timezone tz;
  float delta_time;
  float total_time = 0.0f;
  unsigned int frames = 0;

  gettimeofday (&t1 , &tz);

  while (user_interrupt () == GL_FALSE) {
    gettimeofday (&t2, &tz);
    delta_time = (float) (t2.tv_sec - t1.tv_sec + (t2.tv_usec - t1.tv_usec) * 1e-6);
    t1 = t2;

    if (reshape_func != NULL) {
#ifdef _RPI
      reshape_func (m_State.native_window.width, m_State.native_window.height);
#else
      reshape_func (m_State.width, m_State.height);
#endif
    }
    if (idle_func != NULL) {
      idle_func ();
    }
    if (display_func != NULL) {
      display_func ();
    }

    eglSwapBuffers (m_State.display, m_State.surface);

    total_time += delta_time;
    ++frames;
    if (total_time >  2.0f) {
      cout << frames << " frames rendered in " << total_time << " seconds -> FPS = " << (frames / total_time) << endl;
      total_time -= 2.0f;
      frames = 0;
    }
  }
}
