/*************************************************************************
 *                                                                       *
 * Open Dynamics Engine, Copyright (C) 2001,2002 Russell L. Smith.       *
 * All rights reserved.  Email: russ@q12.org   Web: www.q12.org          *
 *                                                                       *
 * This library is free software; you can redistribute it and/or         *
 * modify it under the terms of EITHER:                                  *
 *   (1) The GNU Lesser General Public License as published by the Free  *
 *       Software Foundation; either version 2.1 of the License, or (at  *
 *       your option) any later version. The text of the GNU Lesser      *
 *       General Public License is included with this library in the     *
 *       file LICENSE.TXT.                                               *
 *   (2) The BSD-style license that is included with this library in     *
 *       the file LICENSE-BSD.TXT.                                       *
 *                                                                       *
 * This library 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 files    *
 * LICENSE.TXT and LICENSE-BSD.TXT for more details.                     *
 *                                                                       *
 *************************************************************************/

// main window and event handling for X11

#define GL_GLEXT_PROTOTYPES

#include <png.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/keysym.h>
#include <GL/glx.h>
#include <GL/glext.h>

#include "DPRSim.hxx"
#include "graphics.h"
#include "internal.h"

//***************************************************************************
// error handling for unix

static void printMessage (char *msg1, char *msg2, va_list ap)
{
  fflush (stderr);
  fflush (stdout);
  fprintf (stderr,"\n%s: ",msg1);
  vfprintf (stderr,msg2,ap);
  fprintf (stderr,"\n");
  fflush (stderr);
}

void dsError (char *msg, ...)
{
  va_list ap;
  va_start (ap,msg);
  printMessage ("Error",msg,ap);
  exit (1);
}

void dsDebug (char *msg, ...)
{
  va_list ap;
  va_start (ap,msg);
  printMessage ("INTERNAL ERROR",msg,ap);
  // *((char *)0) = 0;	 ... commit SEGVicide ?
  abort();
}

void dsPrint (char *msg, ...)
{
  va_list ap;
  va_start (ap,msg);
  vprintf (msg,ap);
}


//***************************************************************************
// openGL window

// X11 display info
Display *display=0;
static int screen=0;
static XVisualInfo *visual=0;		// best visual for openGL
static Colormap colormap=0;		// window's colormap
static Atom wm_protocols_atom = 0;
static Atom wm_delete_window_atom = 0;

// window and openGL
Window win=0;			// X11 window, 0 if not initialized
static Window parent=0;
static GLXContext glx_context=0;	// openGL rendering context
#ifdef CATOM_CAMERA
static GLXContext thr_glx_context=0;	// openGL rendering context
static GLuint fbuffer;
static GLuint dbuffer;
static GLuint cbuffer;
#endif
static GLXFBConfig *fbconfig=NULL;
static int pAttribList[] = {
              GLX_PBUFFER_WIDTH, pwidth,
              GLX_PBUFFER_HEIGHT, pheight,
              GLX_LARGEST_PBUFFER, False,
              None};
static int fbAttribList[] = {
              GLX_RENDER_TYPE, GLX_RGBA_BIT,
              GLX_DOUBLEBUFFER, True,
              GLX_DEPTH_SIZE, 16,
              GLX_DRAWABLE_TYPE, GLX_PBUFFER_BIT | GLX_WINDOW_BIT,
              None};
int width=0,height=0;		// window size
int pwidth=256,pheight=256;
int writeframes=0;		// 1 if frame files to be written

