// Copyright (C) 2010, Guy Barrand. All rights reserved.
// See the file tools.license for terms.

#ifndef toolx_Windows_glarea
#define toolx_Windows_glarea

#include <windows.h>
#include <windowsx.h>
#include <wingdi.h>

#include <tools/sg/device_interactor>
#include <tools/sg/keys>

#include <string>

#if defined(_MSC_VER) && _MSC_VER < 1900
#elif defined(__MINGW32__)
#else
#define TOOLX_WINDOWS_TOUCH
#endif

namespace toolx {
namespace Windows {

#ifdef TOOLX_WINDOWS_TOUCH
inline bool is_message_touch_event() {
  // from https://stackoverflow.com/questions/29857587/detect-if-wm-mousemove-is-caused-by-touch-pen.
  static const LONG_PTR c_SIGNATURE_MASK = 0xFFFFFF00;
  static const LONG_PTR c_MOUSEEVENTF_FROMTOUCH = 0xFF515700;
  LONG_PTR extraInfo = ::GetMessageExtraInfo();
  return ( ( extraInfo & c_SIGNATURE_MASK ) == c_MOUSEEVENTF_FROMTOUCH );
}
#endif

class glarea {
  static const std::string& s_class() {
    static const std::string s_v("toolx::Windows::glarea");
    return s_v;
  }
  static void register_class(){
    static bool s_done = false; //not const, then not thread safe.
    if(!s_done) {
      WNDCLASS         wc;
      wc.style         = CS_HREDRAW | CS_VREDRAW;
      wc.lpfnWndProc   = (WNDPROC)proc;
      wc.cbClsExtra    = 0;
      wc.cbWndExtra    = 0;
      wc.hInstance     = ::GetModuleHandle(NULL);
      wc.hIcon         = LoadIcon(NULL,IDI_APPLICATION);
      wc.hCursor       = LoadCursor(NULL,IDC_ARROW);
      wc.hbrBackground = GetSysColorBrush(COLOR_BTNFACE);
      wc.lpszMenuName  = (PTSTR)s_class().c_str();
      wc.lpszClassName = (PTSTR)s_class().c_str();
      ::RegisterClass(&wc);
      s_done = true;
    }
  }
public:
  virtual void resize(unsigned int,unsigned int){}
  virtual void paint(unsigned int,unsigned int) {}
  virtual void close(){}
  
