//-----------------------------------------------------------------------------------------------
// Application.cpp
//
#include "GypsyPrecompiledHeader.hpp"

//-----------------------------------------------------------------------------------------------
#include "Engine/Core/Application.hpp"
#include "Engine/Core/GameInterface.hpp"
#include "Engine/Core/Time.hpp"
#include "Engine/Renderer/Renderer.hpp"
#include "Engine/Audio/Audio.hpp"
#include "Engine/Console/Console.hpp"



//-----------------------------------------------------------------------------------------------
EXTERN Application* theApplication = nullptr;


//-----------------------------------------------------------------------------------------------
Application::Application()
	: m_isQuitting( false )
	, m_hasFocus( false )
	, m_appName( "Unnamed Gypsy App Name" )
	, m_appTitle( "Unnamed Gypsy App Title" )
	, m_createRenderWindow( true )
//	, m_windowClientResolution( 1920, 1080 )
//	, m_windowClientResolution( 1366, 768 )
//	, m_windowClientResolution( 1600, 900 )
//	, m_windowClientResolution( 1024, 576 )
//	, m_windowClientResolution( 1920, 1200 )
//	, m_windowClientResolution( 1728, 1080 )
//	, m_windowClientResolution( 1600, 1000 )
//	, m_windowClientResolution( 1280, 800 )
//	, m_windowClientResolution( 1024, 640 )
	, m_windowClientResolution( 0, 0 ) // (0,0) means "use biggest window (or fullscreen region) of preferred aspect that will fit"; y=0 means "set from width and aspect", etc.
	, m_windowClientAspect( 0.f ) // 0.f means "use default aspect", which is desktop current resolution aspect for fullscreen mode, or some fraction of that for windowed (e.g. aspect of 9/10 of screen)
	, m_isFullscreen( false )
//	, m_isFullscreen( true )
	, m_maxFramerate( 0.f )
	, m_cursorNormalizedCoords( 0.f, 0.f )
	, m_cursorClientCoords( 0, 0 )
	, m_isCursorVisible( true )
{
	theApplication = this;

	m_preInitSettings.m_appName = m_appName;
	m_preInitSettings.m_appTitle = m_appTitle;
	m_preInitSettings.m_createRenderWindow = m_createRenderWindow;
	m_preInitSettings.m_isFullscreenPreferred = m_isFullscreen;
	m_preInitSettings.m_preferredAspectRatio = m_windowClientAspect;
	m_preInitSettings.m_preferredMaxFramerate = m_maxFramerate;
	m_preInitSettings.m_preferredResolution = m_windowClientResolution;
}


//-----------------------------------------------------------------------------------------------
Application::~Application()
{
	theApplication = nullptr;
}


//-----------------------------------------------------------------------------------------------
// void Application::PreConfigure( const NamedProperties& configParams )
// {
// 	configParams.Get( "createRenderWindow", m_createRenderWindow );
// 	configParams.Get( "windowClientResolution", m_windowClientResolution );
// 	configParams.Get( "windowClientAspect", m_windowClientAspect );
// 	configParams.Get( "isFullscreen", m_isFullscreen );
// 	configParams.Get( "maxFramerate", m_maxFramerate );
// }


//-----------------------------------------------------------------------------------------------
void Application::ConfigureFromPreInitSettings()
{
	m_appName					= m_preInitSettings.m_appName;
	m_appTitle					= m_preInitSettings.m_appTitle;
	m_createRenderWindow		= m_preInitSettings.m_createRenderWindow;
	m_maxFramerate				= m_preInitSettings.m_preferredMaxFramerate;
	m_isFullscreen				= m_preInitSettings.m_isFullscreenPreferred;
	m_windowClientAspect		= m_preInitSettings.m_preferredAspectRatio;
	m_windowClientResolution	= m_preInitSettings.m_preferredResolution;
}


//-----------------------------------------------------------------------------------------------
void Application::StartUp()
{
	ConfigureFromPreInitSettings();
	if( m_createRenderWindow )
	{
		SetWindowClientAspectAndResolution();
		CreateOSWindow( m_isFullscreen, m_windowClientResolution );
	}

	theEventSystem.RegisterCommandCallbackForObject( "Quit", &Application::Command_Quit, *this, "Quit the application." );
}


