/*
** i_main.cpp
** System-specific startup code. Eventually calls D_DoomMain.
**
**---------------------------------------------------------------------------
** Copyright 1998-2009 Randy Heit
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
**    notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
**    notice, this list of conditions and the following disclaimer in the
**    documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
**    derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
*/

// HEADER FILES ------------------------------------------------------------

#define WIN32_LEAN_AND_MEAN
#define _WIN32_WINNT 0x0501
#include <windows.h>
#include <mmsystem.h>
#include <objbase.h>
#include <commctrl.h>
#include <richedit.h>

#ifdef _MSC_VER
#pragma warning(disable:4244)
#endif

//#include <wtsapi32.h>
#define NOTIFY_FOR_THIS_SESSION 0

#ifdef _MSC_VER
#include <eh.h>
#include <new.h>
#include <crtdbg.h>
#endif
#include "resource.h"

#include "doomerrors.h"
#include "hardware.h"

#include "m_argv.h"
#include "d_main.h"
#include "i_module.h"
#include "c_console.h"
#include "version.h"
#include "i_input.h"
#include "w_wad.h"
#include "cmdlib.h"
#include "g_game.h"
#include "r_utility.h"
#include "g_levellocals.h"
#include "s_sound.h"
#include "vm.h"
#include "i_system.h"
#include "gstrings.h"

#include "stats.h"
#include "st_start.h"

#include "optwin32.h"

// MACROS ------------------------------------------------------------------

// The main window's title.
#ifdef _M_X64
#define X64 " 64-bit"
#else
#define X64 ""
#endif

// The maximum number of functions that can be registered with atterm.
#define MAX_TERMS	64

// TYPES -------------------------------------------------------------------

// EXTERNAL FUNCTION PROTOTYPES --------------------------------------------

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM);
void CreateCrashLog (char *custominfo, DWORD customsize, HWND richedit);
void DisplayCrashLog ();
extern uint8_t *ST_Util_BitsForBitmap (BITMAPINFO *bitmap_info);
void I_FlushBufferedConsoleStuff();

// PUBLIC FUNCTION PROTOTYPES ----------------------------------------------

// PRIVATE FUNCTION PROTOTYPES ---------------------------------------------

// EXTERNAL DATA DECLARATIONS ----------------------------------------------

extern EXCEPTION_POINTERS CrashPointers;
extern BITMAPINFO *StartupBitmap;
extern UINT TimerPeriod;

// PUBLIC DATA DEFINITIONS -------------------------------------------------

// The command line arguments.
FArgs *Args;

HINSTANCE		g_hInst;
DWORD			SessionID;
HANDLE			MainThread;
DWORD			MainThreadID;
HANDLE			StdOut;
bool			FancyStdOut, AttachedStdOut;
bool			ConWindowHidden;

// The main window
HWND			Window;

// The subwindows used for startup and error output
HWND			ConWindow, GameTitleWindow;
HWND			ErrorPane, ProgressBar, NetStartPane, StartupScreen, ErrorIcon;

HFONT			GameTitleFont;
LONG			GameTitleFontHeight;
LONG			DefaultGUIFontHeight;
LONG			ErrorIconChar;

FModule Kernel32Module{"Kernel32"};
FModule Shell32Module{"Shell32"};
FModule User32Module{"User32"};

namespace OptWin32 {
#define DYN_WIN32_SYM(x) decltype(x) x{#x}

DYN_WIN32_SYM(SHGetKnownFolderPath);

#undef DYN_WIN32_SYM
} // namespace OptWin32

// PRIVATE DATA DEFINITIONS ------------------------------------------------

static const WCHAR WinClassName[] = WGAMENAME "MainWindow";
static HMODULE hwtsapi32;		// handle to wtsapi32.dll
static void (*TermFuncs[MAX_TERMS])(void);
static int NumTerms;

// CODE --------------------------------------------------------------------

//==========================================================================
//
// atterm
//
// Our own atexit because atexit can be problematic under Linux, though I
// forget the circumstances that cause trouble.
//
//==========================================================================

void atterm (void (*func)(void))
{
	// Make sure this function wasn't already registered.
	for (int i = 0; i < NumTerms; ++i)
	{
		if (TermFuncs[i] == func)
		{
			return;
		}
	}
	if (NumTerms == MAX_TERMS)
	{
		func ();
		I_FatalError ("Too many exit functions registered.\nIncrease MAX_TERMS in i_main.cpp");
	}
	TermFuncs[NumTerms++] = func;
}

//==========================================================================
//
// popterm
//
// Removes the most recently register atterm function.
//
//==========================================================================

void popterm ()
{
	if (NumTerms)
		NumTerms--;
}

//==========================================================================
//
// call_terms
//
//==========================================================================

static void call_terms (void)
{
	while (NumTerms > 0)
	{
		TermFuncs[--NumTerms]();
	}
}

#ifdef _MSC_VER
static int NewFailure (size_t size)
{
	I_FatalError ("Failed to allocate %d bytes from process heap", size);
	return 0;
}
#endif

//==========================================================================
//
// UnCOM
//
// Called by atterm if CoInitialize() succeeded.
//
//==========================================================================

static void UnCOM (void)
{
	CoUninitialize ();
}

//==========================================================================
//
// UnWTS
//
// Called by atterm if RegisterSessionNotification() succeeded.
//
//==========================================================================

static void UnWTS (void)
{
	if (hwtsapi32 != 0)
	{
		typedef BOOL (WINAPI *ursn)(HWND);
		ursn unreg = (ursn)GetProcAddress (hwtsapi32, "WTSUnRegisterSessionNotification");
		if (unreg != 0)
		{
			unreg (Window);
		}
		FreeLibrary (hwtsapi32);
		hwtsapi32 = 0;
	}
}

