// Copyright (c) 2023 Alex Diener. All rights reserved.

#include "audioplayer/AudioManager.h"
#include "audioplayer/AudioPlayer.h"
#include "binaryserialization/BinaryDeserializationContext.h"
#include "gamemath/Vector2i.h"
#include "gamepad/Gamepad.h"
#include "imageio/ImageIO.h"
#include "inputcontroller/InputRecorder.h"
#include "inputcontroller/InputPlayback.h"
#include "PROJECT_NAME/Atoms.h"
#include "PROJECT_NAME/Events.h"
#include "PROJECT_NAME/FileUtilities.h"
#include "PROJECT_NAME/GameplayScreen.h"
#include "PROJECT_NAME/GameSession.h"
#include "PROJECT_NAME/Globals.h"
#include "PROJECT_NAME/Music.h"
#include "PROJECT_NAME/Shaders.h"
#include "PROJECT_NAME/SharedDefinitions.h"
#include "PROJECT_NAME/Sounds.h"
#include "renderer/Renderer.h"
#include "renderer/RenderTarget.h"
#include "shadercollection/ShaderCollection.h"
#include "shadercollection/ShaderConfiguration2DTexture.h"
#include "shell/Shell.h"
#include "shell/ShellCallbacks.h"
#include "shell/ShellKeyCodes.h"
#include "uitoolkit/UIDrawingInterface2DMultitexture.h"
#include "uitoolkit/UIToolkitAppearance.h"
#include "uitoolkit/UIToolkitCursor.h"
#include "uitoolkit/UIToolkitDrawing.h"
#include "utilities/AutoFreePool.h"
#include "utilities/IOUtilities.h"
#include "MetadataKeys.h"

#if defined(STEM_PLATFORM_macosx)
#include "nsopenglshell/NSOpenGLShell.h"
#include "nsopenglshell/NSOpenGLTarget.h"
#elif defined(STEM_PLATFORM_windows)
#include "wglshell/WGLShell.h"
#include "wglshell/WGLTarget.h"
#elif defined(STEM_PLATFORM_linux)
#include "glxshell/GLXShell.h"
#include "glxshell/GLXTarget.h"
#elif defined(STEM_PLATFORM_iphonesimulator) || defined(STEM_PLATFORM_iphoneos)
#include "eaglshell/EAGLShell.h"
#include "eaglshell/EAGLTarget.h"
#elif defined(STEM_PLATFORM_android)
#include "eglshell/EGLShell.h"
#include "eglshell/EGLTarget.h"
#include <android/log.h>
#define printf(format, ...) __android_log_print(ANDROID_LOG_INFO, STEM_HUMAN_READABLE_TARGET_NAME, format, ##__VA_ARGS__);
#define fprintf(stderr, format, ...) __android_log_print(ANDROID_LOG_INFO, STEM_HUMAN_READABLE_TARGET_NAME, format, ##__VA_ARGS__);
#else
#error Unsupported platform
#endif

#include "stem_core.h"
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>

#define AUDIO_TIMER_INTERVAL 0.5
#define GAMEPAD_TIMER_INTERVAL (1.0 / 60.0)
#define GAMEPAD_DETECT_DEVICES_INTERVAL (1.0 / 5.0)

static ScreenManager * screenManager;
static ShellTimer gamepadTimerID, gamepadDetectDevicesTimerID, audioTimerID;
static double lastGamepadTime;
static bool backgrounded;
static bool audioDisabled;
static unsigned int maxScaleMultiplier = UINT_MAX;
static const char * recordInputFileName;
static const char * playbackInputFileName;
static const char * videoOutDirectory;
static bool headless;
static bool noInputDelay;
static bool synchronousLoad;
static bool noInputRecording;
static InputRecorder * inputRecorder;
static InputPlayback * inputPlayback;

static void initShaderConfiguration(void) {
	Matrix4x4f projectionMatrix = Matrix4x4f_ortho(MATRIX4x4f_IDENTITY, 0.0f, g_renderTarget->width, 0.0f, g_renderTarget->height, -2000.0f, 2000.0f);
	ShaderUniformConfiguration_setMat4_Matrix4x4f(&g_projectionUniform, projectionMatrix);
	
	g_shaderConfiguration = ShaderConfiguration2DMultitexture_create(ShaderCollection_get2DMultitextureShader());
	call_virtual(referenceProjectionMatrix, g_shaderConfiguration, &g_projectionUniform);
	
	ShaderConfiguration * renderTargetShaderConfiguration = ShaderCollection_getRenderTargetShaderConfiguration();
	call_virtual(setTexture, renderTargetShaderConfiguration, 0, g_renderTarget->attachments[0].texture, false);
	ShaderConfiguration * renderTargetAreaShaderConfiguration = ShaderCollection_getRenderTargetAreaFilterShaderConfiguration();
	call_virtual(setTexture, renderTargetAreaShaderConfiguration, 0, g_renderTarget->attachments[0].texture, false);
}

