/* Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *  * Neither the name of NVIDIA CORPORATION nor the names of its
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>

Display *display;
int screen;
Window win = 0;

void error_exit(const char *format, ...) {
  va_list args;
  va_start(args, format);
  vfprintf(stderr, format, args);
  va_end(args);
  exit(1);
}

#include <EGL/egl.h>
#include <EGL/eglext.h>

#define GET_GLERROR(ret)                                                       \
  {                                                                            \
    GLenum err = glGetError();                                                 \
    if (err != GL_NO_ERROR) {                                                  \
      fprintf(stderr, "[%s line %d] OpenGL Error: 0x%x\n", __FILE__, __LINE__, \
              err);                                                            \
      fflush(stderr);                                                          \
                                                                               \
      switch (err) {                                                           \
        case GL_INVALID_ENUM:                                                  \
          printf("GL_INVALID_ENUM\n");                                         \
          break;                                                               \
        case GL_INVALID_VALUE:                                                 \
          printf("GL_INVALID_VALUE\n");                                        \
          break;                                                               \
        case GL_INVALID_OPERATION:                                             \
          printf("GL_INVALID_OPERATION\n");                                    \
          break;                                                               \
        case GL_OUT_OF_MEMORY:                                                 \
          printf("GL_OUT_OF_MEMORY\n");                                        \
          break;                                                               \
        case GL_INVALID_FRAMEBUFFER_OPERATION:                                 \
          printf("GL_INVALID_FRAMEBUFFER_OPERATION\n");                        \
          break;                                                               \
        default:                                                               \
          printf("UKNOWN OPENGL ERROR CODE 0x%x\n", err);                      \
      };                                                                       \
    }                                                                          \
  }

EGLDisplay eglDisplay = EGL_NO_DISPLAY;
EGLSurface eglSurface = EGL_NO_SURFACE;
EGLContext eglContext = EGL_NO_CONTEXT;

int graphics_setup_window(int xpos, int ypos, int width, int height,
                          const char *windowname) {
  EGLint configAttrs[] = {
      EGL_RED_SIZE, 1, EGL_GREEN_SIZE, 1, EGL_BLUE_SIZE, 1, EGL_DEPTH_SIZE, 16,
      EGL_SAMPLE_BUFFERS, 0, EGL_SAMPLES, 0,
      // EGL_CONFORMANT,      EGL_OPENGL_BIT,
      EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,  // 3_BIT_KHR,
      EGL_NONE};
  EGLint contextAttrs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};

  EGLConfig *configList = NULL;
  EGLint configCount;

  display = XOpenDisplay(NULL);
  if (!display) error_exit("Error opening X display.\n");

  screen = DefaultScreen(display);

  eglDisplay = eglGetDisplay(0);

  if (eglDisplay == EGL_NO_DISPLAY)
    error_exit("EGL failed to obtain display\n");

  if (!eglInitialize(eglDisplay, 0, 0))
    error_exit("EGL failed to initialize\n");

  if (!eglChooseConfig(eglDisplay, configAttrs, NULL, 0, &configCount) ||
      !configCount)
    error_exit("EGL failed to return any matching configurations\n");

  configList = (EGLConfig *)malloc(configCount * sizeof(EGLConfig));

  if (!eglChooseConfig(eglDisplay, configAttrs, configList, configCount,
                       &configCount) ||
      !configCount)
    error_exit("EGL failed to populate configuration list\n");

  Window xRootWindow = DefaultRootWindow(display);
  XSetWindowAttributes xCreateWindowAttributes;
  xCreateWindowAttributes.event_mask = ExposureMask;
  win = XCreateWindow(display, xRootWindow, 0, 0, width, height, 0,
                      CopyFromParent, InputOutput, CopyFromParent, CWEventMask,
                      &xCreateWindowAttributes);
  XMapWindow(display, win);

  Atom netWmStateAtom = XInternAtom(display, "_NET_WM_STATE", false);
  XEvent xEvent;
  memset(&xEvent, 0, sizeof(xEvent));
  xEvent.type = ClientMessage;
  xEvent.xclient.window = win;
  xEvent.xclient.message_type = netWmStateAtom;
  xEvent.xclient.format = 32;
  xEvent.xclient.data.l[0] = 1;
  xEvent.xclient.data.l[1] = false;
  XSendEvent(display, xRootWindow, false, SubstructureNotifyMask, &xEvent);

  XStoreName(display, win, windowname);

  XSelectInput(display, win,
               ExposureMask | KeyPressMask | ButtonPressMask |
                   ButtonReleaseMask | KeyReleaseMask | VisibilityChangeMask |
                   PointerMotionMask);

  EGLint windowAttrs[] = {EGL_NONE};

  eglSurface = eglCreateWindowSurface(eglDisplay, configList[0],
                                      (EGLNativeWindowType)win, windowAttrs);

  if (!eglSurface) error_exit("EGL couldn't create window\n");

  eglBindAPI(EGL_OPENGL_ES_API);

  eglContext = eglCreateContext(eglDisplay, configList[0], NULL, contextAttrs);
  if (!eglContext) error_exit("EGL couldn't create context\n");

  if (!eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext))
    error_exit("EGL couldn't make context/surface current\n");

  EGLint Context_RendererType;
  eglQueryContext(eglDisplay, eglContext, EGL_CONTEXT_CLIENT_TYPE,
                  &Context_RendererType);

  switch (Context_RendererType) {
    case EGL_OPENGL_API:
      printf("Using OpenGL API is not supported\n");
      exit(EXIT_FAILURE);
      break;
    case EGL_OPENGL_ES_API:
      printf("Using OpenGL ES API\n");
      break;
    case EGL_OPENVG_API:
      error_exit("Context Query Returned OpenVG. This is Unsupported\n");
    default:
      error_exit("Unknown Context Type. %04X\n", Context_RendererType);
  }

  return 1;
}

void graphics_set_windowtitle(const char *windowname) {
  XStoreName(display, win, windowname);
}

void graphics_swap_buffers() { eglSwapBuffers(eglDisplay, eglSurface); }

void graphics_close_window() {
  if (eglDisplay != EGL_NO_DISPLAY) {
    eglMakeCurrent(eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);

    if (eglContext != EGL_NO_CONTEXT) eglDestroyContext(eglDisplay, eglContext);

    if (eglSurface != EGL_NO_SURFACE) eglDestroySurface(eglDisplay, eglSurface);

    eglTerminate(eglDisplay);
  }

  if (display) {
    if (win) XDestroyWindow(display, win);

    XCloseDisplay(display);
  }
}