/*
 * Copyright (C) 2010 Yamagi Burmeister
 * Copyright (C) 1997-2001 Id Software, Inc.
 *
 * 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.
 *
 * =======================================================================
 *
 * This is the client side of the render backend, implemented trough SDL.
 * The SDL window and related functrion (mouse grap, fullscreen switch)
 * are implemented here, everything else is in the renderers.
 *
 * =======================================================================
 */

#include "../../common/header/common.h"
#include "header/ref.h"

#include <SDL2/SDL.h>
#include <SDL2/SDL_video.h>

float glimp_refreshRate = -1.0f;

static cvar_t *vid_displayrefreshrate;
static cvar_t *vid_displayindex;
static cvar_t *vid_highdpiaware;
static cvar_t *vid_rate;

static int last_flags = 0;
static int last_display = 0;
static int last_position_x = SDL_WINDOWPOS_UNDEFINED;
static int last_position_y = SDL_WINDOWPOS_UNDEFINED;
static SDL_Window* window = NULL;
static qboolean initSuccessful = false;
static char **displayindices = NULL;
static int num_displays = 0;

/*
 * Resets the display index Cvar if out of bounds
 */
static void
ClampDisplayIndexCvar(void)
{
	if (!vid_displayindex)
	{
		// uninitialized render?
		return;
	}

	if (vid_displayindex->value < 0 || vid_displayindex->value >= num_displays)
	{
		Cvar_SetValue("vid_displayindex", 0);
	}
}

static void
ClearDisplayIndices(void)
{
	if ( displayindices )
	{
		for ( int i = 0; i < num_displays; i++ )
		{
			free( displayindices[ i ] );
		}

		free( displayindices );
		displayindices = NULL;
	}
}

