#include "Graphics.hpp"
#include <sstream>
#ifdef _MSC_VER
  #include <windows.h>
  #define GLEW_STATIC
  #include <GL/glew.h>
#else
  #include <X11/X.h>
  #include <X11/Xlib.h>
  #include <X11/keysym.h>
  #include <fcntl.h>
  #include <unistd.h>
  #include <linux/joystick.h>
  #define GL_GLEXT_PROTOTYPES
  #include <GL/gl.h>
  #include <GL/glu.h>
  #include <GL/glx.h>
#endif
#ifdef GL_VERSION_3_0
  #define GLT_IMPLEMENTATION
  #define GLT_MANUAL_VIEWPORT
  #include "gltext/gltext.h"
#endif

namespace {
  Graphics* graphics = nullptr;
  struct {
    unsigned int width = 0;
    unsigned int height = 0;
    struct {
      unsigned int x = 0;
      unsigned int y = 0;
    } center;
  } screen;
  #ifdef _MSC_VER
    HWND hWnd = NULL;
    HDC hDC = NULL;
    HGLRC hRC = NULL;
  #else
    Display *dpy;
    Window root;
    GLint att[] = {
      GLX_RGBA,
      GLX_DEPTH_SIZE,
      24,
      GLX_DOUBLEBUFFER,
      None
    };
    XVisualInfo *vi;
    Colormap cmap;
    XSetWindowAttributes swa;
    Window win;
    GLXContext glc;
    XWindowAttributes gwa;
    XEvent xev;
    int js;
  #endif
  unsigned int wire = 0;
  void SwitchWire() {
    glPolygonMode(
      wire == 1 ? GL_BACK : GL_FRONT_AND_BACK,
      wire == 0 ? GL_FILL : GL_LINE
    );
    if (++wire > 2) {
      wire = 0;
    }
  }
  #ifdef GL_VERSION_3_0
    GLTtext* text = nullptr;
  #endif
}

void OnPaint() {
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  graphics->render();
  #ifdef GL_VERSION_3_0
  {
    gltBeginDraw();
    gltColor(
      graphics->textColor.r,
      graphics->textColor.g,
      graphics->textColor.b,
      1.0f
    );
    gltDrawText2DAligned(
      text,
      screen.center.x,
      screen.height * 0.925,
      screen.height / 175.0, // scale
      GLT_CENTER,
      GLT_CENTER
    );
    gltEndDraw();
  }
  #endif
  glFlush();
  #ifdef _MSC_VER
    SwapBuffers(hDC);
  #else
    glXSwapBuffers(
      dpy,
      win
    );
  #endif
}

#define JOY_ON_AXIS(LKEY, RKEY, VALUE, THIS) {\
  static const int min = INT16_MIN / 3;\
  static const int max = INT16_MAX / 3;\
  static int oldValue = 0;\
  const int value = VALUE;\
  if (oldValue != value) {\
    oldValue = value;\
    if (VALUE < min) {\
      THIS->onKey(Input::LKEY);\
    } else if (VALUE > max) {\
      THIS->onKey(Input::RKEY);\
    } else {\
      THIS->onKey(\
        Input::LKEY,\
        false\
      ).onKey(\
        Input::RKEY,\
        false\
      );\
    }\
  }\
}

