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

#ifndef toolx_X11_base_session
#define toolx_X11_base_session

// pure X11 code, no GL.

#include <ostream>
#include <vector>

#include "dispatcher"
#include <tools/forit>

#include <X11/Xatom.h>     //XA_INTEGER

#include <X11/Xutil.h>     //XVisualInfo

namespace toolx {
namespace X11 {

class base_session {
public:
  base_session(std::ostream& a_out,unsigned int a_monitor = 0)
  :m_out(a_out)
  ,m_monitor(a_monitor)
  ,m_display(0)
  ,m_WM_DELETE_WINDOW_atom(None)
  ,m_SESSION_EXIT_STEER_atom(None)
  {
    //NOTE : macOS : XOpenDisplay leaks 128 bytes.
    m_display = ::XOpenDisplay(NULL);
    if(!m_display) {
      m_out << "toolx::X11::base_session::base_session : can't open display." << std::endl;
      return;
    }

    int monitors = ::XScreenCount(m_display);
    if(int(m_monitor)>=monitors) {
      m_out << "toolx::X11::base_session::base_session : bad monitor index "
            << m_monitor << ". (#monitors " << monitors << ")." << std::endl;
      ::XCloseDisplay(m_display);
      m_display = 0;
      return;
    }

    m_WM_DELETE_WINDOW_atom = ::XInternAtom(m_display,"WM_DELETE_WINDOW",False);
    if(m_WM_DELETE_WINDOW_atom==None) {
      m_out << "toolx::X11::base_session::base_session : can't create WM_DELETE_WINDOW Atom." << std::endl;
      ::XCloseDisplay(m_display);
      m_display = 0;
      return;
    }

    m_SESSION_EXIT_STEER_atom = ::XInternAtom(m_display,"TOOLX_X11_SESSION_EXIT_STEER",False);
    if(m_SESSION_EXIT_STEER_atom==None) {
      m_out << "toolx::X11::base_session::base_session :"
            << " can't create TOOLX_X11_SESSION_EXIT_STEER Atom." << std::endl;
      ::XCloseDisplay(m_display);
      m_display = 0;
      return;
    }
  }
  virtual ~base_session() {
    clear_dispatchers();
    if(m_display) ::XCloseDisplay(m_display);
    m_display = 0;
  }
protected:
  base_session(const base_session& a_from)
  :m_out(a_from.m_out)
  ,m_monitor(a_from.m_monitor)
  ,m_display(0)
  ,m_WM_DELETE_WINDOW_atom(None)
  ,m_SESSION_EXIT_STEER_atom(None)
  {}
  base_session& operator=(const base_session& a_from){
    if(&a_from==this) return *this;
    return *this;
  }
public:
  std::ostream& out() const {return m_out;}

  Atom WM_DELETE_WINDOW_atom() const {return m_WM_DELETE_WINDOW_atom;}
  Atom SESSION_EXIT_STEER_atom() const {return m_SESSION_EXIT_STEER_atom;}

  bool is_valid() const {return m_display?true:false;}

  unsigned int monitor() const {return m_monitor;}
  Display* display() const {return m_display;}

  bool steer() {
    if(!m_display) return false;

    while(true) {
      XEvent xevent;
      ::XNextEvent(m_display,&xevent);
      if(xevent.type==ClientMessage) {
        if(xevent.xclient.data.l[0]==(long)m_SESSION_EXIT_STEER_atom) break;
      }
      dispatch(xevent);
    }

    return true;
  }

  bool sync() {
    if(!m_display) return false;
    ::XSync(m_display,False);
    while(true) {
      XEvent xevent;
      if(::XPending(m_display)) {
        ::XNextEvent(m_display,&xevent);
        if(xevent.type==ClientMessage) {
          if(xevent.xclient.data.l[0]==(long)m_SESSION_EXIT_STEER_atom) return false;
        }
        dispatch(xevent);
      } else {
        break;
      }
    }
    return true;
  }

  bool event_pending(bool& a_is) {
    if(!m_display) {a_is = false;return false;}
    a_is = ::XPending(m_display)?true:false;
    return true;
  }

  bool next_event(bool& a_to_exit) {
    if(!m_display) {a_to_exit = false;return false;}
    XEvent xevent;
    ::XNextEvent(m_display,&xevent);
    if(xevent.type==ClientMessage) {
      if(xevent.xclient.data.l[0]==(long)m_SESSION_EXIT_STEER_atom) {
        a_to_exit = true;
        return true;
      }
    }
    dispatch(xevent);
    a_to_exit = false;
    return true;
  }

