module gedo.system.gameloop;



private import gedo.system.scene;
private import gedo.video.ggl;
private import gedo.video.texture;
private import gedo.video.staticmesh;
private import gedo.video.skinnedmesh;
private import gedo.audio.gal;
private import gedo.audio.sound;
private import gedo.input.joystick;
private import gedo.text.convert;
private import gedo.file.config;
private import gedo.file.path;

private import sdl.std;

private import std.cstream;
private import std.conv;
private import std.path;
private import std.gc;



public class GameLoop
{
public:
	Config config;


	static GameLoop create(string title, string[] args)
	{
		if(!_instance)
		{
			_instance = new GameLoop(title, args);
		}

		return _instance;
	}


	static GameLoop opCall()
	{
		return _instance;
	}


	final int run(Scene delegate() topScene)
	{
		if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_JOYSTICK) >= 0)
		{
			scope(exit)
			{
				SDL_Quit();
			}

			{
				auto v = SDL_Linked_Version();
				dout.writefln("SDL Version: %d.%d.%d", v.major, v.minor, v.patch);
			}

			SDL_ShowCursor(SDL_DISABLE);

			uint sdlVideoFlags = SDL_OPENGL;
			if(_isFullScreen)
			{
				sdlVideoFlags |= SDL_FULLSCREEN;
			}

			SDL_GL_SetAttribute(SDL_GL_SWAP_CONTROL, 1);
			SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
			if(SDL_SetVideoMode(_windowSize[0], _windowSize[1], 0, sdlVideoFlags))
			{
				/* Set the window manager title bar */
				SDL_WM_SetCaption(cast(char*)_title.ptr, cast(char*)_title.ptr);

				// 1: use OpenGL extension
				GGL_initialize(_isExtensionEnable);
				scope(exit)
				{
					GGL_quit();
				}

				Sound.initialize(_args);
				scope(exit)
				{
					Sound.clearCache();
					Texture.clearCache();
					StaticMesh.clearCache();
					SkinnedMesh.clearCache();
					.std.gc.fullCollect();
					Sound.quit();
				}
				GAL_setVolume(_volumes[0], _volumes[1]);

				GGL_setViewport(
						(_windowSize[0] - _viewportSize[0]) >> 1,
						(_windowSize[1] - _viewportSize[1]) >> 1,
						_viewportSize[0],
						_viewportSize[1]
					);

				Joysticks.initialize();
				Joysticks().config(config);

				loop(topScene());

				return(0);
			}
		}

		dout.writefln("%s: SDL error '%s'", _title, getSDLError());

		return -1;
	}


	void quit()
	{
		throw new GameQuitException();
	}


	void isExtensionEnable(bool b)
	{
		_isExtensionEnable = b;
	}


	void isFullScreen(bool b)
	{
		_isFullScreen = b;
	}


	uint[] windowSize()
	{
		return _windowSize;
	}


	float[] volumes()
	{
		return _volumes;
	}


	void saveConfig()
	{
		Joysticks().getConfig(config);

		config.save();
	}