//-----------------------------------------------------------------------------------------------
void Application::Main( CommandLineCommands& commandLineCommands )
{
	GameInterface& game = GameInterface::GetInstance();
	bool manageEngineSystemsAutomatically = game.ProcessCommandLine( commandLineCommands );
	if( manageEngineSystemsAutomatically )
	{
		CreateRequestedEngineSystems();
		game.PreStartup();
		StartUpRequestedEngineSystems();
		game.PostStartup();
	}

	MainLoop(); // Does not return until app is ready to close

	if( manageEngineSystemsAutomatically )
	{
		game.PreShutdown();
		ShutDownEngineSystems();
		game.PostShutdown();
		DestroyEngineSystems();
	}
}


//-----------------------------------------------------------------------------------------------
// This function should not return until the game is exiting and it's time to shut everything down.
//
void Application::MainLoop()
{
	while( !m_isQuitting )
	{
		RunFrame();
		m_profiler.AdvanceToNextFrame();
	}
}


//-----------------------------------------------------------------------------------------------
void Application::RunFrame()
{
	static int pcIndex = GetProfileCategoryIndexForName( "Frame", Rgba::DARKGREY );
	ScopedProfileSection profileSection( pcIndex );

	BeginFrame(); // on the Application itself and all active subsystems

	GameInterface& game = GameInterface::GetInstance();
	game.RunFrame();

	EndFrame(); // on the Application itself and all active subsystems
}


//-----------------------------------------------------------------------------------------------
void Application::BeginFrame()
{
	static int pcIndex = GetProfileCategoryIndexForName( "BeginFrame", Rgba::RED );
	ScopedProfileSection profileSection( pcIndex );

	UpdateCursorNormalizedCoords();

#if defined( GYPSY_ENABLE_RENDERER )
	BeginFrameOnEngineSubsystemIfItExists( theRenderer );
#endif
#if defined( GYPSY_ENABLE_AUDIO )
	BeginFrameOnEngineSubsystemIfItExists( theAudioSystem );
#endif
#if defined( GYPSY_ENABLE_CONSOLE )
	BeginFrameOnEngineSubsystemIfItExists( theConsole );
#endif
}


//-----------------------------------------------------------------------------------------------
void Application::EndFrame()
{
	static int profileID = GetProfileCategoryIndexForName( "EndFrame", Rgba::RED );
	BeginProfileSection( profileID );

#if defined( GYPSY_ENABLE_CONSOLE )
	EndFrameOnEngineSubsystemIfItExists( theConsole );
#endif
#if defined( GYPSY_ENABLE_AUDIO )
	EndFrameOnEngineSubsystemIfItExists( theAudioSystem );
#endif
#if defined( GYPSY_ENABLE_RENDERER )
	EndFrameOnEngineSubsystemIfItExists( theRenderer );
#endif

//	EndProfileSection( profileID );

	m_profiler.RenderPreviousFrameStats();

//	BeginProfileSection( profileID );
	if( m_maxFramerate > 0.f )
		WaitUntilNextFrame( m_maxFramerate );

	SwapBuffers();

	EndProfileSection( profileID );
}


//-----------------------------------------------------------------------------------------------
void Application::ShutDown()
{
}


//-----------------------------------------------------------------------------------------------
void Application::SwapBuffers()
{
	static int pcIndex = GetProfileCategoryIndexForName( "EndFrame.SwapBuffers", Rgba( 255, 0, 0 ) );
	ScopedProfileSection profileSection( pcIndex );

#if defined( GYPSY_ENABLE_RENDERER )
	if( theRenderer )
		theRenderer->SwapBuffers();
#endif
}


//-----------------------------------------------------------------------------------------------
void Application::CreateRequestedEngineSystems()
{
#if defined( GYPSY_ENABLE_RENDERER )
	Renderer::Create();
	theRenderer->m_state = ENGINE_SUBSYSTEM_STATE_CONSTRUCTED;
#endif
#if defined( GYPSY_ENABLE_AUDIO )
	AudioSystem::Create();
	theAudioSystem->m_state = ENGINE_SUBSYSTEM_STATE_CONSTRUCTED;
#endif
#if defined( GYPSY_ENABLE_CONSOLE )
	Console::Create();
	theConsole->m_state = ENGINE_SUBSYSTEM_STATE_CONSTRUCTED;
#endif
}


//-----------------------------------------------------------------------------------------------
void Application::DestroyEngineSystems()
{
#if defined( GYPSY_ENABLE_CONSOLE )
	DestroyEngineSubsystemIfItExists( theConsole );
#endif
#if defined( GYPSY_ENABLE_AUDIO )
	DestroyEngineSubsystemIfItExists( theAudioSystem );
#endif
#if defined( GYPSY_ENABLE_RENDERER )
	DestroyEngineSubsystemIfItExists( theRenderer );
#endif
}