//==========================================================================
//
// LayoutErrorPane
//
// Lays out the error pane to the desired width, returning the required
// height.
//
//==========================================================================

static int LayoutErrorPane (HWND pane, int w)
{
	HWND ctl;
	RECT rectc;

	// Right-align the Quit button.
	ctl = GetDlgItem (pane, IDOK);
	GetClientRect (ctl, &rectc);	// Find out how big it is.
	MoveWindow (ctl, w - rectc.right - 1, 1, rectc.right, rectc.bottom, TRUE);
	InvalidateRect (ctl, NULL, TRUE);

	// Return the needed height for this layout
	return rectc.bottom + 2;
}

//==========================================================================
//
// LayoutNetStartPane
//
// Lays out the network startup pane to the specified width, returning
// its required height.
//
//==========================================================================

int LayoutNetStartPane (HWND pane, int w)
{
	HWND ctl;
	RECT margin, rectc;
	int staticheight, barheight;

	// Determine margin sizes.
	SetRect (&margin, 7, 7, 0, 0);
	MapDialogRect (pane, &margin);

	// Stick the message text in the upper left corner.
	ctl = GetDlgItem (pane, IDC_NETSTARTMESSAGE);
	GetClientRect (ctl, &rectc);
	MoveWindow (ctl, margin.left, margin.top, rectc.right, rectc.bottom, TRUE);

	// Stick the count text in the upper right corner.
	ctl = GetDlgItem (pane, IDC_NETSTARTCOUNT);
	GetClientRect (ctl, &rectc);
	MoveWindow (ctl, w - rectc.right - margin.left, margin.top, rectc.right, rectc.bottom, TRUE);
	staticheight = rectc.bottom;

	// Stretch the progress bar to fill the entire width.
	ctl = GetDlgItem (pane, IDC_NETSTARTPROGRESS);
	barheight = GetSystemMetrics (SM_CYVSCROLL);
	MoveWindow (ctl, margin.left, margin.top*2 + staticheight, w - margin.left*2, barheight, TRUE);

	// Center the abort button underneath the progress bar.
	ctl = GetDlgItem (pane, IDCANCEL);
	GetClientRect (ctl, &rectc);
	MoveWindow (ctl, (w - rectc.right) / 2, margin.top*3 + staticheight + barheight, rectc.right, rectc.bottom, TRUE);

	return margin.top*4 + staticheight + barheight + rectc.bottom;
}

//==========================================================================
//
// LayoutMainWindow
//
// Lays out the main window with the game title and log controls and
// possibly the error pane and progress bar.
//
//==========================================================================

void LayoutMainWindow (HWND hWnd, HWND pane)
{
	RECT rect;
	int errorpaneheight = 0;
	int bannerheight = 0;
	int progressheight = 0;
	int netpaneheight = 0;
	int leftside = 0;
	int w, h;

	GetClientRect (hWnd, &rect);
	w = rect.right;
	h = rect.bottom;

	if (DoomStartupInfo.Name.IsNotEmpty() && GameTitleWindow != NULL)
	{
		bannerheight = GameTitleFontHeight + 5;
		MoveWindow (GameTitleWindow, 0, 0, w, bannerheight, TRUE);
		InvalidateRect (GameTitleWindow, NULL, FALSE);
	}
	if (ProgressBar != NULL)
	{
		// Base the height of the progress bar on the height of a scroll bar
		// arrow, just as in the progress bar example.
		progressheight = GetSystemMetrics (SM_CYVSCROLL);
		MoveWindow (ProgressBar, 0, h - progressheight, w, progressheight, TRUE);
	}
	if (NetStartPane != NULL)
	{
		netpaneheight = LayoutNetStartPane (NetStartPane, w);
		SetWindowPos (NetStartPane, HWND_TOP, 0, h - progressheight - netpaneheight, w, netpaneheight, SWP_SHOWWINDOW);
	}
	if (pane != NULL)
	{
		errorpaneheight = LayoutErrorPane (pane, w);
		SetWindowPos (pane, HWND_TOP, 0, h - progressheight - netpaneheight - errorpaneheight, w, errorpaneheight, 0);
	}
	if (ErrorIcon != NULL)
	{
		leftside = GetSystemMetrics (SM_CXICON) + 6;
		MoveWindow (ErrorIcon, 0, bannerheight, leftside, h - bannerheight - errorpaneheight - progressheight - netpaneheight, TRUE);
	}
	// If there is a startup screen, it covers the log window
	if (StartupScreen != NULL)
	{
		SetWindowPos (StartupScreen, HWND_TOP, leftside, bannerheight, w - leftside,
			h - bannerheight - errorpaneheight - progressheight - netpaneheight, SWP_SHOWWINDOW);
		InvalidateRect (StartupScreen, NULL, FALSE);
		MoveWindow (ConWindow, 0, 0, 0, 0, TRUE);
	}
	else
	{
		// The log window uses whatever space is left.
		MoveWindow (ConWindow, leftside, bannerheight, w - leftside,
			h - bannerheight - errorpaneheight - progressheight - netpaneheight, TRUE);
	}
}


//==========================================================================
//
// I_SetIWADInfo
//
//==========================================================================

void I_SetIWADInfo()
{
	// Make the startup banner show itself
	LayoutMainWindow(Window, NULL);
}

//==========================================================================
//
// LConProc
//
// The main window's WndProc during startup. During gameplay, the WndProc
// in i_input.cpp is used instead.
//
//==========================================================================