static qboolean
CreateSDLWindow(int flags, int w, int h)
{
	if (SDL_WINDOWPOS_ISUNDEFINED(last_position_x) || SDL_WINDOWPOS_ISUNDEFINED(last_position_y) || last_position_x < 0 ||last_position_y < 24)
	{
		last_position_x = last_position_y = SDL_WINDOWPOS_UNDEFINED_DISPLAY((int)vid_displayindex->value);
	}

	/* Force the window to minimize when focus is lost. This was the
	 * default behavior until SDL 2.0.12 and changed with 2.0.14.
	 * The windows staying maximized has some odd implications for
	 * window ordering under Windows and some X11 window managers
	 * like kwin. See:
	 *  * https://github.com/libsdl-org/SDL/issues/4039
	 *  * https://github.com/libsdl-org/SDL/issues/3656 */
	SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "1");

	window = SDL_CreateWindow("Yamagi Quake II", last_position_x, last_position_y, w, h, flags);

	if (window)
	{

		/* save current display as default */
		last_display = SDL_GetWindowDisplayIndex(window);
		SDL_GetWindowPosition(window, &last_position_x, &last_position_y);

		/* Check if we're really in the requested diplay mode. There is
		   (or was) an SDL bug were SDL switched into the wrong mode
		   without giving an error code. See the bug report for details:
		   https://bugzilla.libsdl.org/show_bug.cgi?id=4700 */
		SDL_DisplayMode real_mode;

		if ((flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_FULLSCREEN_DESKTOP)) == SDL_WINDOW_FULLSCREEN)
		{
			if (SDL_GetWindowDisplayMode(window, &real_mode) != 0)
			{
				SDL_DestroyWindow(window);
				window = NULL;

				Com_Printf("Can't get display mode: %s\n", SDL_GetError());

				return false;
			}
		}

		/* SDL_WINDOW_FULLSCREEN_DESKTOP implies SDL_WINDOW_FULLSCREEN! */
		if (((flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_FULLSCREEN_DESKTOP)) == SDL_WINDOW_FULLSCREEN)
				&& ((real_mode.w != w) || (real_mode.h != h)))
		{

			Com_Printf("Current display mode isn't requested display mode\n");
			Com_Printf("Likely SDL bug #4700, trying to work around it\n");

			/* Mkay, try to hack around that. */
			SDL_DisplayMode wanted_mode = {0};

			wanted_mode.w = w;
			wanted_mode.h = h;

			if (SDL_SetWindowDisplayMode(window, &wanted_mode) != 0)
			{
				SDL_DestroyWindow(window);
				window = NULL;

				Com_Printf("Can't force resolution to %ix%i: %s\n", w, h, SDL_GetError());

				return false;
			}

			/* The SDL doku says, that SDL_SetWindowSize() shouldn't be
			   used on fullscreen windows. But at least in my test with
			   SDL 2.0.9 the subsequent SDL_GetWindowDisplayMode() fails
			   if I don't call it. */
			SDL_SetWindowSize(window, wanted_mode.w, wanted_mode.h);

			if (SDL_GetWindowDisplayMode(window, &real_mode) != 0)
			{
				SDL_DestroyWindow(window);
				window = NULL;

				Com_Printf("Can't get display mode: %s\n", SDL_GetError());

				return false;
			}

			if ((real_mode.w != w) || (real_mode.h != h))
			{
				SDL_DestroyWindow(window);
				window = NULL;

				Com_Printf("Still in wrong display mode: %ix%i instead of %ix%i\n", real_mode.w, real_mode.h, w, h);

				return false;
			}
		}

		/* Normally SDL stays at desktop refresh rate or chooses something
		   sane. Some player may want to override that.

		   Reminder: SDL_WINDOW_FULLSCREEN_DESKTOP implies SDL_WINDOW_FULLSCREEN! */
		if ((flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_FULLSCREEN_DESKTOP)) == SDL_WINDOW_FULLSCREEN)
		{
			if (vid_rate->value > 0)
			{
				SDL_DisplayMode closest_mode;
				SDL_DisplayMode requested_mode = real_mode;

				requested_mode.refresh_rate = (int)vid_rate->value;

				if (SDL_GetClosestDisplayMode(last_display, &requested_mode, &closest_mode) == NULL)
				{
					Com_Printf("SDL was unable to find a mode close to %ix%i@%i\n", w, h, requested_mode.refresh_rate);
					Cvar_SetValue("vid_rate", -1);
				}
				else
				{
					Com_Printf("User requested %ix%i@%i, setting closest mode %ix%i@%i\n",
							w, h, requested_mode.refresh_rate, w, h, closest_mode.refresh_rate);

					if (SDL_SetWindowDisplayMode(window, &closest_mode) != 0)
					{
						Com_Printf("Couldn't switch to mode %ix%i@%i, staying at current mode\n",
								w, h, closest_mode.refresh_rate);
						Cvar_SetValue("vid_rate", -1);
					}
					else
					{
						Cvar_SetValue("vid_rate", closest_mode.refresh_rate);
					}
				}

			}
		}
	}
	else
	{
		Com_Printf("Creating window failed: %s\n", SDL_GetError());
		return false;
	}

	return true;
}