//-----------------------------------------------------------------------------------------------
void Application::StartUpRequestedEngineSystems()
{
	theEventSystem.StartUp();
	StartUp(); // The Application itself

#if defined( GYPSY_ENABLE_RENDERER )
	StartUpEngineSubsystemIfItExists( theRenderer );
#endif
#if defined( GYPSY_ENABLE_AUDIO )
	StartUpEngineSubsystemIfItExists( theAudioSystem );
#endif
#if defined( GYPSY_ENABLE_CONSOLE )
	StartUpEngineSubsystemIfItExists( theConsole );
#endif
}


//-----------------------------------------------------------------------------------------------
void Application::ShutDownEngineSystems()
{
#if defined( GYPSY_ENABLE_CONSOLE )
	ShutDownEngineSubsystemIfItExists( theConsole );
#endif
#if defined( GYPSY_ENABLE_AUDIO )
	ShutDownEngineSubsystemIfItExists( theAudioSystem );
#endif
#if defined( GYPSY_ENABLE_RENDERER )
	ShutDownEngineSubsystemIfItExists( theRenderer );
#endif

	ShutDown(); // The Application itself
	theEventSystem.ShutDown();
}


//-----------------------------------------------------------------------------------------------
void Application::StartUpEngineSubsystemIfItExists( EngineSubsystem* subsystem )
{
	if( !subsystem )
		return;

	FATAL_ASSERTION( subsystem->m_state == ENGINE_SUBSYSTEM_STATE_CONSTRUCTED, Stringf( "Application::StartUpEngineSubsystemIfItExists() called for engine subsystem %s when it was in an improper state.", subsystem->GetName() ) );
	subsystem->m_state = ENGINE_SUBSYSTEM_STATE_STARTING_UP;
	subsystem->StartUp();
	subsystem->m_state = ENGINE_SUBSYSTEM_STATE_READY;
}


//-----------------------------------------------------------------------------------------------
void Application::BeginFrameOnEngineSubsystemIfItExists( EngineSubsystem* subsystem )
{
	if( subsystem && subsystem->IsReady() )
	{
		subsystem->BeginFrame();
	}
}


//-----------------------------------------------------------------------------------------------
void Application::EndFrameOnEngineSubsystemIfItExists( EngineSubsystem* subsystem )
{
	if( subsystem && subsystem->IsReady() )
	{
		subsystem->EndFrame();
	}
}


//-----------------------------------------------------------------------------------------------
void Application::ShutDownEngineSubsystemIfItExists( EngineSubsystem* subsystem )
{
	if( !subsystem )
		return;

	FATAL_ASSERTION( subsystem->m_state == ENGINE_SUBSYSTEM_STATE_READY, Stringf( "Application::ShutDownEngineSubsystemIfItExists() called for engine subsystem %s when it was in an improper state.", subsystem->GetName() ) );
	subsystem->m_state = ENGINE_SUBSYSTEM_STATE_SHUTTING_DOWN;
	subsystem->ShutDown();
	subsystem->m_state = ENGINE_SUBSYSTEM_STATE_CONSTRUCTED;
}


//-----------------------------------------------------------------------------------------------
void Application::DestroyEngineSubsystemIfItExists( EngineSubsystem* subsystem )
{
	if( !subsystem )
		return;

	FATAL_ASSERTION( subsystem->m_state == ENGINE_SUBSYSTEM_STATE_CONSTRUCTED, Stringf( "Application::DestroyEngineSubsystemIfItExists() called for engine subsystem %s when it was in an improper state.", subsystem->GetName() ) );
	subsystem->m_state = ENGINE_SUBSYSTEM_STATE_DESTRUCTING;
	delete subsystem; // Automatically self-unregisters (e.g. makes theConsole = nullptr)
}


//-----------------------------------------------------------------------------------------------
void Application::Quit()
{
	m_isQuitting = true;
}


