module coneneko.window;
import
	sdl,
	coneneko.rendertarget,
	coneneko.scenegraph,
	coneneko.math,
	std.signals,
	coneneko.unit,
	coneneko.rgba,
	opengl,
	coneneko.camera,
	std.math,
	coneneko.glext;

version (D_Version2) // d2
{
	import core.memory;
	private void gcCollect() { core.memory.GC.collect(); }
}
else
{
	private void gcCollect() { std.gc.fullCollect(); }
}

///
class InputState
{
	enum State { RELEASED, PRESSING, PRESSED, RELEASING } ///
	private State map[int];
	void press(int a) { map[a] = State.PRESSING; } ///
	void release(int a) { map[a] = State.RELEASING; } ///
	void opPostInc() /// ++
	{
		int[] releaseKeyList;
		foreach (inout v; map)
		{
			if (v == State.RELEASING) releaseKeyList ~= v;
			else if (v == State.PRESSING) v = State.PRESSED;
		}
		foreach (v; releaseKeyList) map.remove(v);
	}
	bool released(int a) { return !(a in map); } ///
	bool pressing(int a) { return a in map && map[a] == State.PRESSING; } ///
	bool pressed(int a) { return a in map && map[a] == State.PRESSED; } ///
	bool releasing(int a) { return a in map && map[a] == State.RELEASING; } ///
	abstract void handler(SDL_Event e); ///
	void clear() { map = null; } ///
	State opIndex(int a) { return a in map ? map[a] : State.RELEASED; } ///
	string toString(int a) { return ["RELEASED", "PRESSING", "PRESSED", "RELEASING"][this[a]]; } ///
}

/// SDLK_ESCAPE等
class KeyState : InputState
{
	unittest
	{
		auto a = new KeyState();
		a.press(SDLK_ESCAPE);
		assert(a.pressing(SDLK_ESCAPE));
		a++;
		assert(!a.pressing(SDLK_ESCAPE));
		assert(a.pressed(SDLK_ESCAPE));
	}
	
	void handler(SDL_Event e)
	{
		with (e.key)
		{
			if (type == SDL_KEYDOWN) press(keysym.sym);
			else if (type == SDL_KEYUP) release(keysym.sym);
		}
	}
	
	override void clear()
	{
		super.clear();
		
		int length;
		auto p = SDL_GetKeyState(&length);
		for (int i = 0; i < length; i++) p[i] = SDL_RELEASED;
	}
}

/// SDL_BUTTON_LEFT, SDL_BUTTON_RIGHT等
class MouseState : InputState
{
	void handler(SDL_Event e)
	{
		with (e.button)
		{
			if (type == SDL_MOUSEBUTTONDOWN) press(button);
			else if (type == SDL_MOUSEBUTTONUP) release(button);
		}
	}
	
	Vector position() /// クライアント範囲は[-1, 1)
	{
		int rx, ry;
		SDL_GetMouseState(&rx, &ry);
		return vector(
			cast(float)rx / cast(float)(SDL_GetVideoSurface().w) * 2.0 - 1.0,
			-(cast(float)ry / cast(float)(SDL_GetVideoSurface().h) * 2.0 - 1.0)
		);
	}
}

///
enum Joy
{
	LEFT = 100, RIGHT, UP, DOWN
}

template AxisState(short MIN_NAME, short MAX_NAME)
{
	private int lp;
	void move(short a)
	{
		static assert(is(int == typeof(lp)));
		if (lp == 1) goto _1;
		if (lp == 2) goto _2;
		if (lp == 3) goto _3;
		while (true)
		{
			while (!nearMin(a) && !nearMax(a)) {lp=1;return;_1:;}
			if (nearMin(a))
			{
				press(MIN_NAME);
				while (nearMin(a)) {lp=2;return;_2:;}
				release(MIN_NAME);
			}
			else if (nearMax(a))
			{
				press(MAX_NAME);
				while (nearMax(a)) {lp=3;return;_3:;}
				release(MAX_NAME);
			}
		}
	}
}