static int
GetFullscreenType()
{
	if (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP)
	{
		return 2;
	}
	else if (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}

static qboolean
GetWindowSize(int* w, int* h)
{
	if (window == NULL || w == NULL || h == NULL)
	{
		return false;
	}

	SDL_DisplayMode m;

	if (SDL_GetWindowDisplayMode(window, &m) != 0)
	{
		Com_Printf("Can't get Displaymode: %s\n", SDL_GetError());

		return false;
	}

	*w = m.w;
	*h = m.h;

	return true;
}

static void
InitDisplayIndices()
{
	displayindices = malloc((num_displays + 1) * sizeof(char *));
	YQ2_COM_CHECK_OOM(displayindices, "malloc()", (num_displays + 1) * sizeof(char *))
	if (!displayindices)
	{
		/* unaware about YQ2_ATTR_NORETURN_FUNCPTR? */
		return;
	}

	for ( int i = 0; i < num_displays; i++ )
	{
		/* There are a maximum of 12 digits in 32 bit int + 1 for the NULL terminator. */
		displayindices[ i ] = malloc(16 * sizeof( char ));
		YQ2_COM_CHECK_OOM(displayindices[i], "malloc()", 16 * sizeof( char ))
		if (!displayindices[i])
		{
			/* unaware about YQ2_ATTR_NORETURN_FUNCPTR? */
			break;
		}

		snprintf( displayindices[ i ], 16, "%d", i );
	}

	/* The last entry is NULL to indicate the list of strings ends. */
	displayindices[ num_displays ] = 0;
}

/*
 * Lists all available display modes.
 */
static void
PrintDisplayModes(void)
{
	int curdisplay = window ? SDL_GetWindowDisplayIndex(window) : 0;

	// On X11 (at least for me)
	// curdisplay is always -1.
	// DG: probably because window was NULL?
	if (curdisplay < 0) {
		curdisplay = 0;
	}

	int nummodes = SDL_GetNumDisplayModes(curdisplay);

	if (nummodes < 1)
	{
		Com_Printf("Can't get display modes: %s\n", SDL_GetError());
		return;
	}

	for (int i = 0; i < nummodes; i++)
	{
		SDL_DisplayMode mode;

		if (SDL_GetDisplayMode(curdisplay, i, &mode) != 0)
		{
			Com_Printf("Can't get display mode: %s\n", SDL_GetError());
			return;
		}

		Com_Printf(" - Mode %2i: %ix%i@%i\n", i, mode.w, mode.h, mode.refresh_rate);
	}
}

/*
 * Sets the window icon
 */
static void
SetSDLIcon()
{
	#include "icon/q2icon64.h" // 64x64 32 Bit

	/* these masks are needed to tell SDL_CreateRGBSurface(From)
	   to assume the data it gets is byte-wise RGB(A) data */
	Uint32 rmask, gmask, bmask, amask;

#if SDL_BYTEORDER == SDL_BIG_ENDIAN
	int shift = (q2icon64.bytes_per_pixel == 3) ? 8 : 0;
	rmask = 0xff000000 >> shift;
	gmask = 0x00ff0000 >> shift;
	bmask = 0x0000ff00 >> shift;
	amask = 0x000000ff >> shift;
#else /* little endian, like x86 */
	rmask = 0x000000ff;
	gmask = 0x0000ff00;
	bmask = 0x00ff0000;
	amask = (q2icon64.bytes_per_pixel == 3) ? 0 : 0xff000000;
#endif

	SDL_Surface* icon = SDL_CreateRGBSurfaceFrom((void*)q2icon64.pixel_data, q2icon64.width,
		q2icon64.height, q2icon64.bytes_per_pixel*8, q2icon64.bytes_per_pixel*q2icon64.width,
		rmask, gmask, bmask, amask);
	SDL_SetWindowIcon(window, icon);
	SDL_FreeSurface(icon);
}

// FIXME: We need a header for this.
// Maybe we could put it in vid.h.
void GLimp_GrabInput(qboolean grab);

/*
 * Shuts the SDL render backend down
 */
static void
ShutdownGraphics(void)
{
	ClampDisplayIndexCvar();

	if (window)
	{
		/* save current display as default */
		last_display = SDL_GetWindowDisplayIndex(window);

		/* or if current display isn't the desired default */
		if (last_display != vid_displayindex->value) {
			last_position_x = last_position_y = SDL_WINDOWPOS_UNDEFINED;
			last_display = vid_displayindex->value;
		}
		else {
			SDL_GetWindowPosition(window,
				      &last_position_x, &last_position_y);
		}

		/* cleanly ungrab input (needs window) */
		GLimp_GrabInput(false);
		SDL_DestroyWindow(window);

		window = NULL;
	}

	// make sure that after vid_restart the refreshrate will be queried from SDL2 again.
	glimp_refreshRate = -1;

	initSuccessful = false; // not initialized anymore
}
// --------

/*
 * Initializes the SDL video subsystem. Must
 * be called before anything else.
 */
qboolean
GLimp_Init(void)
{
	vid_displayrefreshrate = Cvar_Get("vid_displayrefreshrate", "-1", CVAR_ARCHIVE);
	vid_displayindex = Cvar_Get("vid_displayindex", "0", CVAR_ARCHIVE);
	vid_highdpiaware = Cvar_Get("vid_highdpiaware", "0", CVAR_ARCHIVE);
	vid_rate = Cvar_Get("vid_rate", "-1", CVAR_ARCHIVE);

	if (!SDL_WasInit(SDL_INIT_VIDEO))
	{
		if (SDL_Init(SDL_INIT_VIDEO) == -1)
		{
			Com_Printf("Couldn't init SDL video: %s.\n", SDL_GetError());

			return false;
		}

		SDL_version version;

		SDL_GetVersion(&version);
		Com_Printf("-------- vid initialization --------\n");
		Com_Printf("SDL version is: %i.%i.%i\n", (int)version.major, (int)version.minor, (int)version.patch);
		Com_Printf("SDL video driver is \"%s\".\n", SDL_GetCurrentVideoDriver());

		num_displays = SDL_GetNumVideoDisplays();
		InitDisplayIndices();
		ClampDisplayIndexCvar();
		Com_Printf("SDL display modes:\n");

		PrintDisplayModes();
		Com_Printf("------------------------------------\n\n");
	}

	return true;
}

/*
 * Shuts the SDL video subsystem down. Must
 * be called after evrything's finished and
 * clean up.
 */
void
GLimp_Shutdown(void)
{
	ShutdownGraphics();

	// SDL_INIT_VIDEO implies SDL_INIT_EVENTS
	const Uint32 subsystems = SDL_INIT_VIDEO | SDL_INIT_EVENTS;
	if (SDL_WasInit(SDL_INIT_EVERYTHING) == subsystems)
	{
		SDL_Quit();
	}
	else
	{
		SDL_QuitSubSystem(SDL_INIT_VIDEO);
	}

	ClearDisplayIndices();
}

/*
 * Determine if we want to be high dpi aware. If
 * we are we must scale ourself. If we are not the
 * compositor might scale us.
 */
static int
Glimp_DetermineHighDPISupport(int flags)
{
#if SDL_VERSION_ATLEAST(2, 26, 0)
	/* Make sure that high dpi is never set when we don't want it. */
	flags &= ~SDL_WINDOW_ALLOW_HIGHDPI;

	if (vid_highdpiaware->value == 0)
	{
		return flags;
	}

	/* Handle high dpi awareness based on the render backend.
	   SDL doesn't support high dpi awareness for all backends
	   and the quality and behavior differs between them. */
	if ((strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0))
	{
		flags |= SDL_WINDOW_ALLOW_HIGHDPI;
	}
#endif

	return flags;
}

/*
 * (Re)initializes the actual window.
 */
qboolean
GLimp_InitGraphics(int fullscreen, int *pwidth, int *pheight)
{
	int flags;
	int curWidth, curHeight;
	int width = *pwidth;
	int height = *pheight;
	unsigned int fs_flag = 0;

	if (fullscreen == 1)
	{
		fs_flag = SDL_WINDOW_FULLSCREEN;
	}
	else if (fullscreen == 2)
	{
		fs_flag = SDL_WINDOW_FULLSCREEN_DESKTOP;
	}

	/* Only do this if we already have a working window and a fully
	initialized rendering backend GLimp_InitGraphics() is also
	called when recovering if creating GL context fails or the
	one we got is unusable. */
	if (initSuccessful && GetWindowSize(&curWidth, &curHeight)
			&& (curWidth == width) && (curHeight == height))
	{
		/* If we want fullscreen, but aren't */
		if (GetFullscreenType())
		{
			SDL_SetWindowFullscreen(window, fs_flag);
			Cvar_SetValue("vid_fullscreen", fullscreen);
		}

		/* Are we now? */
		if (GetFullscreenType())
		{
			return true;
		}
	}

	/* Is the surface used? */
	if (window)
	{
		re.ShutdownContext();
		ShutdownGraphics();

		window = NULL;
	}

	if(last_flags != -1 && (last_flags & SDL_WINDOW_OPENGL))
	{
		/* Reset SDL. */
		SDL_GL_ResetAttributes();
	}

	/* Let renderer prepare things (set OpenGL attributes).
	   FIXME: This is no longer necessary, the renderer
	   could and should pass the flags when calling this
	   function. */
	flags = re.PrepareForWindow();

	if (flags == -1)
	{
		/* It's PrepareForWindow() job to log an error */
		return false;
	}

	if (fs_flag)
	{
		flags |= fs_flag;
	}

	/* Check for high dpi support. */
	flags = Glimp_DetermineHighDPISupport(flags);

	/* Mkay, now the hard work. Let's create the window. */
	cvar_t *gl_msaa_samples = Cvar_Get("r_msaa_samples", "0", CVAR_ARCHIVE);

	while (1)
	{
		if (!CreateSDLWindow(flags, width, height))
		{
			if((flags & SDL_WINDOW_OPENGL) && gl_msaa_samples->value)
			{
				int msaa_samples = gl_msaa_samples->value;

				if (msaa_samples > 0)
				{
					msaa_samples /= 2;
				}

				Com_Printf("SDL SetVideoMode failed: %s\n", SDL_GetError());
				Com_Printf("Reverting to %s r_mode %i (%ix%i) with %dx MSAA.\n",
					        (flags & fs_flag) ? "fullscreen" : "windowed",
					        (int) Cvar_VariableValue("r_mode"), width, height,
					        msaa_samples);

				/* Try to recover */
				Cvar_SetValue("r_msaa_samples", msaa_samples);

				SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS,
					msaa_samples > 0 ? 1 : 0);
				SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES,
					msaa_samples);
			}
			else if (width != 640 || height != 480 || (flags & fs_flag))
			{
				Com_Printf("SDL SetVideoMode failed: %s\n", SDL_GetError());
				Com_Printf("Reverting to windowed r_mode 4 (640x480).\n");

				/* Try to recover */
				Cvar_SetValue("r_mode", 4);
				Cvar_SetValue("vid_fullscreen", 0);
				Cvar_SetValue("vid_rate", -1);

				fullscreen = 0;
				*pwidth = width = 640;
				*pheight = height = 480;
				flags &= ~fs_flag;
			}
			else
			{
				Com_Printf("Failed to revert to r_mode 4. Will try another render backend...\n");
				return false;
			}
		}
		else
		{
			break;
		}
	}

	last_flags = flags;

	/* Now that we've got a working window print it's mode. */
	int curdisplay = SDL_GetWindowDisplayIndex(window);

	if (curdisplay < 0) {
		curdisplay = 0;
	}

	SDL_DisplayMode mode;

	if (SDL_GetCurrentDisplayMode(curdisplay, &mode) != 0)
	{
		Com_Printf("Can't get current display mode: %s\n", SDL_GetError());
	}
	else
	{
		Com_Printf("Real display mode: %ix%i@%i\n", mode.w, mode.h, mode.refresh_rate);
	}

	/* Initialize rendering context. */
	if (!re.InitContext(window))
	{
		/* InitContext() should have logged an error. */
		return false;
	}

	/* We need the actual drawable size for things like the
	   console, the menus, etc. This might be different to
	   the resolution due to high dpi awareness.

	   The fullscreen window is special. We want it to fill
	   the screen when native resolution is requestes, all
	   other cases should look broken. */
	if (flags & SDL_WINDOW_ALLOW_HIGHDPI)
	{
		if (fullscreen != 2)
		{
			re.GetDrawableSize(&viddef.width, &viddef.height);
		}
		else
		{
			cvar_t *r_mode = Cvar_Get("r_mode", "4", 0);

			if (r_mode->value == -2 )
			{
				re.GetDrawableSize(&viddef.width, &viddef.height);
			}
			else
			{
				/* User likes it broken. */
				viddef.width = *pwidth;
				viddef.height = *pheight;
			}
		}
	}
	else
	{
		/* Another bug or design failure in SDL: When we are
		   not high dpi aware the drawable size returned by
		   SDL may be too small. It seems like the window
		   decoration are taken into account when they shouldn't.
		   It can be seen when creating a fullscreen window.

		   Work around that by always using the resolution and
		   not the drawable size when we are not high dpi aware. */
		viddef.width = *pwidth;
		viddef.height = *pheight;
	}

	Com_Printf("Drawable size: %ix%i\n", viddef.width, viddef.height);

	/* Set the window icon - For SDL2, this must be done after creating the window */
	SetSDLIcon();

	/* No cursor */
	SDL_ShowCursor(0);

	initSuccessful = true;

	return true;
}

