#include "window.h"

#include <assert.h>
#include <fcntl.h>
#include <limits.h>
#include <poll.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <GL/gl.h>
#include <GL/glx.h>
#include <X11/Xatom.h>
#include <X11/Xlib.h>

#include <xcb/xcb.h>
#include <xkbcommon/xkbcommon-x11.h>

#include "keyboard.h"
#include "log.h"

struct imv_window {
  Display    *x_display;
  Window     x_window;
  GLXContext x_glc;
  Atom       x_state;
  Atom       x_fullscreen;
  Atom       wm_delete_window;
  Atom       wm_protocols;
  int width;
  int height;
  struct {
    struct {
      int x, y;
    } last;
    struct {
      int x, y;
    } current;
    bool mouse1;
  } pointer;

  struct imv_keyboard *keyboard;
  int pipe_fds[2];
};

static void set_nonblocking(int fd)
{
  int flags = fcntl(fd, F_GETFL);
  assert(flags != -1);
  flags |= O_NONBLOCK;
  int rc = fcntl(fd, F_SETFL, flags);
  assert(rc != -1);
}

static void setup_keymap(struct imv_window *window)
{
  xcb_connection_t *conn = xcb_connect(NULL, NULL);
  if (xcb_connection_has_error(conn)) {
    imv_log(IMV_ERROR, "x11_window: Failed to load keymap. Could not connect via xcb.");
    return;
  }

  if (!xkb_x11_setup_xkb_extension(conn,
        XKB_X11_MIN_MAJOR_XKB_VERSION,
        XKB_X11_MIN_MINOR_XKB_VERSION,
        0, NULL, NULL, NULL, NULL)) {
    xcb_disconnect(conn);
    imv_log(IMV_ERROR, "x11_window: Failed to load keymap. xkb extension not supported by server.");
    return;
  }

  int32_t device = xkb_x11_get_core_keyboard_device_id(conn);

  struct xkb_context *context = xkb_context_new(0);
  if (!context) {
    xcb_disconnect(conn);
    imv_log(IMV_ERROR, "x11_window: Failed to load keymap. Failed to initialise xkb context.");
    return;
  }

  struct xkb_keymap *keymap =
    xkb_x11_keymap_new_from_device(context, conn, device, 0);
  if (keymap) {
    char *keymap_str = xkb_keymap_get_as_string(keymap, XKB_KEYMAP_USE_ORIGINAL_FORMAT);
    imv_keyboard_set_keymap(window->keyboard, keymap_str);
    free(keymap_str);
    xkb_keymap_unref(keymap);
  } else {
    imv_log(IMV_ERROR, "x11_window: Failed to load keymap. xkb_x11_keymap_new_from_device returned NULL.");
  }
  xkb_context_unref(context);

  xcb_disconnect(conn);
}

extern PFNGLGENERATEMIPMAPPROC imv_glGenerateMipmap;

static void load_gl_functions(void) {
  if (atoi((const char*)glGetString(GL_VERSION)) >= 3) {
    imv_glGenerateMipmap = (PFNGLGENERATEMIPMAPPROC)glXGetProcAddressARB(
      (const GLubyte *)"glGenerateMipmap"
    );
  }
}

struct imv_window *imv_window_create(int w, int h, const char *title,
                                     const char *app_id)
{
  /* Ensure event writes will always be atomic */
  assert(sizeof(struct imv_event) <= PIPE_BUF);

  struct imv_window *window = calloc(1, sizeof *window);
  window->pointer.last.x = -1;
  window->pointer.last.y = -1;
  pipe(window->pipe_fds);
  set_nonblocking(window->pipe_fds[0]);
  set_nonblocking(window->pipe_fds[1]);

  window->x_display = XOpenDisplay(NULL);
  if (window->x_display == NULL) {
    return NULL;
  }
  Window root = DefaultRootWindow(window->x_display);
  assert(root);

  GLint att[] = {
    GLX_RGBA,
    GLX_DEPTH_SIZE,
    24,
    GLX_DOUBLEBUFFER,
    None
  };

  XVisualInfo *vi = glXChooseVisual(window->x_display, 0, att);
  assert(vi);

  Colormap cmap = XCreateColormap(window->x_display, root,
      vi->visual, AllocNone);

  XSetWindowAttributes wa = {
    .colormap = cmap,
    .event_mask = ExposureMask | KeyPressMask | KeyReleaseMask
                | ButtonPressMask | ButtonReleaseMask | PointerMotionMask
  };

  window->x_window = XCreateWindow(window->x_display, root, 0, 0, w, h,
      0, vi->depth, InputOutput, vi->visual, CWColormap | CWEventMask, &wa);

  window->x_state = XInternAtom(window->x_display, "_NET_WM_STATE", true);
  window->x_fullscreen = XInternAtom(window->x_display, "_NET_WM_STATE_FULLSCREEN", true);

  XClassHint hint = {
    .res_name = (char*)app_id,
    .res_class= "imv",
  };
  XSetClassHint(window->x_display, window->x_window, &hint);
  XMapWindow(window->x_display, window->x_window);
  window->wm_protocols = XInternAtom(window->x_display, "WM_PROTOCOLS", false);
  window->wm_delete_window = XInternAtom(window->x_display, "WM_DELETE_WINDOW", false);
  XSetWMProtocols(window->x_display, window->x_window, &window->wm_delete_window, 1);
  imv_window_set_title(window, title);

  window->x_glc = glXCreateContext(window->x_display, vi, NULL, GL_TRUE);
  assert(window->x_glc);
  glXMakeCurrent(window->x_display, window->x_window, window->x_glc);
  load_gl_functions();

  window->keyboard = imv_keyboard_create();
  assert(window->keyboard);

  setup_keymap(window);

  XFree(vi);

  return window;
}