//-----------------------------------------------------------------------------------------------
// Called before the game has been shown the event.
//	Note: if this returns true (was consumed), the game will not receive the event!
//
bool Application::ProcessSystemEventPreGame( const SystemEvent& systemEvent )
{
	static int pcIndex = GetProfileCategoryIndexForName( "Input.Msg.PreGame", Rgba( 255, 255, 0 ) );
	ScopedProfileSection profileSection( pcIndex );

	if( systemEvent.m_eventType == EVENT_TYPE_IGNORE_AND_CONSUME )
	{
		return true;
	}
	else if( systemEvent.m_eventType == EVENT_TYPE_IGNORE_AND_PERMIT )
	{
		return false;
	}
	else if( systemEvent.m_eventType == EVENT_TYPE_APP_GAINED_FOCUS )
	{
		m_hasFocus = true;
		DebuggerPrintf( "GAINED FOCUS!\n" );
	}
	else if( systemEvent.m_eventType == EVENT_TYPE_APP_LOST_FOCUS )
	{
		m_hasFocus = false;
		DebuggerPrintf( "LOST FOCUS!\n" );
	}
	else if( systemEvent.m_eventType == EVENT_TYPE_BUTTON_PRESSED )
	{
		ButtonState& buttonState = m_keyButtonStates[ systemEvent.m_keyButtonCode ];
		buttonState.m_isPressed = true;
	}
	else if( systemEvent.m_eventType == EVENT_TYPE_BUTTON_RELEASED )
	{
		ButtonState& buttonState = m_keyButtonStates[ systemEvent.m_keyButtonCode ];
		buttonState.m_isPressed = false;
	}

	bool wasConsumed = false;

#if defined( GYPSY_ENABLE_CONSOLE )
	if( !wasConsumed )	wasConsumed = theConsole->ProcessSystemEventPreGame( systemEvent );
#endif
#if defined( GYPSY_ENABLE_AUDIO )
	if( !wasConsumed )	wasConsumed = theAudioSystem->ProcessSystemEventPreGame( systemEvent );
#endif
#if defined( GYPSY_ENABLE_RENDERER )
	if( !wasConsumed )	wasConsumed = theRenderer->ProcessSystemEventPreGame( systemEvent );
#endif

	return wasConsumed;
}


//-----------------------------------------------------------------------------------------------
// Called only after the game has been shown the event, and has decided to not consume it.
//
bool Application::ProcessSystemEventPostGame( const SystemEvent& systemEvent )
{
	static int pcIndex = GetProfileCategoryIndexForName( "Input.Msg.PostGame", Rgba( 255, 255, 0 ) );
	ScopedProfileSection profileSection( pcIndex );

	bool wasConsumed = false;

	if( systemEvent.m_eventType ==  EVENT_TYPE_APP_CLOSE )
	{
		wasConsumed = true;
		Quit();
	}

#if defined( GYPSY_ENABLE_CONSOLE )
	if( !wasConsumed )	wasConsumed = theConsole->ProcessSystemEventPostGame( systemEvent );
#endif
#if defined( GYPSY_ENABLE_AUDIO )
	if( !wasConsumed )	wasConsumed = theAudioSystem->ProcessSystemEventPostGame( systemEvent );
#endif
#if defined( GYPSY_ENABLE_RENDERER )
	if( !wasConsumed )	wasConsumed = theRenderer->ProcessSystemEventPostGame( systemEvent );
#endif

	return wasConsumed;
}


//-----------------------------------------------------------------------------------------------
void Application::WaitUntilNextFrame( float maxFramesPerSecond )
{
	const double minFrameSeconds = (1.0 / static_cast< double >( maxFramesPerSecond ));
	SleepSeconds( 0.000 ); // Relinquish our time slice (for playing nice with other apps)
#if defined( GYPSY_ENABLE_RENDERER )
	if( theRenderer )
	{
		static int pcIndex = GetProfileCategoryIndexForName( "EndFrame.FlushAndFinish", Rgba( 255, 0, 0 ) );
		ScopedProfileSection profileSection( pcIndex );

//		theRenderer->FlushAndFinish(); // Force us to finish any outstanding rendering activity
	}
#endif

	static int pcIndex = GetProfileCategoryIndexForName( "EndFrame.FrameLimiter", Rgba( 255, 0, 0 ) );
	ScopedProfileSection profileSection( pcIndex );

	double timeNow = GetCurrentTimeSeconds();
	static double targetTime = timeNow; // first-time initialization
	while( timeNow < targetTime )
	{
		if( targetTime - timeNow > 0.002 ) // If we have more than 2ms left to burn, sleep for 1ms
			SleepSeconds( 0.001 );
		else if( targetTime - timeNow > 0.001 ) // If we have more than 1ms left to burn, sleep for 0ms (relinquish time slice)
			SleepSeconds( 0.000 );
		// If we have less than 1 ms left to burn, tight-spin until done

		timeNow = GetCurrentTimeSeconds();
	}

	targetTime = timeNow + (minFrameSeconds * 0.99);
}