  virtual void left_button_up(unsigned int,unsigned int) {}
  virtual void left_button_down(unsigned int,unsigned int) {}
  virtual void mouse_move(unsigned int,unsigned int,bool) {}
public:
  glarea(HWND a_parent)
  :m_parent(a_parent)
  ,m_hwnd(0)
  ,m_context(0)
  ,m_HDC(0)
  ,m_touch_available(false)
  ,m_interactor(0)
  {
    register_class();
    RECT rect;
    ::GetClientRect(m_parent,&rect);
    m_hwnd = ::CreateWindow((PTSTR)s_class().c_str(),
                             NULL,
                             WS_CHILD | WS_VISIBLE,
                             0,0,
                             rect.right-rect.left,
                             rect.bottom-rect.top,
                             m_parent,NULL,
                             GetWindowInstance(m_parent),
                             NULL);
    if(!m_hwnd) return;
    ::SetWindowLongPtr(m_hwnd,GWLP_USERDATA,LONG_PTR(this));

    // initialize OpenGL rendering :
    m_HDC = ::GetDC(m_hwnd);
    if(m_HDC && SetWindowPixelFormat(m_HDC) ) {
      m_context = ::wglCreateContext(m_HDC);
    }

#ifdef TOOLX_WINDOWS_TOUCH
   {BYTE digitizer_status = (BYTE)::GetSystemMetrics(SM_DIGITIZER);
    if((digitizer_status & (0x80 + 0x40)) == 0) {
      m_touch_available = false;
    } else {
      m_touch_available = true;
      if(!::RegisterTouchWindow(m_hwnd,0)) m_touch_available = false;
    }}
#endif

  }
  virtual ~glarea(){
    if(::wglGetCurrentContext()!=NULL) ::wglMakeCurrent(NULL,NULL);
    if(m_context)        {
      ::wglDeleteContext(m_context);
      m_context = 0;
    }
    if(m_HDC && m_hwnd) ::ReleaseDC(m_hwnd,m_HDC);
    if(m_hwnd) {
      ::SetWindowLongPtr(m_hwnd,GWLP_USERDATA,LONG_PTR(NULL));
      ::DestroyWindow(m_hwnd);
      m_hwnd = 0;
    }
  }
protected:
  glarea(const glarea& a_from)
  :m_parent(a_from.m_parent)
  ,m_hwnd(0)
  ,m_context(0)
  ,m_HDC(0)
  ,m_touch_available(a_from.m_touch_available)
  ,m_interactor(0)
  {
    if(!m_parent) return;
    register_class();
    RECT rect;
    ::GetClientRect(m_parent,&rect);
    m_hwnd = ::CreateWindow((PTSTR)s_class().c_str(),
                             NULL,
                             WS_CHILD | WS_VISIBLE,
                             0,0,
                             rect.right-rect.left,
                             rect.bottom-rect.top,
                             m_parent,NULL,
                             GetWindowInstance(m_parent),
                             NULL);
    if(!m_hwnd) return;
    ::SetWindowLongPtr(m_hwnd,GWLP_USERDATA,LONG_PTR(this));

    // initialize OpenGL rendering :
    m_HDC = ::GetDC(m_hwnd);
    if(m_HDC && SetWindowPixelFormat(m_HDC) ) {
      m_context = ::wglCreateContext(m_HDC);
    }

#ifdef TOOLX_WINDOWS_TOUCH
    if(a_from.m_touch_available) {
      if(!::RegisterTouchWindow(m_hwnd,0)) m_touch_available = false;
    }
#endif
  }
protected:
  glarea& operator=(const glarea&){return *this;}
public:
  void set_client_area_size(unsigned int a_w,unsigned int a_h) {
    RECT rect;
    ::GetClientRect(m_hwnd,&rect);
    ::MoveWindow(m_hwnd,rect.left,rect.top,a_w,a_h,TRUE);
  }
  HWND hwnd() const {return m_hwnd;}
  void post_WM_PAINT() const {
    ::PostMessage(m_hwnd,WM_PAINT,(WPARAM)0,(LPARAM)0);
  }
  void send_WM_PAINT() const {
    ::SendMessage(m_hwnd,WM_PAINT,(WPARAM)0,(LPARAM)0);
  }
  void wm_paint() {
    PAINTSTRUCT ps;
    BeginPaint(m_hwnd,&ps);
    if(m_HDC && m_context) {
      ::wglMakeCurrent(m_HDC,m_context);

      RECT rect;
      ::GetClientRect(m_hwnd,&rect);
      unsigned int w = rect.right-rect.left;
      unsigned int h = rect.bottom-rect.top;

      paint(w,h);

      ::SwapBuffers(m_HDC);
      ::wglMakeCurrent(m_HDC,0);
    }
    EndPaint(m_hwnd,&ps);
  }
public:
  void set_device_interactor(tools::sg::device_interactor* a_interactor) {m_interactor = a_interactor;} //we do not have ownership.
protected:
  bool is_touch_event() {
#ifdef TOOLX_WINDOWS_TOUCH
    if(!m_touch_available) return false;
    return is_message_touch_event();
#else
    return false;
#endif
  }
protected:
  static LRESULT CALLBACK proc(HWND a_hwnd,UINT a_msg,WPARAM a_wparam,LPARAM a_lparam) {
    switch (a_msg) {
    case WM_SIZE:{
      int width = LOWORD(a_lparam);
      int height = HIWORD(a_lparam);
      glarea* _this = (glarea*)::GetWindowLongPtr(a_hwnd,GWLP_USERDATA);
      if(_this) {
        _this->resize(width,height);
      } else {
        // CreateWindow send a WM_SIZE but GWLP_USERDATA not yet set.
      }
    }return 0;
    case WM_PAINT:{
      glarea* _this = (glarea*)::GetWindowLongPtr(a_hwnd,GWLP_USERDATA);
      if(_this) _this->wm_paint();
    }return 0;

    case WM_KEYDOWN:{
      glarea* _this = (glarea*)::GetWindowLongPtr(a_hwnd,GWLP_USERDATA);
      if(_this) {
        if(_this->m_interactor) {
          tools::sg::key_down_event event(_this->convert(a_wparam));
          _this->m_interactor->key_press(event);
        }
      }	
    } return 0;
    case WM_KEYUP:{
      glarea* _this = (glarea*)::GetWindowLongPtr(a_hwnd,GWLP_USERDATA);
      if(_this) {
        if(_this->m_interactor) {
          tools::sg::key_up_event event(_this->convert(a_wparam));
          _this->m_interactor->key_release(event);
        }
      }	
    } return 0;

    case WM_LBUTTONDOWN:{
      glarea* _this = (glarea*)::GetWindowLongPtr(a_hwnd,GWLP_USERDATA);
      if(_this) {
        if(_this->m_interactor) {
          bool shift_modifier = ::GetKeyState(VK_SHIFT)  & 0x8000;
          bool control_modifier = ::GetKeyState(VK_CONTROL) & 0x8000;
          tools::sg::mouse_down_event event(LOWORD(a_lparam),HIWORD(a_lparam),shift_modifier,control_modifier);
          _this->m_interactor->mouse_press(event);
        } else {
          RECT rect;
          ::GetClientRect(a_hwnd,&rect);
          unsigned int h = rect.bottom-rect.top;
          if(!_this->is_touch_event()) _this->left_button_down(LOWORD(a_lparam),h-HIWORD(a_lparam));
        }
      }
    }return 0;
    case WM_LBUTTONUP:{
      glarea* _this = (glarea*)::GetWindowLongPtr(a_hwnd,GWLP_USERDATA);
      if(_this) {
        if(_this->m_interactor) {
          bool shift_modifier = ::GetKeyState(VK_SHIFT)  & 0x8000;
          bool control_modifier = ::GetKeyState(VK_CONTROL) & 0x8000;
          tools::sg::mouse_up_event event(LOWORD(a_lparam),HIWORD(a_lparam),shift_modifier,control_modifier);
          _this->m_interactor->mouse_release(event);
        } else {
          RECT rect;
          ::GetClientRect(a_hwnd,&rect);
          unsigned int h = rect.bottom-rect.top;
	  if(!_this->is_touch_event()) _this->left_button_up(LOWORD(a_lparam),h-HIWORD(a_lparam));
        }
      }
    } return 0;
    case WM_MOUSEMOVE:{
      glarea* _this = (glarea*)::GetWindowLongPtr(a_hwnd,GWLP_USERDATA);
      if(_this) {
        WPARAM state = a_wparam;
        bool ldown = ((state & MK_LBUTTON)==MK_LBUTTON)?true:false;
        if(_this->m_interactor) {
          if(ldown) {
            bool shift_modifier = ::GetKeyState(VK_SHIFT)  & 0x8000;
            bool control_modifier = ::GetKeyState(VK_CONTROL) & 0x8000;
            tools::sg::mouse_move_event event(LOWORD(a_lparam),HIWORD(a_lparam),shift_modifier,control_modifier,0,0,false);
            _this->m_interactor->mouse_move(event);
          }
        } else {
          RECT rect;
          ::GetClientRect(a_hwnd,&rect);
          unsigned int h = rect.bottom-rect.top;
          if(!_this->is_touch_event()) _this->mouse_move(LOWORD(a_lparam),h-HIWORD(a_lparam),ldown);
        }
      }

    }return 0;
    
    case WM_MOUSEWHEEL:{
      glarea* _this = (glarea*)::GetWindowLongPtr(a_hwnd,GWLP_USERDATA);
      if(_this) {
        if(_this->m_interactor) {
          bool shift_modifier = ::GetKeyState(VK_SHIFT)  & 0x8000;
          bool control_modifier = ::GetKeyState(VK_CONTROL) & 0x8000;
          POINT p;
          p.x = LOWORD(a_lparam);
          p.y = HIWORD(a_lparam);
          if(!::ScreenToClient(a_hwnd,&p)) {}
          tools::sg::wheel_rotate_event event(GET_WHEEL_DELTA_WPARAM(a_wparam),int(p.x),int(p.y),shift_modifier,control_modifier);
	  _this->m_interactor->wheel_rotate(event);
        }	  
      }	
    } return 0;
      
#ifdef TOOLX_WINDOWS_TOUCH
    case WM_TOUCH:{
      glarea* _this = (glarea*)::GetWindowLongPtr(a_hwnd,GWLP_USERDATA);
      if(_this && _this->m_touch_available) {
        RECT rect;
        ::GetClientRect(a_hwnd,&rect);
        unsigned int h = rect.bottom-rect.top;

        unsigned int num_inputs = (int)a_wparam;
        TOUCHINPUT* ti = new TOUCHINPUT[num_inputs];
        POINT p;
        if(::GetTouchInputInfo((HTOUCHINPUT)a_lparam,num_inputs,ti,sizeof(TOUCHINPUT))) {
          for(unsigned int i=0;i<num_inputs; ++i) {
            p.x = TOUCH_COORD_TO_PIXEL(ti[i].x);
            p.y = TOUCH_COORD_TO_PIXEL(ti[i].y);
            if(!::ScreenToClient(a_hwnd,&p)) {}
            if(ti[i].dwFlags & TOUCHEVENTF_DOWN) {
              _this->left_button_down(p.x,h-p.y);
            } else if (ti[i].dwFlags & TOUCHEVENTF_UP) {
              _this->left_button_up(p.x,h-p.y);
            } else if (ti[i].dwFlags & TOUCHEVENTF_MOVE) {
              bool ldown = true; //we assume that a TOUCHEVENT_DOWN had been done.
              _this->mouse_move(p.x,h-p.y,ldown);
            }
          }
        }
        ::CloseTouchInputHandle((HTOUCHINPUT)a_lparam);
        delete [] ti;
      }
    }return 0;
#endif //TOOLX_WINDOWS_TOUCH
    case WM_DESTROY:wm__destroy(a_hwnd);return 0;
    }
    return (DefWindowProc(a_hwnd,a_msg,a_wparam,a_lparam));
  }
  static bool SetWindowPixelFormat(HDC a_HDC){
    PIXELFORMATDESCRIPTOR pfd;
    pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
    pfd.nVersion = 1;
    pfd.dwFlags =
      PFD_DRAW_TO_WINDOW |
      PFD_SUPPORT_OPENGL |
      PFD_DOUBLEBUFFER |
      PFD_STEREO_DONTCARE;
    pfd.iPixelType = PFD_TYPE_RGBA;
    pfd.cColorBits = 32;
    pfd.cRedBits = 8;
    pfd.cRedShift = 16;
    pfd.cGreenBits = 8;
    pfd.cGreenShift = 8;
    pfd.cBlueBits = 8;
    pfd.cBlueShift = 0;
    pfd.cAlphaBits = 0;
    pfd.cAlphaShift = 0;
    pfd.cAccumBits = 64;
    pfd.cAccumRedBits = 16;
    pfd.cAccumGreenBits = 16;
    pfd.cAccumBlueBits = 16;
    pfd.cAccumAlphaBits = 0;
    pfd.cDepthBits = 32;
    pfd.cStencilBits = 8;
    pfd.cAuxBuffers = 0;
    pfd.iLayerType = PFD_MAIN_PLANE;
    pfd.bReserved = 0;
    pfd.dwLayerMask = 0;
    pfd.dwVisibleMask = 0;
    pfd.dwDamageMask = 0;

    int pixelIndex = ::ChoosePixelFormat(a_HDC,&pfd);
    if (pixelIndex==0) {
      // Let's choose a default index.
      pixelIndex = 1;
      if (::DescribePixelFormat(a_HDC,
                                pixelIndex,
                                sizeof(PIXELFORMATDESCRIPTOR),
                                &pfd)==0) {
        return false;
      }
    }

    if (::SetPixelFormat(a_HDC,pixelIndex,&pfd)==FALSE) return false;

    return true;
  }
  static void wm__destroy(HWND a_hwnd) {
    glarea* _this = (glarea*)::GetWindowLongPtr(a_hwnd,GWLP_USERDATA);
    if(_this) { //How to be sure that we have a glarea* ???
      _this->close();
      if(_this->m_hwnd!=a_hwnd) {
      }
      _this->m_hwnd = 0;
    }
    ::SetWindowLongPtr(a_hwnd,GWLP_USERDATA,LONG_PTR(NULL));
  }
protected:
  tools::key_code convert(WPARAM a_key) {
    if(a_key==VK_SHIFT) return tools::sg::key_shift();
    return (tools::key_code)a_key;
  }
protected:
  HWND m_parent;
  HWND m_hwnd;
  HGLRC m_context;
  HDC m_HDC;
  bool m_touch_available;
  tools::sg::device_interactor* m_interactor;
};

}}

#endif