static void createMainWindow()
{
  int fblsize;
  int major, minor;
  int errorBase, eventBase;
  bool GLX;

  // GL visual & fbconfig attributes
  if (strcasecmp(worldPtr->search_key_value_list("CATOM_CAMERA_WIDTH").c_str(), "") != 0) {
    pAttribList[1] = pwidth = atoi(worldPtr->search_key_value_list("CATOM_CAMERA_WIDTH").c_str());
  }
  if (strcasecmp(worldPtr->search_key_value_list("CATOM_CAMERA_HEIGHT").c_str(), "") != 0) {
    pAttribList[3] = pheight = atoi(worldPtr->search_key_value_list("CATOM_CAMERA_HEIGHT").c_str());
  }

  // Connect to the X11 display
  display = XOpenDisplay (NULL);
  if (!display)
    dsError ("can not open X11 display");
  screen = DefaultScreen(display);

  // Check for GLX
  GLX = glXQueryExtension(display, &errorBase, &eventBase);
  if (!GLX) {
    dsError("dprsimgr requires GLX to display the world\n");
  }

  // Check the server GLX version to make sure it is >= 1.3
  glXQueryVersion(display, &major, &minor);
  // dsPrint("GLX version %d.%d\n", major, minor);
  if (major < 1 || (major == 1 && minor < 3)) {
    dsPrint("GLX version 1.3 or higher required for dprsimgr -- you have %d.%d\n", major, minor);
  }

  // Get a list of frame buffer configurations
  fbconfig = glXChooseFBConfig(display, screen, fbAttribList, &fblsize);
  if (fblsize < 1) {
    dsError("Unable to find appropriate fbconfig");
  }

  // Extract a visual from the first frame buffer configuration 
  visual = glXGetVisualFromFBConfig(display, fbconfig[0]); 
  if (!visual) 
    dsError ("no good X11 visual found for OpenGL");

  // initialize variables
  win = 0;
  glx_context = 0;
  if (width < 1 || height < 1) 
    dsDebug (0,"bad window width or height");

  // create colormap
  colormap = XCreateColormap(display, RootWindow(display,screen), 
                             visual->visual, AllocNone);

  // create the parent GTK window
  parent = create_gtk_win();

  if (parent == 0 || (strcasecmp(worldPtr->search_key_value_list("SEPERATE_WINDOWS").c_str(), "true") == 0)) {
    parent = RootWindow(display, screen);
  }

  // create the OpenGL window
  XSetWindowAttributes attributes;
  attributes.background_pixel = BlackPixel(display, screen);
  attributes.colormap = colormap;
  attributes.event_mask = ButtonPressMask | ButtonReleaseMask |
    KeyPressMask | KeyReleaseMask | ButtonMotionMask | StructureNotifyMask;
  win = XCreateWindow(display, parent, 0, 0, width, height,
		       0, visual->depth, InputOutput, visual->visual,
		       CWBackPixel | CWColormap | CWEventMask, &attributes);

  // associate a GLX context with the window
  glx_context = glXCreateContext(display, visual, 0, GL_TRUE);
  if (!glx_context) 
    dsError ("can't make an OpenGL context");

  // set the window title
  XTextProperty window_name;
  window_name.value = (unsigned char *) WINDOW_TITLE;
  window_name.encoding = XA_STRING;
  window_name.format = 8;
  window_name.nitems = strlen((char *) window_name.value);
  XSetWMName (display,win,&window_name);

  // participate in the window manager 'delete yourself' protocol
  wm_protocols_atom = XInternAtom (display,"WM_PROTOCOLS",False);
  wm_delete_window_atom = XInternAtom (display,"WM_DELETE_WINDOW",False);
  if (XSetWMProtocols (display,win,&wm_delete_window_atom,1)==0)
    dsError ("XSetWMProtocols() call failed");

  // pop up the window
  XMapWindow (display,win);
  XSync (display,win);
}


static void destroyMainWindow()
{
  glXMakeCurrent(display, None, NULL);
#ifdef CATOM_CAMERA
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); 
  glDeleteFramebuffersEXT(1, &fbuffer);
  glDeleteRenderbuffersEXT(1, &dbuffer);
  glDeleteRenderbuffersEXT(1, &cbuffer);
  glXDestroyContext(display, thr_glx_context);
#endif
  XFree(fbconfig);
  glXDestroyContext(display, glx_context);
  XDestroyWindow(display, win);
  XSync(display, 0);
  display = 0;
  win = 0;
  glx_context = 0;
}

static void handleEvent (XEvent &event)
{
  switch (event.type) {
    case ButtonPress:
      handleMouseClick(event.xbutton.button, event.xbutton.x, event.xbutton.y);
      return;

    case ButtonRelease:
      handleMouseRelease(event.xbutton.button, event.xbutton.x, event.xbutton.y);
      return;

    case MotionNotify:
      handleMouseMotion(event.xmotion.x, event.xmotion.y);
      return;

    case KeyPress:
      KeySym key;
      bool ctrl;
      XLookupString (&event.xkey,NULL,0,&key,0);
      ctrl = (event.xkey.state & ControlMask) ? true : false;
      handleKeyPress(key, ctrl);
      return;

    case KeyRelease:
      return;

    case ClientMessage:
      if (event.xclient.message_type == wm_protocols_atom &&
          event.xclient.format == 32 &&
          Atom(event.xclient.data.l[0]) == wm_delete_window_atom) {
          simStop();
      }
      return;

    case ConfigureNotify:
      width = event.xconfigure.width;
      height = event.xconfigure.height;
      return;
  }
}


#if 0
// return the index of the highest bit
static int getHighBitIndex (unsigned int x)
{
  int i = 0;
  while (x) {
    i++;
    x >>= 1;
  }
  return i-1;
}
#endif

// shift x left by i, where i can be positive or negative
#define SHIFTL(x,i) (((i) >= 0) ? ((x) << (i)) : ((x) >> (-i)))

static void captureFrame (int num)
{
  char s[100];
  char p[100];
  char com[255];
  char del[255];
  sprintf (s,"frame/frame%04d.ppm",num);
  sprintf (p,"frame/frame%04d.png",num);
  sprintf (com,"convert %s %s",s,p);
  sprintf (del,"rm -f %s",s);

  unsigned char *image_buf;
  image_buf = new unsigned char[width*height*4];
  glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, image_buf);
  
  FILE *f = fopen (s,"wb");
  if (!f) dsError ("can't open \"%s\" for writing",s);
  fprintf (f,"P6\n%d %d\n255\n",width,height);
