#if defined(_WIN32)
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
#elif defined(__linux__)
# include <X11/X.h>
# include <X11/Xlib.h>
# include <X11/Xutil.h>
#endif

#include <string>

#include <GL/gl.h>
#if defined(_WIN32)
# include <GL/glu.h>
#elif defined(__linux__)
# include <GL/glx.h>
#endif

#if defined(_WIN32)
# define WINCLASS "WinAppWnd"
#endif

#define SAFE_RELEASE(p) if (p) { p->Release(); p = NULL; }

class WinApp
{
public:
    WinApp(int width, int height, std::string& window_name)
    {
        m_width       = width;
        m_height      = height;
        m_window_name = window_name;
#if defined(_WIN32)
        m_hInstance   = ::GetModuleHandle(NULL);
#endif
    }

    virtual ~WinApp()
    {
#if defined(_WIN32)
        ::UnregisterClass(WINCLASS, m_hInstance);
#endif
    }

    int create()
    {
#if defined(_WIN32)
        WNDCLASSEX wcex;

        wcex.cbSize        = sizeof(WNDCLASSEX);
        wcex.style         = CS_HREDRAW | CS_VREDRAW;
        wcex.lpfnWndProc   = &WinApp::StaticWndProc;
        wcex.cbClsExtra    = 0;
        wcex.cbWndExtra    = 0;
        wcex.hInstance     = m_hInstance;
        wcex.hIcon         = LoadIcon(0, IDI_APPLICATION);
        wcex.hCursor       = LoadCursor(0, IDC_ARROW);
        wcex.hbrBackground = 0;
        wcex.lpszMenuName  = 0L;
        wcex.lpszClassName = WINCLASS;
        wcex.hIconSm       = 0;

        ATOM wc = ::RegisterClassEx(&wcex);

        RECT rc = { 0, 0, m_width, m_height };
        ::AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, false);

        m_hWnd = ::CreateWindow(
                     (LPCTSTR)wc, m_window_name.c_str(),
                     WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
                     rc.right - rc.left, rc.bottom - rc.top,
                     NULL, NULL, m_hInstance, (void*)this);

        if (!m_hWnd)
            return -1;

        ::ShowWindow(m_hWnd, SW_SHOW);
        ::UpdateWindow(m_hWnd);
        ::SetFocus(m_hWnd);
#elif defined(__linux__)
        m_display = XOpenDisplay(NULL);

        if (m_display == NULL)
        {
            return -1;
        }

        m_WM_DELETE_WINDOW = XInternAtom(m_display, "WM_DELETE_WINDOW", False);

        static GLint visual_attributes[] = { GLX_RGBA, GLX_DEPTH_SIZE, 24, GLX_DOUBLEBUFFER, None };
        m_visual_info = glXChooseVisual(m_display, 0, visual_attributes);

        if (m_visual_info == NULL)
        {
            XCloseDisplay(m_display);
            return -2;
        }

        Window root = DefaultRootWindow(m_display);

        m_event_mask = ExposureMask | KeyPressMask;

        XSetWindowAttributes window_attributes;
        window_attributes.colormap = XCreateColormap(m_display, root, m_visual_info->visual, AllocNone);
        window_attributes.event_mask = m_event_mask;

        m_window = XCreateWindow(
            m_display, root, 0, 0, m_width, m_height, 0, m_visual_info->depth,
            InputOutput, m_visual_info->visual, CWColormap | CWEventMask, &window_attributes);

        XMapWindow(m_display, m_window);
        XSetWMProtocols(m_display, m_window, &m_WM_DELETE_WINDOW, 1);
        XStoreName(m_display, m_window, m_window_name.c_str());
#endif

        return init();
    }

    virtual void cleanup()
    {
#if defined(_WIN32)
        ::DestroyWindow(m_hWnd);
#elif defined(__linux__)
        XDestroyWindow(m_display, m_window);
        XCloseDisplay(m_display);
#endif
    }

#if defined(_WIN32)
    virtual LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) = 0;
#endif

    int run()
    {
#if defined(_WIN32)
        MSG msg;

        ::ZeroMemory(&msg, sizeof(msg));

        while (msg.message != WM_QUIT)
        {
            if (::PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE))
            {
                ::TranslateMessage(&msg);
                ::DispatchMessage(&msg);
            }
            else
            {
                idle();
            }
        }

        return static_cast<int>(msg.wParam);
#elif defined(__linux__)
        m_end_loop = false;

        do {
            XEvent e;

            if (!XCheckWindowEvent(m_display, m_window, m_event_mask, &e) || !handle_event(e))
            {
                idle();
            }
        } while (!m_end_loop);

        return 0;
#endif
    }

protected:

#if defined(_WIN32)
    static LRESULT CALLBACK StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
        WinApp* pWnd;

        if (message == WM_NCCREATE)
        {
            LPCREATESTRUCT pCreateStruct = ((LPCREATESTRUCT)lParam);
            pWnd = (WinApp*)(pCreateStruct->lpCreateParams);
            ::SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)pWnd);
        }

        pWnd = GetObjectFromWindow(hWnd);

        if (pWnd)
            return pWnd->WndProc(hWnd, message, wParam, lParam);
        else
            return ::DefWindowProc(hWnd, message, wParam, lParam);
    }

    inline static WinApp* GetObjectFromWindow(HWND hWnd)
    {
        return (WinApp*)::GetWindowLongPtr(hWnd, GWLP_USERDATA);
    }
#endif

#if defined(__linux__)
    virtual int handle_event(XEvent& e) = 0;
#endif

    virtual int init() = 0;
    virtual int render() = 0;

    virtual void idle() = 0;

#if defined(_WIN32)
    HINSTANCE     m_hInstance;
    HWND          m_hWnd;
#elif defined(__linux__)
    Display*      m_display;
    XVisualInfo*  m_visual_info;
    Window        m_window;
    long          m_event_mask;
    Atom          m_WM_DELETE_WINDOW;
    bool          m_end_loop;
#endif
    int           m_width;
    int           m_height;
    std::string   m_window_name;
    cv::TickMeter m_timer;
};
