/*
 * zoom.cpp
 *
 * Copyright (C) 2009 Matthias Schiffer <matthias@gamezock.de>
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include "Game.h"
#include "MathUtil.h"
#include "config.h"
#include "gl.h"

#ifdef _WIN32
#else
#include <iostream>
#include <X11/X.h>
#include <X11/extensions/XInput2.h>
#include <GL/glx.h>
#include <sys/time.h>
#include <unistd.h>
#endif


#ifdef _WIN32

bool WGLInit(HINSTANCE hInstance, HWND *hWnd, HDC *hDC, HGLRC *hRC);
void WGLUninit(HWND hWnd, HDC hDC, HGLRC hRC);
LRESULT CALLBACK wndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

bool WGLInit(HINSTANCE hInstance, HWND *hWnd, HDC *hDC, HGLRC *hRC) {
  WNDCLASS wc;
  wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
  wc.lpfnWndProc = (WNDPROC)wndProc;
  wc.cbClsExtra = 0;
  wc.cbWndExtra = 0;
  wc.hInstance = hInstance;
  wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  wc.hCursor = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = 0;
  wc.lpszMenuName = 0;
  wc.lpszClassName = "Zoom";

  if (!RegisterClass(&wc)) {
    MessageBox(0, "Failed To Register The Window Class.", "ERROR", MB_OK|MB_ICONEXCLAMATION);
    return false;
  }

  DWORD dwExStyle = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;
  DWORD dwStyle = WS_OVERLAPPEDWINDOW;

  RECT windowRect;
  windowRect.left = 0;
  windowRect.right = DEFAULT_WIDTH;
  windowRect.top = 0;
  windowRect.bottom = DEFAULT_HEIGHT;

  AdjustWindowRectEx(&windowRect, dwStyle, FALSE, dwExStyle);

  *hWnd = CreateWindowEx(dwExStyle, "Zoom" /* Class Name */, "Zoom" /* Title*/, WS_CLIPSIBLINGS | WS_CLIPCHILDREN | dwStyle,
                        0, 0, windowRect.right-windowRect.left, windowRect.bottom-windowRect.top, 0, 0, hInstance, 0);
  if(!*hWnd) {
    MessageBox(0, "Window Creation Error.", "ERROR", MB_OK|MB_ICONEXCLAMATION);
    return false;
  }

  static PIXELFORMATDESCRIPTOR pfd = {
      sizeof(PIXELFORMATDESCRIPTOR), // Size Of This Pixel Format Descriptor
      1,                             // Version Number
      PFD_DRAW_TO_WINDOW |           // Format Must Support Window
      PFD_SUPPORT_OPENGL |           // Format Must Support OpenGL
      PFD_DOUBLEBUFFER,              // Must Support Double Buffering
      PFD_TYPE_RGBA,                 // Request An RGBA Format
      16,                            // Select Our Color Depth
      0, 0, 0, 0, 0, 0,              // Color Bits Ignored
      0,                             // No Alpha Buffer
      0,                             // Shift Bit Ignored
      0,                             // No Accumulation Buffer
      0, 0, 0, 0,                    // Accumulation Bits Ignored
      16,                            // 16Bit Z-Buffer (Depth Buffer)
      0,                             // No Stencil Buffer
      0,                             // No Auxiliary Buffer
      PFD_MAIN_PLANE,                // Main Drawing Layer
      0,                             // Reserved
      0, 0, 0                        // Layer Masks Ignored
  };

  *hDC=GetDC(*hWnd);
  if (!*hDC) {
    WGLUninit(*hWnd, 0, 0);
    MessageBox(0, "Can't Create A GL Device Context.", "ERROR", MB_OK|MB_ICONEXCLAMATION);
    return false;
  }

  GLuint pixelFormat = ChoosePixelFormat(*hDC, &pfd);
  if(!pixelFormat) {
    WGLUninit(*hWnd, *hDC, 0);
    MessageBox(0, "Can't Find A Suitable PixelFormat.", "ERROR", MB_OK|MB_ICONEXCLAMATION);
    return false;
  }

  if(!SetPixelFormat(*hDC, pixelFormat, &pfd)) {
    WGLUninit(*hWnd, *hDC, 0);
    MessageBox(0, "Can't Set The PixelFormat.", "ERROR", MB_OK|MB_ICONEXCLAMATION);
    return false;
  }

  *hRC=wglCreateContext(*hDC);
  if(!*hRC) {
    WGLUninit(*hWnd, *hDC, 0);
    MessageBox(0, "Can't Create A GL Rendering Context.", "ERROR", MB_OK|MB_ICONEXCLAMATION);
    return false;
  }

  if(!wglMakeCurrent(*hDC, *hRC)) {
    WGLUninit(*hWnd, *hDC, *hRC);
    MessageBox(0, "Can't Activate The GL Rendering Context.", "ERROR", MB_OK|MB_ICONEXCLAMATION);
    return false;
  }

  ShowWindow(*hWnd, SW_SHOW);
  SetForegroundWindow(*hWnd);
  SetFocus(*hWnd);

  return true;
}