namespace {
  void CenterPointer() {
    #ifdef _MSC_VER
      static POINT p;
      p.x = screen.center.x;
      p.y = screen.center.y;
      if (
        ClientToScreen(
          hWnd,
          &p
        )
      ) {
        SetCursorPos(
          p.x,
          p.y
        );
      } else {
        ShowCursor(false);
      }
    #else
      XWarpPointer(
        dpy,
        None,
        win,
        0,
        0,
        0,
        0,
        screen.center.x,
        screen.center.y
      );
    #endif
  }
  void OnResize(
    const unsigned int w,
    const unsigned int h
  ) {
    screen.center.x = (screen.width = w) / 2;
    screen.center.y = (screen.height = h) / 2;
    CenterPointer();
    glViewport(
      0,
      0,
      screen.width,
      screen.height
    );
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(
      90,
      static_cast<double>(w) / static_cast<double>(h),
      Graphics::get()->Near,
      Graphics::get()->R * 3
    );
    glMatrixMode(GL_MODELVIEW);
    #ifdef GL_VERSION_3_0
      gltViewport(
        screen.width,
        screen.height
      );
    #endif
  }
  #ifdef _MSC_VER
    const Input::Key Translate(const UINT wParam) {
      switch (wParam) {
        case VK_UP:
        case 'S':
          return Input::Forward;
        case VK_DOWN:
        case 'D':
          return Input::Backward;
        case VK_LEFT:
        case 'H':
          return Input::Left;
        case VK_RIGHT:
        case 'L':
          return Input::Right;
        case 'A':
          return Input::LStep;
        case 'F':
          return Input::RStep;
        case VK_RETURN:
        case 'K':
          return Input::Jump;
        case VK_SPACE:
        case 'J':
          return Input::Fire;
      }
      return Input::Unknown;
    }
    void Close() {
      SendMessage(
        hWnd,
        WM_CLOSE,
        0,
        0
      );
    }
    LRESULT CALLBACK WndProc(
      HWND hWnd,
      UINT msg,
      WPARAM wParam,
      LPARAM lParam
    ) {
      static bool joyIsCaptured = false;
      #define WIN_ON_AXIS(LKEY, RKEY, HILO) JOY_ON_AXIS(\
        LKEY,\
        RKEY,\
        INT16_MIN + HILO(lParam),\
        graphics\
      )
      #define KEY_BUTTON(BUTTON) static_cast<UINT>(wParam) & BUTTON
      #define KEY_ON(KEY, VALUE) graphics->onKey(\
        Input::KEY,\
        VALUE\
      )
      #define KEY_CHECK(CHECKS, KEY) {\
        static bool needOff = false;\
        if (CHECKS) {\
          KEY_ON(\
            KEY,\
            needOff = true\
          );\
        } else if (needOff) {\
          KEY_ON(\
            KEY,\
            needOff = false\
          );\
        }\
      }
      switch (msg) {
        case WM_CREATE:
          if (
            joyGetNumDevs() > 0
          ) {
            static JOYINFO joyinfo;
            joyIsCaptured = joyGetPos(
              JOYSTICKID1,
              &joyinfo
            ) != JOYERR_UNPLUGGED && !joySetCapture(
              hWnd,
              JOYSTICKID1,
              NULL,
              FALSE
            );
          }
          break;
        case MM_JOY1MOVE:
          if (joyIsCaptured) {
            WIN_ON_AXIS(
              LStep,
              RStep,
              LOWORD
            );
            WIN_ON_AXIS(
              Forward,
              Backward,
              HIWORD
            );
            KEY_CHECK(
              KEY_BUTTON(JOY_BUTTON2)
              ||
              KEY_BUTTON(JOY_BUTTON7)
              ||
              KEY_BUTTON(JOY_BUTTON8),
              Fire
            );
            KEY_CHECK(
              KEY_BUTTON(JOY_BUTTON4)
              ||
              KEY_BUTTON(JOY_BUTTON5)
              ||
              KEY_BUTTON(JOY_BUTTON6),
              Jump
            );
            if (
              KEY_BUTTON(JOY_BUTTON9)
              ||
              KEY_BUTTON(JOY_BUTTON10)
            ) {
              Close();
            }
          }
          break; 
        case MM_JOY1ZMOVE:
          if (joyIsCaptured) {
            WIN_ON_AXIS(
              Left,
              Right,
              LOWORD
            );
            KEY_CHECK(
              KEY_BUTTON(JOY_BUTTON1),
              Left
            );
            KEY_CHECK(
              KEY_BUTTON(JOY_BUTTON3),
              Right
            );
          }
          break; 
        case WM_KEYDOWN:
          switch (wParam) {
            case VK_ESCAPE:
              Close();
              break;
            case 'W':
              SwitchWire();
          }
        case WM_KEYUP:
          graphics->onKey(
            Translate(wParam),
            msg == WM_KEYDOWN
          );
          break;
        case WM_PAINT:
          OnPaint();
          break;
        case WM_SIZE:
          OnResize(
            LOWORD(lParam),
            HIWORD(lParam)
          );
          PostMessage(
            hWnd,
            WM_PAINT,
            0,
            0
          );
          break;
        case WM_SYSCOMMAND:
          if (wParam == SC_CLOSE) {
            Close();
            return 0;
          }
          break;
        case WM_DESTROY:
          PostQuitMessage(0);
          return 0;
      }
      #undef KEY_CHECK
      #undef KEY_ON
      #undef KEY_BUTTON
      #undef WIN_ON_AXIS
      return DefWindowProc(
        hWnd,
        msg,
        wParam,
        lParam
      );
    }
  #else
    const Input::Key Translate(const KeySym keySym) {
      switch (keySym) {
        case XK_Up:
        case XK_S:
        case XK_s:
          return Input::Forward;
        case XK_Down:
        case XK_D:
        case XK_d:
          return Input::Backward;
        case XK_Left:
        case XK_H:
        case XK_h:
          return Input::Left;
        case XK_Right:
        case XK_L:
        case XK_l:
          return Input::Right;
        case XK_A:
        case XK_a:
          return Input::LStep;
        case XK_F:
        case XK_f:
          return Input::RStep;
        case XK_Return:
        case XK_K:
        case XK_k:
          return Input::Jump;
        case XK_space:
        case XK_J:
        case XK_j:
          return Input::Fire;
      }
      return Input::Unknown;
    }
  #endif
}