static void initTileset(void) {
	extern const char EMBEDDATA_main_imagecollection[];
	extern unsigned int EMBEDSIZE_main_imagecollection;
	FileBundle * fileBundle = FileBundle_loadData(EMBEDDATA_main_imagecollection, EMBEDSIZE_main_imagecollection);
	BitmapImage * atlasImage;
	TextureAtlasData * atlasData;
	unpackImageCollectionBundle(fileBundle, &g_imageCollection, &atlasImage, &atlasData);
	FileBundle_dispose(fileBundle);
	BitmapImage_premultiply(atlasImage);
	g_imageCollectionTexture = Texture_createWithTexels2D(atlasImage->pixels, atlasImage->width, atlasImage->height, 0, 4, TEXEL_COMPONENT_UINT8_NORM, TEXEL_SWIZZLE_DEFAULT, TEXTURE_OPTION_MAGNIFY_NEAREST);
	TextureAtlas * imageAtlas = TextureAtlasData_createTextureAtlas(atlasData, atlasImage->width, atlasImage->height);
	BitmapImage_dispose(atlasImage);
	TextureAtlasData_dispose(atlasData);
	ImageCollection_resolveAtlasEntries(g_imageCollection, imageAtlas);
	TextureAtlas_dispose(imageAtlas);
	
	extern const char EMBEDDATA_main_tileset_edit[];
	extern unsigned int EMBEDSIZE_main_tileset_edit;
	g_tileset = readTilesetEditData(EMBEDDATA_main_tileset_edit, EMBEDSIZE_main_tileset_edit);
	
	g_tileProperties = HashTable_create(sizeof(TilePropertyBits));
	for (unsigned int tileIndex = 0; tileIndex < g_tileset->tileCount; tileIndex++) {
		TilePropertyBits tileProperties = 0;
		if (valueGetBoolean(hashGet(g_tileset->tiles[tileIndex].metadata, METADATA_KEY_floor))) {
			tileProperties |= TILE_STATIC_FLOOR;
		}
		if (valueGetBoolean(hashGet(g_tileset->tiles[tileIndex].metadata, METADATA_KEY_wall))) {
			tileProperties |= TILE_STATIC_WALL;
		}
		HashTable_set(g_tileProperties, HashTable_uint32Key(g_tileset->tiles[tileIndex].identifier), &tileProperties);
	}
	
	extern const char EMBEDDATA_main_tileset_adjacency[];
	extern unsigned int EMBEDSIZE_main_tileset_adjacency;
	g_blendMap = readTilesetAdjacencyBlendMapData(EMBEDDATA_main_tileset_adjacency, EMBEDSIZE_main_tileset_adjacency);
	
	extern const char EMBEDDATA_main_spritecollection[];
	extern unsigned int EMBEDSIZE_main_spritecollection;
	g_spriteCollection = readSpriteCollectionData(EMBEDDATA_main_spritecollection, EMBEDSIZE_main_spritecollection, g_imageCollection);
}

static Rect4f getAtlasEntryCallback(uint32_t codepoint, const char * glyphName, void * context) {
	TextureAtlas * atlas = context;
	return TextureAtlas_lookup(atlas, HashTable_stringKey(glyphName));
}

static void initAppearance(void) {
	extern const char EMBEDDATA_appearance_atlas[];
	extern unsigned int EMBEDSIZE_appearance_atlas;
	BinaryDeserializationContext * context = BinaryDeserializationContext_createWithBytes(EMBEDDATA_appearance_atlas, EMBEDSIZE_appearance_atlas);
	TextureAtlasData * atlasData = TextureAtlasData_deserialize(context);
	BinaryDeserializationContext_dispose(context);
	
	extern const char EMBEDDATA_appearance_atlas_png[];
	extern unsigned int EMBEDSIZE_appearance_atlas_png;
	BitmapImage * image = ImageIO_readImageData(EMBEDDATA_appearance_atlas_png, EMBEDSIZE_appearance_atlas_png, BITMAP_PIXEL_FORMAT_RGBA_8888, true);
	g_texture = Texture_createWithTexels2D(image->pixels, image->width, image->height, 0, 4, TEXEL_COMPONENT_UINT8_NORM, TEXEL_SWIZZLE_DEFAULT, TEXTURE_OPTION_MAGNIFY_NEAREST | TEXTURE_OPTION_MINIFY_NEAREST);
	TextureAtlas * atlas = TextureAtlasData_createTextureAtlas(atlasData, image->width, image->height);
	BitmapImage_dispose(image);
	TextureAtlasData_dispose(atlasData);
	
	extern const char EMBEDDATA_17pt_bitmapfont2[];
	extern unsigned int EMBEDSIZE_17pt_bitmapfont2;
	context = BinaryDeserializationContext_createWithBytes(EMBEDDATA_17pt_bitmapfont2, EMBEDSIZE_17pt_bitmapfont2);
	g_font17pt = BitmapFont2_deserialize(context);
	BinaryDeserializationContext_dispose(context);
	BitmapFont2_resolveAtlasEntries(g_font17pt, getAtlasEntryCallback, atlas);
	g_whiteAtlasEntry = TextureAtlas_lookup(atlas, HashTable_stringKey("white"));
	
	g_uiTypeface = UITypeface_BitmapFont2_create(g_font17pt, 1.0f, 0, TEXT_OPTION_PIXEL_SNAPPING);
	UIDrawingInterface2DMultitexture * drawingInterface = UIDrawingInterface2DMultitexture_create(0);
	drawingInterface->scaleFactor = g_scaleFactor;
	g_uiContext = UIToolkitContext_init(default2DRenderPipelineConfiguration(RENDER_BLEND_ALPHA_PREMULTIPLIED), g_shaderConfiguration, drawingInterface);
	UIToolkit_setContext(&g_uiContext);
	g_uiAppearance = UIAppearance_init(NULL);
	UIToolkit_registerAppearanceParameters(&g_uiAppearance, atlas, g_uiTypeface, NULL);
	TextureAtlas_dispose(atlas);
	
	call_virtual(setTexture, g_shaderConfiguration, 0, g_texture, false);
	call_virtual(setTexture, g_shaderConfiguration, 1, g_imageCollectionTexture, false);
}

