// Emacs style mode select   -*- C++ -*-
//-----------------------------------------------------------------------------
//
// $Id: 6bc3f1cf163bc3187dee97ba4c2c0d818494bcd7 $
//
// Copyright (C) 2006-2026 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:
//
// Low-level video hardware management.
//
//-----------------------------------------------------------------------------


#include "odamex.h"

#include <cstdlib>
#include <climits>
#include <algorithm>

#include "i_sdl.h"

#include "i_video.h"
#include "v_video.h"

#if defined(SDL20)
#include "i_video_sdl20.h"
#else
#error "no video subsystem selected"
#endif

#include "i_system.h"
#include "m_misc.h"
#include "i_input.h"
#include "m_fileio.h"

#include "w_wad.h"
#include "cmdlib.h"

// [Russell] - Just for windows, display the icon in the system menu and
// alt-tab display
#if defined(_WIN32)
	#include "win32inc.h"
    #include "SDL_syswm.h"
    #include "resource.h"
#endif	// _WIN32

// Global IVideoSubsystem instance for video startup and shutdown
static IVideoSubsystem* video_subsystem = NULL;

// Global IWindowSurface instance for the application window
static IWindowSurface* primary_surface = NULL;

// Global IWindowSurface instance for converting 8bpp data to a 32bpp surface
static IWindowSurface* converted_surface = NULL;

// Global IWindowSurface instance constructed from primary_surface.
// Used when matting is required (letter-boxing/pillar-boxing)
static IWindowSurface* matted_surface = NULL;

// Global IWindowSurface instance of size 320x200 or 640x400.
// Emulates low-resolution video modes by rendering to a small surface
// and then stretch it to the primary surface after rendering is complete.
static IWindowSurface* emulated_surface = NULL;

extern int NewWidth, NewHeight, NewBits, DisplayBits;

static int loading_icon_expire = -1;
static IWindowSurface* loading_icon_background_surface = NULL;

extern IWindowSurface* scaled_screenblocks_surface;

EXTERN_CVAR(vid_32bpp)
EXTERN_CVAR(vid_fullscreen)
EXTERN_CVAR(vid_vsync)
EXTERN_CVAR(vid_filter)
EXTERN_CVAR(vid_overscan)
EXTERN_CVAR(vid_320x200)
EXTERN_CVAR(vid_640x400)
EXTERN_CVAR(vid_autoadjust)
EXTERN_CVAR(vid_displayfps)
EXTERN_CVAR(vid_ticker)
EXTERN_CVAR(vid_widescreen)
EXTERN_CVAR(sv_allowwidescreen)


// ****************************************************************************


// ============================================================================
//
// IWindowSurface abstract base class implementation
//
// Default implementation for the IWindowSurface base class.
//
// ============================================================================

//
// IWindowSurface::IWindowSurface
//
// Initializes a new IWindowSurface given a width, height, and PixelFormat.
// If a buffer is not given, one will be allocated and marked for deallocation
// from the destructor. If the pitch is not given, the width will be used as
// the basis for the pitch.
//
IWindowSurface::IWindowSurface(uint16_t width, uint16_t height, const PixelFormat* format,
								void* buffer, uint16_t pitch) :
	mCanvas(NULL),
	mSurfaceBuffer((uint8_t*)buffer), mOwnsSurfaceBuffer(buffer == NULL),
	mPalette(V_GetDefaultPalette()->colors), mPixelFormat(*format),
	mWidth(width), mHeight(height), mPitch(pitch), mLocks(0)
{
	static constexpr uintptr_t alignment = 16;

	// Not given a pitch? Just base pitch on the given width
	if (pitch == 0)
	{
		// make the pitch a multiple of the alignment value
		mPitch = (mWidth * mPixelFormat.getBytesPerPixel() + alignment - 1) & ~(alignment - 1);
		// add a little to the pitch to prevent cache thrashing if it's 512 or 1024
		if ((mPitch & 511) == 0)
			mPitch += alignment;
	}

	mPitchInPixels = mPitch / mPixelFormat.getBytesPerPixel();

	if (mOwnsSurfaceBuffer)
	{
		uint8_t* buffer = new uint8_t[mPitch * mHeight + alignment];

		// calculate the offset from buffer to the next aligned memory address
		uintptr_t offset = ((uintptr_t)(buffer + alignment) & ~(alignment - 1)) - (uintptr_t)buffer;

		mSurfaceBuffer = buffer + offset;

		// verify we'll have enough room to store offset immediately
		// before mSurfaceBuffer's address and that offset's value is sane.
		assert(offset > 0 && offset <= alignment);

		// store mSurfaceBuffer's offset from buffer so that mSurfaceBuffer
		// can be properly freed later.
		buffer[offset - 1] = offset;
	}
}