void WGLUninit(HWND hWnd, HDC hDC, HGLRC hRC) {
  wglMakeCurrent(0, 0);

  if(hRC)
    wglDeleteContext(hRC);

  if(hDC)
    ReleaseDC(hWnd, hDC);

  if(hWnd)
    DestroyWindow(hWnd);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
  HWND hWnd = 0;
  HDC hDC = 0;
  HGLRC hRC = 0;

  if(!WGLInit(hInstance, &hWnd, &hDC, &hRC)) {
    return 0;
  }

  glewInit();

  resize(DEFAULT_WIDTH, DEFAULT_HEIGHT);

  bool running = true;
  MSG msg;

  while(running) {
    unsigned long delta = 0;
    unsigned long ticks = GetTickCount();

    while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
      if(msg.message == WM_QUIT)
      {
        running = false;
        break;
      }
      else              // If Not, Deal With Window Messages
      {
        TranslateMessage(&msg);       // Translate The Message
        DispatchMessage(&msg);        // Dispatch The Message
      }
    }

    delta = GetTickCount()-ticks;

    if(delta < MIN_FRAME_DELTA) {
      Sleep(MIN_FRAME_DELTA - delta);
      delta = GetTickCount()-ticks;
    }

    ticks += delta;

    static DisplayClass render;
    render.renderScene(delta);
    SwapBuffers(hDC);
  }

  WGLUninit(hWnd, hDC, hRC);
  return msg.wParam;
}

LRESULT CALLBACK wndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
  switch(uMsg) {
    case WM_CLOSE:
      PostQuitMessage(0);
      return 0;

    case WM_SIZE:
      resize(LOWORD(lParam), HIWORD(lParam));
      return 0;

    default:
      return DefWindowProc(hWnd, uMsg, wParam, lParam);
  }
}

#else

bool XIInit(Display *disp, int *opcode) {
  int event, error;
  if (!XQueryExtension(disp, "XInputExtension", opcode, &event, &error)) {
    std::cerr << "X Input extension not available." << std::endl;
    return false;
  }

  int major = 2, minor = 0;
  if (XIQueryVersion(disp, &major, &minor) == BadRequest) {
    std::cerr << "XI2 not available. Server supports " << major << "." << minor << std::endl;
    return false;
  }

  return true;
}

