class UIKit extends Framework {
	static registerViewControllerType(viewControllerClass) {
		if (!UIKit.viewControllerTypes) {
			UIKit.viewControllerTypes = { };
		}

		UIKit.viewControllerTypes[viewControllerClass.name] = viewControllerClass;
	}

	static registerViewType(viewClass, tagName = null) {
		if (!UIKit.viewTypes) {
			UIKit.viewTypes = { };
		}

		UIKit.viewTypes[viewClass.name] = viewClass;

		if (tagName) {
			if (!UIKit.viewTagNames) {
				UIKit.viewTagNames = { };
			}

			UIKit.viewTagNames[tagName.toLowerCase()] = viewClass;
		}
	}

	static registerPopoverType(popoverClass, tagName = null) {
		if (!UIKit.popoverTypes) {
			UIKit.popoverTypes = {
				Popover
			};
		}

		UIKit.popoverTypes[popoverClass.name] = popoverClass;

		if (tagName) {
			if (!UIKit.popoverTagNames) {
				UIKit.popoverTagNames = {
					popover: Popover
				};
			}

			UIKit.popoverTagNames[tagName.toLowerCase()] = popoverClass;
		}
	}

	static getViewControllerType(typeName) {
		if (!UIKit.viewControllerTypes) {
			return null;
		}
		return UIKit.viewControllerTypes[typeName];
	}

	static getPopoverType(typeName) {
		if (!UIKit.popoverTypes) {
			return null;
		}
		return UIKit.popoverTypes[typeName];
	}

	static main() {
		UIKit.scenes = { };
		UIKit.viewControllers = { };
		UIKit.navigationListeners = [ ];
	}

	static onApplicationDidFinishLaunching() {
		var viewControllers = document.body.querySelectorAll("view-controller");

		var initialHash = null;
		var initialPath = null;

		for (var i = 0; i < viewControllers.length; i++) {
			var viewController = viewControllers[i];

			var controllerClassName = viewController.getAttribute("controller-class");
			if (!controllerClassName) {
				controllerClassName = "ViewController";
			}
			var ControllerClass = UIKit.getViewControllerType(controllerClassName);
			if (!ControllerClass) {
				console.error("Could not find controller class \"" + controllerClassName + "\". Are you sure it's loaded?");
				continue;
			}

			var controllerInstance = new ControllerClass(viewController);

			var isInitialViewController = (viewController.getAttribute("initial-view-controller") === "true");

			//Check the hash.
			var viewControllerHash = viewController.getAttribute("hash");
			if (viewControllerHash) {
				controllerInstance.hash = viewControllerHash;

				if (isInitialViewController) {
					initialHash = viewControllerHash;
				}
			}

			//Check the request path.
			var viewControllerRequestPath = viewController.getAttribute("request-path");
			if (viewControllerRequestPath) {
				controllerInstance.requestPath = viewControllerRequestPath;

				if (isInitialViewController) {
					initialPath = viewControllerRequestPath;
				}
			}
			else {
				controllerInstance.requestPath = "/";
			}

			var sceneId = viewController.getAttribute("scene-id");
			if (sceneId !== null) {
				var scene = UIKit.getScene(sceneId);
				scene.addViewController(controllerInstance);

				if (isInitialViewController) {
					scene.initialViewController = controllerInstance;
				}
			}

			//Read the modal flag.
			var modal = viewController.getAttribute("modal");
			controllerInstance.showsModally = modal === "true";

			UIKit.viewControllers[viewController.getAttribute("id")] = controllerInstance;

			viewController.style.display = "";
			UIKit.processViewControllerElement(viewController, controllerInstance);

			document.body.removeChild(viewController);
		}

		//Call the viewDidLoad() functions on all loaded view controllers.
		for (var id in UIKit.viewControllers) {
			UIKit.viewControllers[id].viewDidLoad();
		}

		//Load all the popovers.
		UIKit.popovers = { };
		var popoverTypesString = Object.keys(UIKit.popoverTagNames).join(", ");
		var popovers = document.body.querySelectorAll(popoverTypesString);
		for (var i = 0; i < popovers.length; i++) {
			var popover = popovers[i];

			if (!popover.hasAttribute("id")) {
				console.error("Skipping a <" + popover.tagName.toLowerCase() + "> tag with no ID attribute.");
			}

			var controllerClassName = popover.getAttribute("controller-class");
			if (!controllerClassName) {
				controllerClassName = UIKit.popoverTagNames[popover.tagName.toLowerCase()].name;
			}
			var ControllerClass = UIKit.getPopoverType(controllerClassName);
			if (!ControllerClass) {
				console.error("Could not find popover class \"" + controllerClassName + "\". Are you sure it's loaded?");
				continue;
			}

			var popoverController = new ControllerClass(popover);

			UIKit.popovers[popover.getAttribute("id")] = popoverController;

			document.body.removeChild(popover);

			UIKit.processViewControllerElement(popover, popoverController);

			popoverController.viewDidLoad();
		}

		UIKit.onPopState();

		if (!window.location.hash && initialHash) {
			window.location.hash = initialHash;
		}
		else if (window.location.hash) {
			UIKit.onHashChanged();
		}

		//Add the mouse movement listener to track the cursor position.
		document.body.addEventListener("mousemove", UIKit.onBodyMouseMoved);
	}

