/*
===========================================================================

Doom 3 GPL Source Code
Copyright (C) 1999-2011 id Software LLC, a ZeniMax Media company. 

This file is part of the Doom 3 GPL Source Code (?Doom 3 Source Code?).  

Doom 3 Source Code is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Doom 3 Source Code 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with Doom 3 Source Code.  If not, see <http://www.gnu.org/licenses/>.

In addition, the Doom 3 Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 Source Code.  If not, please request a copy in writing from id Software at the address below.

If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.

===========================================================================
*/
#include "../../idlib/precompiled.h"
#include "../../renderer/tr_local.h"
#include "local.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

extern "C" {
#	include "libXNVCtrl/NVCtrlLib.h"
}

idCVar sys_videoRam( "sys_videoRam", "0", CVAR_SYSTEM | CVAR_ARCHIVE | CVAR_INTEGER, "Texture memory on the video card (in megabytes) - 0: autodetect", 0, 512 );

Display *dpy = NULL;
static int scrnum = 0;

Window win = 0;

bool dga_found = false;

static GLXContext ctx = NULL;

static bool vidmode_ext = false;
static int vidmode_MajorVersion = 0, vidmode_MinorVersion = 0;	// major and minor of XF86VidExtensions

static XF86VidModeModeInfo **vidmodes;
static int num_vidmodes;
static bool vidmode_active = false;

// backup gamma ramp
static int save_rampsize = 0;
static unsigned short *save_red, *save_green, *save_blue;

void GLimp_WakeBackEnd(void *a) {
	common->DPrintf("GLimp_WakeBackEnd stub\n");
}

#ifdef ID_GL_HARDLINK
void GLimp_EnableLogging(bool log) {
	static bool logging;
	if (log != logging)
	{
		common->DPrintf("GLimp_EnableLogging - disabled at compile time (ID_GL_HARDLINK)\n");
		logging = log;
	}
}
#endif

void GLimp_FrontEndSleep() {
	common->DPrintf("GLimp_FrontEndSleep stub\n");
}

void *GLimp_BackEndSleep() {
	common->DPrintf("GLimp_BackEndSleep stub\n");
	return 0;
}

bool GLimp_SpawnRenderThread(void (*a) ()) {
	common->DPrintf("GLimp_SpawnRenderThread stub\n");
	return false;
}

void GLimp_ActivateContext() {
	assert( dpy );
	assert( ctx );
	qglXMakeCurrent( dpy, win, ctx );
}

void GLimp_DeactivateContext() {
	assert( dpy );
	qglXMakeCurrent( dpy, None, NULL );
}

/*
=================
GLimp_SaveGamma

save and restore the original gamma of the system
=================
*/
void GLimp_SaveGamma() {
	if ( save_rampsize ) {
		return;
	}

	assert( dpy );
	
	XF86VidModeGetGammaRampSize( dpy, scrnum, &save_rampsize);
	save_red = (unsigned short *)malloc(save_rampsize*sizeof(unsigned short));
	save_green = (unsigned short *)malloc(save_rampsize*sizeof(unsigned short));
	save_blue = (unsigned short *)malloc(save_rampsize*sizeof(unsigned short));
	XF86VidModeGetGammaRamp( dpy, scrnum, save_rampsize, save_red, save_green, save_blue);
}

/*
=================
GLimp_RestoreGamma

save and restore the original gamma of the system
=================
*/
void GLimp_RestoreGamma() {
	if (!save_rampsize)
		return;
	
	XF86VidModeSetGammaRamp( dpy, scrnum, save_rampsize, save_red, save_green, save_blue);
	
	free(save_red); free(save_green); free(save_blue);
	save_rampsize = 0;
}