/*
//-----------------------------------------------------------------------------------------------
// In order to use the native desktop resolution fullscreen, pass default arguments (or none),
//	which are isFullscreen(true), resolution(0,0), and aspect(0).
//
VIRTUAL void Application::SetDefaultVideoMode( bool isFullscreen, const IntVector2& clientAreaResolution, float clientAreaAspect )
{
	m_windowClientResolution = clientAreaResolution;
	m_windowClientAspect = clientAreaAspect;
	m_isFullscreen = isFullscreen;
	here;
	if( m_windowClientResolution.x <= 0 )
	{
		m_windowClientResolution = GetDesktopResolution();
	}
	else if( m_windowClientResolution.y <= 0 )
	{
		// Width was specified, but not height; assume a 16:9 aspect and infer height
		m_windowClientResolution.y = (m_windowClientResolution.x * 9) / 16;
	}
}
*/


//-----------------------------------------------------------------------------------------------
// Sets up resolution and/or aspect to reasonable values.  If specific values are provided for
//	any of width, height, and/or aspect, the other values follow suit.  Default setting is
//	native (desktop) resolution for fullscreen modes, or 16:9 window sized to fit onscreen.
//
VIRTUAL void Application::SetWindowClientAspectAndResolution()
{
	const IntVector2 desktopRes = GetDesktopResolution();
	const float maxAutoWindowFractionOfDesktop = 0.9f;
	const Vector2 maxAutoWindowSize = maxAutoWindowFractionOfDesktop * Vector2( desktopRes );

	if( m_windowClientResolution.x > 0 && m_windowClientResolution.y > 0 )
	{
		// Width & Height specified
		m_windowClientAspect = (float) m_windowClientResolution.x / (float) m_windowClientResolution.y;
	}
	else if( m_windowClientResolution.x > 0 && m_windowClientAspect != 0.f )
	{
		// Width & Aspect specified
		m_windowClientResolution.y = (int)( (float) m_windowClientResolution.x / m_windowClientAspect );
	}
	else if( m_windowClientResolution.y > 0 && m_windowClientAspect != 0.f )
	{
		// Height & Aspect specified
		m_windowClientResolution.x = (int)( m_windowClientAspect * (float) m_windowClientResolution.y );
	}
	else if( m_windowClientAspect != 0.f )
	{
		// Aspect only specified
		if( m_isFullscreen )
		{
			Vector2 autoFullscreenSize = CalcLargestRectThatFitsWithinArea( m_windowClientAspect, Vector2( desktopRes ) );
			m_windowClientResolution = IntVector2( autoFullscreenSize );
		}
		else
		{
			Vector2 autoWindowSize = CalcLargestRectThatFitsWithinArea( m_windowClientAspect, maxAutoWindowSize );
			m_windowClientResolution = IntVector2( autoWindowSize );
		}
	}
	else if( m_windowClientResolution.x > 0 )
	{
		// Width only specified
		if( m_isFullscreen )
			m_windowClientAspect = desktopRes.CalcAspect();
		else
			m_windowClientAspect = maxAutoWindowSize.CalcAspect();

		m_windowClientResolution.y = (int)( (float) m_windowClientResolution.x / m_windowClientAspect );
	}
	else if( m_windowClientResolution.y > 0 )
	{
		// Height only specified
		if( m_isFullscreen )
			m_windowClientAspect = desktopRes.CalcAspect();
		else
			m_windowClientAspect = maxAutoWindowSize.CalcAspect();

		m_windowClientResolution.x = (int)( (float) m_windowClientResolution.y * m_windowClientAspect );
	}
	else
	{
		// Nothing (width, height, nor aspect) specified
		if( m_isFullscreen )
			m_windowClientResolution = desktopRes;
		else
			m_windowClientResolution = IntVector2( maxAutoWindowSize );

		m_windowClientAspect = m_windowClientResolution.CalcAspect();
	}
}


//-----------------------------------------------------------------------------------------------
void Application::SetCursorVisibility( bool isVisible )
{
	if( isVisible != m_isCursorVisible )
	{
		SetNativeCursorVisibility( isVisible );
	}
}


//-----------------------------------------------------------------------------------------------
// Note: Subclasses should call this base version in order to make sure the internal bookkeeping
//	for last known cursor position isn't delayed by a frame, etc.
//
VIRTUAL void Application::SnapCursorToNormalizedCoords( const Vector2& newNormalizedCoords )
{
	m_cursorNormalizedCoords = newNormalizedCoords;
}



//-----------------------------------------------------------------------------------------------
void Application::Command_Quit( const NamedProperties& params )
{
	UNUSED( params );
	Quit();
}