	static addNavigationListener(callback) {
		UIKit.navigationListeners.push(callback);
	}

	/**
	 * Outlets are attached to the view instance of the parent. Actions are attached to the current element's view instance.
	 * The view controller will get an instance reference to the view.
	 */
	static processViewControllerElement(element, viewController, viewInstance = null) {
		var outlet = element.getAttribute("outlet");
		if (outlet) {
			viewController[outlet] = element;
			if (viewInstance) {
				viewInstance[outlet] = element;
			}
		}

		//First look for a specified view class.
		var viewClassName = element.getAttribute("view-class");
		var ViewClass = (viewClassName in UIKit.viewTypes) ? UIKit.viewTypes[viewClassName] : null;

		//If none is specified, look if there is a registered class for this tag name.
		if (!ViewClass) {
			ViewClass = (UIKit.viewTagNames && (element.tagName.toLowerCase() in UIKit.viewTagNames)) ? UIKit.viewTagNames[element.tagName.toLowerCase()] : null;
		}

		//Instantiate the view class of the element if one was found.
		var parentViewInstance = viewInstance;
		if (ViewClass) {
			viewInstance = new ViewClass(element);
			viewInstance.viewController = viewController;

			//If there are no children to this view, it shall be built by code.
			if (element.children.length == 0) {
				viewInstance.element = viewInstance.build(element);
			}
		}
		else if (viewClassName) {
			console.error("Can't find view class " + viewClassName + ". Is it loaded?");
		}

		var action = element.getAttribute("action");
		if (action) {
			//Get the action type(s) from the action-type attribute.
			var actionTypes = element.getAttribute("action-type");

			//Use the click action type as a fallback.
			if (!actionTypes) {
				actionTypes = "click";
			}

			//Register the action.
			UIKit.registerAction(viewController, element, action, actionTypes, viewInstance);
		}

		if (outlet && viewInstance) {
			viewController[outlet + "Controller"] = viewInstance;
			if (parentViewInstance) {
				parentViewInstance[outlet + "Controller"] = viewInstance;
			}
		}

		if (ViewClass) {
			viewController.allSubviews.push(viewInstance);
		}

		for (var i = 0; i < element.children.length; i++) {
			UIKit.processViewControllerElement(element.children[i], viewController, viewInstance);
		}

		if (ViewClass) {
			viewInstance.viewDidLoad();

			if (!UIKit.allViewInstances) {
				UIKit.allViewInstances = [ ];
			}
			UIKit.allViewInstances.push(viewInstance);
		}
	}

	/**
	 * Tries to find and return a view instance for the given element.
	 * @return null if no view instance contains the given element or the instance if it was found.
	 */
	static getViewInstanceForElement(element) {
		if (!UIKit.allViewInstances) {
			return null;
		}

		for (var i = UIKit.allViewInstances.length - 1; i >= 0; i--) {
			if (UIKit.allViewInstances[i].element == element) {
				return UIKit.allViewInstances[i];
			}
		}

		return null;
	}