/*
=================
GLimp_SetGamma

gamma ramp is generated by the renderer from r_gamma and r_brightness for 256 elements
the size of the gamma ramp can not be changed on X (I need to confirm this)
=================
*/
void GLimp_SetGamma(unsigned short red[256], unsigned short green[256], unsigned short blue[256]) {
	if ( dpy ) {		
		int size;
		
		GLimp_SaveGamma();
		XF86VidModeGetGammaRampSize( dpy, scrnum, &size);
		common->DPrintf("XF86VidModeGetGammaRampSize: %d\n", size);
		if ( size > 256 ) {
			// silly generic resample
			int i;
			unsigned short *l_red, *l_green, *l_blue;
			l_red = (unsigned short *)malloc(size*sizeof(unsigned short));
			l_green = (unsigned short *)malloc(size*sizeof(unsigned short));
			l_blue = (unsigned short *)malloc(size*sizeof(unsigned short));
			//int r_size = 256;
			int r_i; float r_f;
			for(i=0; i<size-1; i++) {
				r_f = (float)i*255.0f/(float)(size-1);
				r_i = (int)floor(r_f);
				r_f -= (float)r_i;
				l_red[i] = (int)round((1.0f-r_f)*(float)red[r_i]+r_f*(float)red[r_i+1]);
				l_green[i] = (int)round((1.0f-r_f)*(float)green[r_i]+r_f*(float)green[r_i+1]);
				l_blue[i] = (int)round((1.0f-r_f)*(float)blue[r_i]+r_f*(float)blue[r_i+1]);				
			}
			l_red[size-1] = red[255]; l_green[size-1] = green[255]; l_blue[size-1] = blue[255];
			XF86VidModeSetGammaRamp( dpy, scrnum, size, l_red, l_green, l_blue );
			free(l_red); free(l_green); free(l_blue);
		} else {
			XF86VidModeSetGammaRamp( dpy, scrnum, size, red, green, blue );
		}
	}
}

void GLimp_Shutdown() {
	if ( dpy ) {
		
		Sys_XUninstallGrabs();
	
		GLimp_RestoreGamma();

		qglXDestroyContext( dpy, ctx );
		
#if !defined( ID_GL_HARDLINK )
		GLimp_dlclose();
#endif
		
		XDestroyWindow( dpy, win );
		if ( vidmode_active ) {
			XF86VidModeSwitchToMode( dpy, scrnum, vidmodes[0] );
		}

		XFlush( dpy );

		// FIXME: that's going to crash
		//XCloseDisplay( dpy );

		vidmode_active = false;
		dpy = NULL;
		win = 0;
		ctx = NULL;
	}
}

void GLimp_SwapBuffers() {
	assert( dpy );
	qglXSwapBuffers( dpy, win );
}

/*
GLX_TestDGA
Check for DGA	- update in_dgamouse if needed
*/
void GLX_TestDGA() {
	int dga_MajorVersion = 0, dga_MinorVersion = 0;

	assert( dpy );

#if defined( ID_ENABLE_DGA )
	if ( !XF86DGAQueryVersion( dpy, &dga_MajorVersion, &dga_MinorVersion ) ) {
		// unable to query, probalby not supported
		common->Printf( "Failed to detect DGA DirectVideo Mouse\n" );
		cvarSystem->SetCVarBool( "in_dgamouse", false );
		dga_found = false;
	} else {
		common->Printf( "DGA DirectVideo Mouse (Version %d.%d) initialized\n",
				   dga_MajorVersion, dga_MinorVersion );
		dga_found = true;
	}
#else
    dga_found = false;
#endif
}

/*
** XErrorHandler
**   the default X error handler exits the application
**   I found out that on some hosts some operations would raise X errors (GLXUnsupportedPrivateRequest)
**   but those don't seem to be fatal .. so the default would be to just ignore them
**   our implementation mimics the default handler behaviour (not completely cause I'm lazy)
*/
int idXErrorHandler(Display * l_dpy, XErrorEvent * ev) {
	char buf[1024];
	common->Printf( "Fatal X Error:\n" );
	common->Printf( "  Major opcode of failed request: %d\n", ev->request_code );
	common->Printf( "  Minor opcode of failed request: %d\n", ev->minor_code );
	common->Printf( "  Serial number of failed request: %lu\n", ev->serial );
	XGetErrorText( l_dpy, ev->error_code, buf, 1024 );
	common->Printf( "%s\n", buf );
	return 0;
}