static void audioTimer(ShellTimer timerID, void * context) {
	AudioPlayer_run();
}

static void gamepadDetectDevicesTimer(ShellTimer timerID, void * context) {
	Gamepad_detectDevices();
}

static void gamepadEventTimer(ShellTimer timerID, void * context) {
	double currentTime = Shell_getCurrentTime();
	Gamepad_processEvents();
	InputController_processRepeats(g_gameSession->inputController, currentTime, currentTime - lastGamepadTime);
	lastGamepadTime = currentTime;
}

static PCMAudio * loadSoundCallback(SoundID soundID, void * context) {
	return loadSoundEffect(soundID);
}

static PCMAudio * loadMusicCallback(MusicID musicID, AudioFrameIndex * outLoopDuration, void * context) {
	return loadMusic(musicID, outLoopDuration);
}

static void initAudio(void) {
	if (!audioDisabled) {
		AudioOut_sampleFormat sampleFormat = {2, AUDIO_DEFAULT_SAMPLE_RATE, 2};
		AudioPlayer_init(STEM_HUMAN_READABLE_TARGET_NAME, AUDIO_SOURCE_COUNT_MAX, AUDIO_STREAM_COUNT_MAX, AUDIO_CATEGORY_COUNT, sampleFormat, NULL, NULL, NULL);
		g_audioSampleRate = AudioOut_getTransportFormat().sampleRate;
		if (!GameSession_getSoundEnabled(g_gameSession)) {
			AudioPlayer_setCategoryVolume(AUDIO_CATEGORY_SOUND, 0.0f);
		} else {
			AudioPlayer_setCategoryVolume(AUDIO_CATEGORY_SOUND, GameSession_getSoundVolume(g_gameSession));
		}
		if (!GameSession_getMusicEnabled(g_gameSession)) {
			AudioPlayer_setCategoryVolume(AUDIO_CATEGORY_MUSIC, 0.0f);
		} else {
			AudioPlayer_setCategoryVolume(AUDIO_CATEGORY_MUSIC, GameSession_getMusicVolume(g_gameSession));
		}
		AudioPlayer_setPausesAudioDeviceWhenIdle(false);
		audioTimerID = Shell_setTimer(AUDIO_TIMER_INTERVAL, true, audioTimer, NULL);
		if (videoOutDirectory != NULL) {
			AudioPlayer_setSpeakerOutputEnabled(false);
		}
		AudioManager_init(AUDIO_MUSIC_LANE_COUNT, 4, 8, loadSoundCallback, loadMusicCallback, NULL);
		initSoundEffects();
		initMusic();
		if (synchronousLoad || headless) {
			AudioManager_loadAllAudioImmediate();
		} else {
			AudioManager_loadAllAudioAsync();
		}
		
	} else {
		AudioManager_init(0, 1, 1, NULL, NULL, NULL);
	}
}

static void initScreens(void) {
	GameplayScreen * gameplayScreen = GameplayScreen_create();
	ScreenManager_addScreen(screenManager, gameplayScreen);
	ScreenManager_setScreen(screenManager, gameplayScreen);
}

static void markInputSessionClean(void) {
	if (inputRecorder != NULL) {
		uint8_t cleanExit = 1;
		InputRecorder_rewriteReplayStartupData(inputRecorder, &cleanExit, sizeof(cleanExit));
	}
}

#ifdef DEBUG
static bool inputPlaybackComplete(Atom eventID, void * eventData, void * context) {
	fprintf(stderr, "Playback complete\n");
	return true;
}
#endif