#if 0
  XImage *image = XGetImage (display,win,0,0,width,height,~0,ZPixmap);

  int rshift = 7 - getHighBitIndex (image->red_mask);
  int gshift = 7 - getHighBitIndex (image->green_mask);
  int bshift = 7 - getHighBitIndex (image->blue_mask);
#endif

  for (int y=0; y<height; y++) {
    for (int x=0; x<width; x++) {
#if 0
      unsigned long pixel = XGetPixel (image,x,y);
      unsigned char b[3];
      b[0] = SHIFTL(pixel & image->red_mask,rshift);
      b[1] = SHIFTL(pixel & image->green_mask,gshift);
      b[2] = SHIFTL(pixel & image->blue_mask,bshift);
      fwrite (b,3,1,f);
#endif
      fwrite ((image_buf+(x+((height-y-1)*width))*3), 3, 1, f);
      }
  }
  fclose (f);
  delete [] image_buf;
#if 0
  XDestroyImage (image);
#endif
  
  //png hack!!!
  system(com);
  system(del);
}

static inline void reapEvent()
{
  XEvent event;
  XNextEvent(display, &event);
  handleEvent(event);
}

void dsPlatformReapEvents()
{
  while (XPending (display)) {
    reapEvent();
  }
}

void dsPlatformPreStart() {
  dsPrint("dsPlatformPreStart...\n");
  if (!XInitThreads()) {
    dsError("X11 is not thread safe on this machine!\n");
  }
}

void dsPlatformThreadStart() {
}

void dsPlatformStart() {
  width = strtol(worldPtr->res_x.c_str(), 0, 0 );
  height = strtol(worldPtr->res_y.c_str(), 0, 0 );
  createMainWindow();

  // Set up the second glx context
#ifdef CATOM_CAMERA
  thr_glx_context = glXCreateContext(display, visual, glx_context, GL_TRUE);
  glXMakeCurrent(display, win, thr_glx_context);
  glGenFramebuffersEXT(1, &fbuffer);
  glGenRenderbuffersEXT(1, &dbuffer);
  glGenRenderbuffersEXT(1, &cbuffer);
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbuffer);
  glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, dbuffer);
  glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, pwidth, pheight);
  glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, dbuffer);
  glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, cbuffer);
  glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_RGB, pwidth, pheight);
  glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_RENDERBUFFER_EXT, cbuffer);
#endif

  // Attach to the primary for this thread
  glXMakeCurrent(display, win, glx_context);
  fprintf (stderr,
	   "Keyboard CTRL Commands:\n"
	   "   Ctrl-S : toggle shadows (or say `SHADOWS=false' on command line).\n"
	   "   Ctrl-N : toggle last two GUID digits printed on catom surfaces.\n"
	   "   Ctrl-H : toggle speech bubbles (or say `speech=no' on command line).\n"
	   "   Ctrl-V : print current viewpoint coordinates (x,y,z,h,p,r).\n"
	   "   Ctrl-W : write frames to png files: frame/frameNNNN.png\n"
	   "   Ctrl-P : pause. (or PAUSED=true on command line).\n"
	   "   Ctrl-X : exit with save.\n"
	   "   Ctrl-C : exit without save.\n"
	   "\n"
	   "Mouse/Keyboard Controls:\n"
	   "   Keyboard Arrow keys :  move along camera defined direction.\n"
	   "   Left button : pan and tilt.\n"
	   "   Right button : select Catom under cursor.\n"
	   "   Left + Right button (or middle button) : sideways and up.\n"
	   "   Mouse scroll : move along Z-axis (up and down).\n"
	   "\n");
}

void dsPlatformTick()
{
  static int frame = 1;  

  glXSwapBuffers(display, win);
  XSync(display, false);
  
  if (!simGetPaused() && writeframes) {
    captureFrame (frame);
    frame++;
  }
}

void dsPlatformGetViewWrap(float xyz[3], float hpr[3], void *data) {
  dsPlatformGetView(xyz, hpr, data);
  glXMakeCurrent(display, win, glx_context);
}

void dsPlatformGetView(float xyz[3], float hpr[3], void *data)
{
#ifdef CATOM_CAMERA
  if (!glXMakeCurrent(display, win, thr_glx_context)) {
    dsError("Error attaching to thr_glx_context!\n");
  }
  dsDrawFrame(xyz, hpr, pwidth, pheight); 
  glFlush(); 
  glReadPixels(0, 0, pwidth, pheight, GL_RGB, GL_UNSIGNED_BYTE, data);
  glXMakeCurrent(display, None, NULL);
#else
  dsError("Catom camera support not compiled in\n");
#endif
}

void dsPlatformEnd()
{
  destroyMainWindow();
  destroy_gtk_win();
}
