// Emacs style mode select   -*- C++ -*-
//-----------------------------------------------------------------------------
//
// $Id: v_video.cpp 4671 2014-03-22 21:23:28Z dr_sean $
//
// Copyright (C) 1993-1996 by id Software, Inc.
// Copyright (C) 2006-2014 by The Odamex Team.
//
// 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.
//
// DESCRIPTION:
//		Functions to draw patches (by post) directly to screen->
//		Functions to blit a block to the screen->
//
//-----------------------------------------------------------------------------



#include <stdio.h>
#include <assert.h>

#include "minilzo.h"
// [RH] Output buffer size for LZO compression.
//		Extra space in case uncompressable.
#define OUT_LEN(a)		((a) + (a) / 64 + 16 + 3)

#include "m_alloc.h"

#include "i_system.h"
#include "i_video.h"
#include "r_local.h"
#include "r_draw.h"
#include "r_plane.h"
#include "r_state.h"

#include "doomdef.h"
#include "doomdata.h"
#include "doomstat.h"
#include "d_main.h"

#include "c_console.h"
#include "hu_stuff.h"

#include "m_argv.h"
#include "m_bbox.h"
#include "m_swap.h"
#include "m_menu.h"

#include "v_video.h"
#include "v_text.h"

#include "w_wad.h"
#include "z_zone.h"

#include "c_cvars.h"
#include "c_dispatch.h"
#include "cmdlib.h"

IMPLEMENT_CLASS (DCanvas, DObject)

int DisplayWidth, DisplayHeight, DisplayBits;
int SquareWidth;

argb_t Col2RGB8[65][256];
palindex_t RGB32k[32][32][32];

void I_FlushInput();

// [RH] The framebuffer is no longer a mere byte array.
// There's also only one, not four.
DCanvas *screen;

DBoundingBox dirtybox;

EXTERN_CVAR (vid_defwidth)
EXTERN_CVAR (vid_defheight)
EXTERN_CVAR (vid_32bpp)
EXTERN_CVAR (vid_autoadjust)
EXTERN_CVAR (vid_overscan)

CVAR_FUNC_IMPL (vid_maxfps)
{
	if (var == 0)
	{
		capfps = false;
		maxfps = 99999.0f;
	}
	else
	{
		if (var < 35.0f)
			var.Set(35.0f);
		else
		{
			capfps = true;
			maxfps = var;
		}
	}
}

EXTERN_CVAR (ui_dimamount)
EXTERN_CVAR (ui_dimcolor)

// [RH] Set true when vid_setmode command has been executed
BOOL	setmodeneeded = false;
// [RH] Resolution to change to when setmodeneeded is true
int		NewWidth, NewHeight, NewBits;

CVAR_FUNC_IMPL (vid_fullscreen)
{
	setmodeneeded = true;
	NewWidth = I_GetVideoWidth();
	NewHeight = I_GetVideoHeight();
	NewBits = I_GetVideoBitDepth();
}

CVAR_FUNC_IMPL (vid_32bpp)
{
	setmodeneeded = true;
	NewBits = (int)vid_32bpp ? 32 : 8;
}


//
// V_MarkRect
//
void V_MarkRect (int x, int y, int width, int height)
{
	dirtybox.AddToBox (x, y);
	dirtybox.AddToBox (x+width-1, y+height-1);
}