static void applyRunOptions(void) {
	const char * supportPath = Shell_getSupportPath(SUPPORT_SUBDIRECTORY_NAME);
	char defaultInputFileName[PATH_MAX];
	snprintf_safe(defaultInputFileName, sizeof(defaultInputFileName), "%s/last.inputsession", supportPath);
	if (recordInputFileName == NULL && !noInputRecording && playbackInputFileName == NULL) {
		recordInputFileName = defaultInputFileName;
	}
	
	enum InputSessionError error;
	InputSession * lastInputSession = InputSession_loadFile(defaultInputFileName, &error);
	if (lastInputSession != NULL && lastInputSession->replayStartupDataSize > 0) {
		uint8_t * cleanExit = lastInputSession->replayStartupData;
		if (!*cleanExit) {
			const char * crashFileName = uniqueFileInDirectory(supportPath, "crash", "inputsession", 9999);
			if (crashFileName != NULL) {
				int result = rename(defaultInputFileName, crashFileName);
#ifdef DEBUG
				if (result != 0) {
					fprintf(stderr, "Warning: Failed to rename crashed input session with errno %d; disabling recording for this session\n", errno);
					recordInputFileName = NULL;
				} else {
					fprintf(stderr, "Saved crash report to %s\n", crashFileName);
				}
			} else {
				fprintf(stderr, "Warning: Couldn't find a unique file name for saving a crash report; disabling recording for this session\n");
#endif
			}
		}
		InputSession_dispose(lastInputSession);
	}
	
	if (recordInputFileName != NULL) {
		uint8_t cleanExit = 0;
		inputRecorder = InputRecorder_createWithFileOutput(g_gameSession->inputController, &cleanExit, sizeof(cleanExit), recordInputFileName);
		atexit(markInputSessionClean);
#ifdef DEBUG
		if (inputRecorder == NULL) {
			fprintf(stderr, "Couldn't create recording output file \"%s\" (errno = %d)\n", recordInputFileName, errno);
		}
#endif
	}
	
	if (playbackInputFileName != NULL) {
		enum InputSessionError error;
		InputSession * inputSession = InputSession_loadFile(playbackInputFileName, &error);
		if (inputSession == NULL) {
#ifdef DEBUG
			fprintf(stderr, "Couldn't read input session from file \"%s\" (error %d)", playbackInputFileName, error);
#endif
		} else {
			inputPlayback = InputPlayback_create(g_gameSession->inputController, inputSession);
#ifdef DEBUG
			EventDispatcher_registerForEvent(inputPlayback->eventDispatcher, ATOM(INPUT_PLAYBACK_EVENT_PLAYBACK_COMPLETE), inputPlaybackComplete, NULL);
			fprintf(stderr, "Playing back input session from file %s\n", playbackInputFileName);
#endif
		}
	}
	
	if (videoOutDirectory != NULL) {
		int result = mkdir(videoOutDirectory
#ifndef WIN32
		, 0777
#endif
		);
		if (result != 0 && errno != EEXIST) {
#ifdef DEBUG
			fprintf(stderr, "Couldn't create video output directory \"%s\" (errno = %d)\n", videoOutDirectory, errno);
#endif
			videoOutDirectory = NULL;
		}
	}
}

static void initAfterFirstDraw(void) {
#ifdef STEM_PLATFORM_linux
	// XRRGetScreenInfo (called by Shell_getDisplayRefreshRate() in glxshell) is extremely slow and causes noticeably delayed
	// application startup time when called here. Even if called from a secondary thread, screen updates stop for a noticeable
	// amount of time. Applications can choose to accept the default 60hz, accept the slowdown at startup, or provide a
	// mechanism to manually set the refresh rate on a preferences screen.
	Shell_setDrawThrottleRate(1.0 / 60.0f);
#else
	Shell_setDrawThrottleRate(1.0 / Shell_getDisplayRefreshRate(Shell_getDisplayIndexFromWindow()));
#endif
	
	pcg32_seed(&g_pcgState, time(NULL), 0);
	pcg32_stir(&g_pcgState, 50);
	
	lastGamepadTime = Shell_getCurrentTime();
	gamepadTimerID = Shell_setTimer(GAMEPAD_TIMER_INTERVAL, true, gamepadEventTimer, NULL);
	gamepadDetectDevicesTimerID = Shell_setTimer(GAMEPAD_DETECT_DEVICES_INTERVAL, true, gamepadDetectDevicesTimer, NULL);
	
	initShaders();
	initShaderConfiguration();
	initTileset();
	initAppearance();
	initAudio();
	applyRunOptions();
	initScreens();
}

static void drawFrame(double referenceTime, double activeDrawDelta) {
	if (inputPlayback != NULL) {
		if (noInputDelay) {
			InputPlayback_skipToNextEventFrame(inputPlayback);
		}
		InputPlayback_step(inputPlayback, referenceTime);
	}
	
	Screen * currentScreen = screenManager->currentScreen;
	struct drawEvent event = {referenceTime, activeDrawDelta};
	bool handled = EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_draw, &event);
	if (!handled && screenManager->currentScreen != currentScreen) {
		handled = EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_draw, &event);
	}
	
	if (inputRecorder != NULL) {
		InputRecorder_nextFrame(inputRecorder);
	}
	if (videoOutDirectory != NULL) {
		static unsigned int frameIndex;
		static BitmapImage * image;
		char filePath[PATH_MAX];
		snprintf_safe(filePath, sizeof(filePath), "%s/frame%05u.png", videoOutDirectory, frameIndex);
		if (image == NULL) {
			image = BitmapImage_create(BITMAP_PIXEL_FORMAT_RGBA_8888, DISPLAY_WIDTH, DISPLAY_HEIGHT);
		}
		RenderTarget_readData(g_renderTarget, image->pixels, 0, RenderTarget_pixelFormat_rgba, RenderTarget_dataType_uint8, 0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);
		ImageIO_writePNGFile(image, filePath, BITMAP_PIXEL_FORMAT_RGB_888, true);
		frameIndex++;
	}
}