  bool flush() {
    if(!m_display) return false;
    ::XFlush(m_display);
    return true;
  }


  //////////////////////////////////////////////////
  //////////////////////////////////////////////////
  //////////////////////////////////////////////////
  Window create_window(const char* a_title,int a_x,int a_y,unsigned int a_width,unsigned int a_height) {
    if(!m_display) return 0L;

    XSetWindowAttributes swa;
    swa.event_mask = StructureNotifyMask | ExposureMask
       | ButtonPressMask | ButtonReleaseMask | ButtonMotionMask
       | PointerMotionMask
       | KeyPressMask;

    swa.background_pixel = ::XWhitePixel(m_display,m_monitor);

    Window window = ::XCreateWindow(m_display,
                                    ::XRootWindow(m_display,m_monitor),
                                    a_x,a_y,a_width,a_height,
				    0,
                                    (int)CopyFromParent,
                                    InputOutput,
                                    (Visual*)CopyFromParent,
				    CWEventMask|CWBackPixel,&swa);
    if(window==0L) {
      m_out << "toolx::X11::base_session::create_window : can't create a X11 window." << std::endl;
      return 0L;
    }

    XTextProperty tp;
    ::XStringListToTextProperty((char**)&a_title,1,&tp);
    XSizeHints sh;
    sh.flags = USPosition | USSize;
    ::XSetWMProperties(m_display,window,&tp,&tp,0,0,&sh,0,0);
    ::XFree(tp.value);

    ::XSetWMProtocols(m_display,window,&m_WM_DELETE_WINDOW_atom,1);
    return window;
  }

  void map_raise_window(Window a_window) const {
    if(!m_display) return;
    ::XMapWindow(m_display,a_window);
    ::XRaiseWindow(m_display,a_window);
  }

  void show_window(Window a_window) const {
    if(!m_display) return;

    XWindowAttributes watbs;
    ::XGetWindowAttributes(m_display,a_window,&watbs);
    if(watbs.map_state!=IsUnmapped) return;

    ::XMapWindow(m_display,a_window);
    ::XRaiseWindow(m_display,a_window);

   {XEvent event;
    ::XIfEvent(m_display,&event,wait_map_notify,(char*)a_window);}
  }

  void hide_window(Window a_window) const {
    if(!m_display) return;

    XWindowAttributes watbs;
    ::XGetWindowAttributes(m_display,a_window,&watbs);
    if(watbs.map_state==IsUnmapped) return;

    ::XUnmapWindow(m_display,a_window);

   {XEvent event;
    ::XIfEvent(m_display,&event,wait_unmap_notify,(char*)a_window);}
  }

  void resize_window(Window a_window,unsigned int a_width,unsigned int a_height) const {
    if(!m_display) return;
    unsigned int mask = CWWidth | CWHeight | CWBorderWidth;
    XWindowChanges changes;
    changes.border_width = 0;
    changes.width = a_width;
    changes.height = a_height;
    ::XConfigureWindow(m_display,a_window,mask,&changes);
  }

  bool window_size(Window a_window,int& a_width,int& a_height) const {
    if(!m_display) {a_width = 0;a_height = 0;return false;}
    XWindowAttributes watbs;
    if(!XGetWindowAttributes(m_display,a_window,&watbs)) {a_width = 0;a_height = 0;return false;}
    a_width  = watbs.width;
    a_height = watbs.height;
    return true;
  }

  void delete_window(Window a_window) const {
    if(!m_display) return;
    ::XDestroyWindow(m_display,a_window);
  }

  void set_override_redirect(Window a_window) const {
    //must be executed before window is mapped.
    if(!m_display) return;
    XSetWindowAttributes swa;
    swa.override_redirect = True;
    ::XChangeWindowAttributes(m_display,a_window,CWOverrideRedirect,&swa);
  }