// [RH] Fill an area with a 64x64 flat texture
//		right and bottom are one pixel *past* the boundaries they describe.
void DCanvas::FlatFill (int left, int top, int right, int bottom, const byte *src) const
{
	int x, y;
	int advance;
	int width;

	width = right - left;
	right = width >> 6;

	if (is8bit())
	{
		byte *dest;

		advance = pitch - width;
		dest = buffer + top * pitch + left;

		for (y = top; y < bottom; y++)
		{
			for (x = 0; x < right; x++)
			{
				memcpy (dest, src + ((y&63)<<6), 64);
				dest += 64;
			}

			if (width & 63)
			{
				memcpy (dest, src + ((y&63)<<6), width & 63);
				dest += width & 63;
			}
			dest += advance;
		}
	}
	else
	{
		unsigned int *dest;
		int z;
		const byte *l;

		advance = (pitch - (width << 2)) >> 2;
		dest = (unsigned int *)(buffer + top * pitch + (left << 2));

		for (y = top; y < bottom; y++)
		{
			l = src + ((y&63)<<6);
			for (x = 0; x < right; x++)
			{
				for (z = 0; z < 64; z += 4, dest += 4)
				{
					// Try and let the optimizer pair this on a Pentium
					// (even though VC++ doesn't anyway)
					dest[0] = V_Palette.shade(l[z+0]);
					dest[1] = V_Palette.shade(l[z+1]);
					dest[2] = V_Palette.shade(l[z+2]);
					dest[3] = V_Palette.shade(l[z+3]);
				}
			}

			if (width & 63)
			{
				// Do any odd pixel left over
				if (width & 1)
					*dest++ = V_Palette.shade(l[0]);

				// Do the rest of the pixels
				for (z = 1; z < (width & 63); z += 2, dest += 2)
				{
					dest[0] = V_Palette.shade(l[z+0]);
					dest[1] = V_Palette.shade(l[z+1]);
				}
			}

			dest += advance;
		}
	}
}


// [SL] Stretches a patch to fill the full-screen while maintaining a 4:3
// aspect ratio. Pillarboxing is used in widescreen resolutions.
void DCanvas::DrawPatchFullScreen(const patch_t* patch) const
{
	Clear(0, 0, width, height, 0);

	if (isProtectedRes())
	{
		DrawPatch(patch, 0, 0);
	}
	else if (width * 3 > height * 4)
	{
		// widescreen resolution - draw pic in 4:3 ratio in center of screen
		int picwidth = 4 * height / 3;
		int picheight = height;
		DrawPatchStretched(patch, (width - picwidth) / 2, 0, picwidth, picheight);
	}
	else
	{
		// 4:3 resolution - draw pic to the entire screen
		DrawPatchStretched(patch, 0, 0, width, height);
	}
}


// [RH] Set an area to a specified color
void DCanvas::Clear (int left, int top, int right, int bottom, int color) const
{
	int x, y;

	if (is8bit())
	{
		byte *dest;

		dest = buffer + top * pitch + left;
		x = right - left;
		for (y = top; y < bottom; y++)
		{
			memset (dest, color, x);
			dest += pitch;
		}
	}
	else
	{
		unsigned int *dest;

		dest = (unsigned int *)(buffer + top * pitch + (left << 2));
		right -= left;

		for (y = top; y < bottom; y++)
		{
			for (x = 0; x < right; x++)
			{
				dest[x] = color;
			}
			dest += pitch >> 2;
		}
	}
}


void DCanvas::Dim(int x1, int y1, int w, int h, const char* color_str, float famount) const
{
	if (!buffer)
		return;

	if (x1 < 0 || x1 + w > width || y1 < 0 || y1 + h > height)
		return;

	if (famount <= 0.0f)
		return;
	else if (famount > 1.0f)
		famount = 1.0f;

	if (is8bit())
	{
		int bg;
		int x, y;

		fixed_t amount = (fixed_t)(famount * 64.0f);
		argb_t *fg2rgb = Col2RGB8[amount];
		argb_t *bg2rgb = Col2RGB8[64-amount];
		unsigned int fg = fg2rgb[V_GetColorFromString(GetDefaultPalette()->basecolors, color_str)];

		byte *dest = buffer + y1 * pitch + x1;
		int gap = pitch - w;

		int xcount = w / 4;
		int xcount_remainder = w % 4;

		for (y = h; y > 0; y--)
		{
			for (x = xcount; x > 0; x--)
			{
				// Unroll the loop for a speed improvement
				bg = bg2rgb[*dest];
				bg = (fg+bg) | 0x1f07c1f;
				*dest++ = RGB32k[0][0][bg&(bg>>15)];

				bg = bg2rgb[*dest];
				bg = (fg+bg) | 0x1f07c1f;
				*dest++ = RGB32k[0][0][bg&(bg>>15)];

				bg = bg2rgb[*dest];
				bg = (fg+bg) | 0x1f07c1f;
				*dest++ = RGB32k[0][0][bg&(bg>>15)];

				bg = bg2rgb[*dest];
				bg = (fg+bg) | 0x1f07c1f;
				*dest++ = RGB32k[0][0][bg&(bg>>15)];
			}
			for (x = xcount_remainder; x > 0; x--)
			{
				// account for widths that aren't multiples of 4
				bg = bg2rgb[*dest];
				bg = (fg+bg) | 0x1f07c1f;
				*dest++ = RGB32k[0][0][bg&(bg>>15)];
			}
			dest += gap;
		}
	}
	else
	{
		argb_t color = V_GetColorFromString(NULL, color_str);
		color = MAKERGB(newgamma[RPART(color)], newgamma[GPART(color)], newgamma[BPART(color)]);
		r_dimpatchD(this, color, (int)(famount * 256.0f), x1, y1, w, h);
	}
}