#if defined(STEM_PLATFORM_linux)
#define EXTRA_DRAW_COUNT_MAX 1
#elif defined(STEM_PLATFORM_windows)
#define EXTRA_DRAW_COUNT_MAX 4
#endif
#define INITIAL_DRAW_COUNT_BEFORE_INIT 1
#define INITIAL_DRAW_COUNT_AFTER_INIT 1
#define INITIAL_DRAW_COUNT_BEFORE_RENDER (INITIAL_DRAW_COUNT_BEFORE_INIT + 1 + INITIAL_DRAW_COUNT_AFTER_INIT)

static bool Target_draw(double referenceTime, double activeDrawDelta) {
	static unsigned int initialDrawCount;
	
	if (initialDrawCount == INITIAL_DRAW_COUNT_BEFORE_INIT) {
		initAfterFirstDraw();
		Renderer_clear(g_renderer, COLOR4f(0.0f, 0.0f, 0.0f, 0.0f));
		Shell_redisplay();
		initialDrawCount++;
		
	} else if (initialDrawCount < INITIAL_DRAW_COUNT_BEFORE_RENDER) {
		Renderer_clear(g_renderer, COLOR4f(0.0f, 0.0f, 0.0f, 0.0f));
		Shell_redisplay();
		initialDrawCount++;
		
	} else {
		if (initialDrawCount == INITIAL_DRAW_COUNT_BEFORE_RENDER) {
			initialDrawCount++;
		}
		
		Renderer_setRenderTarget(g_renderer, g_renderTarget);
		drawFrame(referenceTime, activeDrawDelta);
		Renderer_setRenderTarget(g_renderer, NULL);
		Renderer_clear(g_renderer, COLOR4f(0.0f, 0.0f, 0.0f, 0.0f));
		drawRenderTarget(g_renderer, g_renderTarget, ShaderCollection_getRenderTargetAdaptiveAreaFilterShaderConfiguration(g_viewWidth, g_viewHeight, g_renderTarget->width, g_renderTarget->height));
	}
	
	AutoFreePool_empty();
	return true;
}

static void Target_keyDown(unsigned int charCode, unsigned int keyCode, unsigned int modifiers, bool isRepeat, double referenceTime) {
	if (keyCode == KEY_CODE_ENTER && (modifiers & MODIFIER_ALT_BIT) && !isRepeat) {
		if (Shell_isFullScreen()) {
			Shell_exitFullScreen();
		} else {
			Shell_enterFullScreen(Shell_getDisplayIndexFromWindow(), false);
		}
		EventDispatcher_dispatchEvent(screenManager->eventDispatcher, Shell_isFullScreen() ? ATOM_event_fullscreened : ATOM_event_windowed, &referenceTime);
		
	} else if (keyCode == KEY_CODE_Q && (modifiers & MODIFIER_PLATFORM_MENU_COMMAND_BIT)) {
		exit(EXIT_SUCCESS);
		
	} else if (keyCode == KEY_CODE_EQUAL && (modifiers & MODIFIER_CONTROL_BIT) && (modifiers & MODIFIER_SHIFT_BIT) && !isRepeat) {
		const char * pngExtension = "png";
		struct ShellFileDialogFilter filter = {"Image files", 1, &pngExtension};
		const char * filePath = Shell_saveFileDialog(NULL, NULL, "screenshot.png", 1, &filter);
		if (filePath != NULL) {
			BitmapImage * image = BitmapImage_create(BITMAP_PIXEL_FORMAT_RGBA_8888, DISPLAY_WIDTH, DISPLAY_HEIGHT);
			RenderTarget_readData(g_renderTarget, image->pixels, 0, RenderTarget_pixelFormat_rgba, RenderTarget_dataType_uint8, 0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);
			ImageIO_writePNGFile(image, filePath, BITMAP_PIXEL_FORMAT_RGB_888, true);
			BitmapImage_dispose(image);
		}
		
	} else {
		struct keyEvent event;
		event.keyCode = keyCode;
		event.charCode = charCode;
		event.modifiers = modifiers;
		event.isRepeat = isRepeat;
		event.timestamp = referenceTime;
		bool handled = EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_key_down, &event);
		if (!handled) {
			handled = InputController_keyDown(g_gameSession->inputController, keyCode, modifiers, referenceTime, false);
			if (handled) {
				Shell_hideCursorUntilMouseMoves();
			}
		}
	}
}

static void Target_keyUp(unsigned int keyCode, unsigned int modifiers, double referenceTime) {
	struct keyEvent event;
	event.keyCode = keyCode;
	event.modifiers = modifiers;
	event.timestamp = referenceTime;
	bool handled = EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_key_up, &event);
	if (!handled) {
		handled = InputController_keyUp(g_gameSession->inputController, keyCode, referenceTime, false);
	}
}

static void Target_keyModifiersChanged(unsigned int modifiers, unsigned int lastModifiers, double referenceTime) {
	struct keyEvent event;
	event.modifiers = modifiers;
	event.lastModifiers = lastModifiers;
	event.timestamp = referenceTime;
	bool handled = EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_key_modifiers_changed, &event);
	if (!handled) {
		handled = InputController_keyModifiersChanged(g_gameSession->inputController, modifiers, lastModifiers, referenceTime, false);
	}
}