	/**
	 * Used to register an action (like a click event) with a view controller or view instance during runtime.
	 * The first parameter may be either a view controller or a view instance. If you want to bind the event to both,
	 * pass the view instance as the last parameter.
	 * @param viewController The ViewController or View instance to which the action should be bound.
	 * @param element The element which should cause the event.
	 * @param action The action to be fired. This may be either a function or a string with the function's name or that has a "#", "/" or "contextmenu:" prefix.
	 * @param actionTypes (optional) A string or a string array with the type(s) of the action, i.e. "click" or [ "click", "mouseup" ]. Default is "clicl".
	 * @param viewInstance (optional) An optional View instance to also attach the event to.
	 */
	static registerAction(viewController, element, action, actionTypes = "click", viewInstance = null) {
		if (typeof actionTypes === "string") {
			//Convert the string into an array.
			actionTypes = actionTypes.replaceAll(", ", ",");
			actionTypes = actionTypes.split(",");

			if (typeof actionTypes === "string") {
				actionTypes = [ actionTypes ];
			}
		}

		for (var i = 0; i < actionTypes.length; i++) {
			var actionType = actionTypes[i];

			if (typeof action === "string" && (action.indexOf("#") == 0 || action.indexOf("/") == 0 || action.indexOf("contextmenu:") == 0)) {
				//It's a badger badger badger – uhm... no, it's a SNAAAKE, uh, no, it's a request path or context menu action.
				element.addEventListener(actionType, UIKit.onElementAction);
			}
			else {
				//It's a function call action.
				var actionFunction = (typeof action === "string") ? viewController[action] : action;
				if (actionFunction) {
					element.addEventListener(actionType, actionFunction.bind(viewController));
				}
				
				//Also attach the event listener to the view instance if one is provided.
				if (viewInstance) {
					actionFunction = (typeof action === "string") ? viewInstance[action] : action;
					if (actionFunction) {
						element.addEventListener(actionType, actionFunction.bind(viewInstance));
					}
				}
			}
		}

		//Re-assign the action to the element.
		//This is because when this function is called on runtime, the attribute might not be set.
		element.setAttribute("action", action);
		element.setAttribute("action-type", actionTypes.join(", "));
	}

	static onElementAction(event) {
		var action = event.currentTarget.getAttribute("action");

		if (action.indexOf("#") == 0) {
			window.location.hash = action.substring(1);
			return;
		}

		if (action.indexOf("/") == 0) {
			window.history.pushState(null, "", action);
			UIKit.onPopState();
			return;
		}

		if (action.indexOf("contextmenu:") == 0) {
			//Get the view controller which summoned the context menu.
			var summoningViewController = null;
			for (var id in UIKit.viewControllers) {
				if (UIKit.viewControllers[id].constructor.name != "SplitViewController" && UIKit.viewControllers[id].element.contains(event.currentTarget)) {
					summoningViewController = UIKit.viewControllers[id];
					break;
				}
			}

			//Get the view which summoned the context menu.
			var summoningView = null;

			var checkingElement = event.currentTarget;
			//For all parent elements of the event target...
			while (checkingElement != summoningViewController.element && !summoningView) {
				//...iterate over the subviews of the summoning view controller.
				for (var i = summoningViewController.allSubviews.length - 1; i >= 0; i--) {
					//If the view's element is the current parent element, this view is the summoning view.
					if (summoningViewController.allSubviews[i].element == checkingElement) {
						summoningView = summoningViewController.allSubviews[i];
						break;
					}
				}

				checkingElement = checkingElement.parentNode;
			}

			var contextMenuId = action.replace("contextmenu:", "");
			var contextMenu = UIKit.getPopoverById(contextMenuId);

			//Inform the context menu about the summoning view controller and view.
			contextMenu.setViewController(summoningViewController);
			contextMenu.setView(summoningView);

			contextMenu.showForElement(event.currentTarget, "bottom");

			//Stop the event from getting propagated because this might hide the context menu again immediately.
			event.stopPropagation();
		}
	}

	static getScene(sceneId) {
		if (!(sceneId in UIKit.scenes)) {
			UIKit.scenes[sceneId] = new Scene(sceneId);
		}
		return UIKit.scenes[sceneId];
	}