///
class JoyState : InputState
{
	private static SDL_Joystick* joystick;
	
	static void init() ///
	{
		if (SDL_WasInit(SDL_INIT_JOYSTICK)) return;
		SDL_InitSubSystem(SDL_INIT_JOYSTICK);
		if (1 <= SDL_NumJoysticks()) joystick = SDL_JoystickOpen(0);
	}
	
	static void close() ///
	{
		if (!SDL_WasInit(SDL_INIT_JOYSTICK)) return;
		if (joystick) SDL_JoystickClose(joystick);
		SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
	}
	
	private class AxisState0 { mixin AxisState!(Joy.LEFT, Joy.RIGHT); }
	private class AxisState1 { mixin AxisState!(Joy.UP, Joy.DOWN); }
	private AxisState0 axis0;
	private AxisState1 axis1;
	
	private bool nearMax(short a) { return short.max / 2 < a; }
	private bool nearMin(short a) { return a < short.min / 2; }
	
	this()
	{
		axis0 = new AxisState0();
		axis1 = new AxisState1();
	}
	
	void handler(SDL_Event e)
	{
		if (e.type == SDL_JOYAXISMOTION)
		{
			with (e.jaxis)
			{
				if (axis == 0) axis0.move(value);
				else if (axis == 1) axis1.move(value);
			}
		}
		else if (e.type == SDL_JOYBUTTONDOWN) press(e.jbutton.button);
		else if (e.type == SDL_JOYBUTTONUP) release(e.jbutton.button);
	}
}

///
class BackBuffer : RenderTarget
{
	private const uint width, height;
	private ViewPort viewPort;
	
	///
	this(uint width, uint height)
	{
		this.width = width;
		this.height = height;
		viewPort = new ViewPort(width, height);
	}
	
	void draw(SceneGraph a) ///
	in
	{
		assert(a.root !is null, "scene graph has not root");
		assert(a.isTree(a.root), "scene graph is not tree");
		foreach (SceneGraph v; a) assert(false, "scene graph has scene graph");
	}
	body
	{
		viewPort.attach();
		a.callTree(
			a.root,
			delegate void(Object obj, bool into)
			{
				auto unit = cast(Unit)obj;
				if (unit !is null) into ? unit.attach() : unit.detach();
			}
		);
		viewPort.detach();
	}
	
	Rgba rgba() ///
	{
		uint[] result = new uint[width * height];
		glReadBuffer(GL_BACK);
		glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, result.ptr);
		
		const uint ALPHA_FILTER = 0xff000000;
		foreach (inout uint v; result) v |= ALPHA_FILTER;
		
		return flipVertical(new Rgba(width, height, result));
	}
}

///
class Window : RenderTarget
{
	invariant()
	{
		assert(key !is null);
		assert(mouse !is null);
		assert(joy !is null);
	}
	
	BackBuffer backBuffer; ///
	InputState key, joy; ///
	MouseState mouse;
	private bool _closed = false;
	bool closed() { return _closed; } ///
	uint width() { return SDL_GetVideoSurface().w; } ///
	uint height() { return SDL_GetVideoSurface().h; } ///
	Vector size() { return vector(width, height); } ///
	private string getSdlError() { return std.string.toString(SDL_GetError()); }
	
	///
	this(uint width = 640, uint height = 480, bool fullScreen = false)
	{
		if(SDL_Init(SDL_INIT_VIDEO) == -1) throw new Error("SDL_Init" ~ getSdlError());
		SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
		SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
		SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
		SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
		SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
		if (!SDL_SetVideoMode(width, height, 24, SDL_OPENGL | (fullScreen ? SDL_FULLSCREEN : 0)))
		{
			throw new Error("SDL_SetVideoMode" ~ getSdlError());
		}
		
		loadGlextFunctions();
		
		key = new KeyState();
		mouse = new MouseState();
		JoyState.init();
		joy = new JoyState();
		connect(&key.handler);
		connect(&mouse.handler);
		connect(&joy.handler);
		connect(&quitHandler);
		
		backBuffer = new BackBuffer(width, height);
	}
	