bool GLXinit(Display *disp, Atom windele, Window *wnd, GLXContext *gc, int *xi_opcode, int *pointer) {
  static const int msAttributeList[] = {GLX_RENDER_TYPE, GLX_RGBA_BIT,
                                        GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT,
                                        GLX_X_RENDERABLE, True,
                                        GLX_DOUBLEBUFFER, True,
                                        GLX_DEPTH_SIZE, 1,
                                        GLX_STENCIL_SIZE, 1,
                                        GLX_SAMPLE_BUFFERS, 1,
                                        GLX_SAMPLES, 4,
                                        None};

  static const int attributeList[] = {GLX_RENDER_TYPE, GLX_RGBA_BIT,
                                      GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT,
                                      GLX_X_RENDERABLE, True,
                                      GLX_DOUBLEBUFFER, True,
                                      GLX_DEPTH_SIZE, 1,
                                      GLX_STENCIL_SIZE, 1,
                                      None};
  
  if(!XIInit(disp, xi_opcode))
    return false;

  bool ok = false;

  int nElements;
  GLXFBConfig *fbConfig = glXChooseFBConfig(disp, DefaultScreen(disp), msAttributeList, &nElements);
  if(!fbConfig || !nElements) {
    if(fbConfig) {
      XFree(fbConfig);
    }

    fbConfig = glXChooseFBConfig(disp, DefaultScreen(disp), attributeList, &nElements);
  }

  if(fbConfig && nElements) {
    XVisualInfo *vi = glXGetVisualFromFBConfig(disp, *fbConfig);

    if(vi) {
      Colormap cmap = XCreateColormap(disp, RootWindow(disp, vi->screen), vi->visual, AllocNone);
      XSetWindowAttributes swa;
      swa.colormap = cmap;
      swa.border_pixel = 0;
      swa.event_mask = StructureNotifyMask | KeyPressMask | KeyReleaseMask/* | PointerMotionMask | ButtonPressMask | ButtonReleaseMask*/;

      *wnd = XCreateWindow(disp, RootWindow(disp, vi->screen), 0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT, 0, vi->depth, InputOutput,
          vi->visual, CWBorderPixel|CWColormap|CWEventMask, &swa);

      XClassHint chint;
      chint.res_name = const_cast<char*>("Zoom");
      chint.res_class = const_cast<char*>("zoom");
      XSetClassHint(disp, *wnd, &chint);

      char *str = const_cast<char*>("Zoom");
      XTextProperty name;
      if(XStringListToTextProperty(&str, 1, &name) != 0)
      {
        XSetWMName(disp, *wnd, &name);
        XFree(name.value);
      }

      XSetWMProtocols(disp, *wnd, &windele, 1);

      XColor color;
      color.pixel = WhitePixel(disp, vi->screen);
      XQueryColor(disp, cmap, &color);
      Pixmap pixmap = XCreatePixmap(disp, *wnd, 1, 1, 1);
      Cursor cursor = XCreatePixmapCursor(disp, pixmap, pixmap, &color, &color, 0, 0);
      XSync(disp, False);

      XFreePixmap(disp, pixmap);

      XMapWindow(disp, *wnd);
      XSync(disp, False);

      XIGetClientPointer(disp, *wnd, pointer);

      XIEventMask eventmask;
      unsigned char mask[4] = {0};

      eventmask.deviceid = 2;
      eventmask.mask_len = sizeof(mask);
      eventmask.mask = mask;
      /*XISetMask(mask, XI_ButtonPress);
      XISetMask(mask, XI_Motion);
      XISetMask(mask, XI_KeyPress);*/
      XISetMask(mask, XI_RawMotion);
      XIGrabDevice(disp, *pointer, *wnd, CurrentTime, cursor, GrabModeAsync, GrabModeAsync, True, &eventmask);

      *gc = glXCreateContext(disp, vi, NULL, True);

      glXMakeCurrent(disp, *wnd, *gc);

      ok = true;

      XFree(vi);
    }
  }

  if(fbConfig)
    XFree(fbConfig);

  XSync(disp, 0);

  return ok;
}