//
// IWindowSurface::IWindowSurface
//
// Initializes the IWindowSurface from an existing IWindowSurface, but
// with possibly different values for width and height. If width or height
// are less than that of the existing surface, the new surface will be centered
// inside the existing surface.
//
IWindowSurface::IWindowSurface(IWindowSurface* base_surface, uint16_t width, uint16_t height) :
	mCanvas(NULL), mOwnsSurfaceBuffer(false),
	mPixelFormat(*base_surface->getPixelFormat()),
	mPitch(base_surface->getPitch()), mPitchInPixels(base_surface->getPitchInPixels()),
	mLocks(0)
{
	mWidth = std::min(base_surface->getWidth(), width);
	mHeight = std::min(base_surface->getHeight(), height);

	// adjust mSurfaceBuffer so that the new surface is centered in base_surface
	uint16_t x = (base_surface->getWidth() - mWidth) / 2;
	uint16_t y = (base_surface->getHeight() - mHeight) / 2;

	mSurfaceBuffer = (uint8_t*)base_surface->getBuffer(x, y);

	mPalette = base_surface->mPalette;
}


//
// IWindowSurface::~IWindowSurface
//
// Frees all of the DCanvas objects that were instantiated by this surface.
//
IWindowSurface::~IWindowSurface()
{
	// free all DCanvas objects allocated by this surface
	for (auto& canvas : mCanvasStore)
		delete canvas;

	// calculate the buffer's original address when freeing mSurfaceBuffer
	if (mOwnsSurfaceBuffer)
	{
		ptrdiff_t offset = mSurfaceBuffer[-1];
		delete [] (mSurfaceBuffer - offset);
	}
}


//
// Pixel format conversion function used by IWindowSurface::blit
//
template <typename SOURCE_PIXEL_T, typename DEST_PIXEL_T>
static inline DEST_PIXEL_T ConvertPixel(SOURCE_PIXEL_T value, const argb_t* palette);

template <>
inline palindex_t ConvertPixel(palindex_t value, const argb_t* palette)
{	return value;	}

template <>
inline argb_t ConvertPixel(palindex_t value, const argb_t* palette)
{	return palette[value];	}

template <>
inline palindex_t ConvertPixel(argb_t value, const argb_t* palette)
{	return 0;	}

template <>
inline argb_t ConvertPixel(argb_t value, const argb_t* palette)
{	return value;	}


template <typename SOURCE_PIXEL_T, typename DEST_PIXEL_T>
static void BlitLoop(DEST_PIXEL_T* dest, const SOURCE_PIXEL_T* source,
					int destpitchpixels, int srcpitchpixels, int destw, int desth,
					fixed_t xstep, fixed_t ystep, const argb_t* palette)
{
	fixed_t yfrac = 0;
	for (int y = 0; y < desth; y++)
	{
		if (sizeof(DEST_PIXEL_T) == sizeof(SOURCE_PIXEL_T) && xstep == FRACUNIT)
		{
			memcpy(dest, source, destw * sizeof(SOURCE_PIXEL_T));
		}
		else
		{
			fixed_t xfrac = 0;
			for (int x = 0; x < destw; x++)
			{
				dest[x] = ConvertPixel<SOURCE_PIXEL_T, DEST_PIXEL_T>(source[xfrac >> FRACBITS], palette);
				xfrac += xstep;
			}
		}

		dest += destpitchpixels;
		yfrac += ystep;

		source += srcpitchpixels * (yfrac >> FRACBITS);
		yfrac &= (FRACUNIT - 1);
	}
}

template <typename SOURCE_PIXEL_T, typename DEST_PIXEL_T>
static void BlitLoopCrop(DEST_PIXEL_T* dest, const SOURCE_PIXEL_T* source,
					int destpitchpixels, int srcpitchpixels,
					int destw, int desth,
					int off_top, int off_bottom, int off_left, int off_right,
					fixed_t xstep, fixed_t ystep, const argb_t* palette)
{
	fixed_t yfrac = 0;

	int pixelcur = 0;
	for (int y = 0; y < desth; y++)
	{
		// Find if we're off the top or bottom of page
		if (y - off_top >= 0 && y < desth - off_bottom)
		{
			if (sizeof(DEST_PIXEL_T) == sizeof(SOURCE_PIXEL_T) && xstep == FRACUNIT)
			{
				for (int x = 0; x < destw; x++)
				{
					// Find if we're off the left or right of page
					if (x - off_left >= 0 && x < destw - off_right)
					{
						dest[pixelcur] = source[x];
						pixelcur++;
					}
				}
				pixelcur = 0;
			}
			else
			{
				fixed_t xfrac = 0;
				for (int x = 0; x < destw; x++)
				{
					// Find if we're off the left or right of page
					if (x - off_left >= 0 && x < destw - off_right)
					{
						dest[pixelcur] = ConvertPixel<SOURCE_PIXEL_T, DEST_PIXEL_T>(source[xfrac >> FRACBITS], palette);
						pixelcur++;
					}

					xfrac += xstep;
				}
				pixelcur = 0;
			}
		}

		dest += destpitchpixels;
		yfrac += ystep;

		source += srcpitchpixels * (yfrac >> FRACBITS);
		yfrac &= (FRACUNIT - 1);
	}
}