void DCanvas::Dim(int x1, int y1, int w, int h) const
{
	if (ui_dimamount < 0.0f)
		ui_dimamount.Set (0.0f);
	else if (ui_dimamount > 1.0f)
		ui_dimamount.Set (1.0f);

	if (ui_dimamount == 0.0f)
		return;

	Dim(x1, y1, w, h, ui_dimcolor.cstring(), ui_dimamount);
}

std::string V_GetColorStringByName (const char *name)
{
	/* Note: The X11R6RGB lump used by this function *MUST* end
	 * with a NULL byte. This is so that COM_Parse is able to
	 * detect the end of the lump.
	 */
	char *rgbNames, *data, descr[5*3];
	int c[3], step;

	if (!(rgbNames = (char *)W_CacheLumpName ("X11R6RGB", PU_CACHE))) {
		Printf (PRINT_HIGH, "X11R6RGB lump not found\n");
		return NULL;
	}

	// skip past the header line
	data = strchr (rgbNames, '\n');
	step = 0;

	while ( (data = COM_Parse (data)) ) {
		if (step < 3) {
			c[step++] = atoi (com_token);
		} else {
			step = 0;
			if (*data >= ' ') {		// In case this name contains a space...
				char *newchar = com_token + strlen(com_token);

				while (*data >= ' ') {
					*newchar++ = *data++;
				}
				*newchar = 0;
			}

			if (!stricmp (com_token, name)) {
				sprintf (descr, "%04x %04x %04x",
						 (c[0] << 8) | c[0],
						 (c[1] << 8) | c[1],
						 (c[2] << 8) | c[2]);
				return descr;
			}
		}
	}
	return "";
}

BEGIN_COMMAND (setcolor)
{
	if (argc < 3)
	{
		Printf (PRINT_HIGH, "Usage: setcolor <cvar> <color>\n");
		return;
	}

	std::string name = C_ArgCombine(argc - 2, (const char **)(argv + 2));
	if (name.length())
	{
		std::string desc = V_GetColorStringByName (name.c_str());

		if (desc.length())
		{
			std::string setcmd = "set ";
			setcmd += argv[1];
			setcmd += " \"";
			setcmd += desc;
			setcmd += "\"";
			AddCommandString (setcmd);
		}
	}
}
END_COMMAND (setcolor)

// Build the tables necessary for translucency
static void BuildTransTable (argb_t *palette)
{
	{
		int r, g, b;

		// create the small RGB table
		for (r = 0; r < 32; r++)
			for (g = 0; g < 32; g++)
				for (b = 0; b < 32; b++)
					RGB32k[r][g][b] = BestColor (palette,
						(r<<3)|(r>>2), (g<<3)|(g>>2), (b<<3)|(b>>2), 256);
	}

	{
		int x, y;

		for (x = 0; x < 65; x++)
			for (y = 0; y < 256; y++)
				Col2RGB8[x][y] = (((RPART(palette[y])*x)>>4)<<20)  |
								  ((GPART(palette[y])*x)>>4) |
								 (((BPART(palette[y])*x)>>4)<<10);
	}
}

void DCanvas::Lock ()
{
	m_LockCount++;
	if (m_LockCount == 1)
	{
		I_LockScreen (this);

		if (this == screen)
		{
			if (dcol.pitch != pitch << detailyshift)
			{
				dcol.pitch = pitch << detailyshift;
				R_InitFuzzTable ();
			}

			if (1 << detailxshift != dspan.colsize)
			{
				dspan.colsize = 1 << detailxshift;
			}
		}
	}
}

