/*
特に難しい実装はない、フレームワークも適当でいいだろう
注意すべき点は、Windows依存のコードが含まれていること
特定のOSに依存しないようにスーパークラスにまとめるべき
*/

import std.string, std.utf, std.stdio, std.file, std.path, std.math, std.c.time, std.conv,
	coneneko.all, sdl, opengl;
import std.windows.charset : toMBSz, fromMBSz;

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

const BONE_LENGTH_LIMIT = 34;
const BONE_LIMIT_OVER_MSG = "ボーン数制限(34)を越えました、正しく表示できません";

void main(string[] args_)
{
	try
	{
		auto args = new Args(args_);
		auto wnd = new MvWindow();
		auto scene = new MvScene(wnd);
		auto modelData = new MvModelData();
		wnd.setCaption(modelData.toString());
		CameraOperation cameraOperation = change!(CameraOperationA)('a', scene);
		size_t currentIndex;
		
		if (args.hasConv)
		{
			if (!args.hasMqoFileName) throw new Error("mqo file nameがない");
			if (!args.has3dFileName) throw new Error("3d file nameがない");
			modelData.loadMqo(args.getMqoFileName(), new SimpleIndicator(wnd));
			if (args.hasMkmFileName) modelData.loadMkm(args.getMkmFileName());
			modelData.save3d(args.get3dFileName());
			return;
		}
		else
		{
			if (args.has3dFileName) load(args.get3dFileName(), wnd, scene, modelData);
			else if (args.hasMqoFileName) load(args.getMqoFileName(), wnd, scene, modelData);
			if (args.hasMkmFileName) load(args.getMkmFileName(), wnd, scene, modelData);
		}
		
		while (!wnd.closed && !wnd.calledClose)
		{
			if (wnd.calledCancel) wnd.setCaption(modelData.toString());
			else if (wnd.calledHelp) scene.menuVisible = !scene.menuVisible;
			else if (wnd.calledSystemMenu) wnd.popSystemMenu();
			else if (wnd.calledNew)
			{
				modelData.initialize();
				scene.initializeModel();
				wnd.setCaption(modelData.toString());
				gcCollect();
			}
			else if (wnd.calledOpen) load(getFileNameFromOpenDialog(), wnd, scene, modelData);
			else if (wnd.calledSave)
			{
				auto fileName = modelData.getDefaultSaveFileName();
				if (getFileNameFromSaveDialog(fileName)) modelData.save3d(fileName);
			}
			else if (wnd.calledReload) reload(wnd, scene, modelData);
			else if (wnd.key.pressing('a')) cameraOperation = change!(CameraOperationA)('a', scene);
			else if (wnd.key.pressing('z')) cameraOperation = change!(CameraOperationZ)('z', scene);
			else if (wnd.key.pressing('x')) cameraOperation = change!(CameraOperationX)('x', scene);
			else if (wnd.key.pressed(SDLK_RIGHT)) cameraOperation.right();
			else if (wnd.key.pressed(SDLK_LEFT)) cameraOperation.left();
			else if (wnd.key.pressed(SDLK_UP)) cameraOperation.up();
			else if (wnd.key.pressed(SDLK_DOWN)) cameraOperation.down();
			else if (scene.model !is null)
			{
				auto model = scene.model;
				auto caption = modelData.toString();
				model++;
				
				size_t index;
				auto morph = currentIndex in model ? cast(MorphSubset)model[currentIndex] : null;
				if (wnd.key.pressing('t')
					&& wnd.waitInputIndex(caption, "t", index)
					&& index in model
				)
				{
					model[index].visible = true;
					currentIndex = index;
					scene.menu.setSubsetVisible(index, true);
				}
				else if (wnd.key.pressing('f')
					&& wnd.waitInputIndex(caption, "f", index)
					&& index in model
				)
				{
					model[index].visible = false;
					scene.menu.setSubsetVisible(index, false);
				}
				else if (morph !is null
					&& wnd.key.pressing('r')
					&& wnd.waitInputIndex(caption, format("r%d-", currentIndex), index)
					&& index in morph
				)
				{
					morph.motion.t = 0;
					auto b = morph.motion.valid ? morph.motion.next : index;
					morph.motion = [b, index];
					morph.motion.span = 60;
					morph.waitingMotion = [index, index];
					morph.waitingMotion.span = 60;
					scene.menu.selectMorphTarget(index);
				}
				else if (model.hasMotion
					&& wnd.key.pressing('e')
					&& wnd.waitInputIndex(caption, "e", index)
					&& index < model.motionLength
				)
				{
					model.waitingMotion = model.createMotion(index);
					scene.menu.selectMotion(index);
				}
			}
			wnd.update(scene);
		}
	}
	catch (Object e) messageBox(e.toString());
}