//
// IWindowSurface::blitcrop
//
// Blits a surface into this surface, automatically scaling the source image
// to fit the destination dimensions. However, instead of scaling the image to fit
// the surface dimensions, it scales the image to destination dimensions and then
// crops it to the screen if it's off the side of the surface in any direction.
//
void IWindowSurface::blitcrop(const IWindowSurface* source_surface, int srcx, int srcy,
			int srcw, int srch, int destx, int desty, int destw, int desth)
{
	int off_left = 0;
	int off_right = 0;
	int off_top = 0;
	int off_bottom = 0;

	int bufferx = 0;
	int buffery = 0;

	// clamp to source surface edges
	if (srcx < 0)
	{
		srcw += srcx;
		srcx = 0;
	}

	if (srcy < 0)
	{
		srch += srcy;
		srcy = 0;
	}

	if (srcx + srcw > source_surface->getWidth())
		srcw = source_surface->getWidth() - srcx;
	if (srcy + srch > source_surface->getHeight())
		srch = source_surface->getHeight() - srcy;

	if (srcw == 0 || srch == 0)
		return;

	// clamp buffer to source edges, but keep destx and desty
	// for clipping purposes

	if (destx < 0)
	{
		off_left -= destx;
		bufferx = 0;
	}
	else
	{
		bufferx = destx;
	}

	if (desty < 0)
	{
		off_top -= desty;
		buffery = 0;
	}
	else
	{
		buffery = desty;
	}

	if (destx + destw > getWidth())
	{
		off_right = destw + destx - getWidth();
	}
	if (desty + desth > getHeight())
	{
		off_bottom = desth + desty - getHeight();
	}

	if (destw == 0 || desth == 0)
		return;

	// Our starting position is off the screen
	// And we will never recover from this
	// Someone messed their x/y or desth/w calc up!
	// We're done here!

	// Too far right to draw anything
	if (destx > getWidth())
		return;

	// Too far down to draw anything
	if (desty > getHeight())
		return;

	// Box at the top will never be drawn
	if (desty + desth < 0)
		return;

	// Box at the left will never be drawn
	if (destx + destw < 0)
		return;

	fixed_t xstep = FixedDiv(srcw << FRACBITS, destw << FRACBITS);
	fixed_t ystep = FixedDiv(srch << FRACBITS, desth << FRACBITS);

	int srcbits = source_surface->getBitsPerPixel();
	int destbits = getBitsPerPixel();
	int srcpitchpixels = source_surface->getPitchInPixels();
	int destpitchpixels = getPitchInPixels();

	const argb_t* palette = source_surface->getPalette();

	if (srcbits == 8 && destbits == 8)
	{
		const palindex_t* source =
		    (palindex_t*)source_surface->getBuffer() + srcy * srcpitchpixels + srcx;
		palindex_t* dest = (palindex_t*)getBuffer() + buffery * destpitchpixels + bufferx;

		BlitLoopCrop(dest, source, destpitchpixels, srcpitchpixels,
			destw, desth,
			off_top, off_bottom, off_left, off_right,
			xstep, ystep, palette);
	}
	else if (srcbits == 8 && destbits == 32)
	{
		if (palette == NULL)
			return;

		const palindex_t* source =
		    (palindex_t*)source_surface->getBuffer() + srcy * srcpitchpixels + srcx;
		argb_t* dest = (argb_t*)getBuffer() + buffery * destpitchpixels + bufferx;

		BlitLoopCrop(dest, source, destpitchpixels, srcpitchpixels,
				destw, desth,
				off_top, off_bottom, off_left, off_right,
				xstep, ystep, palette);
	}
	else if (srcbits == 32 && destbits == 8)
	{
		// we can't quickly convert from 32bpp source to 8bpp dest so don't bother
		return;
	}
	else if (srcbits == 32 && destbits == 32)
	{
		const argb_t* source =
		    (argb_t*)source_surface->getBuffer() + srcy * srcpitchpixels + srcx;
		argb_t* dest = (argb_t*)getBuffer() + buffery * destpitchpixels + bufferx;

		BlitLoopCrop(dest, source, destpitchpixels, srcpitchpixels,
			destw, desth,
			off_top, off_bottom, off_left, off_right,
			xstep, ystep, palette);
	}
}