void DCanvas::Unlock ()
{
	if (m_LockCount)
		if (--m_LockCount == 0)
			I_UnlockScreen (this);
}

void DCanvas::Blit (int srcx, int srcy, int srcwidth, int srcheight,
			 DCanvas *dest, int destx, int desty, int destwidth, int destheight)
{
	I_Blit (this, srcx, srcy, srcwidth, srcheight, dest, destx, desty, destwidth, destheight);
}

CVAR_FUNC_IMPL (vid_widescreen)
{
	static bool last_value = !var;	// force setmodeneeded when loading cvar
	if (last_value != var)
		setmodeneeded = true;
	last_value = var;
}

CVAR_FUNC_IMPL (sv_allowwidescreen)
{
	// change setmodeneeded when the value of sv_allowwidescreen
	// changes our ability to use wide-fov
	bool wide_fov = V_UseWidescreen() || V_UseLetterBox();
	static bool last_value = !wide_fov;

	if (last_value != wide_fov)
		setmodeneeded = true;
	last_value = wide_fov;
}

//
// V_UsePillarBox
//
// Determines if the display should use pillarboxing. If the resolution is a
// widescreen mode and either the user or the server doesn't allow
// widescreen usage, use pillarboxing.
//
bool V_UsePillarBox()
{
	if (I_GetVideoWidth() == 0 || I_GetVideoHeight() == 0)
		return false;

	if (I_GetVideoWidth() == 320 && I_GetVideoHeight() == 200)
		return false;
	if (I_GetVideoWidth() == 640 && I_GetVideoHeight() == 400)
		return false;

	return (!vid_widescreen || (!serverside && !sv_allowwidescreen))
		&& (3 * I_GetVideoWidth() > 4 * I_GetVideoHeight());
}

//
// V_UseLetterBox
//
// Determines if the display should use letterboxing. If the resolution is a
// standard 4:3 mode and both the user and the server allow widescreen
// usage, use letterboxing.
//
bool V_UseLetterBox()
{
	if (I_GetVideoWidth() == 0 || I_GetVideoHeight() == 0)
		return false;

	if (I_GetVideoWidth() == 320 && I_GetVideoHeight() == 200)
		return false;
	if (I_GetVideoWidth() == 640 && I_GetVideoHeight() == 400)
		return false;

	return (vid_widescreen && (serverside || sv_allowwidescreen))
		&& (3 * I_GetVideoWidth() <= 4 * I_GetVideoHeight());
}

//
// V_UseWidescreen
//
//
bool V_UseWidescreen()
{
	if (I_GetVideoWidth() == 0 || I_GetVideoHeight() == 0)
		return false;

	if (I_GetVideoWidth() == 320 && I_GetVideoHeight() == 200)
		return false;
	if (I_GetVideoWidth() == 640 && I_GetVideoHeight() == 400)
		return false;

	return (vid_widescreen && (serverside || sv_allowwidescreen))
		&& (3 * I_GetVideoWidth() > 4 * I_GetVideoHeight());
}

//
// V_SetResolution
//
static bool V_DoModeSetup(int width, int height, int bits)
{
	int basew = 320, baseh = 200;

	// Free the virtual framebuffer
	if (screen)
	{
		I_FreeScreen(screen);
		screen = NULL;
	}

	if (!I_SetMode(width, height, bits))
		return false;

	I_SetOverscan (vid_overscan);

	if (V_UsePillarBox())
		width = (4 * height) / 3;
	else if (V_UseLetterBox())
		height = (9 * width) / 16;

	// This uses the smaller of the two results. It's still not ideal but at least
	// this allows con_scaletext to have some purpose...

    CleanXfac = width / basew;
    CleanYfac = height / baseh;

	if (CleanXfac == 0 || CleanYfac == 0)
		CleanXfac = CleanYfac = 1;
	else
	{
		if (CleanXfac < CleanYfac)
			CleanYfac = CleanXfac;
		else
			CleanXfac = CleanYfac;
	}

	DisplayWidth = width;
	DisplayHeight = height;
	DisplayBits = bits;

	SquareWidth = (4 * DisplayHeight) / 3;

	if (SquareWidth > DisplayWidth)
        SquareWidth = DisplayWidth;

	// Allocate a new virtual framebuffer
	bool primary = (vid_fullscreen == 0);

	// [SL] Add a bit to the screen width if it's a power-of-two to avoid
	// cache thrashing
	int cache_fudge = (width % 256) == 0 ? 4 : 0;

	screen = I_AllocateScreen(width + cache_fudge, height, bits, primary);

	V_ForceBlend (0,0,0,0);
	GammaAdjustPalettes ();
		RefreshPalettes ();
	R_ReinitColormap ();

	R_InitColumnDrawers();
	R_MultiresInit();

	// [SL] 2011-11-30 - Prevent the player's view angle from moving
	I_FlushInput();

    gotconback = false;

	return true;
}