abstract class CameraOperation
{
	protected MqoCamera camera;
	this(MqoCamera camera) { this.camera = camera; }
	void right() {}
	void left() {}
	void up();
	void down();
}

class CameraOperationA : CameraOperation // head, pitch
{
	this(MqoCamera camera) { super(camera); }
	const DELTA = 0.04;
	void right() { camera.head -= DELTA; }
	void left() { camera.head += DELTA; }
	void up() { if (-PI_2 < camera.pitch - DELTA) camera.pitch -= DELTA; }
	void down() { if (camera.pitch + DELTA < PI_2) camera.pitch += DELTA; }
}

class CameraOperationZ : CameraOperation // zoom
{
	this(MqoCamera camera) { super(camera); }
	const DELTA = 5.0;
	void up() { if (0.0 < camera.zoom - DELTA) camera.zoom -= DELTA; }
	void down() { camera.zoom += DELTA; }
}

class CameraOperationX : CameraOperation // at逆移動(モデル移動の様に)
{
	this(MqoCamera camera) { super(camera); }
	private Vector leftToRight() { return cross(Vector(0, 1, 0), normalize(camera.at - camera.position)); }
	const DELTA = 1.0;
	void right() { camera.at += leftToRight * DELTA; }
	void left() { camera.at -= leftToRight * DELTA; }
	void up() { camera.at.y -= DELTA; }
	void down() { camera.at.y += DELTA; }
}

CameraOperation change(T : CameraOperation)(char key, MvScene scene)
{
	scene.menu.selectCameraOperation(key);
	return new T(scene.camera);
}

void reload(MvWindow wnd, MvScene scene, MvModelData modelData)
{
	if (!modelData.reload(new SimpleIndicator(wnd))) return;
	
	wnd.setCaption(
		modelData.toString(),
		BONE_LENGTH_LIMIT < modelData.boneLength ? BONE_LIMIT_OVER_MSG : "更新しました"
	);
	scene.model = new Model(modelData);
	gcCollect();
}

void load(string fileName, MvWindow wnd, MvScene scene, MvModelData modelData)
{
	if (fileName.fnmatch("*.3d") != 0) modelData.load3d(fileName);
	else if (fileName.fnmatch("*.mqo") != 0)
	{
		try
		{
			modelData.loadMqo(fileName, new SimpleIndicator(wnd));
		}
		catch (Cancel)
		{
			modelData.initialize();
			scene.initializeModel();
			wnd.setCaption(modelData.toString(), "キャンセルしました");
			return;
		}
	}
	else if (fileName.fnmatch("*.mkm") != 0) modelData.loadMkm(fileName);
	else return;
	
	wnd.setCaption(
		modelData.toString(),
		BONE_LENGTH_LIMIT < modelData.boneLength ? BONE_LIMIT_OVER_MSG : ""
	);
	scene.model = new Model(modelData);
	gcCollect();
}

class PlaneRenderState : RenderState
{
	this()
	{
		blend = true;
	}
}

class ModelRenderState : RenderState
{
	this()
	{
		depthTest = true;
		cullFace = true;
		cullFaceMode = GL_FRONT;
	}
}

class MvScene : SceneGraph
{
	private Object currentModelNode;
	Menu menu;
	private bool _menuVisible = false;
	bool menuVisible() { return _menuVisible; }
	MqoCamera camera;
	
	this(Window wnd)
	{
		camera = new MqoCamera(wnd);
		camera.projection = Matrix.perspectiveFov(PI / 8.0, 512.0 / 512.0, 1.0, 2000.0);
		root = linkParallel(
			new Object(),
				new Clear(),
				linkSerial(
					new ModelRenderState(),
					camera,
					currentModelNode = new Object()
				)
		);
		menu = new Menu();
	}
	
	void initializeModel()
	{
		auto model = new Object();
		swap(currentModelNode, model);
		currentModelNode = model;
	}
	
	void model(Model model)
	{
		swap(currentModelNode, model);
		currentModelNode = model;
		camera.initialize();
		menu.reset(model);
	}
	
	Model model()
	{
		return cast(Model)currentModelNode;
	}
	
	void menuVisible(bool a)
	{
		_menuVisible = a;
		a ? linkAnother(root, menu) : cut(root, menu.root);
	}
}