LRESULT CALLBACK LConProc (HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	HWND view;
	HDC hdc;
	HBRUSH hbr;
	HGDIOBJ oldfont;
	RECT rect;
	SIZE size;
	LOGFONT lf;
	TEXTMETRIC tm;
	HINSTANCE inst = (HINSTANCE)(LONG_PTR)GetWindowLongPtr(hWnd, GWLP_HINSTANCE);
	DRAWITEMSTRUCT *drawitem;
	CHARFORMAT2W format;

	switch (msg)
	{
	case WM_CREATE:
		// Create game title static control
		memset (&lf, 0, sizeof(lf));
		hdc = GetDC (hWnd);
		lf.lfHeight = -MulDiv(12, GetDeviceCaps(hdc, LOGPIXELSY), 72);
		lf.lfCharSet = ANSI_CHARSET;
		lf.lfWeight = FW_BOLD;
		lf.lfPitchAndFamily = VARIABLE_PITCH | FF_ROMAN;
		wcscpy (lf.lfFaceName, L"Trebuchet MS");
		GameTitleFont = CreateFontIndirect (&lf);

		oldfont = SelectObject (hdc, GetStockObject (DEFAULT_GUI_FONT));
		GetTextMetrics (hdc, &tm);
		DefaultGUIFontHeight = tm.tmHeight;
		if (GameTitleFont == NULL)
		{
			GameTitleFontHeight = DefaultGUIFontHeight;
		}
		else
		{
			SelectObject (hdc, GameTitleFont);
			GetTextMetrics (hdc, &tm);
			GameTitleFontHeight = tm.tmHeight;
		}
		SelectObject (hdc, oldfont);

		// Create log read-only edit control
		view = CreateWindowExW (WS_EX_NOPARENTNOTIFY, L"RichEdit20W", nullptr,
			WS_CHILD | WS_VISIBLE | WS_VSCROLL |
			ES_LEFT | ES_MULTILINE | WS_CLIPSIBLINGS,
			0, 0, 0, 0,
			hWnd, NULL, inst, NULL);
		HRESULT hr;
		hr = GetLastError();
		if (view == NULL)
		{
			ReleaseDC (hWnd, hdc);
			return -1;
		}
		SendMessage (view, EM_SETREADONLY, TRUE, 0);
		SendMessage (view, EM_EXLIMITTEXT, 0, 0x7FFFFFFE);
		SendMessage (view, EM_SETBKGNDCOLOR, 0, RGB(70,70,70));
		// Setup default font for the log.
		//SendMessage (view, WM_SETFONT, (WPARAM)GetStockObject (DEFAULT_GUI_FONT), FALSE);
		format.cbSize = sizeof(format);
		format.dwMask = CFM_BOLD | CFM_COLOR | CFM_FACE | CFM_SIZE | CFM_CHARSET;
		format.dwEffects = 0;
		format.yHeight = 200;
		format.crTextColor = RGB(223,223,223);
		format.bCharSet = ANSI_CHARSET;
		format.bPitchAndFamily = FF_SWISS | VARIABLE_PITCH;
		wcscpy(format.szFaceName, L"DejaVu Sans");	// At least I have it. :p
		SendMessageW(view, EM_SETCHARFORMAT, SCF_ALL, (LPARAM)&format);

		ConWindow = view;
		ReleaseDC (hWnd, hdc);

		view = CreateWindowExW (WS_EX_NOPARENTNOTIFY, L"STATIC", NULL, WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | SS_OWNERDRAW, 0, 0, 0, 0, hWnd, nullptr, inst, nullptr);
		if (view == nullptr)
		{
			return -1;
		}
		SetWindowLong (view, GWL_ID, IDC_STATIC_TITLE);
		GameTitleWindow = view;

		return 0;

	case WM_SIZE:
		if (wParam != SIZE_MAXHIDE && wParam != SIZE_MAXSHOW)
		{
			LayoutMainWindow (hWnd, ErrorPane);
		}
		return 0;

	case WM_DRAWITEM:
		// Draw title banner.
		if (wParam == IDC_STATIC_TITLE && DoomStartupInfo.Name.IsNotEmpty())
		{
			const PalEntry *c;

			// Draw the game title strip at the top of the window.
			drawitem = (LPDRAWITEMSTRUCT)lParam;

			// Draw the background.
			rect = drawitem->rcItem;
			rect.bottom -= 1;
			c = (const PalEntry *)&DoomStartupInfo.BkColor;
			hbr = CreateSolidBrush (RGB(c->r,c->g,c->b));
			FillRect (drawitem->hDC, &drawitem->rcItem, hbr);
			DeleteObject (hbr);

			// Calculate width of the title string.
			SetTextAlign (drawitem->hDC, TA_TOP);
			oldfont = SelectObject (drawitem->hDC, GameTitleFont != NULL ? GameTitleFont : (HFONT)GetStockObject (DEFAULT_GUI_FONT));
			auto widename = DoomStartupInfo.Name.WideString();
			GetTextExtentPoint32W (drawitem->hDC, widename.c_str(), (int)widename.length(), &size);

			// Draw the title.
			c = (const PalEntry *)&DoomStartupInfo.FgColor;
			SetTextColor (drawitem->hDC, RGB(c->r,c->g,c->b));
			SetBkMode (drawitem->hDC, TRANSPARENT);
			TextOutW (drawitem->hDC, rect.left + (rect.right - rect.left - size.cx) / 2, 2, widename.c_str(), (int)widename.length());
			SelectObject (drawitem->hDC, oldfont);
			return TRUE;
		}
		// Draw startup screen
		else if (wParam == IDC_STATIC_STARTUP)
		{
			if (StartupScreen != NULL)
			{
				drawitem = (LPDRAWITEMSTRUCT)lParam;

				rect = drawitem->rcItem;
				// Windows expects DIBs to be bottom-up but ours is top-down,
				// so flip it vertically while drawing it.
				StretchDIBits (drawitem->hDC, rect.left, rect.bottom - 1, rect.right - rect.left, rect.top - rect.bottom,
					0, 0, StartupBitmap->bmiHeader.biWidth, StartupBitmap->bmiHeader.biHeight,
					ST_Util_BitsForBitmap(StartupBitmap), StartupBitmap, DIB_RGB_COLORS, SRCCOPY);

				// If the title banner is gone, then this is an ENDOOM screen, so draw a short prompt
				// where the command prompt would have been in DOS.
				if (GameTitleWindow == NULL)
				{
					auto quitmsg = WideString(GStrings("TXT_QUITENDOOM"));

					SetTextColor (drawitem->hDC, RGB(240,240,240));
					SetBkMode (drawitem->hDC, TRANSPARENT);
					oldfont = SelectObject (drawitem->hDC, (HFONT)GetStockObject (DEFAULT_GUI_FONT));
					TextOutW (drawitem->hDC, 3, drawitem->rcItem.bottom - DefaultGUIFontHeight - 3, quitmsg.c_str(), (int)quitmsg.length());
					SelectObject (drawitem->hDC, oldfont);
				}
				return TRUE;
			}
		}
		// Draw stop icon.
		else if (wParam == IDC_ICONPIC)
		{
			HICON icon;
			POINTL char_pos;
			drawitem = (LPDRAWITEMSTRUCT)lParam;

			// This background color should match the edit control's.
			hbr = CreateSolidBrush (RGB(70,70,70));
			FillRect (drawitem->hDC, &drawitem->rcItem, hbr);
			DeleteObject (hbr);

			// Draw the icon aligned with the first line of error text.
			SendMessage (ConWindow, EM_POSFROMCHAR, (WPARAM)&char_pos, ErrorIconChar);
			icon = (HICON)LoadImage (0, IDI_ERROR, IMAGE_ICON, 0, 0, LR_DEFAULTSIZE | LR_SHARED);
			DrawIcon (drawitem->hDC, 6, char_pos.y, icon);
			return TRUE;
		}
		return FALSE;

	case WM_COMMAND:
		if (ErrorIcon != NULL && (HWND)lParam == ConWindow && HIWORD(wParam) == EN_UPDATE)
		{
			// Be sure to redraw the error icon if the edit control changes.
			InvalidateRect (ErrorIcon, NULL, TRUE);
			return 0;
		}
		break;

	case WM_CLOSE:
		PostQuitMessage (0);
		break;

	case WM_DESTROY:
		if (GameTitleFont != NULL)
		{
			DeleteObject (GameTitleFont);
		}
		break;
	}
	return DefWindowProc (hWnd, msg, wParam, lParam);
}

