define(["wc/dom/classList",
	"wc/dom/event",
	"wc/dom/initialise",
	"wc/dom/shed",
	"wc/dom/tag",
	"wc/dom/uid",
	"wc/dom/Widget",
	"wc/i18n/i18n",
	"wc/ui/ajaxRegion",
	"wc/ui/ajax/processResponse",
	"wc/ui/containerload",
	"wc/timers",
	"wc/ui/dialogFrame",
	"wc/ui/getForm"],
function(classList, event, initialise, shed, tag, uid, Widget, i18n, ajaxRegion, processResponse, eagerLoader, timers, dialogFrame, getForm) {
	"use strict";
	var instance;

	/**
	 * @constructor
	 * @alias module:wc/ui/dialog~Dialog
	 * @private
	 */
	function Dialog() {
		var BUTTON = new Widget("button"),
			ANCHOR,
			// OPENER = BUTTON.extend("", {"data-wc-dialogconf": null}),
			BASE_CLASS = "wc-dialog",
			registry = {},
			registryByDialogId = {},
			keepContentOnClose = false,
			openOnLoadTimer,
			openThisDialog,
			GET_ATTRIB = "data-wc-get";

		/**
		 * Ensure a dialog trigger element has the aria-haspopup attribute.
		 *
		 * @function
		 * @private
		 * @param {String} id the id of the element to manipulate
		 */
		function setHasPopup(id) {
			var popupAttr = "aria-haspopup",
				el = document.getElementById(id);
			if (el && !el.getAttribute(popupAttr)) {
				el.setAttribute(popupAttr, "true");
			}
		}

		/**
		 * Array.forEach function to add each dialog definition object to the registry.
		 * @see module:wc/ui/dialog#register
		 * @function
		 * @private
		 * @param {module:wc/ui/dialog~regObject} dialogObj The dialog dto.
		 */
		function _register(dialogObj) {
			var triggerId = dialogObj.triggerid || dialogObj.id,
				add = function(title) {
					registry[triggerId] = {
						id: dialogObj.id,
						className: BASE_CLASS + (dialogObj.className ? (" " + dialogObj.className) : ""),
						width: dialogObj.width,
						height: dialogObj.height,
						modal: dialogObj.modal || false,
						openerId: dialogObj.triggerid,
						title: dialogObj.title || title
					};
					registryByDialogId[dialogObj.id] = triggerId;

					if (dialogObj.open) {
						openThisDialog = triggerId;
					}
				};

			if (triggerId) {
				if (dialogObj.title) {
					add(dialogObj.title);
				} else {
					i18n.translate("dialog_noTitle").then(add);  // This is called too early for a synchronous i18n call
				}
			}
		}

		/*
		 * Is this element inside the dialog content?
		 * @function
		 * @private
		 * @param {String} id The id of the element to test.
		 */
		function isInsideDialog(id) {
			var element, content = dialogFrame.getContent();
			if (content && id && (element = document.getElementById(id))) {
				return !!(content.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_CONTAINED_BY);
			}
			return false;
		}

		/**
		 * Find a dialog opener from a given start point.
		 *
		 * @function
		 * @private
		 * @param {Element} element the start element
		 * @param {boolean} ignoreAncestor if {@code} true then stop without checking ancestors for a trigger
		 * @returns {Element} a dialog trigger element if found
		 */
		function getTrigger(element, ignoreAncestor) {
			var parent,
				id = element.id,
				regObj;

			if (element.tagName === tag.FORM) {
				return null;
			}
			if ((regObj = registry[id])) {
				if (regObj.id === id) {
					// Auto open on load dialogs are their own trigger
					return null;
				}
				return element;
			}
			if (ignoreAncestor) {
				return null;
			}
			parent = element;
			while ((parent = parent.parentNode) && parent.nodeType === Node.ELEMENT_NODE) {
				if (parent.tagName === tag.FORM) {
					return null;
				}
				if ((id = parent.id)) {
					if ((regObj = registry[id])) {
						if (regObj.id === id) {
							// Auto open on load dialogs are their own trigger
							return null;
						}
						return parent;
					}
				}
			}
			return null;
		}

		/**
		 * We need to know if an element is a submit element so that we can prevent the submit action if it opens a dialog.
		 * @function
		 * @private
		 * @param {Element} element the element to test
		 * @returns {Boolean} {@code true} if the element is a submitting element
		 */
		function isSubmitElement(element) {
			var result = false,
				type = element.type;
			if (type === "submit" || type === "image") {
				result = true;
			}
			return result;
		}

		/**
		 * Action click events on a dialog trigger or within a dialog.
		 * @function
		 * @private
		 * @param {Element} element The element which was clicked.
		 * @returns {boolean} {@code true} if the click is activated and we _may_ want to prevent the default action
		 */
		function activateClick(element) {
			var _element,
				content,
				trigger,
				targets,
				dialog = dialogFrame.getDialog();

			// Are we opening a dialog?
			if ((_element = getTrigger(element)) && !isInsideDialog(element.id)) {
				if (shed.isDisabled(_element)) { // This is needed because IE is broken and we have a potential race with the global fix.
					return false;
				}
				instance.open(_element);
				return isSubmitElement(_element);
			}

			if (!(
				dialog &&
				!shed.isHidden(dialog, true) &&
				(content = dialogFrame.getContent()) &&
				(content.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_CONTAINED_BY)
			)) {
				// we are not inside a dialog's content.
				return false;
			}

			keepContentOnClose = false;
			// we need to know if a click is on an ajax trigger inside a dialog
			if ((trigger = ajaxRegion.getTrigger(element, true))) {
				_element = element;
			} else {
				// this is a chrome thing: it honours clicks on img elements and does not pass them through to the underlying link/button
				ANCHOR = ANCHOR || new Widget("A");
				_element = Widget.findAncestor(element, [ BUTTON, ANCHOR ]);
				if (_element) {
					trigger = ajaxRegion.getTrigger(_element, true);
				}
			}

			if (!trigger) {
				return false;
			}
			targets = trigger.loads;

			if (targets && targets.length && !targets.some(isInsideDialog)) {
				keepContentOnClose = true;
				dialogFrame.close();
			}
			return false;
		}

		/**
		 * Open a dialog.
		 * @function
		 * @private
		 * @param {String} triggerId The id of the trigger.
		 */
		function openDlg(triggerId) {
			var regObj = registry[triggerId], trigger, form, formId;

			function populateOnLoad() {
				var content = dialogFrame.getContent(),
					openerId,
					opener;
				if (content) {
					content.id = regObj.id;
					if (!(openThisDialog && openThisDialog === triggerId) && (openerId = regObj.openerId)) {
						opener = document.getElementById(openerId);
						content.setAttribute(GET_ATTRIB, openerId + "=" + (opener ? encodeURIComponent(opener.value) : "x"));
					} else {
						content.removeAttribute(GET_ATTRIB);
					}
					classList.add(content, "wc_magic");
					classList.add(content, "wc_dynamic");
					eagerLoader.load(content, false, false);
				} else {
					console.warn("Could not find dialog content wrapper.");
				}
				openThisDialog = null;
			}

			if (regObj) {
				if (!regObj.formId) {
					if ((trigger = document.getElementById(triggerId)) && (form = getForm(trigger))) {
						formId = form.id || (form.id = uid());
						regObj["formId"] = formId;
					}
				}
				dialogFrame.open(regObj).then(populateOnLoad).catch(function(err) {
					console.warn(err);
					openThisDialog = null; // belt **and** braces
				});
			}
		}

		/**
		 * Set the focus into the dialog after the AJAX malarkey has finished. This looks at the dialog content
		 * component which is an unnamed wrapper in the Java side so will ONLY grab focus when a dialog is opened and
		 * will not continually grab it each time an ajax action occurs inside a dialog even if the target is the
		 * WComponent (most commonly a WPanel) which is used as the "content" of the dialog. Tricky that.
		 *
		 * @param {Element} element The root element from the ajax response.
		 */
		function postOpenSubscriber(element) {
			var regObj;
			if (element && element === dialogFrame.getContent() && element.id && (regObj = getRegistryObjectByDialogId(element.id))) {
				// set the initial position
				if (!(regObj.top || regObj.left || regObj.top === 0 || regObj.left === 0)) {
					dialogFrame.reposition({width: regObj.width, height: regObj.height});
				}
			}
		}

		/**
		 * Get a registry object based on a WDialog id attribute.
		 * @param {String} id the ID of the WDialog to get.
		 * @returns {module:wc/ui/dialog~regObject} the registry object if found.
		 */
		function getRegistryObjectByDialogId(id) {
			var triggerId = registryByDialogId[id];
			if (triggerId) {
				return registry[triggerId];
			}
			return null;
		}

		/**
		 * Listen for shed.hide and clear out the transient aspects of the dialog.
		 *
		 * @function
		 * @private
		 * @param {Event} $event A shed hide event.
		 */
		function shedSubscriber($event) {
			var element = $event.target,
				content,
				id,
				regObj;
			if (element && element === dialogFrame.getDialog() && (content = dialogFrame.getContent()) && (id = content.id) && (regObj = getRegistryObjectByDialogId(id))) { // we are ONLY interested in WDialog inited dialogs.
				try {
					dialogFrame.unsetAllDimensions();
					dialogFrame.resetContent(keepContentOnClose, (keepContentOnClose ? "" : regObj.id));
				} finally {
					keepContentOnClose = false;
				}
			}
		}

		/**
		 * Click listener for dialog opening buttons and controls within a dialog.
		 * @function
		 * @private
		 * @param {Event} $event a click event.
		 */
		function clickEvent($event) {
			if ($event.defaultPrevented) {
				return;
			}
			if (activateClick($event.target)) {
				$event.preventDefault();
			}
		}

		/**
		 * Component initialisation.
		 * @function module:wc/ui/dialog.initialise
		 * @public
		 * @param {Element} element The element being initialised, usually document.body.
		 */
		this.initialise = function(element) {
			event.add(element, "click", clickEvent);
			event.add(element, shed.events.HIDE, shedSubscriber);
		};

		/**
		 * Late initialisation.
		 * @function module:wc/ui/dialog.postInit
		 * @public
		 */
		this.postInit = function() {
			processResponse.subscribe(postOpenSubscriber, true);
		};

		/**
		 * Add the object definitions of each dialog to the registry.
		 * @function module:wc/ui/dialog.register
		 * @public
		 * @param {module:wc/ui/dialog~regObject[]} array An array of dialog definition objects.
		 */
		this.register = function(array) {
			if (array && array.length) {
				array.forEach(_register);
				initialise.addCallback(function() {
					var o;
					for (o in registry) {
						if (registry.hasOwnProperty(o)) {
							setHasPopup(o);
						}
					}
					if (openThisDialog) {
						if (openOnLoadTimer) {
							timers.clearTimeout(openOnLoadTimer);
						}
						openOnLoadTimer = timers.setTimeout(openDlg, 0, openThisDialog);
					}
				});
			}
		};

		/**
		 * Open a dialog for a given trigger.
		 * @function module:wc/ui/dialog.open
		 * @public
		 * @param {Element} trigger an element which _should_ be a dialog trigger.
		 * @returns {boolean} `true` if the element will trigger a dialog on change or click.
		 */
		this.open = function(trigger) {
			var element = getTrigger(trigger);
			if (element) {
				openDlg(element.id);
				return true;
			}
			return false;
		};
	}
	/**
	 * Provides "dialog" functionality.  NOTE: we currently use a custom dialog because IE native dialog does not call and
	 * parse xslt (as of IE10). This is not an issue in ff3.6+ or Chrome 6 but these do not support showModelessDialog.
	 *
	 * The custom dialog also provides somewhat better options for mobile use and cross platform consistency.
	 *
	 * @module
	 * @requires module:wc/dom/classList
	 * @requires module:wc/dom/event
	 * @requires module:wc/dom/initialise
	 * @requires module:wc/dom/shed
	 * @requires module:wc/dom/Widget
	 * @requires module:wc/i18n/i18n
	 * @requires module:wc/ui/ajaxRegion
	 * @requires module:wc/ui/ajax/processResponse
	 * @requires module:wc/timers
	 * @requires module:wc/ui/dialogFrame
	 *
	 * @todo Re-order source, document private members.
	 */
	instance = new Dialog();

	return initialise.register(instance);

	/**
	 * @typedef {Object} module:wc/ui/dialog~regObject An object which stores information about a dialog.
	 * @property {String} id The WDialog id.
	 * @property {int} [width] The dialog width in px.
	 * @property {int} [height] The dialog height in px.
	 * @property {Boolean} [modal] Is the dialog modal?
	 * @property {String} [title] The WDialog title. If not set a default title is used.
	 * @property {Boolean} [open] If true then the dialog is to be open on page load. This is passed in as part of
	 *    the registration object but is not stroed in the registry.
	 */
});
