/* -----------------------------------------------------------------------------
	GSFramework
	Copyright 2001-2013 Emmanuel Julien. All Rights Reserved.
----------------------------------------------------------------------------- */


		#include "viewer_base/viewer_base.h"
		#include "viewer_base/viewer_base_debugger.h"
		#include "physic_bullet/bullet_world.h"
		#include "io_archive/io_archive.h"
		#include "raytracer/raytracer_core.h"
		#include "font_freetype/ft2_font_factory.h"
		#include "script_squirrel/engine_vm_debugger.h"
		#include "script_squirrel/engine_vm.h"
		#include "script/script_variant.h"
		#include "ui/ui.h"
		#include "gpu/gpu_triangle_batch.h"
		#include "gpu/gpu_renderer.h"
		#include "core/embedded_resource_extractor.h"
		#include "core/renderer_toolbox.h"
		#include "metafile/nml_object.h"
		#include "picture/pict_io.h"
		#include "filesystem/filesystem.h"
		#include "filesystem/io_cfile.h"
		#include "input/input_system.h"
		#include "log/log.h"

		using namespace GS;
		using namespace GS::Core;


//------------------------------------------------------------------------------
bool				ViewerBase::OpenViewer()
{
	// Check core file system.
	if	(!Platform::Get().io->Exists("@core/noise.tga"))
		__ERR__(__LOG_E__ << "@core is not properly mounted.\n", false)

	// Embedded resources will be extracted to a ram disk.
	IEmbeddedResourceHandler::Set(new EmbeddedResourceExtractor(true));

	// Create outputs.
	factories = new ResourceFactories;
	if	(!CreateRenderer() || !CreateMixer())
		return false;

	// Load renderer configuration.
	NML::Parser::Load(config_path, renderer->registry);

	// Open output subsystems.
	if	(!OpenVideo() || !OpenAudio())
		return false;

	profiler_font[0] = new Render::RasterFont;
	profiler_font[0]->Load(*factories->render, "@core/fonts/profiler_base.nml", "@core/fonts/profiler_base");
	profiler_font[1] = new Render::RasterFont;
	profiler_font[1]->Load(*factories->render, "@core/fonts/profiler_bold.nml", "@core/fonts/profiler_bold");

	fps_font = new Render::RasterFont;
	fps_font->Load(*factories->render, "@core/fonts/fps.nml", "@core/fonts/fps");

	gpu_batch = new GPU::TriangleBatch((GPU::Renderer &)*renderer);

	// Initialize input interface.
	Platform::Get().input_system->SetHandle(renderer->GetCurrentSystemWindowHandle());

	state = SessionSetup;
	return true;
}
void				ViewerBase::CloseViewer()
{
	CloseSession();

	for	(uint n = 0; n < 2; ++n)
		_safe_delete(profiler_font[n]);
	_safe_delete(fps_font);

	factories = NULL;

	if	(renderer.IsValid())
	{
		renderer->Close();
		renderer = NULL;
	}
	if	(mixer.IsValid())
	{
		mixer->Close();
		mixer = NULL;
	}
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
bool				ViewerBase::SetupSessionSource()	// session
{
	switch (session_source)
	{
		case SessionSourceFilesystem:
			Platform::Get().io->Mount(new IO::CFile(session_source_path));
			break;
		case SessionSourceArchive:
			Platform::Get().io->Mount(new IO::Archive(session_source_path));
			break;
		case SessionSourceArchiveBootstrap:
			break;
	}
	return true;
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
bool				ViewerBase::OpenScriptVM()		// session
{
	// Compile core library.
	if	(!script_vm->CompileFile("@core/script/nad.nut"))
		return false;

	// Set defines.
	ListForeachPtr(Variant *, prop, define_list)
		script_vm->Set(prop->id, *prop);

	// Load optional includes.
	for (uint n = 0; n < include_list.GetCount(); ++n)
		if	(!script_vm->CompileFile(include_list.ObjectAt(n)))
			return false;

	// Compile bootstrap.
	if	(!bootstrap_script.IsEmpty() && !script_vm->Compile(bootstrap_script, bootstrap_script.Len(), NULL, "Bootstrap"))
		return false;

	return true;
}
void				ViewerBase::SetVMGlobals()
{
	script_vm->Set("g_project", Script::Variant(project, Script::typetag_Project));

	script_vm->Set("g_factory", Script::Variant(factories, Script::typetag_ResourceFactories));
	script_vm->Set("g_render", Script::Variant(renderer, Script::typetag_Renderer));
	script_vm->Set("g_mixer", Script::Variant(mixer, Script::typetag_Mixer));

	script_vm->Set("g_dt_frame", 1.f / 60.f);
	script_vm->Set("g_raw_dt_frame", 1.f / 60.f);
	script_vm->Set("g_clock", 0);
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
bool				ViewerBase::LoadSessionData()
{
	if	(remote)
		session_type = remote_scene.isEmpty() ? SessionProject : SessionScene;

	__LOG__ << "Session type: " << session_type << "\n";

	using namespace NML;

	if	(session_type == SessionProject)
	{
		if	(!remote)
		{
			if	(!LoadFromFile(*project, input_path))
				return false;
		}
		else
			if	(!LoadFromFile(*project, remote_project))
				return false;
	}

	if	(!project->Open(session_type == SessionProject))
		return false;

	switch (session_type)
	{
		case SessionScene:
		{
			// Determine the scene type.
			if	(!remote)
				if	(!Parser::Load(input_path, remote_scene))	// load local scene over the remote_scene meta file
					return false;

			Tag *t_scene2d = remote_scene.GetTag("Scene2D"),
				*t_scene3d = remote_scene.GetTag("Scene");

			//
			if	(t_scene3d)
			{
				scene_3d = new S3D::Scene(script_vm);
				scene_3d->SetClock(project->clock);

				if	(!scene_3d->Create(project->iproject_factory->NewPhysicWorld()))
					return false;
				if	(!scene_3d->FromMetaTag(*t_scene3d, tool_mode ? ToolPreview : NoTool))
					return false;

				scene_3d->name = input_path;
				scene_3d->SetAsScriptGlobalScene();
				scene_3d->InstanceSetup();
				scene_3d->RenderSetup(factories);
				scene_3d->Setup(tool_mode ? ToolPreview : NoTool);
				scene_3d->Reset();
			}
			else if (t_scene2d)
			{
				scene_2d = new S2D::Scene(script_vm);
				scene_2d->SetClock(project->clock);

				if	(!scene_2d->FromMetaTag(*t_scene2d, tool_mode ? ToolPreview : NoTool))
					return false;

				scene_2d->name = input_path;
				scene_2d->SetAsScriptGlobalScene();
				scene_2d->RenderSetup(factories);
				scene_2d->Setup();
				scene_2d->Reset();
			}
		}
		break;

		case SessionProject:
			project->Setup();
			break;
	}
	return true;
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
bool				ViewerBase::OpenSession()
{
	if	(!OpenScriptVM())
		return false;

	missing_resource = false;

	// Create project.
	struct ProjectFactory : public IProjectFactory
	{	S3D::PhysicWorld *NewPhysicWorld() const { return new S3D::BulletWorld; }	};

	project = new Project(factories, new Freetype2FontFactory, script_vm);
	project->iproject_factory = new ProjectFactory;

	SetVMGlobals();

	// Setup data source.
	if	(!SetupSessionSource())
		return false;

	// Load viewer data.
	if	(!LoadSessionData())
		return false;

	time_start = Platform::Get().GetClock();
	return true;
}
void				ViewerBase::CloseSession()
{
	paused = false;

	scene_2d = NULL;
	scene_3d = NULL;
	project = NULL;

	// [EJ] explicitly close the VM now as is may hold references to render resources
	if	(script_vm.IsValid())
		script_vm->Close();

//	script_vm = NULL;
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
void				ViewerBase::ExecuteSession()
{
	if	(scene_3d)
		scene_3d->profiler.ResetProfiles();

	project->ResetStatistics();
	renderer->ResetStatistics();

	// Start frame.
	if	(!paused)
	{
		project->clock->Update();
		project->UpdateScriptClock();
	}

	float x = 32, y = 32;
	bool end_session = false;

	switch (session_type)
	{
		case SessionScene:
			if	(scene_2d.IsValid())
			{
				renderer->Clear(0, 0, 0);

				if	(!paused)
					scene_2d->Update();

				// nGPUTriangleBatch batch(*renderer);
				scene_2d->Render(*renderer/*, &batch*/);
			}
			if	(scene_3d.IsValid())
			{
				if	(!paused)
					scene_3d->Update();
				scene_3d->Render(*renderer);
				scene_3d->RenderUI(*renderer, gpu_batch);

				// Viewer-specific
				{
					if	(enable_profiler)
						scene_3d->DrawProfilerText(*renderer, profiler_font, x, y);

					if	(scene_3d->flags.IsSet(S3D::Scene::FlagEnd))
						end_session = true;

					if	(!raytrace_path.IsEmpty())
					{
						int	w = raytrace_width == -1 ? width : raytrace_width,
							h = raytrace_height == -1 ? height : raytrace_height;

						__LOG_H__ << "Raytracing frame " << frame_count << " (" << w << "x" << h << ")...\n";
						Raytrace::Raytracer ray(factories->graphic);

						if	(ray.SetScene(scene_3d))
						{
							Picture out;
							ray.GetConfiguration().aa_sample = raytrace_aa;
							if	(ray.Render(out, w, h))
								PictureIO::Get().TgaSave(out, String::Format("%s/out_%05d.tga", raytrace_path.c_str(), frame_count));
						}
					}
				}
			}
			break;

		case SessionProject:
			if	(!paused)
				project->Update(SceneUpdateAll);

			project->Render(*renderer);

			// Viewer-specific
			{
				if	(enable_profiler)
					project->DrawProfilerText(*renderer, profiler_font, x, y);

				if	(project->flags.IsSet(Project::ProjectFlagEnd))
					end_session = true;
			}
			break;
	}

	// End frame.
	fRect viewport = renderer->GetViewport();

	if	(enable_profiler)
	{
		float x = viewport.GetWidth() - 400.f, y = 32.f;
		renderer->DrawProfilerText(profiler_font, x, y);
	}
	else	if	(memory_profiler)
	{
		float x = 0, y = 0;
		DrawAllocProfilerText(*renderer, profiler_font, x, y);
	}

	if	(enable_profiler || display_fps)
	{
		Color shadow(0, 0, 0, 0.5);
		Render::Renderer::WriterConfig config(false);

		float x = 32, y = viewport.GetHeight() - 102;
		renderer->Write(*fps_font, String::Format("%02.01f\n", fps.GetFps()), x, y, config, 1, &shadow);
		y = viewport.GetHeight() - 112;
		renderer->Write(*fps_font, String::Format("%02.01f\n", fps.GetFps()), x, y, config);
	}

	renderer->ShowFrame();
	frame_count++;

	// Pause support.
/*	if	(Input::Device *keyboard = Platform::Get().input_system->GetDevice("keyboard"))
		if	(keyboard->WasPressed(Input::Device::Key_P))
		{
			paused = !paused;
			if	(!paused)
				project->clock->EatDeltaClock();
		}
		*/
	// Check session runtime error.
	if	(CheckRuntimeError())
		end_session = true;
	if	(time_live && ((Platform::Get().GetClock() - time_start) > (time_live * Platform::Get().GetClockFrequency())))
		end_session = true;

	if	(end_session)
		state = SessionClose;
}
bool				ViewerBase::CheckRuntimeError()
{
	// Check VM state.
	switch (script_vm->GetState())
	{
		case Script::IVM::StateDead:
		case Script::IVM::StateExceptionThrown:
			return true;

		case Script::IVM::StateOk:
			break;
	}

	// Check for a missing resource error.
	if	(missing_resource && !ignore_missing_resource)
		__ERR__(__LOG_E__ << "Closing session due to missing resources.\n", true)

	return false;
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
Script::NetworkDebugger *ViewerBase::CreateVMDebugInterface()
{
	return new Script::ViewerBaseDebugger(*this, new Script::EngineDebugger((Script::EngineVM &)*script_vm), NULL, remote_port);
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
ViewerBase::State	ViewerBase::Execute()
{
	fps.MarkLoop();

	Platform::Get().input_system->Update();
	bool platform_update = PlatformUpdate();

	switch (state)
	{
		//----------------------------------------------------------------------
		case ViewerSetup:
			script_vm = new Script::EngineVM;
			script_vm->Open();

			if (active_debug)
				script_vm->SetDebugInterface(script_debugger, true);

			if	(remote)
			{
				script_debugger = CreateVMDebugInterface();
				script_vm->SetDebugInterface(script_debugger, true);

				// @FIXME make sure the network thread is running ok.

				__LOG_H__ << "Waiting for controller connection...\n";
				state = WaitRemoteController;
			}
			else
				state = SessionSetup;	// local setup done
			break;
		//----------------------------------------------------------------------

		//----------------------------------------------------------------------
		case WaitRemoteController:
			if	(script_debugger && script_debugger->IsConnected())
			{
				__LOG_H__ << "Controller connected.\n";
				state = WaitRemoteSetup;
			}
			if	(!platform_update)
				state = ViewerClose;
			break;

		case WaitRemoteSetup:
			if	(script_debugger && !script_debugger->IsConnected())
			{
				__LOG_H__ << "Controller lost, waiting for controller connection...\n";
				state = WaitRemoteController;
			}
			break;	// state will be altered by the monitor
		//----------------------------------------------------------------------

		case SessionSetup:
			if	(OpenViewer() && OpenSession())
			{
				__LOG_H__ << "Session running.\n";
				state = SessionRunning;
			}
			else
				state = remote ? WaitRemoteSetup : SessionClose;
			break;

		case SessionRunning:
			ExecuteSession();

			if	(state == SessionRunning)
			{
				if	(script_debugger && !script_debugger->IsConnected())
					state = SessionClose;

				if	(!platform_update)
					state = SessionClose;
			}

			if	(state != SessionRunning)
				__LOG_H__ << "Closing session.\n";
			break;

		case SessionClose:
			CloseSession();
			CloseViewer();

			if	(remote)
			{
				script_debugger->Stop();

				if	(remote_fs.IsValid())
				{
					Platform::Get().io->Unmount(remote_fs);
					remote_fs = NULL;
				}

				state = ViewerSetup;
			}
			else
				state = ViewerClose;
			break;

		case ViewerClose:
			break;
	}

	if	(script_debugger)
		while (script_debugger->async.Execute());

	Platform::Get().Sleep(1);
	return state;
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
void				ViewerBase::Suspend()	// app sent to background (iOS/Android)
{
	if	(script_vm && script_vm->SetupFunctionCall("OnSuspend"))
		script_vm->DoFunctionCall();
	if	(mixer)
		mixer->SuspendWorkerThread();
}
void				ViewerBase::Resume()	// app sent back to foreground (iOS/Android)
{
	if	(script_vm && script_vm->SetupFunctionCall("OnResume"))
		script_vm->DoFunctionCall();
	if	(mixer)
		mixer->ResumeWorkerThread();

	if	(project.IsValid())
		project->clock->EatDeltaClock();
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
ViewerBase::ViewerBase()
{
	state = ViewerSetup;

	paused = false;
	missing_resource = false;

	time_start = 0;
	time_live = 0;
	frame_count = 0;

	raytrace_width = -1;
	raytrace_height = -1;

	session_source = SessionSourceFilesystem;
	session_type = SessionScene;
	session_source_path = "./";
	input_path = "scene.nms";
	
	active_debug = false;
	
	remote = false;
	remote_port = 999;

	tool_mode = false;

	script_debugger = NULL;

	safe_mode = false;
	ignore_esc = false;
	ignore_missing_resource = false;
	fullscreen = false;

	enable_profiler = false;
	memory_profiler = false;
	display_fps = false;

	enable_pause = false;

	for (int n = 0; n < 2; ++n)
		profiler_font[n] = NULL;
	fps_font = NULL;

	width = 800;
	height = 600;
	aspect_ratio = 1;
}
//------------------------------------------------------------------------------