//==========================================================================
//
// ErrorPaneProc
//
// DialogProc for the error pane.
//
//==========================================================================

INT_PTR CALLBACK ErrorPaneProc (HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
	switch (msg)
	{
	case WM_INITDIALOG:
		// Appear in the main window.
		LayoutMainWindow (GetParent (hDlg), hDlg);
		return TRUE;

	case WM_COMMAND:
		// There is only one button, and it's "Ok" and makes us quit.
		if (HIWORD(wParam) == BN_CLICKED)
		{
			PostQuitMessage (0);
			return TRUE;
		}
		break;
	}
	return FALSE;
}

//==========================================================================
//
// I_SetWndProc
//
// Sets the main WndProc, hides all the child windows, and starts up
// in-game input.
//
//==========================================================================

void I_SetWndProc()
{
	if (GetWindowLongPtr (Window, GWLP_USERDATA) == 0)
	{
		SetWindowLongPtr (Window, GWLP_USERDATA, 1);
		SetWindowLongPtr (Window, GWLP_WNDPROC, (WLONG_PTR)WndProc);
		ShowWindow (ConWindow, SW_HIDE);
		ConWindowHidden = true;
		ShowWindow (GameTitleWindow, SW_HIDE);
		I_InitInput (Window);
	}
}

//==========================================================================
//
// RestoreConView
//
// Returns the main window to its startup state.
//
//==========================================================================

void RestoreConView()
{
	HDC screenDC = GetDC(0);
	int dpi = GetDeviceCaps(screenDC, LOGPIXELSX);
	ReleaseDC(0, screenDC);
	int width = (512 * dpi + 96 / 2) / 96;
	int height = (384 * dpi + 96 / 2) / 96;

	// Make sure the window has a frame in case it was fullscreened.
	SetWindowLongPtr (Window, GWL_STYLE, WS_VISIBLE|WS_OVERLAPPEDWINDOW);
	if (GetWindowLong (Window, GWL_EXSTYLE) & WS_EX_TOPMOST)
	{
		SetWindowPos (Window, HWND_BOTTOM, 0, 0, width, height,
			SWP_DRAWFRAME | SWP_NOCOPYBITS | SWP_NOMOVE);
		SetWindowPos (Window, HWND_TOP, 0, 0, 0, 0, SWP_NOCOPYBITS | SWP_NOMOVE | SWP_NOSIZE);
	}
	else
	{
		SetWindowPos (Window, NULL, 0, 0, width, height,
			SWP_DRAWFRAME | SWP_NOCOPYBITS | SWP_NOMOVE | SWP_NOZORDER);
	}

	SetWindowLongPtr (Window, GWLP_WNDPROC, (WLONG_PTR)LConProc);
	ShowWindow (ConWindow, SW_SHOW);
	ConWindowHidden = false;
	ShowWindow (GameTitleWindow, SW_SHOW);
	I_ShutdownInput ();		// Make sure the mouse pointer is available.
	// Make sure the progress bar isn't visible.
	if (StartScreen != NULL)
	{
		delete StartScreen;
		StartScreen = NULL;
	}
}

//==========================================================================
//
// ShowErrorPane
//
// Shows an error message, preferably in the main window, but it can
// use a normal message box too.
//
//==========================================================================