	/**
	 * Returns the scene that contains the given view controller, null if none does so.
	 */
	static getSceneWithViewController(viewController) {
		for (var sceneId in UIKit.scenes) {
			var scene = UIKit.scenes[sceneId];

			if (scene.containsViewController(viewController)) {
				return scene;
			}
		}

		return null;
	}

	/**
	 * Looks for the view controller with the given hash inside all scenes and returns it if found.
	 */
	static getViewControllerWithHash(hash) {
		for (var sceneId in UIKit.scenes) {
			var scene = UIKit.scenes[sceneId];

			var viewController = scene.getViewControllerWithHash(hash);

			if (viewController) {
				return viewController;
			}
		}

		return null;
	}

	/**
	 * Looks for the view controller with the given request path inside all scenes and returns it if found.
	 */
	static getViewControllerWithRequestPath(requestPath) {
		for (var sceneId in UIKit.scenes) {
			var scene = UIKit.scenes[sceneId];

			var viewController = scene.getViewControllerWithRequestPath(requestPath);

			if (viewController) {
				return viewController;
			}
		}

		return null;
	}

	static getViewControllerById(id) {
		return UIKit.viewControllers[id];
	}

	static getActiveViewController() {
		return UIKit.activeScene.modalViewController || UIKit.activeScene.activeViewController;
	}

	static getPopoverById(id) {
		return UIKit.popovers[id];
	}

	/*static getTooltipById(id) {
		return UIKit.tooltips[id];
	}*/

	static showScene(sceneId) {
		//Hide the others.
		for (var sceneId in UIKit.scenes) {
			UIKit.scenes[sceneId].hide();
		}

		var scene = UIKit.getScene(sceneId);
		scene.show();
	}

	/**
	 * Called when the URL hash has changed.
	 * Checks if there is any view controller that matches the hash and shows it's scene and itself.
	 */
	static onHashChanged(event) {
		var hash = window.location.hash;

		var viewControllerToShow = null;

		if (hash) {
			hash = hash.substring(1);

			var viewController = UIKit.getViewControllerWithHash(hash);

			if (viewController) {
				viewControllerToShow = viewController;
			}
		}
		
		//Use the initial view controller as a fallback.
		if (!viewControllerToShow) {
			viewControllerToShow = UIKit.getInitialViewController();
		}

		if (viewControllerToShow) {
			if (viewControllerToShow.showsModally) {
				UIKit.getActiveViewController().presentModalViewController(viewControllerToShow);
			}
			else {
				var scene = UIKit.getSceneWithViewController(viewControllerToShow);

				if (scene) {
					scene.showWithViewController(viewControllerToShow);
				}
				else {
					console.error("Can't find any scene with the view controller " + viewControllerToShow.constructor.name + "!");
				}
			}
		}
		else {
			console.error("No view controller to show! Not even a fallback! What is this?!");
		}
	}

	/**
	 * Called when the request path has changed.
	 * Checks if there is any view controller that matches the request path and shows it's scene and itself.
	 */
	static onPopState(event) {
		var requestPath = location.pathname;

		var viewControllerToShow = UIKit.getViewControllerWithRequestPath(requestPath);

		if (!viewControllerToShow) {
			viewControllerToShow = UIKit.getInitialViewController();
		}

		if (viewControllerToShow) {
			var scene = UIKit.getSceneWithViewController(viewControllerToShow);

			if (scene) {
				scene.showWithViewController(viewControllerToShow);
				viewControllerToShow.element.scrollTo(0, 0);
			}
			else {
				console.error("Can't find any scene with the view controller " + viewControllerToShow.constructor.name + "!");
			}
		}
		else {
			console.error("No view controller to show! Not even a fallback! What is this?!");
		}

		for (var i = 0; i < UIKit.navigationListeners.length; i++) {
			UIKit.navigationListeners[i](requestPath);
		}
	}

	/**
	 * Returns the initial view controller of the main scene.
	 */
	static getInitialViewController() {
		var scene = UIKit.getScene("main");
		if (scene) {
			return scene.initialViewController;
		}

		return null;
	}

	static onBodyMouseMoved(event) {
		UIKit.mousePosition = {
			x: event.clientX,
			y: event.clientY
		};
	}
}

window.addEventListener("hashchange", UIKit.onHashChanged);
window.addEventListener("popstate", UIKit.onPopState);