Graphics& Graphics::onLoop() {
  OnPaint();
  return *this;
}

Graphics* Graphics::get() {
  if (graphics == nullptr) {
    throw std::runtime_error("wrong time");
  } else {
    return graphics;
  }
}

Graphics::Graphics(
  const char* name,
  const double R,
  const Color& color
) : R(R), name(
  std::string(name) += " | play.ru.net"
) {
  if (graphics == nullptr) {
    graphics = this;
  } else {
    throw std::runtime_error("there can be only one");
  }
  #ifdef _MSC_VER
    static WNDCLASS wc;
    ZeroMemory(
      &wc,
      sizeof(WNDCLASS)
    );
    wc.hIcon = LoadIcon(
      wc.hInstance = GetModuleHandle(NULL),
      "icon"
    );
    wc.hCursor = LoadCursor(
      NULL,
      IDC_ARROW
    );
    wc.lpfnWndProc = WndProc;
    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOWFRAME);
    wc.lpszClassName = name;
    if (
      RegisterClass(&wc) && (
        hWnd = CreateWindow(
          wc.lpszClassName,
          this->name.c_str(),
          WS_OVERLAPPEDWINDOW,
          CW_USEDEFAULT,
          CW_USEDEFAULT,
          CW_USEDEFAULT,
          CW_USEDEFAULT,
          NULL,
          NULL,
          wc.hInstance,
          NULL
        )
      )
    ) {
      if (
        hDC = GetDC(hWnd)
      ) {
        PIXELFORMATDESCRIPTOR pfd;
        ZeroMemory(
          &pfd,
          sizeof(pfd)
        );
        pfd.nSize = sizeof(pfd);
        pfd.nVersion = 1;
        pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
        pfd.iPixelType = PFD_TYPE_RGBA;
        pfd.cColorBits = 24;
        pfd.cDepthBits = 16;
        pfd.iLayerType = PFD_MAIN_PLANE;
        const int format = ChoosePixelFormat(
          hDC,
          &pfd
        );
        SetPixelFormat(
          hDC,
          format,
          &pfd
        );
        if (
          hRC = wglCreateContext(hDC)
        ) {
          wglMakeCurrent(
            hDC,
            hRC
          );
        } else {
          throw std::runtime_error("can't create GL context");
        }
      } else {
        throw std::runtime_error("can't get HDC");
      }
      if (
        glewInit() != GLEW_OK
      ) {
        throw std::runtime_error("can't init GLEW");
      }
      static RECT rect;
      GetWindowRect(
        GetDesktopWindow(),
        &rect
      );
      const unsigned int width = rect.right / 2;
      const unsigned int height = rect.bottom / 2;
      MoveWindow(
        hWnd,
        rect.left + (rect.right - width) / 2,
        rect.top + (rect.bottom - height) / 2,
        width,
        height,
        TRUE
      );
    } else {
      throw std::runtime_error("can't create window");
    }
  #else
    if (
      (
        dpy = XOpenDisplay(NULL)
      ) == NULL
    ) {
      throw std::runtime_error("cannot connect to X server");
    }
    XGetWindowAttributes(
      dpy,
      root = DefaultRootWindow(dpy),
      &gwa
    );
    if (
      (
        vi = glXChooseVisual(
          dpy,
          0,
          att
        )
      ) == NULL
    ) {
      throw std::runtime_error("no appropriate visual found");
    }
    swa.colormap = (
      cmap = XCreateColormap(
        dpy,
        root,
        vi->visual,
        AllocNone
      )
    );
    swa.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask;
    win = XCreateWindow(
      dpy,
      root,
      0,
      0,
      gwa.width,
      gwa.height,
      0,
      vi->depth,
      InputOutput,
      vi->visual,
      CWColormap | CWEventMask,
      &swa
    );
    XMapWindow(
      dpy,
      win
    );
    XStoreName(
      dpy,
      win,
      this->name.c_str()
    );
    glXMakeCurrent(
      dpy,
      win,
      glc = glXCreateContext(
        dpy,
        vi,
        NULL,
        GL_TRUE
      )
    );
    js = open("/dev/input/js0", O_RDONLY | O_NONBLOCK);
  #endif
  glEnable(GL_DEPTH_TEST); 
  glClearColor(
    color.r,
    color.g,
    color.b,
    0
  );
  glTexParameterf(
    GL_TEXTURE_2D,
    GL_TEXTURE_MAG_FILTER,
    GL_LINEAR 
  );
  glTexParameterf(
    GL_TEXTURE_2D,
    GL_TEXTURE_MIN_FILTER,
    GL_LINEAR 
  );
  glTexEnvf(
    GL_TEXTURE_ENV,
    GL_TEXTURE_ENV_MODE,
    GL_MODULATE
  );
  glPixelStorei(
    GL_UNPACK_ALIGNMENT,
    1
  );
  SwitchWire();
  #ifdef GL_VERSION_3_0
    gltInit();
    text = gltCreateText();
  #endif
}

