/* $Id: vid_so.c,v 1.16 2003/01/21 15:49:39 jaq Exp $
 *
 * Main windowed and fullscreen graphics interface module. This module
 * is used for both the software and OpenGL rendering versions of the
 * Quake refresh engine.
 *
 * Copyright (C) 1997-2001 Id Software, Inc.
 * Copyright (c) 2002 The Quakeforge Project.
 *
 * This program 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 2
 * of the License, or (at your option) any later version.
 * 
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

/* merged in from irix/vid_so.c -- jaq */
/*
#ifdef __sgi
#define SO_FILE "/etc/quake2.conf"
#endif
*/

#include <assert.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>

#ifdef HAVE_DLOPEN
# include <dlfcn.h>
#endif

#include "client.h"
#include "rw.h"

/* Structure containing functions exported from refresh DLL */
refexport_t re;

/* Console variables that we need to access from this module */
cvar_t * vid_gamma;      /* gamma value */
cvar_t * vid_ref;        /* name of refresher dll */
cvar_t * vid_xpos;       /* window position x */
cvar_t * vid_ypos;       /* window position y */
cvar_t * vid_fullscreen; /* fullscreen on or off */

/* global video state; used by other modules */
viddef_t viddef;

/* Handle to refresh DLL */
static void * reflib_library = NULL;

qboolean reflib_active = 0;

#define VID_NUM_MODES ( sizeof( vid_modes ) / sizeof( vid_modes[0] ) )

/** KEYBOARD **************************************************************/

void Do_Key_Event(int key, qboolean down);

void (*KBD_Update_fp)(void);
void (*KBD_Init_fp)(Key_Event_fp_t fp);
void (*KBD_Close_fp)(void);

/** MOUSE *****************************************************************/

in_state_t in_state;

void (*RW_IN_Init_fp)(in_state_t *in_state_p);
void (*RW_IN_Shutdown_fp)(void);
void (*RW_IN_Activate_fp)(qboolean active);
void (*RW_IN_Commands_fp)(void);
void (*RW_IN_Move_fp)(usercmd_t *cmd);
void (*RW_IN_Frame_fp)(void);

void Real_IN_Init (void);

/** CLIPBOARD *************************************************************/

char *(*RW_Sys_GetClipboardData_fp)(void);

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

DLL GLUE

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

#define	MAXPRINTMSG	4096
void VID_Printf (int print_level, char *fmt, ...)
{
	va_list		argptr;
	char		msg[MAXPRINTMSG];
	
	va_start (argptr,fmt);
	vsnprintf (msg,MAXPRINTMSG,fmt,argptr);
	va_end (argptr);

	if (print_level == PRINT_ALL)
		Com_Printf ("%s", msg);
	else
		Com_DPrintf ("%s", msg);
}

void __attribute__((noreturn)) VID_Error (int err_level, char *fmt, ...)
{
	va_list		argptr;
	char		msg[MAXPRINTMSG];
	
	va_start (argptr,fmt);
	vsnprintf (msg,MAXPRINTMSG,fmt,argptr);
	va_end (argptr);

	Com_Error (err_level,"%s", msg);
}

//==========================================================================

/*
============
VID_Restart_f

Console command to re-start the video mode and refresh DLL. We do this
simply by setting the modified flag for the vid_ref variable, which will
cause the entire video mode and refresh DLL to be reset on the next frame.
============
*/
void VID_Restart_f(void) {
	vid_ref->modified = true;
}

/*
** VID_GetModeInfo
*/
typedef struct vidmode_s {
    const char * description;
    int width, height;
    int mode;
} vidmode_t;

vidmode_t vid_modes[] = {
    { "Mode 0: 320x240",     320,  240,  0 },
    { "Mode 1: 400x300",     400,  300,  1 },
    { "Mode 2: 512x384",     512,  384,  2 },
    { "Mode 3: 640x480",     640,  480,  3 },
    { "Mode 4: 800x600",     800,  600,  4 },
    { "Mode 5: 960x720",     960,  720,  5 },
    { "Mode 6: 1024x768",   1024,  768,  6 },
    { "Mode 7: 1152x864",   1152,  864,  7 },
    { "Mode 8: 1280x1024",  1280, 1024,  8 },
    { "Mode 9: 1600x1200",  1600, 1200,  9 },
    { "Mode 10: 2048x1536", 2048, 1536, 10 },
    { "Mode 11: 1024x480",  1024,  480, 11 }, /* Sony VAIO Pocketbook */
    { "Mode 12: 1152x768",  1152,  768, 12 }, /* Apple TiBook */
    { "Mode 13: 1280x854",  1280,  854, 13 }, /* Apple TiBook */
    { "Mode 14: 1440x900",  1440,  900, 14 }  /* Apple 17" Powerbook G4 */
};