//
// IWindowSurface::blit
//
// Blits a surface into this surface, automatically scaling the source image
// to fit the destination dimensions.
//
void IWindowSurface::blit(const IWindowSurface* source_surface, int srcx, int srcy, int srcw, int srch,
			int destx, int desty, int destw, int desth)
{
	// clamp to source surface edges
	if (srcx < 0)
	{
		srcw += srcx;
		srcx = 0;
	}

	if (srcy < 0)
	{
		srch += srcy;
		srcy = 0;
	}

	if (srcx + srcw > source_surface->getWidth())
		srcw = source_surface->getWidth() - srcx;
	if (srcy + srch > source_surface->getHeight())
		srch = source_surface->getHeight() - srcy;

	if (srcw == 0 || srch == 0)
		return;

	// clamp to dest surface edges
	if (destx < 0)
	{
		destw += destx;
		destx = 0;
	}

	if (desty < 0)
	{
		desth += desty;
		desty = 0;
	}

	if (destx + destw > getWidth())
		destw = getWidth() - destx;
	if (desty + desth > getHeight())
		desth = getHeight() - desty;

	if (destw == 0 || desth == 0)
		return;

	fixed_t xstep = FixedDiv(srcw << FRACBITS, destw << FRACBITS);
	fixed_t ystep = FixedDiv(srch << FRACBITS, desth << FRACBITS);

	int srcbits = source_surface->getBitsPerPixel();
	int destbits = getBitsPerPixel();
	int srcpitchpixels = source_surface->getPitchInPixels();
	int destpitchpixels = getPitchInPixels();

	const argb_t* palette = source_surface->getPalette();

	if (srcbits == 8 && destbits == 8)
	{
		const palindex_t* source = (palindex_t*)source_surface->getBuffer() + srcy * srcpitchpixels + srcx;
		palindex_t* dest = (palindex_t*)getBuffer() + desty * destpitchpixels + destx;

		BlitLoop(dest, source, destpitchpixels, srcpitchpixels, destw, desth, xstep, ystep, palette);
	}
	else if (srcbits == 8 && destbits == 32)
	{
		if (palette == NULL)
			return;

		const palindex_t* source = (palindex_t*)source_surface->getBuffer() + srcy * srcpitchpixels + srcx;
		argb_t* dest = (argb_t*)getBuffer() + desty * destpitchpixels + destx;

		BlitLoop(dest, source, destpitchpixels, srcpitchpixels, destw, desth, xstep, ystep, palette);
	}
	else if (srcbits == 32 && destbits == 8)
	{
		// we can't quickly convert from 32bpp source to 8bpp dest so don't bother
		return;
	}
	else if (srcbits == 32 && destbits == 32)
	{
		const argb_t* source = (argb_t*)source_surface->getBuffer() + srcy * srcpitchpixels + srcx;
		argb_t* dest = (argb_t*)getBuffer() + desty * destpitchpixels + destx;

		BlitLoop(dest, source, destpitchpixels, srcpitchpixels, destw, desth, xstep, ystep, palette);
	}
}


//
// IWindowSurface::clear
//
// Clears the surface to black.
//
void IWindowSurface::clear()
{
	const argb_t color(255, 0, 0, 0);

	lock();

	if (getBitsPerPixel() == 8)
	{
		const argb_t* palette_colors = V_GetDefaultPalette()->basecolors;
		palindex_t color_index = V_BestColor(palette_colors, color);
		palindex_t* dest = (palindex_t*)getBuffer();

		for (int y = 0; y < getHeight(); y++)
		{
			memset(dest, color_index, getWidth());
			dest += getPitchInPixels();
		}
	}
	else
	{
		argb_t* dest = (argb_t*)getBuffer();

		for (int y = 0; y < getHeight(); y++)
		{
			for (int x = 0; x < getWidth(); x++)
				dest[x] = color;

			dest += getPitchInPixels();
		}
	}

	unlock();
}


//
// IWindowSurface::createCanvas
//
// Generic factory function to instantiate a DCanvas object capable of drawing
// to this surface.
//
DCanvas* IWindowSurface::createCanvas()
{
	DCanvas* canvas = new DCanvas(this);
	mCanvasStore.push_back(canvas);

	return canvas;
}


//
// IWindowSurface::getDefaultCanvas
//
// Returns the default DCanvas object for the surface, creating it if it
// has not yet been instantiated.
//
DCanvas* IWindowSurface::getDefaultCanvas()
{
	if (mCanvas == NULL)
		mCanvas = createCanvas();

	return mCanvas;
}


//
// IWindowSurface::releaseCanvas
//
// Manually frees a DCanvas object that was instantiated by this surface
// via the createCanvas function. Note that the destructor takes care of
// freeing all of the instantiated DCanvas objects.
//
void IWindowSurface::releaseCanvas(DCanvas* canvas)
{
	if (canvas->getSurface() != this)
		I_Error("IWindowSurface::releaseCanvas: releasing canvas not owned by this surface\n");

	// Remove the DCanvas pointer from the surface's list of allocated canvases
	DCanvasCollection::iterator it = std::find(mCanvasStore.begin(), mCanvasStore.end(), canvas);
	if (it != mCanvasStore.end())
		mCanvasStore.erase(it);

	if (canvas == mCanvas)
		mCanvas = NULL;

	delete canvas;
}


// ****************************************************************************

//
// I_GetVideoModeString
//
// Returns a string with a text description of the given video mode.
//
std::string I_GetVideoModeString(const IVideoMode& mode)
{
	const char window_strs[][25] = {
		"window",
		"full screen exclusive",
		"full screen window"
	};

	return fmt::sprintf("%dx%d %dbpp (%s)", mode.width, mode.height, mode.bpp, window_strs[I_GetWindow()->getWindowMode()]);
}


//
// I_IsModeSupported
//
// Helper function for I_ValidateVideoMode. Returns true if there is a video
// mode availible with the desired bpp and screen mode.
//
static bool I_IsModeSupported(uint8_t bpp, EWindowMode window_mode)
{
	const IVideoModeList* modelist = I_GetVideoCapabilities()->getSupportedVideoModes();
	return std::any_of(modelist->cbegin(), modelist->cend(), [bpp, window_mode](const auto& mode){ return mode.bpp == bpp && mode.window_mode == window_mode; });
}