bool GLimp_OpenDisplay( void ) {
	if ( dpy ) {
		return true;
	}

	if ( cvarSystem->GetCVarInteger( "net_serverDedicated" ) == 1 ) {
		common->DPrintf( "not opening the display: dedicated server\n" );
		return false;
	}

	common->Printf( "Setup X display connection\n" );

	// that should be the first call into X
	if ( !XInitThreads() ) {
		common->Printf("XInitThreads failed\n");
		return false;
	}
	
	// set up our custom error handler for X failures
	XSetErrorHandler( &idXErrorHandler );

	if ( !( dpy = XOpenDisplay(NULL) ) ) {
		common->Printf( "Couldn't open the X display\n" );
		return false;
	}
	scrnum = DefaultScreen( dpy );
	return true;
}

/*
===============
GLX_Init
===============
*/
int GLX_Init(glimpParms_t a) {
	int attrib[] = {
		GLX_RGBA,				// 0
		GLX_RED_SIZE, 8,		// 1, 2
		GLX_GREEN_SIZE, 8,		// 3, 4
		GLX_BLUE_SIZE, 8,		// 5, 6
		GLX_DOUBLEBUFFER,		// 7
		GLX_DEPTH_SIZE, 24,		// 8, 9
		GLX_STENCIL_SIZE, 8,	// 10, 11
		GLX_ALPHA_SIZE, 8, // 12, 13
		None
	};
	// these match in the array
#define ATTR_RED_IDX 2
#define ATTR_GREEN_IDX 4
#define ATTR_BLUE_IDX 6
#define ATTR_DEPTH_IDX 9
#define ATTR_STENCIL_IDX 11
#define ATTR_ALPHA_IDX 13
	Window root;
	XVisualInfo *visinfo;
	XSetWindowAttributes attr;
	XSizeHints sizehints;
	unsigned long mask;
	int colorbits, depthbits, stencilbits;
	int tcolorbits, tdepthbits, tstencilbits;
	int actualWidth, actualHeight;
	int i;
	const char *glstring;

	if ( !GLimp_OpenDisplay() ) {
		return false;
	}

	common->Printf( "Initializing OpenGL display\n" );

	root = RootWindow( dpy, scrnum );

	actualWidth = glConfig.vidWidth;
	actualHeight = glConfig.vidHeight;

	// Get video mode list
	if ( !XF86VidModeQueryVersion( dpy, &vidmode_MajorVersion, &vidmode_MinorVersion ) ) {
		vidmode_ext = false;
		common->Printf("XFree86-VidModeExtension not available\n");
	} else {
		vidmode_ext = true;
		common->Printf("Using XFree86-VidModeExtension Version %d.%d\n",
				   vidmode_MajorVersion, vidmode_MinorVersion);
	}

	GLX_TestDGA();

	if ( vidmode_ext ) {
		int best_fit, best_dist, dist, x, y;

		XF86VidModeGetAllModeLines( dpy, scrnum, &num_vidmodes, &vidmodes );

		// Are we going fullscreen?  If so, let's change video mode
		if ( a.fullScreen ) {
			best_dist = 9999999;
			best_fit = -1;

			for (i = 0; i < num_vidmodes; i++) {
				if (a.width > vidmodes[i]->hdisplay ||
					a.height > vidmodes[i]->vdisplay)
					continue;

				x = a.width - vidmodes[i]->hdisplay;
				y = a.height - vidmodes[i]->vdisplay;
				dist = (x * x) + (y * y);
				if (dist < best_dist) {
					best_dist = dist;
					best_fit = i;
				}
			}

			if (best_fit != -1) {
				actualWidth = vidmodes[best_fit]->hdisplay;
				actualHeight = vidmodes[best_fit]->vdisplay;

				// change to the mode
				XF86VidModeSwitchToMode(dpy, scrnum, vidmodes[best_fit]);
				vidmode_active = true;

				// Move the viewport to top left
				// FIXME: center?
				XF86VidModeSetViewPort(dpy, scrnum, 0, 0);

				common->Printf( "Free86-VidModeExtension Activated at %dx%d\n", actualWidth, actualHeight );

			} else {
				a.fullScreen = false;
				common->Printf( "Free86-VidModeExtension: No acceptable modes found\n" );
			}
		} else {
			common->Printf( "XFree86-VidModeExtension: not fullscreen, ignored\n" );
		}
	}
	// color, depth and stencil
	colorbits = 24;
	depthbits = 24;
	stencilbits = 8;

	for (i = 0; i < 16; i++) {
		// 0 - default
		// 1 - minus colorbits
		// 2 - minus depthbits
		// 3 - minus stencil
		if ((i % 4) == 0 && i) {
			// one pass, reduce
			switch (i / 4) {
			case 2:
				if (colorbits == 24)
					colorbits = 16;
				break;
			case 1:
				if (depthbits == 24)
					depthbits = 16;
				else if (depthbits == 16)
					depthbits = 8;
			case 3:
				if (stencilbits == 24)
					stencilbits = 16;
				else if (stencilbits == 16)
					stencilbits = 8;
			}
		}

		tcolorbits = colorbits;
		tdepthbits = depthbits;
		tstencilbits = stencilbits;

		if ((i % 4) == 3) {		// reduce colorbits
			if (tcolorbits == 24)
				tcolorbits = 16;
		}

		if ((i % 4) == 2) {		// reduce depthbits
			if (tdepthbits == 24)
				tdepthbits = 16;
			else if (tdepthbits == 16)
				tdepthbits = 8;
		}

		if ((i % 4) == 1) {		// reduce stencilbits
			if (tstencilbits == 24)
				tstencilbits = 16;
			else if (tstencilbits == 16)
				tstencilbits = 8;
			else
				tstencilbits = 0;
		}

		if (tcolorbits == 24) {
			attrib[ATTR_RED_IDX] = 8;
			attrib[ATTR_GREEN_IDX] = 8;
			attrib[ATTR_BLUE_IDX] = 8;
		} else {
			// must be 16 bit
			attrib[ATTR_RED_IDX] = 4;
			attrib[ATTR_GREEN_IDX] = 4;
			attrib[ATTR_BLUE_IDX] = 4;
		}
		
		attrib[ATTR_DEPTH_IDX] = tdepthbits;	// default to 24 depth
		attrib[ATTR_STENCIL_IDX] = tstencilbits;

		visinfo = qglXChooseVisual(dpy, scrnum, attrib);
		if (!visinfo) {
			continue;
		}

		common->Printf( "Using %d/%d/%d Color bits, %d Alpha bits, %d depth, %d stencil display.\n",
			 attrib[ATTR_RED_IDX], attrib[ATTR_GREEN_IDX],
			 attrib[ATTR_BLUE_IDX], attrib[ATTR_ALPHA_IDX],
			 attrib[ATTR_DEPTH_IDX],
			 attrib[ATTR_STENCIL_IDX]);

		glConfig.colorBits = tcolorbits;
		glConfig.depthBits = tdepthbits;
		glConfig.stencilBits = tstencilbits;
		break;
	}

	if (!visinfo) {
		common->Printf("Couldn't get a visual\n");
		return false;
	}
	// window attributes
	attr.background_pixel = BlackPixel(dpy, scrnum);
	attr.border_pixel = 0;
	attr.colormap = XCreateColormap(dpy, root, visinfo->visual, AllocNone);
	attr.event_mask = X_MASK;
	if (vidmode_active) {
		mask = CWBackPixel | CWColormap | CWSaveUnder | CWBackingStore |
			CWEventMask | CWOverrideRedirect;
		attr.override_redirect = True;
		attr.backing_store = NotUseful;
		attr.save_under = False;
	} else {
		mask = CWBackPixel | CWBorderPixel | CWColormap | CWEventMask;
	}

	win = XCreateWindow(dpy, root, 0, 0,
						actualWidth, actualHeight,
						0, visinfo->depth, InputOutput,
						visinfo->visual, mask, &attr);

	XStoreName(dpy, win, GAME_NAME);

	// don't let the window be resized
	// FIXME: allow resize (win32 does)
	sizehints.flags = PMinSize | PMaxSize;
	sizehints.min_width = sizehints.max_width = actualWidth;
	sizehints.min_height = sizehints.max_height = actualHeight;

	XSetWMNormalHints(dpy, win, &sizehints);

	XMapWindow( dpy, win );

	if ( vidmode_active ) {
		XMoveWindow( dpy, win, 0, 0 );
	}

	XFlush(dpy);
	XSync(dpy, False);
	ctx = qglXCreateContext(dpy, visinfo, NULL, True);
	XSync(dpy, False);

	// Free the visinfo after we're done with it
	XFree(visinfo);

	qglXMakeCurrent(dpy, win, ctx);

	glstring = (const char *) qglGetString(GL_RENDERER);
	common->Printf("GL_RENDERER: %s\n", glstring);
	
	glstring = (const char *) qglGetString(GL_EXTENSIONS);
	common->Printf("GL_EXTENSIONS: %s\n", glstring);

	// FIXME: here, software GL test

	glConfig.isFullscreen = a.fullScreen;
	
	if ( glConfig.isFullscreen ) {
		Sys_GrabMouseCursor( true );
	}
	
	return true;
}

