/*
// Matrix Construct
//
// Copyright (C) Matrix Construct Developers, Authors & Contributors
// Copyright (C) 2016-2018 Jason Volk <jason@zemos.net>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice is present in all copies. The
// full license for this software is available in the LICENSE file.
*/

'use strict';

/**************************************
 *
 * Client's Angular application
 *
 */

mc.ng = {};

mc.opts.ng =
{
	resource_url_whitelist:
	[
		/* Allow same origin resource loads. */
		"self",

		/* Allow loading from our assets domain.  Notice the difference between * and **. */
		"https://*.youtube.com/**"

		/**/
	],

	resource_url_blacklist:
	[
	],

	// tell angular to allow mxc:// URL's for images
	img_src_sanitation_whitelist: /^\s*(https?|ftp|file|mxc):/,
};

mc.ng.app = angular.module('ircd',
[
	// 'ngAnimate',
]);

mc.ng.app.config(['$rootScopeProvider', function($rootScopeProvider)
{
	$rootScopeProvider.digestTtl(16);
}]);

mc.ng.app.config(['$compileProvider', function($compileProvider)
{
	$compileProvider.imgSrcSanitizationWhitelist(mc.opts.ng.img_src_sanitation_whitelist);
}]);

mc.ng.app.config(['$sceDelegateProvider', function($sceDelegateProvider)
{
	$sceDelegateProvider.resourceUrlWhitelist(mc.opts.ng.resource_url_whitelist);
	$sceDelegateProvider.resourceUrlBlacklist(mc.opts.ng.resource_url_blacklist);
}]);

/**
 * Things in here can't take place at the global init; they take place after
 * the document/main is ready.
 */
mc.ng.init = function()
{
	let root = mc.ng.root();
	root.$watch(() =>
	{
		mc.instance.digests++;
		return null;
	});
};

/**
 * @returns the formal root scope of the app
 */
mc.ng.root = function()
{
	let body = angular.element(document.body);
	let root = body.scope().$root;
	return root;
};

/**
 * @returns the mc scope which is our root scope for the app. This is directly under
 * the real root, and all our client scopes are children of the mc scope. Other children
 * of the real root may exist but are only indirectly related.
 */
mc.ng.mc = function()
{
	let charybdis = angular.element("#charybdis");
	return charybdis.scope();
};

/**
 * Trigger Angular manually
 */
mc.ng.apply = function(closure = undefined)
{
	let root = mc.ng.root();
	if(root.$$phase == "$apply")
	{
		console.warn("Unnecessary mc.ng.apply(); " + (new Error));
		return;
	}

	return root.$apply(closure);
};

// Alias
mc.apply = mc.ng.apply;

/**
 * Trigger Angular manually
 */
mc.ng.apply.later = function(closure = () => {})
{
	let root = mc.ng.root();
	root.$applyAsync(closure);
};

/**
 * Trigger Angular manually
 */
mc.ng.apply.animate = function(callback)
{
	let handler = () =>
	{
		if(callback() !== false)
			mc.ng.apply.later();
	};

	return window.requestAnimationFrame(handler);
};

/**
 * Trigger Angular manually
 */
mc.ng.apply.idle = function(timeout, callback)
{
	if(typeof(timeout) == "function")
	{
		callback = timeout;
		timeout = undefined;
	}

	let handler = (...params) =>
	{
		if(callback(...params) !== false)
			mc.ng.apply.later();
	};

	let opts =
	{
		timeout: timeout,
	};

	return window.requestIdleCallback(handler, opts);
};

/**
 * Trigger Angular manually
 */
mc.ng.apply.defer = function()
{
	let root = mc.ng.root();
	let watchers = root.$$watchers;
	root.$$watchers = [];
	root.$applyAsync(() =>
	{
		root.$$watchers = watchers;
	});
};

/**
 * Use mc.ng.timeout instead of window.setTimeout(), as the latter
 * issues an event which is unknown to Angular.
 */
mc.ng.timeout = function(timeout, closure)
{
	if(typeof(timeout) == "function")
	{
		closure = timeout;
		timeout = undefined;
	}

	if(typeof(closure) != "function")
		closure = () => {};

	//TODO: XXX: arbitrary
	let $timeout = mc.ng.timeout.$timeout;
	return $timeout(closure, timeout);
};

// Alias
mc.timeout = mc.ng.timeout;