static void Target_mouseDown(unsigned int buttonNumber, unsigned int buttonMask, float x, float y, unsigned int modifiers, double referenceTime) {
	struct mouseEvent event;
	event.buttonNumber = buttonNumber;
	event.buttonMask = buttonMask;
	event.position = VECTOR2f(x, y);
	event.modifiers = modifiers;
	event.timestamp = referenceTime;
	g_lastMousePosition = event.position;
	EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_mouse_down, &event);
}

static void Target_mouseUp(unsigned int buttonNumber, unsigned int buttonMask, float x, float y, unsigned int modifiers, double referenceTime) {
	struct mouseEvent event;
	event.buttonNumber = buttonNumber;
	event.buttonMask = buttonMask;
	event.position = VECTOR2f(x, y);
	event.modifiers = modifiers;
	event.timestamp = referenceTime;
	g_lastMousePosition = event.position;
	EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_mouse_up, &event);
}

static void Target_mouseMoved(float x, float y, float deltaX, float deltaY, unsigned int modifiers, double referenceTime) {
	struct mouseEvent event;
	event.position = VECTOR2f(x, y);
	event.delta = VECTOR2f(deltaX, deltaY);
	event.modifiers = modifiers;
	event.timestamp = referenceTime;
	g_lastMousePosition = event.position;
	EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_mouse_moved, &event);
}

static void Target_mouseDragged(unsigned int buttonMask, float x, float y, float deltaX, float deltaY, unsigned int modifiers, double referenceTime) {
	struct mouseEvent event;
	event.buttonMask = buttonMask;
	event.position = VECTOR2f(x, y);
	event.delta = VECTOR2f(deltaX, deltaY);
	event.modifiers = modifiers;
	event.timestamp = referenceTime;
	g_lastMousePosition = event.position;
	EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_mouse_dragged, &event);
}

static void Target_mouseLeave(unsigned int modifiers, double referenceTime) {
	struct mouseEvent event;
	event.modifiers = modifiers;
	event.timestamp = referenceTime;
	EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_mouse_leave, &event);
}

void gamepadAttached(struct Gamepad_device * device, void * context) {
	EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_gamepad_attached, device);
}

void gamepadRemoved(struct Gamepad_device * device, void * context) {
	EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_gamepad_detached, device);
}

void gamepadButtonDown(struct Gamepad_device * device, unsigned int buttonID, double timestamp, void * context) {
	struct gamepadButtonEvent event;
	event.device = device;
	event.buttonID = buttonID;
	event.timestamp = timestamp;
	bool handled = EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_gamepad_button_down, &event);
	if (!handled) {
		handled = InputController_gamepadButtonDown(g_gameSession->inputController, device, buttonID, timestamp, false);
		if (handled) {
			Shell_hideCursorUntilMouseMoves();
		}
	}
}

void gamepadButtonUp(struct Gamepad_device * device, unsigned int buttonID, double timestamp, void * context) {
	struct gamepadButtonEvent event;
	event.device = device;
	event.buttonID = buttonID;
	event.timestamp = timestamp;
	bool handled = EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_gamepad_button_up, &event);
	if (!handled) {
		handled = InputController_gamepadButtonUp(g_gameSession->inputController, device, buttonID, timestamp, false);
	}
}

void gamepadAxisMoved(struct Gamepad_device * device, unsigned int axisID, float value, float lastValue, double timestamp, void * context) {
	struct gamepadAxisEvent event;
	event.device = device;
	event.axisID = axisID;
	event.value = value;
	event.lastValue = lastValue;
	event.timestamp = timestamp;
	bool handled = EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_gamepad_axis_move, &event);
	if (!handled) {
		handled = InputController_gamepadAxisMoved(g_gameSession->inputController, device, axisID, value, lastValue, timestamp, false);
		if (handled) {
			Shell_hideCursorUntilMouseMoves();
		}
	}
}

static void Target_scrollWheel(float x, float y, int deltaX, int deltaY, unsigned int buttonMask, unsigned int modifiers, double referenceTime) {
	struct mouseWheelEvent event;
	event.position = VECTOR2f(x, y);
	event.deltaX = deltaX;
	event.deltaY = deltaY;
	event.buttonMask = buttonMask;
	event.modifiers = modifiers;
	event.timestamp = referenceTime;
	g_lastMousePosition = event.position;
	EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_mouse_scroll_wheel, &event);
}

static void Target_resized(unsigned int newWidth, unsigned int newHeight, double referenceTime) {
	g_viewWidth = newWidth;
	g_viewHeight = newHeight;
	g_viewRatio = (float) newWidth / newHeight;
	g_scaleFactor = Shell_getDisplayScaleFactor(Shell_getDisplayIndexFromWindow());
	if (g_renderer != NULL) {
		if (g_uiContext.drawingInterface != NULL) {
			g_uiContext.drawingInterface->scaleFactor = g_scaleFactor;
		}
		Renderer_setViewport(g_renderer, 0, 0, g_viewWidth, g_viewHeight, NULL);
		Shell_redisplay();
	}
	if (screenManager != NULL) {
		EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_resized, &referenceTime);
	}
}

static void Target_backgrounded(double referenceTime) {
	backgrounded = true;
	EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_backgrounded, &referenceTime);
	AudioPlayer_pause();
	Shell_cancelTimer(audioTimerID);
	Shell_cancelTimer(gamepadTimerID);
	Shell_cancelTimer(gamepadDetectDevicesTimerID);
}

