/++
	A small extension module to [arsd.minigui] that adds
	functions for creating widgets and windows from short
	XML descriptions.

	If you choose to use this, it will require [arsd.dom]
	to be compiled into your project too.

	---
	import arsd.minigui_xml;
	Window window = createWindowFromXml(`
		<MainWindow>
			<Button label="Hi!" />
		</MainWindow>
	`);
	---


	To add custom widgets to the minigui_xml factory, you need
	to register them with FIXME.

	You can attach some events right in the XML using attributes.
	The attribute names are `onEVENTNAME` or `ondirectEVENTNAME`
	and the values are one of the following three value types:

	$(LIST
		* If it starts with `&`, it is a delegate you need
		  to register using the FIXME function.

		* If it starts with `(`, it is a string passed to
		  the [arsd.dom.querySelector] function to get an
		  element reference

		* Otherwise, it tries to call a script function (if
		  scripting is available).
	)

	Keep in mind
	For example, to make a page widget that changes based on a
	drop down selection, you may:

	```xml
		<DropDownSelection onchange="$(+PageWidget).setCurrentTab">
			<option>Foo</option>
			<option>Bar</option>
		</DropDownSelection>
		<PageWidget name="mypage">
			<!-- contents elided -->
		</PageWidget>
	```

	That will create a select widget that when it changes, it will
	look for the next PageWidget sibling (that's the meaning of `+PageWidget`,
	see css selector syntax for more) and call its `setCurrentTab`
	method.

	Since the function knows `setCurrentTab` takes an integer, it will
	automatically pull the `intValue` member out of the event and pass
	it to the method.

	The given XML is the same as the following D:

	---
		auto select = new DropDownSelection(parent);
		select.addOption("Foo");
		select.addOption("Bar");
		auto page = new PageWidget(parent);
		page.name = "mypage";

		select.addEventListener("change", (Event event)
		{
			page.setCurrentTab(event.intValue);
		});
	---
+/
module arsd.minigui_xml;

public import arsd.minigui;
public import arsd.minigui : Event;

import arsd.textlayouter;

import arsd.dom;

import std.conv;
import std.exception;
import std.functional : toDelegate;
import std.string : strip;
import std.traits;

private template ident(T...)
{
	static if(is(T[0]))
		alias ident = T[0];
	else
		alias ident = void;
}

enum ParseContinue { recurse, next, abort }

alias WidgetFactory = ParseContinue delegate(Widget parent, Element element, out Widget result);
alias WidgetTextHandler = void delegate(Widget widget, string text);

WidgetFactory[string] widgetFactoryFunctions;
WidgetTextHandler[string] widgetTextHandlers;

void delegate(string eventName, Widget, Event, string content) xmlScriptEventHandler;
static this()
{
	xmlScriptEventHandler = toDelegate(&nullScriptEventHandler);
}

void nullScriptEventHandler(string eventName, Widget w, Event e, string)
{
	import std.stdio : stderr;

	stderr.writeln("Ignoring event ", eventName, " ", e, " on widget ", w.elementName, " because xmlScriptEventHandler is not set");
}

private bool startsWith(T)(T[] doesThis, T[] startWithThis)
{
	return doesThis.length >= startWithThis.length && doesThis[0 .. startWithThis.length] == startWithThis;
}

private bool isLower(char c)
{
	return c >= 'a' && c <= 'z';
}

private bool isUpper(char c)
{
	return c >= 'A' && c <= 'Z';
}

private char assumeLowerToUpper(char c)
{
	return cast(char)(c - 'a' + 'A');
}

private char assumeUpperToLower(char c)
{
	return cast(char)(c - 'A' + 'a');
}

string hyphenate(string argname)
{
	int hyphen;
	foreach (i, char c; argname)
		if (c.isUpper && (i == 0 || !argname[i - 1].isUpper))
			hyphen++;

	if (hyphen == 0)
		return argname;
	char[] ret = new char[argname.length + hyphen];
	int i;
	bool prevUpper;
	foreach (char c; argname)
	{
		bool upper = c.isUpper;
		if (upper)
		{
			if (!prevUpper)
				ret[i++] = '-';
			ret[i++] = c.assumeUpperToLower;
		}
		else
		{
			ret[i++] = c;
		}
		prevUpper = upper;
	}
	assert(i == ret.length);
	return cast(string) ret;
}

string unhyphen(string argname)
{
	int hyphen;
	foreach (i, char c; argname)
		if (c == '-' && (i == 0 || argname[i - 1] != '-'))
			hyphen++;

	if (hyphen == 0)
		return argname;
	char[] ret = new char[argname.length - hyphen];
	int i;
	char prev;
	foreach (char c; argname)
	{
		if (c != '-')
		{
			if (prev == '-' && c.isLower)
				ret[i++] = c.assumeLowerToUpper;
			else
				ret[i++] = c;
		}
		prev = c;
	}
	assert(i == ret.length);
	return cast(string) ret;
}

void initMinigui(Modules...)()
{
	import std.traits;
	import std.conv;

	static foreach (alias Module; Modules)
	{
		pragma(msg, Module.stringof);
		appendMiniguiModule!Module;
	}
}