/*
 * Shuts the window down.
 */
void
GLimp_ShutdownGraphics(void)
{
	SDL_GL_ResetAttributes();
	ShutdownGraphics();
}

/*
 * (Un)grab Input
 */
void
GLimp_GrabInput(qboolean grab)
{
	static qboolean seen_error = false;
	if(window != NULL)
	{
		SDL_SetWindowGrab(window, grab ? SDL_TRUE : SDL_FALSE);
	}

	if(SDL_SetRelativeMouseMode(grab ? SDL_TRUE : SDL_FALSE) < 0 && !seen_error)
	{
		seen_error = true;
		Com_Printf("WARNING: Setting Relative Mousemode failed, reason: %s\n", SDL_GetError());
		Com_Printf("         You should probably update to SDL 2.0.3 or newer!\n");
	}
}

/*
 * Returns the current display refresh rate.
 */
float
GLimp_GetRefreshRate(void)
{

	if (vid_displayrefreshrate->value > -1 ||
			vid_displayrefreshrate->modified)
	{
		glimp_refreshRate = vid_displayrefreshrate->value;
		vid_displayrefreshrate->modified = false;
	}
	else if (glimp_refreshRate == -1)
	{
		SDL_DisplayMode mode;

		int i = SDL_GetWindowDisplayIndex(window);

		if (i >= 0 && SDL_GetCurrentDisplayMode(i, &mode) == 0)
		{
			glimp_refreshRate = mode.refresh_rate;
		}

		// Something went wrong, use default.
		if (glimp_refreshRate <= 0)
		{
			glimp_refreshRate = 60;
		}
	}

	return glimp_refreshRate;
}