void ShowErrorPane(const char *text)
{
	auto widetext = WideString(text);
	if (Window == nullptr || ConWindow == nullptr)
	{
		if (text != NULL)
		{
			MessageBoxW (Window, widetext.c_str(),
				WGAMENAME " Fatal Error", MB_OK|MB_ICONSTOP|MB_TASKMODAL);
		}
		return;
	}

	if (StartScreen != NULL)	// Ensure that the network pane is hidden.
	{
		StartScreen->NetDone();
	}
	if (text != NULL)
	{
		FStringf caption("Fatal Error - " GAMENAME " %s " X64 " (%s)", GetVersionString(), GetGitTime());
		auto wcaption = caption.WideString();
		SetWindowTextW (Window, wcaption.c_str());
		ErrorIcon = CreateWindowExW (WS_EX_NOPARENTNOTIFY, L"STATIC", NULL, WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | SS_OWNERDRAW, 0, 0, 0, 0, Window, NULL, g_hInst, NULL);
		if (ErrorIcon != NULL)
		{
			SetWindowLong (ErrorIcon, GWL_ID, IDC_ICONPIC);
		}
	}
	ErrorPane = CreateDialogParam (g_hInst, MAKEINTRESOURCE(IDD_ERRORPANE), Window, ErrorPaneProc, (LONG_PTR)NULL);

	if (text != NULL)
	{
		CHARRANGE end;
		CHARFORMAT2 oldformat, newformat;
		PARAFORMAT2 paraformat;

		// Append the error message to the log.
		end.cpMax = end.cpMin = GetWindowTextLength (ConWindow);
		SendMessage (ConWindow, EM_EXSETSEL, 0, (LPARAM)&end);

		// Remember current charformat.
		oldformat.cbSize = sizeof(oldformat);
		SendMessage (ConWindow, EM_GETCHARFORMAT, SCF_SELECTION, (LPARAM)&oldformat);

		// Use bigger font and standout colors.
		newformat.cbSize = sizeof(newformat);
		newformat.dwMask = CFM_BOLD | CFM_COLOR | CFM_SIZE;
		newformat.dwEffects = CFE_BOLD;
		newformat.yHeight = oldformat.yHeight * 5 / 4;
		newformat.crTextColor = RGB(255,170,170);
		SendMessage (ConWindow, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM)&newformat);

		// Indent the rest of the text to make the error message stand out a little more.
		paraformat.cbSize = sizeof(paraformat);
		paraformat.dwMask = PFM_STARTINDENT | PFM_OFFSETINDENT | PFM_RIGHTINDENT;
		paraformat.dxStartIndent = paraformat.dxOffset = paraformat.dxRightIndent = 120;
		SendMessage (ConWindow, EM_SETPARAFORMAT, 0, (LPARAM)&paraformat);
		SendMessageW (ConWindow, EM_REPLACESEL, FALSE, (LPARAM)L"\n");

		// Find out where the error lines start for the error icon display control.
		SendMessage (ConWindow, EM_EXGETSEL, 0, (LPARAM)&end);
		ErrorIconChar = end.cpMax;

		// Now start adding the actual error message.
		SendMessageW (ConWindow, EM_REPLACESEL, FALSE, (LPARAM)L"Execution could not continue.\n\n");

		// Restore old charformat but with light yellow text.
		oldformat.crTextColor = RGB(255,255,170);
		SendMessage (ConWindow, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM)&oldformat);

		// Add the error text.
		SendMessageW (ConWindow, EM_REPLACESEL, FALSE, (LPARAM)widetext.c_str());

		// Make sure the error text is not scrolled below the window.
		SendMessage (ConWindow, EM_LINESCROLL, 0, SendMessage (ConWindow, EM_GETLINECOUNT, 0, 0));
		// The above line scrolled everything off the screen, so pretend to move the scroll
		// bar thumb, which clamps to not show any extra lines if it doesn't need to.
		SendMessage (ConWindow, EM_SCROLL, SB_PAGEDOWN, 0);
	}

	BOOL bRet;
	MSG msg;

	while ((bRet = GetMessage(&msg, NULL, 0, 0)) != 0)
	{
		if (bRet == -1)
		{
			MessageBoxW (Window, widetext.c_str(), WGAMENAME " Fatal Error", MB_OK|MB_ICONSTOP|MB_TASKMODAL);
			return;
		}
		else if (!IsDialogMessage (ErrorPane, &msg))
		{
			TranslateMessage (&msg);
			DispatchMessage (&msg);
		}
	}
}

void PeekThreadedErrorPane()
{
	// Allow SendMessage from another thread to call its message handler so that it can display the crash dialog
	MSG msg;
	PeekMessage(&msg, 0, 0, 0, PM_NOREMOVE);
}

//==========================================================================
//
// DoMain
//
//==========================================================================