int main() {
  Display *disp = XOpenDisplay(0);
  Atom windele = XInternAtom(disp, "WM_DELETE_WINDOW", False);
  Window wnd;
  GLXContext gc;

  int xi_opcode;
  int pointer;
  if(!GLXinit(disp, windele, &wnd, &gc, &xi_opcode, &pointer))
    return 1;

  glewInit();

  Zoom::Game game;
  game.resize(DEFAULT_WIDTH, DEFAULT_HEIGHT);

  GLint samples;
  glGetIntegerv(GL_SAMPLES, &samples);
  std::cerr << "Using " << samples << " samples" << std::endl;

  bool running = true;
  
  unsigned long delta = 0;
  unsigned long frames = 0, tocks = 0;
  unsigned input = 0;
  
  struct timeval tv;
  gettimeofday(&tv, NULL);
  unsigned long ticks = tv.tv_usec;

  while(running) {
    while(XPending(disp)) {
      XEvent event;
      XNextEvent(disp, &event);
      switch(event.type) {
        case ConfigureNotify:
          game.resize(event.xconfigure.width, event.xconfigure.height);
          break;

        case ClientMessage:
          if(static_cast<Atom>(event.xclient.data.l[0]) == windele)
            running = false;
          break;

        case KeyPress:
          switch(XKeycodeToKeysym(disp, event.xkey.keycode, 0)) {
            case XK_Up: case XK_w: input |= Zoom::Game::FORWARD; break;
            case XK_Down: case XK_s: input |= Zoom::Game::BACKWARD; break;
            case XK_Left: case XK_a: input |= Zoom::Game::LEFT; break;
            case XK_Right: case XK_d: input |= Zoom::Game::RIGHT; break;
            case XK_Escape: running = false; break;
          }
          break;

        case KeyRelease:
          switch(XKeycodeToKeysym(disp, event.xkey.keycode, 0)) {
            case XK_Up: case XK_w: input &= ~Zoom::Game::FORWARD; break;
            case XK_Down: case XK_s: input &= ~Zoom::Game::BACKWARD; break;
            case XK_Left: case XK_a: input &= ~Zoom::Game::LEFT; break;
            case XK_Right: case XK_d: input &= ~Zoom::Game::RIGHT; break;
          }
          break;

        case GenericEvent:
          if(event.xcookie.extension == xi_opcode && XGetEventData(disp, &event.xcookie)) {
            switch(event.xcookie.evtype) {
              case XI_RawMotion:
                XIRawEvent *rawEvent = reinterpret_cast<XIRawEvent*>(event.xcookie.data);

                float deltaX = 0, deltaY = 0;
                double *rawValuator = rawEvent->raw_values;

                if(rawEvent->valuators.mask_len >= 1) {
                  if(XIMaskIsSet(rawEvent->valuators.mask, 0)) {
                    deltaX = (float)*rawValuator;
                    ++rawValuator;
                  }

                  if(XIMaskIsSet(rawEvent->valuators.mask, 1)) {
                    deltaY = (float)*rawValuator;
                    ++rawValuator;
                  }
                }

                if(deltaX != 0 || deltaY != 0)
                  game.turn(deltaX, deltaY);
            }

            XFreeEventData(disp, &event.xcookie);
          }
      }
    }

    if(!running) break;

    game.setInput(input);
    game.run(delta);

    game.render();
    glXSwapBuffers(disp, wnd);
    XSync(disp, 0);

    long slept = 0;
    gettimeofday(&tv, NULL);
    delta = ((tv.tv_usec + 1000000 - ticks)%1000000)/1000;

    if(delta < MIN_FRAME_DELTA) {
      usleep((MIN_FRAME_DELTA-delta)*1000);
      slept += (MIN_FRAME_DELTA-delta);

      gettimeofday(&tv, NULL);
      delta = ((tv.tv_usec + 1000000 - ticks)%1000000)/1000;
    }

    ticks = (ticks + delta*1000) % 1000000;

    frames++;
    tocks += delta*1000;
    if(tocks > 1000000) {
      std::cerr << frames << " fps; slept a total of " << slept << " ms" << std::endl;
      frames = 0;
      tocks -= 1000000;
      slept = 0;
    }
  }

  XIUngrabDevice(disp, pointer, CurrentTime);
  XDestroyWindow(disp, wnd);
  glXDestroyContext(disp, gc);
  XCloseDisplay(disp);

  return 0;
}

#endif