  void set_wm_no_decorations(Window a_window) const {
    //must be executed before window is mapped.
    if(!m_display) return;

    // struct, mwm_hints_decorations taken from OpenMotif MwmUtils.h.
    struct{
      unsigned long flags;
      unsigned long functions;
      unsigned long decorations;
      long          inputMode;
      unsigned long status;
    } prop;

    unsigned long mwm_hints_decorations  = 1L << 1;

    Atom atom = ::XInternAtom(m_display,"_MOTIF_WM_HINTS",False);
    if(atom==None) {
      m_out << "toolx::X11::base_session::set_wm_no_decorations : can't create/get _MOTIF_WM_HINTS Atom." << std::endl;
      return;
    }
    prop.flags = mwm_hints_decorations;
    prop.functions = 0;
    prop.decorations = 0;

    ::XChangeProperty(m_display,a_window,atom,atom,32,PropModeReplace,(unsigned char*)&prop,5);
  }

  bool post(Window a_win,
            long a_0 = 0,long a_1 = 0,
            long a_2 = 0,long a_3 = 0,
            long a_4 = 0) const {
    if(!m_display) return false;
    XEvent event;
    event.type = ClientMessage;
    event.xclient.display = m_display;
    event.xclient.window = a_win;
    event.xclient.message_type = XA_INTEGER;
    event.xclient.format = 8;    /* put 8 because bug with 32 on VMCMS */
    event.xclient.data.l[0] = a_0;
    event.xclient.data.l[1] = a_1;
    event.xclient.data.l[2] = a_2;
    event.xclient.data.l[3] = a_3;
    event.xclient.data.l[4] = a_4;
    Status stat = ::XSendEvent(m_display,a_win,False,0L,&event);
    ::XFlush(m_display);
    return (stat==0?false:true);
  }

  bool post_EXIT_STEER(Window a_win) {
    return post(a_win,SESSION_EXIT_STEER_atom());
  }

  bool post_expose(Window a_win) const {
    int width,height;
    if(!window_size(a_win,width,height)) return false;
    XEvent event;
    event.type = Expose;
    event.xexpose.serial = 0;
    event.xexpose.send_event = True;
    event.xexpose.display = m_display;
    event.xexpose.window = a_win;
    event.xexpose.x = 0;
    event.xexpose.y = 0;
    event.xexpose.width = width;
    event.xexpose.height = height;
    event.xexpose.count = 0;
    //locks
    Status stat = ::XSendEvent(m_display,a_win,False,0L,&event);
    ::XFlush(m_display);
    //unlock();
    return (stat==0?false:true);
  }

  //////////////////////////////////////////////////
  //////////////////////////////////////////////////
  //////////////////////////////////////////////////
  void add_dispatcher(dispatcher* a_dispatcher) {
    m_dispatchers.push_back(a_dispatcher); //take ownership.
  }

  void invalidate_dispatchers_with_window(Window a_win){
    tools_vforit(dispatcher*,m_dispatchers,it) {
      if((*it)->window()==a_win) (*it)->invalidate();
    }
  }

  void remove_dispatchers_with_window(Window a_win){
    for(auto it = m_dispatchers.begin(); it != m_dispatchers.end();) {
      if((*it)->window()==a_win) {
        dispatcher* obj = *it;
        it = m_dispatchers.erase(it);
        delete obj;
      } else {
        it++;
      }
    }
  }

  bool dispatch(XEvent& a_event) {
   {for(auto it = m_dispatchers.begin(); it != m_dispatchers.end();) {
      if(!(*it)->is_valid()) {
        dispatcher* obj = *it;
        it = m_dispatchers.erase(it);
        delete obj;
      } else {
        it++;
      }
    }}
    tools_vforit(dispatcher*,m_dispatchers,it) {
      if((*it)->is_valid()) {
        if((*it)->dispatch(a_event)) return true;
      }
    }
    return false;
  }
protected:
  static Bool wait_map_notify(Display*,XEvent* a_event,char* a_arg){
    return (a_event->type == MapNotify) && (a_event->xmap.window == (Window)a_arg);
  }
  static Bool wait_unmap_notify(Display*,XEvent* a_event,char* a_arg){
    return (a_event->type == UnmapNotify) && (a_event->xmap.window == (Window)a_arg);
  }

  void clear_dispatchers() {
    for(auto it = m_dispatchers.begin(); it != m_dispatchers.end();) {
      dispatcher* obj = *it;
      it = m_dispatchers.erase(it);
      delete obj;
    }
    m_dispatchers.clear();
  }

protected:
  std::ostream& m_out;
  unsigned int  m_monitor;
  Display*      m_display;
  Atom          m_WM_DELETE_WINDOW_atom;
  Atom          m_SESSION_EXIT_STEER_atom;
  std::vector<dispatcher*> m_dispatchers;
};

}}


#endif