static void Target_foregrounded(double referenceTime) {
	backgrounded = false;
	EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_foregrounded, &referenceTime);
	AudioPlayer_unpause();
	audioTimerID = Shell_setTimer(AUDIO_TIMER_INTERVAL, true, audioTimer, NULL);
	gamepadTimerID = Shell_setTimer(GAMEPAD_TIMER_INTERVAL, true, gamepadEventTimer, NULL);
	gamepadDetectDevicesTimerID = Shell_setTimer(GAMEPAD_DETECT_DEVICES_INTERVAL, true, gamepadDetectDevicesTimer, NULL);
	Shell_redisplay();
}

static bool Target_confirmQuit(double referenceTime) {
	return true;
}

static void runHeadless(void) {
	initAfterFirstDraw();
	Renderer_setRenderTarget(g_renderer, g_renderTarget);
	
	int16_t frameAudioBuffer[g_audioSampleRate / 60 * 2];
	char wavHeader[] = "RIFF\xB2\x03\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x02\x00\x80\xBB\x00\x00\x00\xEE\x02\x00\x04\x00\x10\x00""data\x00\x00\x00\x00";
	FILE * audioFile;
	unsigned int audioByteCount = 0;
	char audioPath[PATH_MAX];
	snprintf_safe(audioPath, PATH_MAX, "%s/audio.wav", videoOutDirectory);
	audioFile = fopen(audioPath, "wb");
	fseek(audioFile, sizeof(wavHeader) - 1, SEEK_SET);
	
	double referenceTime = 0.0;
	double deltaTime = 1.0 / 60.0;
	while (inputPlayback->eventIndex < inputPlayback->inputSession->eventCount) {
		drawFrame(referenceTime, deltaTime);
		referenceTime += deltaTime;
		AudioPlayer_readSamples(frameAudioBuffer, g_audioSampleRate / 60);
		fwrite(frameAudioBuffer, 1, sizeof(frameAudioBuffer), audioFile);
		audioByteCount += sizeof(frameAudioBuffer);
		AudioPlayer_run();
		AutoFreePool_empty();
	}
	
	wavHeader[4] = (audioByteCount + 36) & 0xFF;
	wavHeader[5] = (audioByteCount + 36) >> 8 & 0xFF;
	wavHeader[6] = (audioByteCount + 36) >> 16 & 0xFF;
	wavHeader[7] = (audioByteCount + 36) >> 24 & 0xFF;
	wavHeader[40] = audioByteCount & 0xFF;
	wavHeader[41] = audioByteCount >> 8 & 0xFF;
	wavHeader[42] = audioByteCount >> 16 & 0xFF;
	wavHeader[43] = audioByteCount >> 24 & 0xFF;
	fseek(audioFile, 0, SEEK_SET);
	fwrite(wavHeader, 1, sizeof(wavHeader) - 1, audioFile);
	fclose(audioFile);
}

static void cursorChangeCallback(void) {
	EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_cursor_change, NULL);
}

static void registerShellCallbacks() {
	Shell_drawFunc(Target_draw);
	Shell_resizeFunc(Target_resized);
	Shell_keyDownFunc(Target_keyDown);
	Shell_keyUpFunc(Target_keyUp);
	Shell_keyModifiersChangedFunc(Target_keyModifiersChanged);
	Shell_mouseDownFunc(Target_mouseDown);
	Shell_mouseUpFunc(Target_mouseUp);
	Shell_mouseMovedFunc(Target_mouseMoved);
	Shell_mouseDraggedFunc(Target_mouseDragged);
	Shell_mouseLeaveFunc(Target_mouseLeave);
	Shell_scrollWheelFunc(Target_scrollWheel);
	Shell_backgroundedFunc(Target_backgrounded);
	Shell_foregroundedFunc(Target_foregrounded);
	Shell_confirmQuitFunc(Target_confirmQuit);
	UIToolkit_cursorChangeFunc(cursorChangeCallback);
}

static void parseArgs(int argc, const char ** argv) {
	for (int argIndex = 1; argIndex < argc; argIndex++) {
		if (!strcmp(argv[argIndex], "--disable-audio")) {
			audioDisabled = true;
			
		} else if (!strcmp(argv[argIndex], "--max-pixel-scale") && argIndex < argc - 1) {
			sscanf(argv[++argIndex], "%u", &maxScaleMultiplier);
			
		} else if (!strcmp(argv[argIndex], "--record") && argIndex < argc - 1) {
			recordInputFileName = argv[++argIndex];
			
		} else if (!strcmp(argv[argIndex], "--playback") && argIndex < argc - 1) {
			playbackInputFileName = argv[++argIndex];
			
		} else if (!strcmp(argv[argIndex], "--video-out") && argIndex < argc - 1) {
			videoOutDirectory = argv[++argIndex];
			
		} else if (!strcmp(argv[argIndex], "--headless")) {
			headless = true;
			
		} else if (!strcmp(argv[argIndex], "--no-input-delay")) {
			noInputDelay = true;
			
		} else if (!strcmp(argv[argIndex], "--synchronous-load")) {
			synchronousLoad = true;
			
		} else if (!strcmp(argv[argIndex], "--no-recording")) {
			noInputRecording = true;
			
		} else if (!strcmp(argv[argIndex], "--help")) {
			fprintf(stderr, "Available command-line arguments:\n"
			                 "    --disable-audio\n"
			                 "    --max-pixel-scale <integer>\n"
			                 "    --record <outputFile>\n"
			                 "    --playback <inputFile>\n"
			                 "    --video-out <outputDirectory>\n"
			                 "    --headless\n");
		}
	}
}