bool V_SetResolution(int width, int height, int bits)
{
	int oldwidth, oldheight, oldbits;

	if (screen)
	{
		oldwidth = I_GetVideoWidth();
		oldheight = I_GetVideoHeight();
		oldbits = I_GetVideoBitDepth();
	}
	else
	{
		// Harmless if screen wasn't allocated
		oldwidth = width;
		oldheight = height;
		oldbits = bits;
	}

	// Make sure we don't set the resolution smaller than Doom's original 320x200
	// resolution. Bad things might happen.
	width = clamp(width, 320, MAXWIDTH);
	height = clamp(height, 200, MAXHEIGHT);

	if ((int)(vid_autoadjust))
	{
		if (vid_fullscreen)
		{
			// Fullscreen needs to check for a valid resolution.
			I_ClosestResolution(&width, &height);
		}

		// Try specified resolution
		if (!I_CheckResolution(width, height))
		{
			// Try the previous resolution (if any)
			if (!I_CheckResolution(oldwidth, oldheight))
		   		return false;

			width = oldwidth;
			height = oldheight;
			bits = oldbits;
		}
	}

	return V_DoModeSetup(width, height, bits);
}

BEGIN_COMMAND (vid_setmode)
{
	int		width = 0, height = 0;
	int		bits = DisplayBits;

	// No arguments
	if (argc == 1) {
		Printf(PRINT_HIGH, "Usage: vid_setmode <width> <height>\n");
		return;
	}
	// Width
	if (argc > 1)
		width = atoi(argv[1]);

	// Height (optional)
	if (argc > 2)
		height = atoi(argv[2]);
	if (height == 0)
		height = I_GetVideoHeight();

	// Bits
	bits = (int)vid_32bpp ? 32 : 8;

	if (width < 320 || height < 200)
		Printf(PRINT_HIGH, "%dx%d is too small.  Minimum resolution is 320x200.\n", width, height);

	if (width > MAXWIDTH || height > MAXHEIGHT)
		Printf(PRINT_HIGH, "%dx%d is too large.  Maximum resolution is %dx%d.\n", width, height, MAXWIDTH, MAXHEIGHT);

	if (I_CheckResolution(width, height))
	{
		// The actual change of resolution will take place
		// near the beginning of D_Display().
		if (gamestate != GS_STARTUP)
		{
			setmodeneeded = true;
			NewWidth = width;
			NewHeight = height;
			NewBits = bits;
		}
	}
	else
	{
		Printf(PRINT_HIGH, "Unknown resolution %dx%d\n", width, height);
	}
}
END_COMMAND (vid_setmode)

BEGIN_COMMAND (checkres)
{
    Printf (PRINT_HIGH, "Resolution: %d x %d x %d (%s)\n",
			I_GetVideoWidth(), I_GetVideoHeight(), I_GetVideoBitDepth(),
        	(vid_fullscreen ? "FULLSCREEN" : "WINDOWED")); // NES - Simple resolution checker.
}
END_COMMAND (checkres)

//
// V_InitPalette
//

void V_InitPalette (void)
{
	// [RH] Initialize palette subsystem
	if (!(InitPalettes ("PLAYPAL")))
		I_FatalError ("Could not initialize palette");

	BuildTransTable(GetDefaultPalette()->basecolors);

	V_ForceBlend (0, 0, 0, 0);

	RefreshPalettes ();

	assert(GetDefaultPalette()->maps.colormap != NULL);
	assert(GetDefaultPalette()->maps.shademap != NULL);
	V_Palette = shaderef_t(&GetDefaultPalette()->maps, 0); // (unsigned int *)DefaultPalette->colors;
}