qboolean VID_GetModeInfo( unsigned int *width, unsigned int *height, int mode )
{
	if ( mode < 0 || mode >= VID_NUM_MODES )
		return false;

	*width  = vid_modes[mode].width;
	*height = vid_modes[mode].height;

	return true;
}

/*
** VID_NewWindow
*/
void VID_NewWindow(int width, int height) {
    viddef.width  = width;
    viddef.height = height;
}

void VID_FreeReflib(void) {
    if (reflib_library) {
	if (KBD_Close_fp)
	    KBD_Close_fp();
	if (RW_IN_Shutdown_fp)
	    RW_IN_Shutdown_fp();
	dlclose(reflib_library);
    }
    
    KBD_Init_fp = NULL;
    KBD_Update_fp = NULL;
    KBD_Close_fp = NULL;
    RW_IN_Init_fp = NULL;
    RW_IN_Shutdown_fp = NULL;
    RW_IN_Activate_fp = NULL;
    RW_IN_Commands_fp = NULL;
    RW_IN_Move_fp = NULL;
    RW_IN_Frame_fp = NULL;
    RW_Sys_GetClipboardData_fp = NULL;
    
    memset (&re, 0, sizeof(re));
    reflib_library = NULL;
    reflib_active  = false;
}

/*
==============
VID_LoadRefresh
==============
*/
qboolean VID_LoadRefresh(char * name) {
    refimport_t	ri;
    GetRefAPI_t GetRefAPI;
    char fn[MAX_OSPATH];
    /* char * path; */
    struct stat st;
    extern uid_t saved_euid;
	
    /* clean up a previous reflib */
    if (reflib_active) {
	re.Shutdown();
	VID_FreeReflib();
	if (KBD_Close_fp)
	    KBD_Close_fp();
	if (RW_IN_Shutdown_fp)
	    RW_IN_Shutdown_fp();
	KBD_Close_fp = NULL;
	RW_IN_Shutdown_fp = NULL;
    }

    Com_Printf( "------- Loading %s -------\n", name );

    //regain root
    seteuid(saved_euid);

    /*
    path = Cvar_Get("basedir", ".", CVAR_NOSET)->string;
    snprintf(fn, MAX_OSPATH, "%s/%s", path, name);
    */
    snprintf(fn, MAX_OSPATH, PKGLIBDIR"/%s", name);
    if (stat(fn, &st) == -1) {
	Com_Printf("LoadLibrary(\"%s\") failed: %s\n", name, strerror(errno));
	return false;
    }

    // permission checking
    if (strstr(name, "softx") == NULL &&
	strstr(name, "glx") == NULL &&
	strstr(name, "softsdl") == NULL &&
	strstr(name, "sdlgl") == NULL &&
	strstr(name, "fxgl") == NULL) { /* ref_soft doesn't require root */
	/*
	  if (st.st_uid != 0) {
	  Com_Printf( "LoadLibrary(\"%s\") failed: ref is not owned by root\n", name);
	  return false;
	  }
	  if ((st.st_mode & 0777) & ~0700) {
	  Com_Printf( "LoadLibrary(\"%s\") failed: invalid permissions, must be 700 for security considerations\n", name);
	  return false;
	  }
	*/
    } else {
	/* ref_soft requires we give up root now */
	setreuid(getuid(), getuid());
	setegid(getgid());
    }
    
    if ((reflib_library = dlopen(fn, RTLD_LAZY)) == 0) {
	Com_Printf( "LoadLibrary(\"%s\") failed: %s\n", name , dlerror());
	return false;
    }
    
    Com_Printf( "LoadLibrary(\"%s\")\n", name );
    
    ri.Cmd_AddCommand = Cmd_AddCommand;
    ri.Cmd_RemoveCommand = Cmd_RemoveCommand;
    ri.Cmd_Argc = Cmd_Argc;
    ri.Cmd_Argv = Cmd_Argv;
    ri.Cmd_ExecuteText = Cbuf_ExecuteText;
    ri.Con_Printf = VID_Printf;
    ri.Sys_Error = VID_Error;
    ri.FS_LoadFile = FS_LoadFile;
    ri.FS_FreeFile = FS_FreeFile;
    ri.FS_Gamedir = FS_Gamedir;
    ri.Cvar_Get = Cvar_Get;
    ri.Cvar_Set = Cvar_Set;
    ri.Cvar_SetValue = Cvar_SetValue;
    ri.Vid_GetModeInfo = VID_GetModeInfo;
    ri.Vid_MenuInit = VID_MenuInit;
    ri.Vid_NewWindow = VID_NewWindow;
    
    if ((GetRefAPI = (GetRefAPI_t) dlsym(reflib_library, "GetRefAPI")) == 0)
	Com_Error(ERR_FATAL, "dlsym failed on %s", name);
    
    re = GetRefAPI( ri );
    
    if (re.api_version != API_VERSION) {
	VID_FreeReflib ();
	Com_Error (ERR_FATAL, "%s has incompatible api_version", name);
    }
    
    /* Init IN (Mouse) */
    in_state.IN_CenterView_fp = IN_CenterView;
    in_state.Key_Event_fp = Do_Key_Event;
    in_state.viewangles = cl.viewangles;
    in_state.in_strafe_state = &in_strafe.state;

    if ((RW_IN_Init_fp = (void (*)(in_state_t *)) dlsym(reflib_library, "RW_IN_Init")) == NULL ||
	(RW_IN_Shutdown_fp = (void(*)(void)) dlsym(reflib_library, "RW_IN_Shutdown")) == NULL ||
	(RW_IN_Activate_fp = (void(*)(qboolean)) dlsym(reflib_library, "RW_IN_Activate")) == NULL ||
	(RW_IN_Commands_fp = (void(*)(void)) dlsym(reflib_library, "RW_IN_Commands")) == NULL ||
	(RW_IN_Move_fp = (void(*)(usercmd_t *)) dlsym(reflib_library, "RW_IN_Move")) == NULL ||
	(RW_IN_Frame_fp = (void(*)(void)) dlsym(reflib_library, "RW_IN_Frame")) == NULL)
	Sys_Error("No RW_IN functions in REF.\n");
    
    /* this one is optional */
    RW_Sys_GetClipboardData_fp = (char*(*)(void)) dlsym(reflib_library, "RW_Sys_GetClipboardData");
    
    Real_IN_Init();

    if (re.Init(0, 0) == -1) {
	re.Shutdown();
	VID_FreeReflib ();
	return false;
    }

/* merged in from irix/vid_so.c */
/*
#ifdef __sgi
    give up root now
    setreuid(getuid(), getuid());
    setegid(getgid());
#endif
*/

    /* Init KBD */
    if ((KBD_Init_fp = (void(*)(Key_Event_fp_t)) dlsym(reflib_library, "KBD_Init")) == NULL ||
	(KBD_Update_fp = (void(*)(void)) dlsym(reflib_library, "KBD_Update")) == NULL ||
	(KBD_Close_fp = (void(*)(void)) dlsym(reflib_library, "KBD_Close")) == NULL)
	Sys_Error("No KBD functions in REF.\n");

    KBD_Init_fp(Do_Key_Event);
    
    /* for some reason irix has this swapped with elsewhere, kinda dodgy, needs
     * cleaning */
    Key_ClearStates();
    
    // give up root now
    setreuid(getuid(), getuid());
    setegid(getgid());
    /* sgi
    Real_IN_Init();
    */
    
    Com_Printf( "------------------------------------\n");
    reflib_active = true;
    return true;
}