void DoMain (HINSTANCE hInstance)
{
	LONG WinWidth, WinHeight;
	int height, width, x, y;
	RECT cRect;
	TIMECAPS tc;
	DEVMODE displaysettings;

	try
	{
#ifdef _MSC_VER
		_set_new_handler (NewFailure);
#endif

		// Do not use the multibyte __argv here because we want UTF-8 arguments
		// and those can only be done by converting the Unicode variants.
		Args = new FArgs();
		auto argc = __argc;
		auto wargv = __wargv;
		for (int i = 0; i < argc; i++)
		{
			Args->AppendArg(FString(wargv[i]));
		}

		// Load Win32 modules
		Kernel32Module.Load({"kernel32.dll"});
		Shell32Module.Load({"shell32.dll"});
		User32Module.Load({"user32.dll"});

		// Under XP, get our session ID so we can know when the user changes/locks sessions.
		// Since we need to remain binary compatible with older versions of Windows, we
		// need to extract the ProcessIdToSessionId function from kernel32.dll manually.
		HMODULE kernel = GetModuleHandleA ("kernel32.dll");

		if (Args->CheckParm("-stdout"))
		{
			// As a GUI application, we don't normally get a console when we start.
			// If we were run from the shell and are on XP+, we can attach to its
			// console. Otherwise, we can create a new one. If we already have a
			// stdout handle, then we have been redirected and should just use that
			// handle instead of creating a console window.

			StdOut = GetStdHandle(STD_OUTPUT_HANDLE);
			if (StdOut != NULL)
			{
				// It seems that running from a shell always creates a std output
				// for us, even if it doesn't go anywhere. (Running from Explorer
				// does not.) If we can get file information for this handle, it's
				// a file or pipe, so use it. Otherwise, pretend it wasn't there
				// and find a console to use instead.
				BY_HANDLE_FILE_INFORMATION info;
				if (!GetFileInformationByHandle(StdOut, &info))
				{
					StdOut = NULL;
				}
			}
			if (StdOut == NULL)
			{
				// AttachConsole was introduced with Windows XP. (OTOH, since we
				// have to share the console with the shell, I'm not sure if it's
				// a good idea to actually attach to it.)
				typedef BOOL (WINAPI *ac)(DWORD);
				ac attach_console = kernel != NULL ? (ac)GetProcAddress(kernel, "AttachConsole") : NULL;
				if (attach_console != NULL && attach_console(ATTACH_PARENT_PROCESS))
				{
					StdOut = GetStdHandle(STD_OUTPUT_HANDLE);
					DWORD foo; WriteFile(StdOut, "\n", 1, &foo, NULL);
					AttachedStdOut = true;
				}
				if (StdOut == NULL && AllocConsole())
				{
					StdOut = GetStdHandle(STD_OUTPUT_HANDLE);
				}
				FancyStdOut = true;
			}
		}

		// Set the timer to be as accurate as possible
		if (timeGetDevCaps (&tc, sizeof(tc)) != TIMERR_NOERROR)
			TimerPeriod = 1;	// Assume minimum resolution of 1 ms
		else
			TimerPeriod = tc.wPeriodMin;

		timeBeginPeriod (TimerPeriod);

		/*
		killough 1/98:

		This fixes some problems with exit handling
		during abnormal situations.

		The old code called I_Quit() to end program,
		while now I_Quit() is installed as an exit
		handler and exit() is called to exit, either
		normally or abnormally.
		*/

		atexit (call_terms);

		atterm (I_Quit);

		// Figure out what directory the program resides in.
		WCHAR progbuff[1024];
		if (GetModuleFileNameW(nullptr, progbuff, sizeof progbuff) == 0)
		{
			I_FatalError("Could not determine program location.");
		}
		progbuff[1023] = '\0';
		if (auto lastsep = wcsrchr(progbuff, '\\'))
		{
			lastsep[1] = '\0';
		}

		progdir = progbuff;
		FixPathSeperator(progdir);

		HDC screenDC = GetDC(0);
		int dpi = GetDeviceCaps(screenDC, LOGPIXELSX);
		ReleaseDC(0, screenDC);
		width = (512 * dpi + 96 / 2) / 96;
		height = (384 * dpi + 96 / 2) / 96;

		// Many Windows structures that specify their size do so with the first
		// element. DEVMODE is not one of those structures.
		memset (&displaysettings, 0, sizeof(displaysettings));
		displaysettings.dmSize = sizeof(displaysettings);
		EnumDisplaySettings (NULL, ENUM_CURRENT_SETTINGS, &displaysettings);
		x = (displaysettings.dmPelsWidth - width) / 2;
		y = (displaysettings.dmPelsHeight - height) / 2;

		if (Args->CheckParm ("-0"))
		{
			x = y = 0;
		}

		WNDCLASS WndClass;
		WndClass.style			= 0;
		WndClass.lpfnWndProc	= LConProc;
		WndClass.cbClsExtra		= 0;
		WndClass.cbWndExtra		= 0;
		WndClass.hInstance		= hInstance;
		WndClass.hIcon			= LoadIcon (hInstance, MAKEINTRESOURCE(IDI_ICON1));
		WndClass.hCursor		= LoadCursor (NULL, IDC_ARROW);
		WndClass.hbrBackground	= NULL;
		WndClass.lpszMenuName	= NULL;
		WndClass.lpszClassName	= WinClassName;
		
		/* register this new class with Windows */
		if (!RegisterClass((LPWNDCLASS)&WndClass))
			I_FatalError ("Could not register window class");
		
		/* create window */
		FStringf caption("" GAMESIG " %s " X64 " (%s)", GetVersionString(), GetGitTime());
		std::wstring wcaption = caption.WideString();
		Window = CreateWindowExW(
				WS_EX_APPWINDOW,
				WinClassName,
				wcaption.c_str(),
				WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN,
				x, y, width, height,
				(HWND)   NULL,
				(HMENU)  NULL,
						hInstance,
				NULL);

		if (!Window)
			I_FatalError ("Could not open window");

		if (kernel != NULL)
		{
			typedef BOOL (WINAPI *pts)(DWORD, DWORD *);
			pts pidsid = (pts)GetProcAddress (kernel, "ProcessIdToSessionId");
			if (pidsid != 0)
			{
				if (!pidsid (GetCurrentProcessId(), &SessionID))
				{
					SessionID = 0;
				}
				hwtsapi32 = LoadLibraryA ("wtsapi32.dll");
				if (hwtsapi32 != 0)
				{
					FARPROC reg = GetProcAddress (hwtsapi32, "WTSRegisterSessionNotification");
					if (reg == 0 || !((BOOL(WINAPI *)(HWND, DWORD))reg) (Window, NOTIFY_FOR_THIS_SESSION))
					{
						FreeLibrary (hwtsapi32);
						hwtsapi32 = 0;
					}
					else
					{
						atterm (UnWTS);
					}
				}
			}
		}

		GetClientRect (Window, &cRect);

		WinWidth = cRect.right;
		WinHeight = cRect.bottom;

		CoInitialize (NULL);
		atterm (UnCOM);

		C_InitConsole (((WinWidth / 8) + 2) * 8, (WinHeight / 12) * 8, false);

		I_DetectOS ();
		D_DoomMain ();
	}
	catch (class CNoRunExit &)
	{
		I_ShutdownGraphics();
		if (!batchrun)
		{
			if (FancyStdOut && !AttachedStdOut)
			{ // Outputting to a new console window: Wait for a keypress before quitting.
				DWORD bytes;
				HANDLE stdinput = GetStdHandle(STD_INPUT_HANDLE);

				ShowWindow(Window, SW_HIDE);
				WriteFile(StdOut, "Press any key to exit...", 24, &bytes, NULL);
				FlushConsoleInputBuffer(stdinput);
				SetConsoleMode(stdinput, 0);
				ReadConsole(stdinput, &bytes, 1, &bytes, NULL);
			}
			else if (StdOut == NULL)
			{
				ShowErrorPane(NULL);
			}
		}
		exit(0);
	}
	catch (std::exception &error)
	{
		I_ShutdownGraphics ();
		RestoreConView ();
		S_StopMusic(true);
		I_FlushBufferedConsoleStuff();
		auto msg = error.what();
		if (strcmp(msg, "NoRunExit"))
		{
			if (CVMAbortException::stacktrace.IsNotEmpty())
			{
				Printf("%s", CVMAbortException::stacktrace.GetChars());
			}

			if (!batchrun)
			{
				ShowErrorPane(msg);
			}
			else
			{
				Printf("%s\n", msg);
			}
		}
		exit (-1);
	}
}