/**
 * Use mc.ng.timeout instead of window.setTimeout(), as the latter
 * issues an event which is unknown to Angular.
 */
mc.ng.timeout.cancel = function(promise)
{
	//TODO: XXX: arbitrary
	let $timeout = mc.ng.timeout.$timeout;
	$timeout.cancel(promise);
};

/**
 * Non-angular event so we send a notify to handle the resize changing things
 * that need to trigger that model<->view dialectic.
 */
 /*
window.addEventListener("resize", (event) =>
{
	maybe(() => mc.ng.apply());
});
*/

/** Abstract Controller
 *
 * Should be on the prototype chain of all of the controllers in client.
 * This allows us to DRY things across all controllers as well as provide
 * a suite of virtual convenience functions.
 *
 * Overrides can be async. This abstraction provides a digest for that at
 * the base of this virtual call stack. Note: overrides for digesting() will
 * never be waited on.
 */
mc.ng.controller = class
{
	constructor(name, $scope)
	{
		// DEBUG
		//let argc = arguments.length;
		//console.log("init controller [" + name + "] " + typeof($scope));

		this.$scope = $scope;
		this.$apply = $scope.$apply;

		// Register dtors
		this.$scope.$on("$destroy", mc.ng.controller.virtual.bind(this, this.destructor));

		// NOTE: $applyAsync MUST be used here rather than $evalAsync.
		// $evalAsync from a directive runs after DOM update.
		// $evalAsync from a controller runs before the DOM has been manipulated. <---
		this.$scope.$applyAsync(mc.ng.controller.virtual.bind(this, this.constructed));

		// Register digest loop
		this.$scope.$watch(this.digesting.bind(this));

		// DEBUG
		//this.$scope.$watch(() => debug.digest(name, $scope));
	}

	/**
	 * Override to get called when the controller gets $destroy'ed
	 */
	destructor() {}

	/**
	 * Override to get called after the DOM element has been inserted.
	 * This will be on the next digest cycle after construction.
	 */
	constructed() {}

	/**
	 * Ovrride convenience to execute code during a digest
	 */
	digesting() {}
};

/**
 *
 */
mc.ng.controller.virtual = async function(override)
{
	let ret = override.call(this);
	if(ret instanceof Promise)
	{
		ret = await ret;
		this.$scope.$apply();
	}

	return ret;
};

/** Async Angular directives.
 *
 * These ang- prefixed directives are identical to the ng- directives except they
 * add support for ES6 async usage up the stack. This allows the evaluation of a
 * directive to digest after a promise it returns is fulfilled.
 *
 * Regular ng- directives run the digest loop when the evaluation returns -- even if
 * it returns a promise, which happens on the first await up the stack. We need it to
 * happen after the code on the other side of the await too.
 */
mc.ng.ang = function(directive_name, event_name, opts = {})
{
	mc.ng.app.directive(directive_name, function()
	{
		return {
			restrict: 'A',
			link: mc.ng.ang.link.bind(null, directive_name, event_name, opts),
		};
	});
};

mc.ng.ang.link = function(name, on, opts, $scope, element, attr, controller)
{
	let expression = attr[name];
	element.on(on, async function($event)
	{
		let locals =
		{
			$event: $event,
		};

		let onerror = (error) =>
		{
			if(!error.element)
				error.element = $event.delegateTarget;

			mc.unhandled(error);
		};

		let promise; try
		{
			promise = $scope.$eval(expression, locals);
		}
		catch(error)
		{
			onerror(error);
		}

		if(promise instanceof Promise) try
		{
			await promise;
		}
		catch(error)
		{
			onerror(error);
		}
		finally
		{
			if(opts.apply_async !== false)
				$scope.$apply();
		}
		else
		{
			if(opts.apply_sync !== false)
				$scope.$apply();
		}
	});
};

mc.ng.ang.directives =
[
	mc.ng.ang("angClick", "click"),
	mc.ng.ang("angDblclick", "dblclick"),
	mc.ng.ang("angMouseup", "mouseup"),
	mc.ng.ang("angMousedown", "mousedown"),

	// The scroll event is optioned to not digest unless the handler returns a
	// promise. Note that this means a scroll event handler should not be an
	// `async function()` if it has efficient return cases (and it should).
	mc.ng.ang("angScroll", "scroll",
	{
		apply_sync: false,
	}),
];

/**
 * ng-repeat="foo in bar | reverse"
 */
mc.ng.app.filter('reverse', () => (items) => items.slice().reverse());