//
// I_ValidateVideoMode
//
// Transforms the given video mode into a mode that is valid for the current
// video hardware capabilities.
//
static IVideoMode I_ValidateVideoMode(const IVideoMode& mode)
{
	const IVideoMode invalid_mode(0, 0, 0, WINDOW_Windowed);
	IVideoMode desired_mode = mode;

	desired_mode.width = std::clamp<uint16_t>(mode.width, 320, MAXWIDTH);
	desired_mode.height = std::clamp<uint16_t>(mode.height, 200, MAXHEIGHT);
	desired_mode.bpp = mode.bpp;
	desired_mode.window_mode = mode.window_mode;

	// If the user requested a windowed mode, we don't have to worry about
	// the requested dimensions aligning to an actual video resolution.
	if (mode.window_mode != WINDOW_Fullscreen || !vid_autoadjust)
		return desired_mode;

	// Ensure the display type is adhered to
	if (!I_GetVideoCapabilities()->supportsFullScreen())
		desired_mode.window_mode = WINDOW_Windowed;
	else if (!I_GetVideoCapabilities()->supportsWindowed())
		desired_mode.window_mode = WINDOW_Fullscreen;

	// check if the given bit-depth is supported
	if (!I_IsModeSupported(desired_mode.bpp, desired_mode.window_mode))
	{
		// mode is not supported -- check a different bit depth
		desired_mode.bpp = desired_mode.bpp ^ (32 | 8);
		if (!I_IsModeSupported(desired_mode.bpp, desired_mode.window_mode))
			return invalid_mode;
	}

	unsigned int closest_dist = UINT_MAX;
	const IVideoMode* closest_mode = NULL;

	const IVideoModeList& modelist = *I_GetVideoCapabilities()->getSupportedVideoModes();
	for (int iteration = 0; iteration < 2; iteration++)
	{
		for (const auto& mode : modelist)
		{
			if (mode == desired_mode)		// perfect match?
				return mode;

			if (mode.bpp == desired_mode.bpp && mode.window_mode == desired_mode.window_mode)
			{
				if (iteration == 0 && (mode.width < desired_mode.width || mode.height < desired_mode.height))
					continue;

				unsigned int dist = (mode.width - desired_mode.width) * (mode.width - desired_mode.width)
						+ (mode.height - desired_mode.height) * (mode.height - desired_mode.height);

				if (dist < closest_dist)
				{
					closest_dist = dist;
					closest_mode = &mode;
				}
			}
		}

		if (closest_mode != NULL)
			return *closest_mode;
	}

	return invalid_mode;
}


//
// I_SetVideoMode
//
// Main function to set the video mode at the hardware level.
//
void I_SetVideoMode(const IVideoMode& requested_mode)
{
	// ensure the requested mode is valid
	IVideoMode validated_mode = I_ValidateVideoMode(requested_mode);
	validated_mode.vsync = bool(vid_vsync.asInt());
	validated_mode.stretch_mode = std::string(vid_filter);
	assert(validated_mode.isValid());

	IWindow* window = I_GetWindow();

	window->setMode(validated_mode);
	I_ForceUpdateGrab();

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

	// Set up the primary and emulated surfaces
	primary_surface = window->getPrimarySurface();
	int surface_width = primary_surface->getWidth(), surface_height = primary_surface->getHeight();

	I_FreeSurface(converted_surface);
	I_FreeSurface(matted_surface);
	I_FreeSurface(emulated_surface);
	I_FreeSurface(scaled_screenblocks_surface);

	// Handle a requested 8bpp surface when the video capabilities only support 32bpp
	if (requested_mode.bpp != validated_mode.bpp)
	{
		const PixelFormat* format = requested_mode.bpp == 8 ? I_Get8bppPixelFormat() : I_Get32bppPixelFormat();
		converted_surface = new IWindowSurface(surface_width, surface_height, format);
		primary_surface = converted_surface;
	}

	// clear window's surface to all black;
	primary_surface->lock();
	primary_surface->clear();
	primary_surface->unlock();

	// [SL] Determine the size of the matted surface.
	// A matted surface will be used if pillar-boxing or letter-boxing are used, or
	// if vid_320x200/vid_640x400 are being used in a wide-screen video mode, or
	// if vid_overscan is used to create a matte around the entire screen.
	//
	// Only vid_overscan creates a matted surface if the video mode is 320x200 or 640x400.
	//

	if (vid_overscan < 1.0f)
	{
		surface_width *= vid_overscan;
		surface_height *= vid_overscan;
	}

	if (!I_IsProtectedResolution(I_GetVideoWidth(), I_GetVideoHeight()))
	{
		if (vid_320x200 || vid_640x400 || (!serverside && !sv_allowwidescreen))
		{
			surface_width = surface_height * 4 / 3;
		}
		else
		{
			int ratios_x[] = { 4, 0, 16, 16, 21, 32 };
			int ratios_y[] = { 3, 0, 10,  9,  9,  9 };
			int ratio_x = ratios_x[vid_widescreen.asInt()];
			int ratio_y = ratios_y[vid_widescreen.asInt()];

			// Clamp "Auto" aspect ratio.
			if (vid_widescreen.asInt() == 1)
			{
				if (3 * surface_width < 4 * surface_height)
				{
					ratio_x = 4;
					ratio_y = 3;
				}
				else if (9 * surface_width > 32 * surface_height)
				{
					ratio_x = 32;
					ratio_y = 9;
				}
			}

			// Pillarbox or letterbox as needed.
			if (ratio_x > 0 && ratio_y > 0)
			{
				if (surface_height * ratio_x < surface_width * ratio_y)
					surface_width = surface_height * ratio_x / ratio_y;
				else if (surface_height * ratio_x > surface_width * ratio_y)
					surface_height = surface_width * ratio_y / ratio_x;
			}
		}
	}

	// Ensure matted surface dimensions are sane and sanitized.
	surface_width = std::clamp<uint16_t>(surface_width, 320, MAXWIDTH);
	surface_height = std::clamp<uint16_t>(surface_height, 200, MAXHEIGHT);

	// Is matting being used? Create matted_surface based on the primary_surface.
	if (surface_width != primary_surface->getWidth() ||
		surface_height != primary_surface->getHeight())
	{
		matted_surface = new IWindowSurface(primary_surface, surface_width, surface_height);
		primary_surface = matted_surface;
	}

	// Create emulated_surface for emulating low resolution modes.
	if (vid_320x200)
	{
		emulated_surface = new IWindowSurface(320, 200, primary_surface->getPixelFormat());
		emulated_surface->clear();
	}
	else if (vid_640x400)
	{
		emulated_surface = new IWindowSurface(640, 400, primary_surface->getPixelFormat());
		emulated_surface->clear();
	}

	screen = primary_surface->getDefaultCanvas();

	assert(I_VideoInitialized());

	if (window->getVideoMode() != requested_mode)
		DPrintFmt("I_SetVideoMode: could not set video mode to {}. Using {} instead.\n",
						I_GetVideoModeString(requested_mode),
						I_GetVideoModeString(window->getVideoMode()));
	else
		DPrintFmt("I_SetVideoMode: set video mode to {}\n",
					I_GetVideoModeString(window->getVideoMode()));

	const argb_t* palette = V_GetGamePalette()->colors;
	if (matted_surface)
		matted_surface->setPalette(palette);
	if (emulated_surface)
		emulated_surface->setPalette(palette);
	if (converted_surface)
		converted_surface->setPalette(palette);

	window->enableRefresh();
}