void imv_window_free(struct imv_window *window)
{
  imv_keyboard_free(window->keyboard);
  close(window->pipe_fds[0]);
  close(window->pipe_fds[1]);
  glXMakeCurrent(window->x_display, None, NULL);
  glXDestroyContext(window->x_display, window->x_glc);
  XDestroyWindow(window->x_display, window->x_window);
  XCloseDisplay(window->x_display);
  free(window);
}

void imv_window_clear(struct imv_window *window, unsigned char r,
    unsigned char g, unsigned char b)
{
  (void)window;
  glClearColor(r / 255.0f, g / 255.0f, b / 255.0f, 1.0);
  glClear(GL_COLOR_BUFFER_BIT);
}

void imv_window_get_framebuffer_size(struct imv_window *window, int *w, int *h)
{
  if (w) {
    *w = window->width;
  }
  if (h) {
    *h = window->height;
  }
}

int imv_window_get_scale(struct imv_window *window) {
  (void)window;
  return 1;
}

void imv_window_set_title(struct imv_window *window, const char *title)
{
  Atom atom_wm_name;
  Atom atom_utf8;

  XStoreName(window->x_display, window->x_window, title);
  atom_wm_name = XInternAtom(window->x_display, "_NET_WM_NAME", False);
  atom_utf8 = XInternAtom(window->x_display, "UTF8_STRING", False);
  XChangeProperty(window->x_display, window->x_window, atom_wm_name, atom_utf8,
      8, PropModeReplace, (unsigned char*)title, strlen(title));
}

bool imv_window_is_fullscreen(struct imv_window *window)
{
  unsigned long count = 0;
  Atom type;
  int format;
  unsigned long after;
  Atom *props = NULL;
  XGetWindowProperty(window->x_display, window->x_window, window->x_state,
      0, 1024, False, XA_ATOM, &type, &format, &count, &after, (unsigned char**)&props);

  bool fullscreen = false;
  for (unsigned long i = 0; i < count; ++i) {
    if (props[i] == window->x_fullscreen) {
      fullscreen = true;
      break;
    }
  }
  XFree(props);
  return fullscreen;
}

void imv_window_set_fullscreen(struct imv_window *window, bool fullscreen)
{
  Window root = DefaultRootWindow(window->x_display);
  XEvent event = {
    .xclient = {
      .type = ClientMessage,
      .window = window->x_window,
      .format = 32,
      .message_type = window->x_state,
      .data = {
        .l = {
          (fullscreen ? 1 : 0),
          window->x_fullscreen,
          0,
          1
        }
      }
    }
  };
  XSendEvent(window->x_display, root, False,
      SubstructureNotifyMask | SubstructureRedirectMask, &event );
}

bool imv_window_get_mouse_button(struct imv_window *window, int button)
{
  if (button == 1) {
    return window->pointer.mouse1;
  }
  return false;
}

void imv_window_get_mouse_position(struct imv_window *window, double *x, double *y)
{
  if (x) {
    *x = window->pointer.current.x;
  }
  if (y) {
    *y = window->pointer.current.y;
  }
}

void imv_window_present(struct imv_window *window)
{
  glXSwapBuffers(window->x_display, window->x_window);
}

void imv_window_wait_for_event(struct imv_window *window, double timeout)
{
  struct pollfd fds[] = {
    {.fd = ConnectionNumber(window->x_display), .events = POLLIN},
    {.fd = window->pipe_fds[0], .events = POLLIN}
  };
  nfds_t nfds = sizeof fds / sizeof *fds;

  poll(fds, nfds, timeout * 1000);
}