/*
===================
GLimp_Init

This is the platform specific OpenGL initialization function.  It
is responsible for loading OpenGL, initializing it,
creating a window of the appropriate size, doing
fullscreen manipulations, etc.  Its overall responsibility is
to make sure that a functional OpenGL subsystem is operating
when it returns to the ref.

If there is any failure, the renderer will revert back to safe
parameters and try again.
===================
*/
bool GLimp_Init( glimpParms_t a ) {

	if ( !GLimp_OpenDisplay() ) {
		return false;
	}
	
#ifndef ID_GL_HARDLINK
	if ( !GLimp_dlopen() ) {
		return false;
	}
#endif
	
	if (!GLX_Init(a)) {
		return false;
	}
	
	return true;
}

/*
===================
GLimp_SetScreenParms
===================
*/
bool GLimp_SetScreenParms( glimpParms_t parms ) {
	return true;
}

/*
================
Sys_GetVideoRam
returns in megabytes
open your own display connection for the query and close it
using the one shared with GLimp_Init is not stable
================
*/
int Sys_GetVideoRam( void ) {
	static int run_once = 0;
	int major, minor, value;
	Display *l_dpy;
	int l_scrnum;

	if ( run_once ) {
		return run_once;
	}

	if ( sys_videoRam.GetInteger() ) {
		run_once = sys_videoRam.GetInteger();
		return sys_videoRam.GetInteger();
	}

	// try a few strategies to guess the amount of video ram
	common->Printf( "guessing video ram ( use +set sys_videoRam to force ) ..\n" );
	if ( !GLimp_OpenDisplay( ) ) {
		run_once = 64;
		return run_once;
	}
	l_dpy = dpy;
	l_scrnum = scrnum;
	// go for nvidia ext first
	if ( XNVCTRLQueryVersion( l_dpy, &major, &minor ) ) {
		common->Printf( "found XNVCtrl extension %d.%d\n", major, minor );
		if ( XNVCTRLIsNvScreen( l_dpy, l_scrnum ) ) {
			if ( XNVCTRLQueryAttribute( l_dpy, l_scrnum, 0, NV_CTRL_VIDEO_RAM, &value ) ) {
				run_once = value / 1024;
				return run_once;
			} else {
				common->Printf( "XNVCtrlQueryAttribute NV_CTRL_VIDEO_RAM failed\n" );
			}
		} else {
			common->Printf( "default screen %d is not controlled by NVIDIA driver\n", l_scrnum );
		}
	}
	// try ATI /proc read ( for the lack of a better option )
	int fd;
	if ( ( fd = open( "/proc/dri/0/umm", O_RDONLY ) ) != -1 ) {
		int len;
		char umm_buf[ 1024 ];
		char *line;
		if ( ( len = read( fd, umm_buf, 1024 ) ) != -1 ) {
			// should be way enough to get the full file
			// grab "free  LFB = " line and "free  Inv = " lines
			umm_buf[ len-1 ] = '\0';
			line = umm_buf;
			line = strtok( umm_buf, "\n" );
			int total = 0;
			while ( line ) {
				if ( strlen( line ) >= 13 && strstr( line, "max   LFB =" ) == line ) {
					total += atoi( line + 12 );
				} else if ( strlen( line ) >= 13 && strstr( line, "max   Inv =" ) == line ) {
					total += atoi( line + 12 );
				}
				line = strtok( NULL, "\n" );
			}
			if ( total ) {
				run_once = total / 1048576;
				// round to the lower 16Mb
				run_once &= ~15;
				return run_once;
			}
		} else {
			common->Printf( "read /proc/dri/0/umm failed: %s\n", strerror( errno ) );
		}
	}
	common->Printf( "guess failed, return default low-end VRAM setting ( 64MB VRAM )\n" );
	run_once = 64;
	return run_once;
}