class Menu : SceneGraph 
{
	class Subsets : Unit
	{
		private const float y;
		private TextBoard[size_t] texts;
		this(float y) { this.y = y; }
		void detach() {}
		void initialize(size_t[] keys)
		{
			texts = null;
			float x = -0.8;
			foreach (v; keys) texts[v] = new TextBoard(std.string.toString(v), x += 0.1, y);
		}
		void setVisible(size_t index, bool a)
		{
			texts[index].color = a ? Color.RED : Color.WHITE;
		}
		void attach()
		{
			foreach (v; texts.values) { v.attach(); v.detach(); }
		}
	}
	
	class Targets : Unit
	{
		private const float y;
		this(float y) { this.y = y; }
		void detach() {}
		private TextBoard[size_t][size_t] texts; // [subset][target]
		size_t currentSubsetIndex;
		void initialize()
		{
			texts = null;
		}
		void add(size_t subsetIndex, size_t[] targetIndices)
		{
			float x = -0.8;
			foreach (v; targetIndices)
			{
				texts[subsetIndex][v] = new TextBoard(std.string.toString(v), x += 0.1, y);
			}
		}
		void selectTarget(size_t targetIndex)
		{
			foreach (i, v; texts[currentSubsetIndex])
			{
				v.color = i == targetIndex ? Color.RED : Color.WHITE;
			}
		}
		void attach()
		{
			if (!(currentSubsetIndex in texts)) return;
			foreach (v; texts[currentSubsetIndex])
			{
				v.attach();
				v.detach();
			}
		}
	}
	
	private TextBoard cameraOperationA, cameraOperationZ, cameraOperationX;
	private TextBoard motionIndex, motionMax;
	private TextBoard currentSubsetIndex;
	private Subsets subsets;
	private Targets targets;
	
	this()
	{
		auto rs = new PlaneRenderState();
		root = link(rs, new BillBoard(-1, -1, 2, 2, Vector(0, 0, 0, 0.2)));
		float y = 1.0;
		const X = -1.0, H = 0.1;
		link(rs, new TextBoard("Alt+F4 終了", X, y -= H));
		link(rs, new TextBoard("Esc キャンセル", X, y -= H));
		link(rs, new TextBoard("F1 情報表示切替", X, y -= H));
		link(rs, new TextBoard("Alt+Space システムメニュー", X, y -= H));
		link(rs, new TextBoard("Ctrl+n 新規", X, y -= H));
		link(rs, new TextBoard("Ctrl+o 開く", X, y -= H));
		link(rs, new TextBoard("Ctrl+s 名前を付けて保存", X, y -= H));
		link(rs, new TextBoard("F5 更新", X, y -= H));
		link(rs, new TextBoard("上下右左azx カメラ操作", X, y -= H));
		link(rs, cameraOperationA = new TextBoard("a", 0.0, y));
		link(rs, cameraOperationZ = new TextBoard("z", 0.1, y));
		link(rs, cameraOperationX = new TextBoard("x", 0.2, y));
		link(rs, new TextBoard("e-数値-Enter モーション選択", X, y -= H));
		link(rs, motionIndex = new TextBoard("", 0.0, y));
		link(rs, motionMax = new TextBoard("", 0.2, y));
		link(rs, new TextBoard("t-数値-Enter サブセット選択,表示", X, y -= H));
		link(rs, new TextBoard("f-数値-Enter サブセット非表示", X, y -= H));
		link(rs, currentSubsetIndex = new TextBoard("0", -0.9, y -= H));
		link(rs, subsets = new Subsets(y));
		link(rs, new TextBoard("r-数値-Enter モーフターゲット選択", X, y -= H));
		link(rs, targets = new Targets(y -= H));
	}
	
	void selectCameraOperation(char o)
	in
	{
		assert(o == 'a' || o == 'z' || o == 'x');
	}
	body
	{
		cameraOperationA.color = o == 'a' ? Color.RED : Color.WHITE;
		cameraOperationZ.color = o == 'z' ? Color.RED : Color.WHITE;
		cameraOperationX.color = o == 'x' ? Color.RED : Color.WHITE;
	}
	
	void reset(Model model)
	{
		motionMax.text = model.hasMotion ? format("%d]", model.motionLength - 1) : "";
		motionIndex.text = "";
		subsets.initialize(model.keys);
		targets.initialize();
		foreach (v; model.keys)
		{
			auto a = cast(MorphSubset)model[v];
			if (a !is null) targets.add(v, a.keys);
		}
	}
	
	void selectMotion(size_t index)
	{
		motionIndex.text = std.string.toString(index);
	}
	