//
// I_VideoInitialized
//
// Returns true if the video subsystem has been initialized.
//
bool I_VideoInitialized()
{
	return video_subsystem != NULL && I_GetWindow() != NULL && I_GetWindow()->getPrimarySurface() != NULL;
}


//
// I_ShutdownHardware
//
// Destroys the application window and frees its memory.
//
void I_ShutdownHardware()
{
	I_FreeSurface(loading_icon_background_surface);

	delete video_subsystem;
	video_subsystem = NULL;
}


//
// I_InitHardware
//
void I_InitHardware()
{
	if (I_IsHeadless())
	{
		video_subsystem = new IDummyVideoSubsystem();
	}
	else
	{
		#if defined(SDL20)
		video_subsystem = new ISDL20VideoSubsystem();
		#endif
		assert(video_subsystem != NULL);

		const IVideoMode& native_mode = I_GetVideoCapabilities()->getNativeMode();
		PrintFmt(PRINT_HIGH, "I_InitHardware: native resolution: {:s}\n", I_GetVideoModeString(native_mode));
	}
}


//
// I_GetVideoCapabilities
//
const IVideoCapabilities* I_GetVideoCapabilities()
{
	// a valid IWindow is not needed for querying video capabilities
	// so don't call I_VideoInitialized
	if (video_subsystem)
		return video_subsystem->getVideoCapabilities();
	return NULL;
}


//
// I_GetWindow
//
// Returns a pointer to the application window object.
//
IWindow* I_GetWindow()
{
	if (video_subsystem)
		return video_subsystem->getWindow();
	return NULL;
}


//
// I_GetVideoWidth
//
// Returns the width of the current video mode. Assumes that the video
// window has already been created.
//
int I_GetVideoWidth()
{
	if (I_VideoInitialized())
		return I_GetWindow()->getWidth();
	return 0;
}


//
// I_GetVideoHeight
//
// Returns the height of the current video mode. Assumes that the video
// window has already been created.
//
int I_GetVideoHeight()
{
	if (I_VideoInitialized())
		return I_GetWindow()->getHeight();
	return 0;
}


//
// I_GetVideoBitDepth
//
// Returns the bits per pixelof the current video mode. Assumes that the video
// window has already been created.
//
int I_GetVideoBitDepth()
{
	if (I_VideoInitialized())
		return I_GetWindow()->getBitsPerPixel();
	return 0;
}


//
// I_GetPrimarySurface
//
// Returns a pointer to the application window's primary surface object.
//
IWindowSurface* I_GetPrimarySurface()
{
	return primary_surface;
}


//
// I_GetPrimaryCanvas
//
// Returns a pointer to the primary surface's default canvas.
//
DCanvas* I_GetPrimaryCanvas()
{
	if (I_VideoInitialized())
		return I_GetPrimarySurface()->getDefaultCanvas();
	return NULL;
}


//
// I_GetEmulatedSurface
//
IWindowSurface* I_GetEmulatedSurface()
{
	return emulated_surface;
}


//
// I_BlitEmulatedSurface
//
// Blits the emulated surface (320x200 or 640x400) to the primary surface,
// stretching it to fill the entire screen.
//
void I_BlitEmulatedSurface()
{
	if (emulated_surface)
	{
		primary_surface->blit(emulated_surface, 0, 0,
				emulated_surface->getWidth(), emulated_surface->getHeight(),
				0, 0, primary_surface->getWidth(), primary_surface->getHeight());
	}
}