	~this()
	{
		gcCollect();
		JoyState.close();
		SDL_Quit();
	}
	
	private void quitHandler(SDL_Event e)
	{
		if (e.type == SDL_QUIT) _closed = true;
	}
	
	void draw(SceneGraph sg) ///
	{
		if (_closed) throw new Error("window closed");
		backBuffer.draw(sg);
	}
	
	void flip() ///
	{
		SDL_GL_SwapBuffers();
	}
	
	void doEvents() ///
	{
		key++;
		mouse++;
		joy++;
		
		SDL_Event e;
		while (SDL_PollEvent(&e)) emit(e);
	}
	
	mixin Signal!(SDL_Event); ///
	
	uint fps; /// 一秒に一度更新
	
	private uint preUpdateTime, preFpsUpdateTime, drawCounter;
	void update(SceneGraph scene) /// rootを描画して++
	{
		const SPAN = 1000 / 60;
		if (preUpdateTime == 0) preFpsUpdateTime = preUpdateTime = SDL_GetTicks() - SPAN;
		
		auto updateTime = preUpdateTime + SPAN;
		auto currentTime = SDL_GetTicks();
		if (currentTime < updateTime) SDL_Delay(updateTime - currentTime); // 時間が余ってるならsleep
		
		draw(scene);
		flip();
		
		currentTime = SDL_GetTicks();
		auto nextUpdateTime = updateTime + SPAN;
		preUpdateTime = currentTime < nextUpdateTime ? updateTime : currentTime; // 追いつかないなら飛ばす
		drawCounter++;
		
		if (preFpsUpdateTime + 1000 <= SDL_GetTicks()) // fps更新
		{
			fps = drawCounter;
			preFpsUpdateTime += 1000;
			drawCounter = 0;
		}
		
		SDL_Delay(1);
		doEvents();
	}
}

version (Windows)
{
	import std.c.windows.windows, std.string;
	alias SDL_syswm.HWND HWND;
	
	class EditBox
	{
		const HWND handle;
		
		this(HWND owner)
		{
			handle = CreateWindowA(
				toStringz("EDIT") , toStringz(""),
				WS_CHILD | WS_VISIBLE | WS_BORDER | ES_LEFT,
				0, 200, 200, 30,
				cast(HANDLE)owner,
				null, null, null
			);
		}
		
		void setFocus()
		{
			SetFocus(cast(HANDLE)handle);
		}
		
		void doEvents()
		{
			MSG msg;
			while (PeekMessageA(&msg, cast(HANDLE)handle, 0, 0, PM_REMOVE))
			{
				TranslateMessage(&msg);
				DispatchMessageA(&msg);
			}
		}
	}
	
	class WindowHasEditBox : Window
	{
		HWND handle()
		{
			SDL_SysWMinfo info;
			SDL_GetWMInfo(&info);
			return info.window;
		}
		
		this()
		{
			editBox = new EditBox(handle);
		}
		
		EditBox editBox;
		
		override void doEvents()
		{
			editBox.doEvents();
			super.doEvents();
		}
		
		void setFocus()
		{
			SetFocus(cast(HANDLE)handle);
		}
		
		bool hasFocus()
		{
			return handle == cast(HWND)GetFocus();
		}
	}
}

/// 必要なら、キーボード操作も追加する、a回転, zズーム, x移動, escキャンセル, 上下右左
class MqoCamera : Camera
{
	private Window _wnd;
	private const Vector delegate() _getMousePosition;
	private const Vector wndSize;
	private Vector preMousePosition;
	private Vector mouseDiff() { return getMousePosition() - preMousePosition; }
	enum MouseState { DEFAULT, LEFT, RIGHT, BOTH }
	private MouseState mouseState;
	