/* This function gets called once just before drawing each frame, and
 * its sole purpose in life is to check to see if any of the video mode
 * parameters have changed, and if they have to update the rendering DLL
 * and/or video mode to match. */
void VID_CheckChanges(void) {
    char name[100];
    cvar_t *sw_mode;

    if (vid_ref->modified) {
	S_StopAllSounds();
    }

    while (vid_ref->modified) {
	/* refresher has changed */
	vid_ref->modified = false;
	vid_fullscreen->modified = true;
	cl.refresh_prepped = false;
	cls.disable_screen = true;
	
	sprintf(name, "ref_%s.so", vid_ref->string);
	if (!VID_LoadRefresh(name)) {
	    if (strcmp(vid_ref->string, "soft") == 0 || 
		strcmp(vid_ref->string, "softx") == 0) {
		Com_Printf("Refresh failed\n");
		sw_mode = Cvar_Get("sw_mode", "0", 0);
		if (sw_mode->value != 0) {
		    Com_Printf("Trying mode 0\n");
		    Cvar_SetValue("sw_mode", 0);
		    if (!VID_LoadRefresh(name))
			Com_Error(ERR_FATAL, "Couldn't fall back to software refresh!");
		} else
		    Com_Error (ERR_FATAL, "Couldn't fall back to software refresh!");
	    }
	    
	    /* prefer to fall back on X if active */
	    if (getenv("DISPLAY"))
		Cvar_Set("vid_ref", "softx");
	    else
		Cvar_Set("vid_ref", "soft");

	    /* drop the console if we fail to load a refresh */
	    if (cls.key_dest != key_console) {
		Con_ToggleConsole_f();
	    }
	}
	cls.disable_screen = false;
    }
}