void appendMiniguiModule(alias Module, string prefix = null)()
{
	foreach(memberName; __traits(allMembers, Module)) static if(!__traits(isDeprecated, __traits(getMember, Module, memberName)))
	{
		alias Member = ident!(__traits(getMember, Module, memberName));
		static if(is(Member == class) && !isAbstractClass!Member && is(Member : Widget) && __traits(getProtection, Member) != "private")
		{
			widgetFactoryFunctions[prefix ~ memberName] = (Widget parent, Element element, out Widget widget)
			{
				static if(is(Member : Dialog))
				{
					widget = new Member();
				}
				else static if(is(Member : Menu))
				{
					widget = new Menu(null, null);
				}
				else static if(is(Member : Window))
				{
					widget = new Member("test");
				}
				else
				{
					string[string] args = element.attributes;

					enum paramNames = ParameterIdentifierTuple!(__traits(getMember, Member, "__ctor"));
					Parameters!(__traits(getMember, Member, "__ctor")) params;
					static assert(paramNames.length, Member);
					bool[cast(int)paramNames.length - 1] requiredParams;

					static foreach (idx, param; params[0 .. $-1])
					{{
						enum hyphenated = paramNames[idx].hyphenate;
						if (auto arg = hyphenated in args)
						{
							enforce(!requiredParams[idx], "May pass required parameter " ~ hyphenated ~ " only exactly once");
							requiredParams[idx] = true;
							static if(is(typeof(param) == MemoryImage))
							{

							}
							else static if(is(typeof(param) == Color))
							{
								params[idx] = Color.fromString(*arg);
							}
							else static if(is(typeof(param) == TextLayouter))
								params[idx] = null;
							else
								params[idx] = to!(typeof(param))(*arg);
						}
						else
						{
							enforce(false, "Missing required parameter " ~ hyphenated ~ " for Widget " ~ memberName);
							assert(false);
						}
					}}

					params[$-1] = cast(typeof(params[$-1])) parent;

					auto member = new Member(params);
					widget = member;

					foreach (argName, argValue; args)
					{
						if (argName.startsWith("on-"))
						{
							auto eventName = argName[3 .. $].unhyphen;
							widget.addEventListener(eventName, (event) { xmlScriptEventHandler(eventName, member, event, argValue); });
						}
						else
						{
							argName = argName.unhyphen;
							switch (argName)
							{
								static foreach (idx, param; params[0 .. $-1])
								{
									case paramNames[idx]:
								}
									break;
								static if (is(typeof(Member.addParameter)))
								{
								default:
									member.addParameter(argName, argValue);
									break;
								}
								else
								{
									// TODO: add generic parameter setting here (iterate by UDA maybe)
								default:
									enforce(false, "Unknown parameter " ~ argName ~ " for Widget " ~ memberName);
									assert(false);
								}
							}
						}
					}
				}
				return ParseContinue.recurse;
			};

			enum hasText = is(typeof(Member.text) == string) || is(typeof(Member.text()) == string);
			enum hasContent = is(typeof(Member.content) == string) || is(typeof(Member.content()) == string);
			enum hasLabel = is(typeof(Member.label) == string) || is(typeof(Member.label()) == string);
			static if (hasText || hasContent || hasLabel)
			{
				enum member = hasText ? "text" : hasContent ? "content" : hasLabel ? "label" : null;
				widgetTextHandlers[memberName] = (Widget widget, string text)
				{
					auto w = cast(Member)widget;
					assert(w, "Called widget text handler with widget of type "
						~ typeid(widget).name ~ " but it was registered for "
						~ memberName ~ " which is incompatible");
					mixin("w.", member, " = w.", member, " ~ text;");
				};
			}

			// TODO: might want to check for child methods/structs that register as child nodes
		}
	}
}

///
Widget makeWidgetFromString(string xml, Widget parent)
{
	auto document = new Document(xml, true, true);
	auto r = document.root;
	return miniguiWidgetFromXml(r, parent);
}

///
Window createWindowFromXml(string xml)
{
	return createWindowFromXml(new Document(xml, true, true));
}
///
Window createWindowFromXml(Document document)
{
	auto r = document.root;
	return cast(Window) miniguiWidgetFromXml(r, null);
}
///
Widget miniguiWidgetFromXml(Element element, Widget parent)
{
	Widget w;
	miniguiWidgetFromXml(element, parent, w);
	return w;
}
///
ParseContinue miniguiWidgetFromXml(Element element, Widget parent, out Widget w)
{
	assert(widgetFactoryFunctions !is null, "No widget factories have been registered, register them using initMinigui!(arsd.minigui); at startup");

	if (auto factory = element.tagName in widgetFactoryFunctions)
	{
		auto c = (*factory)(parent, element, w);

		if (c == ParseContinue.recurse)
		{
			c = ParseContinue.next;
			Widget dummy;
			foreach (child; element.children)
				if (miniguiWidgetFromXml(child, w, dummy) == ParseContinue.abort)
				{
					c = ParseContinue.abort;
					break;
				}
		}
		return c;
	}
	else if (element.tagName == "#text")
	{
		string text = element.nodeValue.strip;
		if (text.length)
		{
			assert(parent, "got xml text without parent, make sure you only pass elements!");
			if (auto factory = parent.elementName in widgetTextHandlers)
				(*factory)(parent, text);
			else
			{
				import std.stdio : stderr;

				stderr.writeln("WARN: no text handler for widget ", parent.elementName, " ~= ", [text]);
			}
		}
		return ParseContinue.next;
	}
	else
	{
		enforce(false, "Unknown tag " ~ element.tagName);
		assert(false);
	}
}

string elementName(Widget w)
{
	if (w is null)
		return null;
	auto name = typeid(w).name;
	foreach_reverse (i, char c; name)
		if (c == '.')
			return name[i + 1 .. $];
	return name;
}