//
// I_AllocateSurface
//
// Creates a new (non-primary) surface and returns it.
//
IWindowSurface* I_AllocateSurface(int width, int height, int bpp)
{
	const PixelFormat* format;

	if (I_GetPrimarySurface() && bpp == I_GetPrimarySurface()->getBitsPerPixel())
		format = I_GetPrimarySurface()->getPixelFormat();
	else if (bpp == 8)
		format = I_Get8bppPixelFormat();
	else
		format = I_Get32bppPixelFormat();

	return new IWindowSurface(width, height, format);
}


//
// I_FreeSurface
//
void I_FreeSurface(IWindowSurface* &surface)
{
	delete surface;
	surface = NULL;
}


//
// I_GetSurfaceWidth
//
int I_GetSurfaceWidth()
{
	if (I_VideoInitialized())
		return I_GetPrimarySurface()->getWidth();
	return 0;
}


//
// I_GetSurfaceHeight
//
int I_GetSurfaceHeight()
{
	if (I_VideoInitialized())
		return I_GetPrimarySurface()->getHeight();
	return 0;
}


//
// I_WindowToSurfaceCoords
//
// Translates a point in window coordinates to the same point in the
// surface that draws to the window.
//
// Returns true if the function was able to translate the position.
//
bool I_WindowToSurfaceCoords(int window_x, int window_y, int& surface_x, int& surface_y)
{
	if (!I_VideoInitialized() || I_GetWindow() == nullptr)
		return false;

	int x, y;
	if (!I_GetWindow()->windowToSurfaceCoords(window_x, window_y, x, y))
		return false;

	// The surface everything draws to may be a matted sub-surface centered
	// inside the window's surface, so shift into its coordinate space.
	const IWindowSurface* window_surface = I_GetWindow()->getPrimarySurface();
	const IWindowSurface* draw_surface = I_GetPrimarySurface();

	if (window_surface == nullptr || draw_surface == nullptr)
		return false;

	x -= (window_surface->getWidth() - draw_surface->getWidth()) / 2;
	y -= (window_surface->getHeight() - draw_surface->getHeight()) / 2;

	if (x < 0 || x >= draw_surface->getWidth() || y < 0 || y >= draw_surface->getHeight())
		return false;

	surface_x = x;
	surface_y = y;

	return true;
}


//
// I_IsProtectedReslotuion
//
// [ML] If this is 320x200 or 640x400, the resolutions
// "protected" from aspect ratio correction.
//
bool I_IsProtectedResolution(int width, int height)
{
	return (width == 320 && height == 200) || (width == 640 && height == 400);
}

bool I_IsProtectedResolution(const IWindowSurface* surface)
{
	return I_IsProtectedResolution(surface->getWidth(), surface->getHeight());
}

//
// I_IsWideResolution
//
bool I_IsWideResolution(int width, int height)
{
	if (I_IsProtectedResolution(width, height))
		return false;

	// consider the mode widescreen if it's width-to-height ratio is
	// closer to 16:10 than it is to 4:3
	return abs(15 * width - 20 * height) > abs(15 * width - 24 * height);
}

bool I_IsWideResolution(const IWindowSurface* surface)
{
	return I_IsWideResolution(surface->getWidth(), surface->getHeight());
}


//
// I_LockAllSurfaces
//
static void I_LockAllSurfaces()
{
	if (emulated_surface)
		emulated_surface->lock();
	if (matted_surface)
		matted_surface->lock();
	if (converted_surface)
		converted_surface->lock();
	primary_surface->lock();

	I_GetWindow()->lockSurface();
}


//
// I_UnlockAllSurfaces
//
static void I_UnlockAllSurfaces()
{
	I_GetWindow()->unlockSurface();

	primary_surface->unlock();

	if (converted_surface)
		converted_surface->unlock();
	if (matted_surface)
		matted_surface->unlock();
	if (emulated_surface)
		emulated_surface->unlock();
}


//
// I_DrawLoadingIcon
//
// Sets a flag to indicate that I_FinishUpdate should draw the loading (disk)
// icon in the lower right corner of the screen.
//
void I_DrawLoadingIcon()
{
	loading_icon_expire = gametic;
}


//
// I_BlitLoadingIcon
//
// Takes care of the actual drawing of the loading icon.
//
static void I_BlitLoadingIcon()
{
	const patch_t* diskpatch = W_CachePatch("STDISK");
	IWindowSurface* surface = I_GetPrimarySurface();

	surface->lock();

	int bpp = surface->getBitsPerPixel();
	int scale = std::min(CleanXfac, CleanYfac);
	int w = diskpatch->width() * scale;
	int h = diskpatch->height() * scale;
	int x = surface->getWidth() - w;
	int y = surface->getHeight() - h;

	// offset x and y for the lower right corner of the screen
	int ofsx = x + (scale * diskpatch->leftoffset());
	int ofsy = y + (scale * diskpatch->topoffset());

	// save the area where the icon will be drawn to an off-screen surface
	// so that it can be restored after the frame is blitted
	if (!loading_icon_background_surface ||
		loading_icon_background_surface->getWidth() != w ||
		loading_icon_background_surface->getHeight() != h ||
		loading_icon_background_surface->getBitsPerPixel() != bpp)
	{
		I_FreeSurface(loading_icon_background_surface);

		loading_icon_background_surface = I_AllocateSurface(w, h, bpp);
	}

	loading_icon_background_surface->lock();

	loading_icon_background_surface->blit(surface, x, y, w, h, 0, 0, w, h);
	surface->getDefaultCanvas()->DrawPatchStretched(diskpatch, ofsx, ofsy, w, h);

	loading_icon_background_surface->unlock();
	surface->unlock();
}