void imv_window_push_event(struct imv_window *window, struct imv_event *e)
{
  /* Push it down the pipe */
  write(window->pipe_fds[1], e, sizeof *e);
}

static void handle_keyboard(struct imv_window *window, imv_event_handler handler, void *data, const XEvent *xev)
{
  imv_keyboard_update_mods(window->keyboard, (int)xev->xkey.state, 0, 0);

  bool pressed = xev->type == KeyPress;
  int scancode = xev->xkey.keycode - 8;
  imv_keyboard_update_key(window->keyboard, scancode, pressed);

  if (!pressed) {
    return;
  }

  char keyname[32] = {0};
  imv_keyboard_keyname(window->keyboard, scancode, keyname, sizeof keyname);

  char text[64] = {0};
  imv_keyboard_get_text(window->keyboard, scancode, text, sizeof text);

  char *desc = imv_keyboard_describe_key(window->keyboard, scancode);
  if (!desc) {
    desc = strdup("");
  }

  struct imv_event e = {
    .type = IMV_EVENT_KEYBOARD,
    .data = {
      .keyboard = {
        .scancode = scancode,
        .keyname = keyname,
        .description = desc,
        .text = text,
      }
    }
  };

  if (handler) {
    handler(data, &e);
  }
  free(desc);
}

void imv_window_pump_events(struct imv_window *window, imv_event_handler handler, void *data)
{
  XEvent xev;

  while (XPending(window->x_display)) {
    XNextEvent(window->x_display, &xev);

    if (xev.type == Expose) {
      XWindowAttributes wa;
      XGetWindowAttributes(window->x_display, window->x_window, &wa);
      window->width = wa.width;
      window->height = wa.height;
      glViewport(0, 0, wa.width, wa.height);
      struct imv_event e = {
        .type = IMV_EVENT_RESIZE,
        .data = {
          .resize = {
            .width = wa.width,
            .height = wa.height,
            .buffer_width = wa.width,
            .buffer_height = wa.height,
            .scale = 1,
          }
        }
      };
      if (handler) {
        handler(data, &e);
      }
    } else if (xev.type == KeyPress || xev.type == KeyRelease) {
      handle_keyboard(window, handler, data, &xev);
    } else if (xev.type == ButtonPress || xev.type == ButtonRelease) {
      if (xev.xbutton.button == Button1) {
        window->pointer.mouse1 = xev.type == ButtonPress;
        struct imv_event e = {
          .type = IMV_EVENT_MOUSE_BUTTON,
          .data = {
            .mouse_button = {
              .button = 1,
              .pressed = xev.type == ButtonPress
            }
          }
        };
        if (handler) {
          handler(data, &e);
        }
      } else if (xev.xbutton.button == Button4 || xev.xbutton.button == Button5) {
        struct imv_event e = {
          .type = IMV_EVENT_MOUSE_SCROLL,
          .data = {
            .mouse_scroll = {
              .dx = 0,
              .dy = xev.xbutton.button == Button4 ? -1 : 1
            }
          }
        };
        if (handler) {
          handler(data, &e);
        }
      }
    } else if (xev.type == MotionNotify) {
      window->pointer.current.x = xev.xmotion.x;
      window->pointer.current.y = xev.xmotion.y;
      int dx = window->pointer.current.x - window->pointer.last.x;
      int dy = window->pointer.current.y - window->pointer.last.y;
      if (window->pointer.last.x == -1) {
        dx = 0;
      }
      if (window->pointer.last.y == -1) {
        dy = 0;
      }
      window->pointer.last.x = window->pointer.current.x;
      window->pointer.last.y = window->pointer.current.y;

      struct imv_event e = {
        .type = IMV_EVENT_MOUSE_MOTION,
        .data = {
          .mouse_motion = {
            .x = window->pointer.current.x,
            .y = window->pointer.current.y,
            .dx = dx,
            .dy = dy
          }
        }
      };
      if (handler) {
        handler(data, &e);
      }
    } else if (xev.type == ClientMessage) {
      if (xev.xclient.message_type == window->wm_protocols && (Atom)xev.xclient.data.l[0] == window->wm_delete_window) {
        struct imv_event e = {
          .type = IMV_EVENT_CLOSE
        };
        imv_window_push_event(window, &e);
      }
    }
  }

  /* Handle any events in the pipe */
  while (1) {
    struct imv_event e;
    ssize_t len = read(window->pipe_fds[0], &e, sizeof e);
    if (len <= 0) {
      break;
    }
    assert(len == sizeof e);
    if (handler) {
      handler(data, &e);
    }
  }
}