//
// V_Close
//
//
void STACK_ARGS V_Close()
{
	if(screen)
	{
		I_FreeScreen(screen);
		screen = NULL;
	}
}

//
// V_Init
//

void V_Init (void)
{
	int width, height, bits;

	bool firstTime = true;
	if(firstTime)
		atterm (V_Close);

	width = height = bits = 0;

	const char *w = Args.CheckValue ("-width");
	const char *h = Args.CheckValue ("-height");
	const char *b = Args.CheckValue ("-bits");

	if (w)
		width = atoi (w);

	if (h)
		height = atoi (h);

	if (b)
	{
		bits = atoi (b);
		bits = (bits < 32 ? 8 : 32);

		std::string bppcmd = "vid_32bpp ";
		bppcmd += (bits == 32 ? "1":"0");

		AddCommandString(bppcmd);
	}

	if (width == 0)
	{
		if (height == 0)
		{
			width = (int)(vid_defwidth);
			height = (int)(vid_defheight);
		}
		else
		{
			width = (height * 8) / 6;
		}
	}
	else if (height == 0)
	{
		height = (width * 6) / 8;
	}

	if (bits == 0)
	{
		bits = (int)vid_32bpp ? 32 : 8;
	}

    if ((int)(vid_autoadjust))
        I_ClosestResolution (&width, &height);

	if (!V_SetResolution (width, height, bits))
		I_FatalError ("Could not set resolution to %d x %d x %d %s\n", width, height, bits,
            (vid_fullscreen ? "FULLSCREEN" : "WINDOWED"));
	else
        AddCommandString("checkres");

	V_InitPalette ();

	V_InitConChars (0xf7);
	C_InitConsole (screen->width, screen->height, true);
}

void DCanvas::AttachPalette (palette_t *pal)
{
	if (m_Palette == pal)
		return;

	DetachPalette ();

	pal->usecount++;
	m_Palette = pal;
}


void DCanvas::DetachPalette ()
{
	if (m_Palette)
	{
		FreePalette (m_Palette);
		m_Palette = NULL;
	}
}


//
// V_DrawFPSWidget
//
void V_DrawFPSWidget()
{
	static const dtime_t ONE_SECOND = I_ConvertTimeFromMs(1000);

	static dtime_t last_time = I_GetTime();
	static dtime_t time_accum = 0;
	static unsigned int frame_count = 0;

	dtime_t current_time = I_GetTime();
	dtime_t delta_time = current_time - last_time;
	last_time = current_time;
	frame_count++;

	if (delta_time > 0)
	{
		static double last_fps = 0.0;
		static char fpsbuff[40];

		double delta_time_ms = 1000.0 * double(delta_time) / ONE_SECOND;
		int chars = sprintf(fpsbuff, "%5.1fms (%.2f fps)", delta_time_ms, last_fps);
		screen->Clear(0, screen->height - 8, chars * 8, screen->height, 0);
		screen->PrintStr(0, screen->height - 8, fpsbuff, chars);

		time_accum += delta_time;

		// calculate last_fps every 1000ms
		if (time_accum > ONE_SECOND)
		{
			last_fps = double(ONE_SECOND * frame_count) / time_accum;
			time_accum = 0;
			frame_count = 0;
		}
	}
}


//
// V_DrawFPSTicker
//
void V_DrawFPSTicker()
{
	static QWORD lasttic = 0;
	QWORD i = I_MSTime() * TICRATE / 1000;
	QWORD tics = i - lasttic;
	lasttic = i;
	if (tics > 20) tics = 20;

	for (i = 0; i < tics*2; i += 2)
		screen->buffer[(screen->height - 1) * screen->pitch + i] = 0xff;
	for ( ; i < 20*2; i += 2)
		screen->buffer[(screen->height - 1) * screen->pitch + i] = 0x0;
}

VERSION_CONTROL (v_video_cpp, "$Id: v_video.cpp 4671 2014-03-22 21:23:28Z dr_sean $")