//==========================================================================
//
// DoomSpecificInfo
//
// Called by the crash logger to get application-specific information.
//
//==========================================================================

void DoomSpecificInfo (char *buffer, size_t bufflen)
{
	const char *arg;
	char *const buffend = buffer + bufflen - 2;	// -2 for CRLF at end
	int i;

	buffer += mysnprintf (buffer, buffend - buffer, GAMENAME " version %s (%s)", GetVersionString(), GetGitHash());
	buffer += mysnprintf (buffer, buffend - buffer, "\r\nCommand line: %s\r\n", GetCommandLine());

	for (i = 0; (arg = Wads.GetWadName (i)) != NULL; ++i)
	{
		buffer += mysnprintf (buffer, buffend - buffer, "\r\nWad %d: %s", i, arg);
	}

	if (gamestate != GS_LEVEL && gamestate != GS_TITLELEVEL)
	{
		buffer += mysnprintf (buffer, buffend - buffer, "\r\n\r\nNot in a level.");
	}
	else
	{
		buffer += mysnprintf (buffer, buffend - buffer, "\r\n\r\nCurrent map: %s", primaryLevel->MapName.GetChars());

		if (!viewactive)
		{
			buffer += mysnprintf (buffer, buffend - buffer, "\r\n\r\nView not active.");
		}
		else
		{
			auto &vp = r_viewpoint;
			buffer += mysnprintf (buffer, buffend - buffer, "\r\n\r\nviewx = %f", vp.Pos.X);
			buffer += mysnprintf (buffer, buffend - buffer, "\r\nviewy = %f", vp.Pos.Y);
			buffer += mysnprintf (buffer, buffend - buffer, "\r\nviewz = %f", vp.Pos.Z);
			buffer += mysnprintf (buffer, buffend - buffer, "\r\nviewangle = %f", vp.Angles.Yaw);
		}
	}
	*buffer++ = '\r';
	*buffer++ = '\n';
	*buffer++ = '\0';
}

// Here is how the error logging system works.
//
// To catch exceptions that occur in secondary threads, CatchAllExceptions is
// set as the UnhandledExceptionFilter for this process. It records the state
// of the thread at the time of the crash using CreateCrashLog and then queues
// an APC on the primary thread. When the APC executes, it raises a software
// exception that gets caught by the __try/__except block in WinMain.
// I_GetEvent calls SleepEx to put the primary thread in a waitable state
// periodically so that the APC has a chance to execute.
//
// Exceptions on the primary thread are caught by the __try/__except block in
// WinMain. Not only does it record the crash information, it also shuts
// everything down and displays a dialog with the information present. If a
// console log is being produced, the information will also be appended to it.
//
// If a debugger is running, CatchAllExceptions never executes, so secondary
// thread exceptions will always be caught by the debugger. For the primary
// thread, IsDebuggerPresent is called to determine if a debugger is present.
// Note that this function is not present on Windows 95, so we cannot
// statically link to it.
//
// To make this work with MinGW, you will need to use inline assembly
// because GCC offers no native support for Windows' SEH.

//==========================================================================
//
// SleepForever
//
//==========================================================================

void SleepForever ()
{
	Sleep (INFINITE);
}

//==========================================================================
//
// ExitMessedUp
//
// An exception occurred while exiting, so don't do any standard processing.
// Just die.
//
//==========================================================================

LONG WINAPI ExitMessedUp (LPEXCEPTION_POINTERS foo)
{
	ExitProcess (1000);
}

//==========================================================================
//
// ExitFatally
//
//==========================================================================

void CALLBACK ExitFatally (ULONG_PTR dummy)
{
	SetUnhandledExceptionFilter (ExitMessedUp);
	I_ShutdownGraphics ();
	RestoreConView ();
	DisplayCrashLog ();
	exit (-1);
}