/*
============
VID_Init
============
*/
void VID_Init (void)
{
	/* Create the video variables so we know how to start the graphics drivers */
	// if DISPLAY is defined, try X
	if (getenv("DISPLAY"))
		vid_ref = Cvar_Get ("vid_ref", "softx", CVAR_ARCHIVE);
	else
		vid_ref = Cvar_Get ("vid_ref", "soft", CVAR_ARCHIVE);
	vid_xpos = Cvar_Get ("vid_xpos", "3", CVAR_ARCHIVE);
	vid_ypos = Cvar_Get ("vid_ypos", "22", CVAR_ARCHIVE);
	vid_fullscreen = Cvar_Get ("vid_fullscreen", "0", CVAR_ARCHIVE);
	vid_gamma = Cvar_Get( "vid_gamma", "1", CVAR_ARCHIVE );

	/* Add some console commands that we want to handle */
	Cmd_AddCommand ("vid_restart", VID_Restart_f);

	/* Disable the 3Dfx splash screen */
	putenv("FX_GLIDE_NO_SPLASH=0");
		
	/* Start the graphics mode and load refresh DLL */
	VID_CheckChanges();
}

/*
============
VID_Shutdown
============
*/
void VID_Shutdown (void)
{
	if ( reflib_active )
	{
		if (KBD_Close_fp)
			KBD_Close_fp();
		if (RW_IN_Shutdown_fp)
			RW_IN_Shutdown_fp();
		KBD_Close_fp = NULL;
		RW_IN_Shutdown_fp = NULL;
		re.Shutdown ();
		VID_FreeReflib ();
	}
}


/*****************************************************************************/
/* INPUT                                                                     */
/*****************************************************************************/

cvar_t	*in_joystick;

// This is fake, it's acutally done by the Refresh load
void IN_Init (void)
{
	in_joystick	= Cvar_Get ("in_joystick", "0", CVAR_ARCHIVE);
}

void Real_IN_Init (void)
{
	if (RW_IN_Init_fp)
		RW_IN_Init_fp(&in_state);
}

void IN_Shutdown (void)
{
	if (RW_IN_Shutdown_fp)
		RW_IN_Shutdown_fp();
}

void IN_Commands (void)
{
	if (RW_IN_Commands_fp)
		RW_IN_Commands_fp();
}

void IN_Move (usercmd_t *cmd)
{
	if (RW_IN_Move_fp)
		RW_IN_Move_fp(cmd);
}

void IN_Frame (void)
{
/* merged from irix/vid_so.c */
#ifndef __sgi
	if (RW_IN_Activate_fp) 
	{
		if ( !cl.refresh_prepped || cls.key_dest == key_console || cls.key_dest == key_menu)
			RW_IN_Activate_fp(false);
		else
			RW_IN_Activate_fp(true);
	}
#endif

	if (RW_IN_Frame_fp)
		RW_IN_Frame_fp();
}

void IN_Activate (qboolean active) {
/* merged in from irix/vid_so.c -- jaq */
#ifdef __sgi
	if (RW_IN_Activate_fp)
		RW_IN_Activate_fp(active);
#endif
}

void Do_Key_Event(int key, qboolean down)
{
	Key_Event(key, down, Sys_Milliseconds());
}

char *Sys_GetClipboardData(void)
{
	if (RW_Sys_GetClipboardData_fp)
		return RW_Sys_GetClipboardData_fp();
    else
		return NULL;
}