	///
	this(Window wnd, Vector position = vector(0.0, 0.0, 400.0), Vector at = vector())
	{
		super(position, at);
		this._wnd = wnd;
		this._getMousePosition = &wnd.mouse.position;
		this.wndSize = wnd.size;
	}
	
	///
	this(Window wnd, float x, float y, float z, float atX, float atY, float atZ)
	{
		this(wnd, vector(x, y, z), vector(atX, atY, atZ));
	}
	
	private Vector getMousePosition()
	{
		Vector a = _getMousePosition();
		return Vector(
			(a.x * 0.5 + 0.5) * _wnd.width,
			(-a.y * 0.5 + 0.5) * _wnd.height
		);
	}
	
	void initialize()
	{
		at = Vector();
		position = Vector(0.0, 0.0, 400.0);
	}
	
	override void attach()
	{
		if (_wnd.mouse.pressing(SDL_BUTTON_LEFT)) leftDown();
		if (_wnd.mouse.releasing(SDL_BUTTON_LEFT)) leftUp();
		if (_wnd.mouse.pressing(SDL_BUTTON_RIGHT)) rightDown();
		if (_wnd.mouse.releasing(SDL_BUTTON_RIGHT)) rightUp();
		
		if (mouseState == MouseState.LEFT) operateAt();
		else if (mouseState == MouseState.RIGHT) operateHeadPitch();
		else if (mouseState == MouseState.BOTH) operateBankZoom();
		preMousePosition = getMousePosition();
		
		super.attach();
	}
	
	void leftDown() ///
	{
		if (mouseState == MouseState.DEFAULT) mouseState = MouseState.LEFT;
		else if (mouseState == MouseState.RIGHT) mouseState = MouseState.BOTH;
	}
	
	void leftUp() ///
	{
		if (mouseState == MouseState.LEFT) mouseState = MouseState.DEFAULT;
		else if (mouseState == MouseState.BOTH) mouseState = MouseState.RIGHT;
	}
	
	void rightDown() ///
	{
		if (mouseState == MouseState.DEFAULT) mouseState = MouseState.RIGHT;
		else if (mouseState == MouseState.LEFT) mouseState = MouseState.BOTH;
	}
	
	void rightUp() ///
	{
		if (mouseState == MouseState.RIGHT) mouseState = MouseState.DEFAULT;
		else if (mouseState == MouseState.BOTH) mouseState = MouseState.LEFT;
	}
	
	private void operateAt()
	{
		Matrix m = Matrix.translation(0, 0, 1.0)
			* Matrix.rotationX(-pitch)
			* Matrix.rotationY(head);
		Vector cameraPosition = mulW(vector(0, 0, 0), m);
		
		Vector dirAt = vector(0, 0, 0) - cameraPosition; // camera -> at
		Vector dirUp = vector(0, 1, 0) - cameraPosition; // camera -> up
		assert(0.0 != scalar(dirAt));
		assert(0.0 != scalar(dirUp));
		dirAt = normalize(dirAt);
		dirUp = normalize(dirUp);
		
		Vector dirX = cross(dirAt, dirUp);
		Vector dirY = -cross(dirAt, dirX);
		assert(0.0 != scalar(dirX));
		assert(0.0 != scalar(dirY));
		
		auto dx = mouseDiff.x / wndSize.x * PI * 200.0;
		auto dy = mouseDiff.y / wndSize.y * PI * 200.0;
		at = at + normalize(dirY) * dy - normalize(dirX) * dx;
	}
	
	private void operateHeadPitch()
	{
		head = head - mouseDiff.x / wndSize.x * PI * 2;
		pitch = pitch + mouseDiff.y / wndSize.y * PI * 2;
		pitch = clamp(pitch, -PI_2 + 0.00001, PI_2 - 0.00001);
	}
	
	private void operateBankZoom()
	{
		bank = bank - mouseDiff.x / wndSize.x * PI_2;
		zoom = zoom + mouseDiff.y / wndSize.y * 2000.0;
		zoom = clamp(zoom, 1.0, 2000.0);
	}
}