Graphics::~Graphics() {
  #ifdef GL_VERSION_3_0
    gltDeleteText(text);
    gltTerminate();
  #endif
  #ifdef _MSC_VER
    wglMakeCurrent(
      NULL,
      NULL
    );
    wglDeleteContext(hRC);
    ReleaseDC(
      hWnd,
      hDC
    );
    CloseWindow(hWnd);
    DestroyWindow(hWnd);
  #else
    close(js);
    glXMakeCurrent(
      dpy,
      None,
      NULL
    );
    glXDestroyContext(
      dpy,
      glc
    );
    XDestroyWindow(
      dpy,
      win
    );
    XCloseDisplay(dpy);
  #endif
  graphics = nullptr;
}

Graphics& Graphics::render() {
  frames++;
  return *this;
}

Graphics& Graphics::loop() {
  #ifdef _MSC_VER
    ShowWindow(
      hWnd,
      SW_SHOWMAXIMIZED
    );
    UpdateWindow(hWnd);
    static MSG msg;
    ZeroMemory(
      &msg,
      sizeof(MSG)
    );
    do {
      if (
        PeekMessage(
          &msg,
          NULL,
          0,
          0,
          PM_REMOVE
        )
      ) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      } else {
        onLoop();
      }
    } while (msg.message != WM_QUIT);
  #else
    Atom wmDeleteMessage = XInternAtom(
      dpy,
      "WM_DELETE_WINDOW",
      true
    );
    XSetWMProtocols(
      dpy,
      win,
      &wmDeleteMessage,
      1
    );
    bool loop = true;
    do {
      while (
        XCheckMaskEvent(
          dpy,
          swa.event_mask,
          &xev
        )
      ) {
        if (xev.type == Expose) {
          XGetWindowAttributes(
            dpy,
            win,
            &gwa
          );
          OnResize(
            gwa.width,
            gwa.height
          );
          OnPaint();
        } else {
          const KeySym keySym = XLookupKeysym(
            &xev.xkey,
            0
          );
          if (keySym == XK_Escape) {
            loop = false;
            break;
          } else {
            const bool isDown = xev.type == KeyPress;
            switch (keySym) {
              case XK_W:
              case XK_w:
                if (isDown) {
                  SwitchWire();
                }
                break;
              default:
                onKey(
                  Translate(keySym),
                  isDown
                );
            }
          }
        }
      }
      if (js) {
        static js_event event;
        while (
          read(
            js,
            &event,
            sizeof(event)
          ) == sizeof(event)
        ) {
          switch (event.type) {
            case JS_EVENT_BUTTON:
              switch (event.number) {
                case 0:
                  onKey(
                    Left,
                    event.value
                  );
                  break;
                case 2:
                  onKey(
                    Right,
                    event.value
                  );
                  break;
                case 1:
                case 6:
                case 7:
                  onKey(
                    Fire,
                    event.value
                  );
                  break;
                case 3:
                case 4:
                case 5:
                  onKey(
                    Jump,
                    event.value
                  );
                  break;
                case 8:
                case 9:
                  if (event.value) {
                    loop = false;
                  }
              }
              break;
            case JS_EVENT_AXIS:
              {
                size_t axis = event.number / 2;
                if (axis < 3) {
                  #define JS_ON_AXIS(LKEY, RKEY) JOY_ON_AXIS(\
                    LKEY,\
                    RKEY,\
                    event.value,\
                    this\
                  )
                  switch (axis) {
                    case 0:
                    case 2:
                      if (event.number % 2 == 0) {
                        JS_ON_AXIS(
                          LStep,
                          RStep
                        );
                      } else {
                        JS_ON_AXIS(
                          Forward,
                          Backward
                        );
                      }
                      break;
                    case 1:
                      if (event.number % 2 == 0) {
                        JS_ON_AXIS(
                          Left,
                          Right
                        );
                      }
                  }
                  #undef JS_ON_AXIS
                }
              }
          }
        }
      }
      if (loop) {
        onLoop();
      }
    } while (loop);
  #endif
  return *this;
}

#undef JOY_ON_AXIS

Graphics& Graphics::apply(const Matrix& m) {
  glMultMatrixd(m._16);
  return *this;
}

Graphics& Graphics::push() {
  glPushMatrix();
  return *this;
}

Graphics& Graphics::pop() {
  glPopMatrix();
  return *this;
}

Graphics& Graphics::setView(
  const Vector& from,
  const Vector& to,
  const Vector& up
) {
  glLoadIdentity();
  gluLookAt(
    from.x,
    from.y,
    from.z,
    to.x,
    to.y,
    to.z,
    up.x,
    up.y,
    up.z
  );
  return *this;
}

const Graphics& Graphics::setText(const char* text) const {
  std::ostringstream oss;
  oss << name;
  if (text != nullptr) {
    oss << " " << text;
  }
  oss << std::ends;
  #ifdef _MSC_VER
    SetWindowText(
      hWnd,
      oss.str().c_str()
    );
  #else
    XStoreName(
      dpy,
      win,
      oss.str().c_str()
    );
  #endif
  #ifdef GL_VERSION_3_0
    gltSetText(
      ::text,
      text
    );
  #endif
  CenterPointer();
  return *this;
}