	void setSubsetVisible(size_t index, bool a)
	{
		subsets.setVisible(index, a);
		if (!a) return;
		currentSubsetIndex.text = std.string.toString(index);
		targets.currentSubsetIndex = index;
	}
	
	void selectMorphTarget(size_t index)
	{
		targets.selectTarget(index);
	}
}

class MvModelData : ModelData
{
	private string _3dFileName, mqoFileName, mkmFileName;
	private long _3dUpdateTime, mqoUpdateTime, mkmUpdateTime;
	
	override void initialize()
	{
		super.initialize();
		_3dFileName = "";
		mqoFileName = "";
		mkmFileName = "";
	}
	
	private static long getUpdateTime(string fileName)
	{
		long a, result, c;
		getTimes(fileName, a, result, c);
		return result;
	}
	
	void load3d(string _3dFileName, string mkmFileName = "")
	in
	{
		assert(isabs(_3dFileName) != 0);
	}
	body
	{
		initialize();
		this._3dFileName = _3dFileName;
		readFile(_3dFileName, this);
		_3dUpdateTime = getUpdateTime(_3dFileName);
		
		if (mkmFileName != "") loadMkm(mkmFileName);
	}
	
	void loadMqo(string mqoFileName, Indicator indicator, string mkmFileName = "")
	in
	{
		assert(isabs(mqoFileName) != 0);
	}
	body
	{
		initialize();
		this.mqoFileName = mqoFileName;
		chdir(mqoFileName.getDirName());
		translator = new MkmMqoTranslator(indicator);
		readFile(mqoFileName, this);
		mqoUpdateTime = getUpdateTime(mqoFileName);
		
		if (mkmFileName != "") loadMkm(mkmFileName);
	}
	
	void loadMkm(string mkmFileName)
	in
	{
		assert(isabs(mkmFileName) != 0);
	}
	body
	{
		motions = null;
		this.mkmFileName = mkmFileName;
		translator = new MkmTranslator();
		readFile(mkmFileName, this);
		mkmUpdateTime = getUpdateTime(mkmFileName);
	}
	
	string toString()
	{
		return format(
			"model_viewer %s %s %s",
			_3dFileName.getBaseName(),
			mqoFileName.getBaseName(),
			mkmFileName.getBaseName()
		);
	}
	
	string getDefaultSaveFileName()
	{
		auto fileName = mkmFileName != "" ? mkmFileName
			: mqoFileName != "" ? mqoFileName
			: _3dFileName;
		return fileName.getBaseName().addExt("3d");
	}
	
	void save3d(string fileName)
	{
		translator = new ZlibTranslator();
		writeFile(fileName, this);
	}
	
	size_t boneLength()
	{
		return skeleton !is null ? skeleton.boneTriangles.length / 3 : 0;
	}
	
	private static bool updated(string fileName, ref long updateTime)
	{
		auto a = getUpdateTime(fileName);
		if (a == updateTime) return false;
		updateTime = a;
		return true;
	}
	
	bool reload(Indicator indicator) // reloadしたらtrue
	{
		if (_3dFileName != "" && updated(_3dFileName, _3dUpdateTime))
		{
			load3d(_3dFileName, mkmFileName);
			return true;
		}
		else if (mqoFileName != "" && updated(mqoFileName, mqoUpdateTime))
		{
			loadMqo(mqoFileName, indicator, mkmFileName);
			return true;
		}
		else if (mkmFileName != "" && updated(mkmFileName, mkmUpdateTime))
		{
			loadMkm(mkmFileName);
			return true;
		}
		return false;
	}
}

class Args
{
	private const string[] args;
	
	this(string[] args)
	{
		foreach (v; args)
		{
			auto a = fromMBSz(v.toStringz()); // d1
			//auto a = fromMBSz(cast(invariant)v.toStringz()); // d2
			if (a != "-conv" && isabs(a) == 0) a = std.path.join(getcwd(), a);
			this.args ~= a;
		}
	}
	
	bool hasConv() // -conv
	{
		foreach (v; args)
		{
			if (v == "-conv") return true;
		}
		return false;
	}
	
	private string getAnyFileName(string T)()
	{
		foreach (v; args)
		{
			if (v.fnmatch(T) != 0) return v;
		}
		return "";
	}
	
	alias getAnyFileName!("*.mqo") getMqoFileName;
	alias getAnyFileName!("*.mkm") getMkmFileName;
	alias getAnyFileName!("*.3d") get3dFileName;
	bool hasMqoFileName() { return getMqoFileName() != ""; }
	bool hasMkmFileName() { return getMkmFileName() != ""; }
	bool has3dFileName() { return get3dFileName() != ""; }
}