/*
 * Detect current desktop mode
 */
qboolean
GLimp_GetDesktopMode(int *pwidth, int *pheight)
{
	// Declare display mode structure to be filled in.
	SDL_DisplayMode mode;

	if (window)
	{
		/* save current display as default */
		last_display = SDL_GetWindowDisplayIndex(window);
		SDL_GetWindowPosition(window, &last_position_x, &last_position_y);
	}

	if (last_display < 0)
	{
		// In case of error...
		Com_Printf("Can't detect current desktop.\n");
		last_display = 0;
	}

	// We can't get desktop where we start, so use first desktop
	if(SDL_GetCurrentDisplayMode(last_display, &mode) != 0)
	{
		// In case of error...
		Com_Printf("Can't detect default desktop mode: %s\n",
				SDL_GetError());
		return false;
	}
	*pwidth = mode.w;
	*pheight = mode.h;
	return true;
}

const char**
GLimp_GetDisplayIndices(void)
{
	return (const char**)displayindices;
}

int
GLimp_GetNumVideoDisplays(void)
{
	return num_displays;
}

int
GLimp_GetWindowDisplayIndex(void)
{
	return last_display;
}

int
GLimp_GetFrameworkVersion(void)
{
	SDL_version ver;
	SDL_VERSION(&ver);

	return ver.major;
}