//
// I_RestoreLoadingIcon
//
// Restores the background area that was saved prior to blitting the
// loading icon.
//
static void I_RestoreLoadingIcon()
{
	IWindowSurface* surface = I_GetPrimarySurface();

	surface->lock();
	loading_icon_background_surface->lock();

	int w = loading_icon_background_surface->getWidth();
	int h = loading_icon_background_surface->getHeight();
	int x = surface->getWidth() - w;
	int y = surface->getHeight() - h;

	surface->blit(loading_icon_background_surface, 0, 0, w, h, x, y, w, h);

	loading_icon_background_surface->unlock();
	surface->unlock();
}


//
// I_BeginUpdate
//
// Called at the start of drawing a new video frame. This locks the primary
// surface for drawing.
//
void I_BeginUpdate()
{
	if (I_VideoInitialized())
	{
		I_GetWindow()->startRefresh();

		I_LockAllSurfaces();
	}
}


//
// I_FinishUpdate
//
// Called at the end of drawing a video frame. This unlocks the primary
// surface for drawing and blits the contents to the video screen.
//
void I_FinishUpdate()
{
	if (I_VideoInitialized())
	{
		// draws little dots on the bottom of the screen
		if (vid_ticker)
			V_DrawFPSTicker();

		// Draws frame time and cumulative fps
		if (vid_displayfps)
			V_DrawFPSWidget();

		// draws a disk loading icon in the lower right corner
		if (gametic <= loading_icon_expire)
			I_BlitLoadingIcon();

		// Handle blitting our 8bpp surface to the 32bpp video window surface
		if (converted_surface)
		{
			IWindowSurface* real_primary_surface = I_GetWindow()->getPrimarySurface();
			real_primary_surface->blit(converted_surface,
					0, 0, converted_surface->getWidth(), converted_surface->getHeight(),
					0, 0, real_primary_surface->getWidth(), real_primary_surface->getHeight());
		}

		I_UnlockAllSurfaces();

		I_GetWindow()->finishRefresh();

		// restores the background underneath the disk loading icon in the lower right corner
		if (gametic <= loading_icon_expire)
			I_RestoreLoadingIcon();
	}
}


//
// I_SetPalette
//
void I_SetPalette(const argb_t* palette)
{
	if (I_VideoInitialized())
		I_GetWindow()->setPalette(palette);
}


//
// I_SetWindowCaption
//
// Set the window caption.
//
void I_SetWindowCaption(const std::string& caption)
{
	// [Russell] - A basic version string that will eventually get replaced

	std::string title("Odamex ");
	title += NiceVersion();

	if (!caption.empty())
		title += " - " + caption;

	I_GetWindow()->setWindowTitle(title);
}


//
// I_SetWindowIcon
//
// Set the window icon (currently Windows only).
//
void I_SetWindowIcon()
{
	I_GetWindow()->setWindowIcon();
}


//
// I_GetMonitorCount
//
int I_GetMonitorCount()
{
	if (I_VideoInitialized())
		return video_subsystem->getMonitorCount();
	return 0;
}


//
// I_GetVideoDriverName
//
// Returns the name of the current video driver in-use.
//
std::string I_GetVideoDriverName()
{
	if (I_VideoInitialized())
		return I_GetWindow()->getVideoDriverName();
	return std::string();
}


//
// I_Get8bppPixelFormat
//
// Returns the platform's PixelFormat for 8bpp video modes.
//
const PixelFormat* I_Get8bppPixelFormat()
{
	static PixelFormat format(8, 0, 0, 0, 0, 0, 0, 0, 0);
	return &format;
}


//
// I_Get32bppPixelFormat
//
// Returns the platform's PixelFormat for 32bpp video modes.
// This changes on certain platforms depending on the current video mode
// in use (or fullscreen vs windowed).
//
const PixelFormat* I_Get32bppPixelFormat()
{
	if (I_GetPrimarySurface() && I_GetPrimarySurface()->getBitsPerPixel() == 32)
		return I_GetPrimarySurface()->getPixelFormat();

	#ifdef __BIG_ENDIAN__
	static PixelFormat format(32, 0, 0, 0, 0, 0, 8, 16, 24);
	#else
	static PixelFormat format(32, 0, 0, 0, 0, 24, 16, 8, 0);
	#endif

	return &format;
}

int I_GetAspectCorrectWidth(int surface_height, int asset_height, int asset_width)
{
	float aspect_scale_ratio = (float)surface_height / (float)asset_height;
	return aspect_scale_ratio * asset_width;
}

VERSION_CONTROL (i_video_cpp, "$Id: 6bc3f1cf163bc3187dee97ba4c2c0d818494bcd7 $")