extern (C) void popSystemMenuCpp(SDL_syswm.HWND wnd);
extern (C) void setCaptionCpp(SDL_syswm.HWND handle, wchar* text);
class MvWindow : Window
{
	this() { super(512, 512); }
	
	private SDL_syswm.HWND handle()
	{
		SDL_SysWMinfo info;
		SDL_GetWMInfo(&info);
		return info.window;
	}
	
	void popSystemMenu()
	{
		key.clear();
		popSystemMenuCpp(handle);
	}
	
	void setCaption(string textA, string textB = "")
	{
		setCaptionCpp(handle, cast(wchar*)toUTF16(textA ~ "  " ~ textB ~ "\0\0").ptr); // d2
	}
	
	static assert(is(size_t == uint));
	bool waitInputIndex(string caption1, string caption2, out size_t index)
	{
		scope (success) setCaption(caption1);
		const KP = SDLK_KP0 - '0';
		setCaption(caption1 ~ caption2);
		string result;
		while (true)
		{
			doEvents();
			if (key.pressing(SDLK_ESCAPE)) return false;
			else if ((key.pressing(SDLK_RETURN) || key.pressing(SDLK_KP_ENTER)) && result != "")
			{
				index = result.toUint();
				return true;
			}
			foreach (v; digits)
			{
				if (!key.pressing(v) && !key.pressing(v + KP)) continue;
				result ~= v;
				setCaption(caption1 ~ caption2 ~ result);
			}
		}
	}
	
	private bool pressedCtrl() { return key.pressed(SDLK_LCTRL) || key.pressed(SDLK_RCTRL); }
	private bool pressedAlt() { return key.pressed(SDLK_LALT) || key.pressed(SDLK_RALT); }
	bool calledClose() { return pressedAlt && key.pressing(SDLK_F4); }
	bool calledOpen() { return pressedCtrl && key.pressing('o'); }
	bool calledSave() { return pressedCtrl && key.pressing('s'); }
	bool calledNew() { return pressedCtrl && key.pressing('n'); }
	bool calledHelp() { return key.pressing(SDLK_F1); }
	bool calledSystemMenu() { return pressedAlt && key.pressing(SDLK_SPACE); }
	bool calledCancel() { return key.pressing(SDLK_ESCAPE); }
	bool calledReload() { return key.pressing(SDLK_F5); }
}

class Cancel {}
class SimpleIndicator : SceneGraph, Indicator
{
	private Window wnd;
	private BillBoard rect;
	private TextBoard text;
	private clock_t preUpdateTime; // [ms]
	
	this(Window wnd)
	{
		this.wnd = wnd;
		rect = new BillBoard(-1.0, -0.1, 2.0, 0.1, Color.WHITE);
		text = new TextBoard("0%", -0.1, 0.0);
		text.color = Color.WATER;
		root = linkParallel(
			new PlaneRenderState(),
				new Clear(),
				rect,
				text
		);
	}
	
	private bool passed100Millisecond()
	{
		auto time = clock() / (CLOCKS_PER_SEC / 1000);
		if (time < preUpdateTime + 100) return false;
		preUpdateTime = time;
		return true;
	}
	
	void advance(float percent)
	{
		rect.x = percent / 50.0 - 1.0;
		rect.width = 2.0 - rect.x;
		text.text = format("%d%%", cast(int)percent);
		if (passed100Millisecond) wnd.update(this);
		else wnd.doEvents();
		if (wnd.closed || wnd.key.pressed(SDLK_ESCAPE)) throw new Cancel();
	}
}

extern (C) void getFileNameFromOpenDialogCpp(wchar*, size_t);
string getFileNameFromOpenDialog()
{
	auto buffer = new wchar[256];
	buffer[] = 0;
	getFileNameFromOpenDialogCpp(buffer.ptr, buffer.length);
	return buffer.toUTF8().removechars("\0");
}

extern (C) bool getFileNameFromSaveDialogCpp(wchar*, size_t);
bool getFileNameFromSaveDialog(ref string fileName)
in
{
	assert(fileName.length < 256);
}
body
{
	auto buffer = (fileName ~ "\0".repeat(256 - fileName.length)).toUTF16();
	auto result = getFileNameFromSaveDialogCpp(cast(wchar*)buffer.ptr, buffer.length); // d2
	fileName = buffer.toUTF8().removechars("\0");
	return result;
}

extern (C) void messageBoxCpp(wchar* msg);
void messageBox(string msg) { messageBoxCpp(cast(wchar*)toUTF16(msg ~ "\0\0").ptr); } // d2