private:
	static GameLoop _instance = null;


	string[] _args;
	float[2] _volumes;
	uint[2]  _windowSize;
	uint[2]  _viewportSize;
	string   _title;
	bool     _isFullScreen;
	bool     _isExtensionEnable;


	this(string title, string[] args)
	{
		version(Windows)
		{
		}
		else
		{
			setLibDirectory(.std.path.getDirName(args[0]));
		}

		_title = title;
		config = new .gedo.file.config.Config(_title);

		_windowSize[0] = config.get("window.size.x", 640);
		_windowSize[1] = config.get("window.size.y", 480);

		_viewportSize[0] = config.get("viewport.size.x", 0);
		_viewportSize[1] = config.get("viewport.size.y", 0);

		_isFullScreen      = config.get("fullscreen", false);
		_isExtensionEnable = config.get("extension", true);

		_volumes[0] = config.get("volume.bgm", 0.75);
		_volumes[1] = config.get("volume.se", 0.75);

		foreach(i, s; _windowSize)
		{
			if(_viewportSize[i] == 0)
			{
				_viewportSize[i] = s;
			}
		}

		_args = args;
	}


	void handleEvent()
	{
		SDL_Event event;

		while(SDL_PollEvent(&event))
		{
			switch(event.type)
			{
			case SDL_KEYDOWN:
				if(event.key.keysym.sym == SDLK_ESCAPE)
				{
					quit();
				}
				else if((event.key.keysym.sym == SDLK_g) && (event.key.keysym.mod & KMOD_CTRL))
				{
					hotKeyToggleGrab();
				}
				else if ((event.key.keysym.sym == SDLK_z) && (event.key.keysym.mod & KMOD_CTRL))
				{
					hotKeyIconify();
				}
				else if ((event.key.keysym.sym == SDLK_RETURN) && (event.key.keysym.mod & KMOD_ALT))
				{
					hotKeyToggleFullScreen();
				}
				else
				{
					Joysticks().handleEvent(&event);
				}
				break;

			case SDL_QUIT:
				quit();
				break;
				
			default:
				Joysticks().handleEvent(&event);
				break;
			}
		}
	}


	void hotKeyToggleFullScreen()
	{
		SDL_Surface *screen;

		screen = SDL_GetVideoSurface();
		if(SDL_WM_ToggleFullScreen(screen))
		{
			dout.writefln("Toggled fullscreen mode - now %s", (screen.flags & SDL_FULLSCREEN) ? "fullscreen" : "windowed");
		}
		else
		{
			dout.writefln("Unable to toggle fullscreen mode");
		}
	}


	void hotKeyToggleGrab()
	{
		SDL_GrabMode mode;

		dout.writefln("Ctrl-G: toggling input grab!");

		mode = SDL_WM_GrabInput(SDL_GRAB_QUERY);

		if(mode == SDL_GRAB_ON)
		{
			dout.writefln("Grab was on");
		}
		else
		{
			dout.writefln("Grab was off");
		}

		mode = SDL_WM_GrabInput(!mode);

		if(mode == SDL_GRAB_ON)
		{
			dout.writefln("Grab is now on");
		}
		else
		{
			dout.writefln("Grab is now off");
		}
	}


	void hotKeyIconify()
	{
		dout.writefln("Ctrl-Z: iconifying window!");
		SDL_WM_IconifyWindow();
	}


	void checkError()
	{
		if(auto ggl_error = getGGLError())
		{
			dout.writefln("%s: %s", _title, ggl_error);
		}

		if(auto sdl_error = getSDLError())
		{
			dout.writefln("%s: SDL error '%s'", _title, sdl_error);
			SDL_ClearError();
		}
	}


	static char[] getGGLError()
	{
		auto sc = GGL_getError();

		if(sc[0] == 0)
		{
			return null;
		}
		else
		{
			return fromStringz(sc);
		}
	}


	static char[] getSDLError()
	{
		auto sc = SDL_GetError();

		if(sc[0] == 0)
		{
			return null;
		}
		else
		{
			return fromStringz(sc);
		}
	}


	void loop(Scene scene)
	{
		auto  frc = new FrameRateController();

		try
		{
			while(true)
			{
				scene.start();

				frc.wait();

				while(true)
				{
					auto delay = frc.wait();

					Joysticks().update();

					handleEvent();

					if(delay)
					{
						scene.update();
					}
					scene.update();
					if(auto nextScene = scene.nextScene)
					{
						scene.end();

						scene = nextScene;

						break;
					}
					else
					{
						scene.draw();

						/* Check for error conditions. */
						checkError();

						SDL_GL_SwapBuffers();
					}
				}
			}
		}
		catch(GameQuitException e)
		{
		}

		dout.writefln("FPS = %f", frc.fps());
	}
}



private class FrameRateController
{
public:
	uint wait()
	{
		auto tc = SDL_GetTicks();

		auto fc = tc * 3 / 50;

		auto fd = fc - _fp;

		if(fd == 0)
		{
			++fc;

			auto tn = (50*fc + 2) / 3;

			int  td = tn - tc;

			if(td > 0 && td <= 17)
			{
				SDL_Delay(td);
			}
			else
			{
				dout.writefln("fc, tn = %d, %d", fc, tn);
			}
		}
		else
		{
			--fd;
		}

		_fp = fc;

		_counter.update(tc);

		return fd;
	}


	real fps()
	{
		return _counter.fps();
	}


private:
	FPSCounter _counter;
	uint       _fp;
}



private struct FPSCounter
{
	uint[2] time;
	uint    count;


	void update(uint tc)
	{
		++count;
		if(count >= 128)
		{
			count = 0;

			time[0] = time[1];
			time[1] = tc;
		}
	}


	real fps()
	{
		return 128.0 * 1000.0/cast(real)(time[1] - time[0]);
	}
}



private class GameQuitException
{
public:
	override string toString()
	{
		return "GameQuitException";
	}
}