//==========================================================================
//
// CatchAllExceptions
//
//==========================================================================

namespace
{
	CONTEXT MainThreadContext;
}

LONG WINAPI CatchAllExceptions (LPEXCEPTION_POINTERS info)
{
#ifdef _DEBUG
	if (info->ExceptionRecord->ExceptionCode == EXCEPTION_BREAKPOINT)
	{
		return EXCEPTION_CONTINUE_SEARCH;
	}
#endif

	static bool caughtsomething = false;

	if (caughtsomething) return EXCEPTION_EXECUTE_HANDLER;
	caughtsomething = true;

	char *custominfo = (char *)HeapAlloc (GetProcessHeap(), 0, 16384);

	CrashPointers = *info;
	DoomSpecificInfo (custominfo, 16384);
	CreateCrashLog (custominfo, (DWORD)strlen(custominfo), ConWindow);

	// If the main thread crashed, then make it clean up after itself.
	// Otherwise, put the crashing thread to sleep and signal the main thread to clean up.
	if (GetCurrentThreadId() == MainThreadID)
	{
#ifdef _M_X64
		*info->ContextRecord = MainThreadContext;
#else
		info->ContextRecord->Eip = (DWORD_PTR)ExitFatally;
#endif // _M_X64
	}
	else
	{
#ifndef _M_X64
		info->ContextRecord->Eip = (DWORD_PTR)SleepForever;
#else
		info->ContextRecord->Rip = (DWORD_PTR)SleepForever;
#endif
		QueueUserAPC (ExitFatally, MainThread, 0);
	}
	return EXCEPTION_CONTINUE_EXECUTION;
}

//==========================================================================
//
// infiniterecursion
//
// Debugging routine for testing the crash logger.
//
//==========================================================================

#ifdef _DEBUG
static void infiniterecursion(int foo)
{
	if (foo)
	{
		infiniterecursion(foo);
	}
}
#endif

// Setting this to 'true' allows getting the standard notification for a crash
// which offers the very important feature to open a debugger and see the crash in context right away.
CUSTOM_CVAR(Bool, disablecrashlog, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
{
	SetUnhandledExceptionFilter(!*self ? CatchAllExceptions : nullptr);
}

//==========================================================================
//
// WinMain
//
//==========================================================================

int WINAPI wWinMain (HINSTANCE hInstance, HINSTANCE nothing, LPWSTR cmdline, int nCmdShow)
{
	g_hInst = hInstance;

	InitCommonControls ();			// Load some needed controls and be pretty under XP

	// We need to load riched20.dll so that we can create the control.
	if (NULL == LoadLibraryA ("riched20.dll"))
	{
		// This should only happen on basic Windows 95 installations, but since we
		// don't support Windows 95, we have no obligation to provide assistance in
		// getting it installed.
		MessageBoxA(NULL, "Could not load riched20.dll", GAMENAME " Error", MB_OK | MB_ICONSTOP);
		exit(0);
	}

#if !defined(__GNUC__) && defined(_DEBUG)
	if (__argc == 2 && __wargv != nullptr && wcscmp (__wargv[1], L"TestCrash") == 0)
	{
		__try
		{
			*(int *)0 = 0;
		}
		__except(CrashPointers = *GetExceptionInformation(),
			CreateCrashLog ("TestCrash", 9, NULL), EXCEPTION_EXECUTE_HANDLER)
		{
		}
		DisplayCrashLog ();
		exit (0);
	}
	if (__argc == 2 && __wargv != nullptr && wcscmp (__wargv[1], L"TestStackCrash") == 0)
	{
		__try
		{
			infiniterecursion(1);
		}
		__except(CrashPointers = *GetExceptionInformation(),
			CreateCrashLog ("TestStackCrash", 14, NULL), EXCEPTION_EXECUTE_HANDLER)
		{
		}
		DisplayCrashLog ();
		exit (0);
	}
#endif

	MainThread = INVALID_HANDLE_VALUE;
	DuplicateHandle (GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &MainThread,
		0, FALSE, DUPLICATE_SAME_ACCESS);
	MainThreadID = GetCurrentThreadId();

#ifndef _DEBUG
	if (MainThread != INVALID_HANDLE_VALUE)
	{
		SetUnhandledExceptionFilter (CatchAllExceptions);

#ifdef _M_X64
		static bool setJumpResult = false;
		RtlCaptureContext(&MainThreadContext);
		if (setJumpResult)
		{
			ExitFatally(0);
			return 0;
		}
		setJumpResult = true;
#endif // _M_X64
	}
#endif

#if defined(_DEBUG) && defined(_MSC_VER)
	// Uncomment this line to make the Visual C++ CRT check the heap before
	// every allocation and deallocation. This will be slow, but it can be a
	// great help in finding problem areas.
	//_CrtSetDbgFlag (_CRTDBG_ALLOC_MEM_DF | _CRTDBG_CHECK_ALWAYS_DF);

	// Enable leak checking at exit.
	_CrtSetDbgFlag (_CrtSetDbgFlag(0) | _CRTDBG_LEAK_CHECK_DF);

	// Use this to break at a specific allocation number.
	//_crtBreakAlloc = 227524;
#endif

	DoMain (hInstance);

	CloseHandle (MainThread);
	MainThread = INVALID_HANDLE_VALUE;
	return 0;
}

// each platform has its own specific version of this function.
void I_SetWindowTitle(const char* caption)
{
	std::wstring widecaption;
	if (!caption)
	{
		FStringf default_caption("" GAMENAME " %s " X64 " (%s)", GetVersionString(), GetGitTime());
		widecaption = default_caption.WideString();
	}
	else
	{
		widecaption = WideString(caption);
	}
	SetWindowText(Window, widecaption.c_str());
}