#if defined(STEM_PLATFORM_macosx)
void NSOpenGLTarget_configure(int argc, const char ** argv, struct NSOpenGLShellConfiguration * configuration) {
	parseArgs(argc, argv);
	configuration->fullScreenMenuItem = true;
	configuration->useGLCoreProfile = true;
#elif defined(STEM_PLATFORM_windows)
void WGLTarget_configure(void * instance, void * prevInstance, char * commandLine, int command, int argc, const char ** argv, struct WGLShellConfiguration * configuration) {
	parseArgs(argc, argv);
	configuration->useGLCoreProfile = true;
#elif defined(STEM_PLATFORM_linux)
void GLXTarget_configure(int argc, const char ** argv, struct GLXShellConfiguration * configuration) {
#include "IconData_PROJECT_NAME.h"
	parseArgs(argc, argv);
	configuration->icon.data = STATIC_iconData_PROJECT_NAME;
	configuration->icon.size = sizeof_count(STATIC_iconData_PROJECT_NAME);
	configuration->applicationName = STEM_HUMAN_READABLE_TARGET_NAME;
#elif defined(STEM_PLATFORM_iphonesimulator) || defined(STEM_PLATFORM_iphoneos)
void EAGLTarget_configure(int argc, char ** argv, struct EAGLShellConfiguration * configuration) {
#elif defined(STEM_PLATFORM_android)
void EGLTarget_configure(struct EGLShellConfiguration * configuration) {
	readResourceFile = EGLShell_readResourceFile;
#else
#error Unsupported platform
#endif
#if !defined(STEM_PLATFORM_iphonesimulator) && !defined(STEM_PLATFORM_iphoneos) && !defined(STEM_PLATFORM_android)
	int displayX, displayY;
	unsigned int displayWidth, displayHeight, windowWidth, windowHeight;
	
	Shell_getSafeWindowRect(0, &displayX, &displayY, &displayWidth, &displayHeight);
	if (displayWidth / (float) displayHeight > DISPLAY_WIDTH / (float) DISPLAY_HEIGHT) {
		if (displayHeight < DISPLAY_HEIGHT) {
			windowWidth = displayWidth;
			windowHeight = displayHeight;
		} else {
			unsigned int scaleMultiplier = displayHeight / DISPLAY_HEIGHT;
			if (scaleMultiplier > maxScaleMultiplier) {
				scaleMultiplier = maxScaleMultiplier;
			}
			windowHeight = DISPLAY_HEIGHT * scaleMultiplier;
			windowWidth = windowHeight * DISPLAY_WIDTH / DISPLAY_HEIGHT;
		}
	} else {
		if (displayWidth < DISPLAY_WIDTH) {
			windowWidth = displayWidth;
			windowHeight = displayHeight;
		} else {
			unsigned int scaleMultiplier = displayWidth / DISPLAY_WIDTH;
			if (scaleMultiplier > maxScaleMultiplier) {
				scaleMultiplier = maxScaleMultiplier;
			}
			windowWidth = DISPLAY_WIDTH * scaleMultiplier;
			windowHeight = windowWidth * DISPLAY_HEIGHT / DISPLAY_WIDTH;
		}
	}
	configuration->windowX = displayX + (displayWidth - windowWidth) / 2;
	configuration->windowY = displayY + (displayHeight - windowHeight) / 2;
	configuration->windowWidth = g_viewWidth = windowWidth;
	configuration->windowHeight = g_viewHeight = windowHeight;
	configuration->windowTitle = STEM_HUMAN_READABLE_TARGET_NAME;
#endif
	
	registerShellCallbacks();
}

void Target_init() {
	g_renderer = Renderer_create();
	Renderer_setViewport(g_renderer, 0, 0, g_viewWidth, g_viewHeight, NULL);
	
	Gamepad_init();
	Gamepad_deviceAttachFunc(gamepadAttached, NULL);
	Gamepad_deviceRemoveFunc(gamepadRemoved, NULL);
	Gamepad_buttonDownFunc(gamepadButtonDown, NULL);
	Gamepad_buttonUpFunc(gamepadButtonUp, NULL);
	Gamepad_axisMoveFunc(gamepadAxisMoved, NULL);
	
	g_gameSession = GameSession_create();
	g_renderTarget = RenderTarget_createSimple(DISPLAY_WIDTH, DISPLAY_HEIGHT, true, false);
	screenManager = ScreenManager_create();
	
#if defined(STEM_PLATFORM_android)
	EGLShell_setOrientation(EGLShellOrientation_landscape);
	Shell_enterFullScreen(0, true);
#else
	if (GameSession_getFullScreen(g_gameSession)) {
		Shell_enterFullScreen(Shell_getDisplayIndexFromWindow(), false);
	}
#endif
	
	if (headless && playbackInputFileName != NULL) {
		runHeadless();
		return;
	}
	
	Shell_mainLoop();
}
