define(["wc/dom/classList",
	"wc/dom/initialise",
	"wc/dom/shed",
	"wc/dom/event",
	"wc/dom/tag",
	"wc/dom/Widget",
	"wc/dom/getLabelsForElement",
	"wc/ui/ajax/processResponse",
	"wc/dom/role",
	"wc/dom/textContent",
	"wc/dom/wrappedInput",
	"wc/ui/checkBox",
	"wc/ui/feedback"],
function (classList, initialise, shed, event, tag, Widget, getLabelsForElement, processResponse, $role, textContent, wrappedInput, checkBox, feedback) {
	"use strict";
	/**
	 * @constructor
	 * @alias module:wc/ui/label~Label
	 * @private
	 */
	function Label() {
		var TAGS = [tag.INPUT, tag.TEXTAREA, tag.SELECT, tag.FIELDSET],
			CLASS_HINT = "wc-label-hint",
			CHECKBOX_WRAPPER = checkBox.getWrapper(),
			MOVE_WIDGETS = [CHECKBOX_WRAPPER, new Widget("", "wc-radiobutton"), new Widget("button", "wc-selecttoggle")],
			HINT,
			// @ricksbrown this is a workaround for a flaw in our whole rendering system - we should fix the underlying flaw
			movedCbLabelReg = {};

		/**
		 * Helper to do label manipulation.
		 * @function
		 * @private
		 * @param {Element} label the label to manipulate
		 * @param {Function} func the function to apply to the label
		 */
		function mandateLabel(label, func) {
			if (label.tagName !== tag.LEGEND) {
				classList[func](label, "wc_req");
			}
		}

		/**
		 * Helper to do label manipulation.
		 * @function
		 * @private
		 * @param {Element} label the label to manipulate
		 * @param {Function} func the function to apply to the label
		 */
		function showHideLabel(label, func) {
			if (label.tagName !== tag.LEGEND) {
				shed[func](label);
			}
		}

		/**
		 * Manipulate a label when a labelled element is made mandatory or optional.
		 * @function
		 * @private
		 * @param {Event} $event An optional/mandatory event.
		 */
		function shedMandatorySubscriber($event) {
			var input, func, element = $event.target, action = $event.type;
			if (!element) {
				return;
			}
			input = wrappedInput.isOneOfMe(element) ? wrappedInput.getInput(element) : element;
			if (input && input.type !== "radio" && (TAGS.indexOf(input.tagName) > -1 || $role.has(input))) {
				func = action === shed.events.OPTIONAL ? "remove" : "add";
				getLabelsForElement(element).forEach(function (next) {
					mandateLabel(next, func);
				});
			}
		}

		/**
		 * Show/hide label[s] when a labelled element (even readOnly) is shown/hidden.
		 * @function
		 * @private
		 * @param {Event} $event A show/hide event.
		 */
		function shedHideSubscriber($event) {
			var func, element = $event.target, action = $event.type;
			if (element) {
				func = action === shed.events.SHOW ? "show" : "hide";
				// anything, even read-only, can be hidden/shown
				getLabelsForElement(element, true).forEach(function (next) {
					showHideLabel(next, func);
				});
			}
		}

		/**
		 * This is the function which does the heavy lifting of converting a label into and out of its read-only
		 * analogue state when a labelled element is converted. An element may have more than one label (though this
		 * is not a good thing) and this function is a forEach iterator function manipulating a single specific
		 * labelling element. This function is called only if the labelled element (element) has converted between
		 * its active and read-only states.
		 * @function
		 * @private
		 * @param {Element} element The DOM element which is being converted to/from its read-only state via AJAX.
		 * @param {Element} label a label (or read-only analogue) for element
		 * @param {boolean} [isRO] indicates the element is readOnly, already calculated in the caller so pass it thru.
		 */
		function convertLabel(element, label, isRO) {
			var newLabellingElement,
				parent = label.parentNode,
				input;
			if (isRO) {
				newLabellingElement = document.createElement("span");
				newLabellingElement.setAttribute("data-wc-rofor", element.id);
			} else {
				newLabellingElement = document.createElement("label");
				if ((input = wrappedInput.getInput(element))) { // should always be found
					newLabellingElement.setAttribute("for", input.id);
				} else if (TAGS.indexOf(element.tagName) > -1) {
					newLabellingElement.setAttribute("for", element.id);
				}
			}
			newLabellingElement.className = label.className;
			newLabellingElement.innerHTML = label.innerHTML;
			input = input || element;
			if (input && input.type !== "radio") {
				mandateLabel(newLabellingElement, (!isRO && shed.isMandatory(input) ? "add" : "remove"));
			}
			if (shed.isHidden(element, true)) {
				shed.hide(newLabellingElement, true); // nothing depends on the hidden state of a label and we are replicating a load-time state.
			}
			newLabellingElement.id = label.id;
			label.id = "";
			parent.insertBefore(newLabellingElement, label);
			parent.removeChild(label);
			// Add submitOnChange warnings.
			if (!isRO) {
				// this cannot be a module level dependency as it would cause a circular
				// dependency. It is also not really important how long this takes.
				require(["wc/ui/onchangeSubmit"], function (soc) {
					soc.warn(element, newLabellingElement);
				});
			}
		}

		/**
		 * Helper to put checkbox labels in the right place. This is far more complex than it needs to be.
		 * @function
		 * @private
		 * @param {Element} input the labelled WCheckBox
		 * @param {Element|String} label the label or its HTML (single element root)
		 */
		function checkboxLabelPositionHelper(input, label) {
			var labelElement,
				refElement,
				parent;
			if (!(input && label)) {
				throw new TypeError("Input and label must be defined.");
			}
			if (!(input && input.nodeType === Node.ELEMENT_NODE && label)) {
				throw new TypeError("Input must be an element.");
			}

			if (label.constructor === String) {
				labelElement = document.createElement("span");
				labelElement.innerHTML = label.trim();
				labelElement = labelElement.firstElementChild;
			} else {
				labelElement = label;
			}

			if (!(labelElement && labelElement.nodeType === Node.ELEMENT_NODE)) {
				console.error("label arg must be an Element or HTML String representing a single element");
				// do not throw, this function is not that important
				return;
			}

			if (wrappedInput.isReadOnly(input)) {
				parent = input.parentNode;
				refElement = input.nextSibling;
				if (refElement) {
					parent.insertBefore(labelElement, refElement);
				} else {
					parent.appendChild(labelElement);
				}
				return;
			}

			refElement = feedback.getBox(input, -1);
			if (refElement && refElement.parentNode === input) {
				input.insertBefore(labelElement, refElement);
			} else {
				input.appendChild(labelElement);
			}
		}

		function isActiveWCheckBox(el) {
			if (!(el && el.nodeType === Node.ELEMENT_NODE)) {
				return false;
			}
			return CHECKBOX_WRAPPER.isOneOfMe(el) && !wrappedInput.isReadOnly(el);
		}

		/**
		 * Move an individual element's label if required.
		 * @function
		 * @private
		 * @param {Element} el a WRadioButton, WCheckBox or WSelectToggle-button
		 */
		function moveLabel(el) {
			var labels = getLabelsForElement(el, true),
				label, parent, refElement;
			if (!(labels && labels.length)) {
				return;
			}

			label = labels[0];
			// We **almost** always have to move interactive checkbox labels because otherwise error messages make the label go nuts.
			if (isActiveWCheckBox(el)) {
				// We want to put the label inside the wrapper but before any diagnostics.
				// but did we already put it there as part of the ajaxSubscriber?
				if (el.compareDocumentPosition(label) & document.DOCUMENT_POSITION_CONTAINED_BY) {
					// label already inside the wraspper so do nothing
					return;
				}
				checkboxLabelPositionHelper(el, label);
				return;
			}

			// read-only, WRadioButton and WSelectToggle labels go after their labelled component
			if (label === el.nextElementSibling) {
				// already in the right place
				return;
			}
			parent = el.parentNode;
			if ((refElement = el.nextSibling)) {
				parent.insertBefore(label, refElement);
			} else {
				parent.appendChild(label);
			}
		}

		/**
		 * Move labels to their correct position.
		 * TODO: This _should_ be done in the Java Renderers.
		 * @function
		 * @private
		 * @param {Element} [element] a container element
		 */
		function moveLabels(element) {
			var el = element || document.body;
			if (element && Widget.isOneOfMe(element, MOVE_WIDGETS)) {
				moveLabel(el);
			} else {
				Array.prototype.forEach.call(Widget.findDescendants(el, MOVE_WIDGETS), moveLabel);
			}
		}

		/**
		 * Check if we need to restore a label from the moved label registry.
		 * The label for a WCheckBox has to be inside the labelled component's input wrapper to allow for error messages.
		 * @function
		 * @private
		 * @param {Element} element the potentially unlabelled labelled WCheckBox
		 */
		function checkRestoreLabel(element) {
			var refId = element.id,
				missingLabelContent = movedCbLabelReg[refId],
				notMissingLabels;
			try {
				if (missingLabelContent && CHECKBOX_WRAPPER.isOneOfMe(element)) {
					notMissingLabels = getLabelsForElement(element, wrappedInput.isReadOnly(element));
					if (!(notMissingLabels && notMissingLabels.length)) {
						// yep, we don't have a label for this check box any more
						checkboxLabelPositionHelper(element, missingLabelContent);
					}
				}
			} finally {
				if (missingLabelContent) {
					delete movedCbLabelReg[refId];
				}
			}
		}

		/**
		 * Store a nested label before we blow away a WCheckBox. Only needed if the WCheckBox is EXPLICITLY targeted via AJAX.
		 * @param {type} element
		 * @returns {undefined}
		 */
		function preInsertionAjaxSubscriber(element) {
			var labels, label;
			if (!(element && isActiveWCheckBox(element))) {
				return;
			}
			labels = getLabelsForElement(element);
			if (!(labels && labels.length)) {
				return;
			}
			label = labels[0];
			if (!(element.compareDocumentPosition(label) & document.DOCUMENT_POSITION_CONTAINED_BY)) {
				// label not inside the wraspper so do nothing
				return;
			}
			movedCbLabelReg[element.id] = label.outerHTML;
		}

		/**
		 * Post-insertion AJAX subscriber to convert labels from a HTML label element to its read-only analogue and vice-versa when
		 * a labelled element is replaced via AJAX.
		 *
		 * @function
		 * @private
		 * @param {Element} element the new element.
		 */
		function ajaxSubscriber(element) {
			if (!element) {
				return;
			}

			checkRestoreLabel(element);
			moveLabels(element);
			Array.prototype.forEach.call(wrappedInput.get(element, true), function (next) {
				var isRO = wrappedInput.isReadOnly(next),
					labels = getLabelsForElement(next, true);
				labels.forEach(function (label) {
					var isLabel = label.tagName === tag.LABEL,
						input;
					// if the new element is readOnly and the old one
					if (isRO && isLabel || !(isRO || isLabel)) {
						convertLabel(next, label, isRO);
						return;
					}
					// only have to do this if we are not converting the labels.
					if ((input = wrappedInput.getInput(next))) {
						if (input.type !== "radio") {
							mandateLabel(label, !isRO && shed.isMandatory(input) ? "add" : "remove");
						}
					}
					if (shed.isHidden(next, true)) {
						shed.hide(label, true);
					} else {
						shed.show(next);
					}
				});
			});
		}

		/**
		 * Initialiser callback to subscribe to {@link module:wc/dom/shed} and
		 * {@link module:wc/ui/ajax/processResponse}.
		 * @param {Element} element Usually document.body
		 * @function module:wc/ui/label.postInit
		 * @public
		 */
		this.postInit = function (element) {
			event.add(element, shed.events.MANDATORY, shedMandatorySubscriber);
			event.add(element, shed.events.OPTIONAL, shedMandatorySubscriber);
			event.add(element, shed.events.SHOW, shedHideSubscriber);
			event.add(element, shed.events.HIDE, shedHideSubscriber);
			processResponse.subscribe(preInsertionAjaxSubscriber);
			processResponse.subscribe(ajaxSubscriber, true);
		};

		/**
		 * Get the hint from a given label.
		 *
		 * @function module:wc/ui/label.getHint
		 * @public
		 * @param {Element} label the label to test
		 * @returns {Element} the label's hint, if any
		 */
		this.getHint = function (label) {
			if (label) {
				HINT = HINT || new Widget("", CLASS_HINT);
				return HINT.findDescendant(label);
			}
			return null;
		};

		/**
		 * Set (add to or remove) a hint on a label.
		 *
		 * @function module:wc/ui/label.setHint
		 * @public
		 * @param {Element} label the label to which we are modifying hint content
		 * @param {String} [content] the hint content to add; if falsey then an existing hint (if any) is removed
		 */
		this.setHint = function(label, content) {
			var hint = this.getHint(label),
				BEFORE_END = "beforeend";
			if (hint) {
				if (content) {
					if (textContent.get(hint)) {
						hint.insertAdjacentHTML(BEFORE_END, "<br>");
					}
					hint.insertAdjacentHTML(BEFORE_END, content);
				} else {
					hint.parentNode.removeChild(hint);
				}
			} else if (content) {
				hint = tag.toTag(tag.SPAN, false, "class='" + CLASS_HINT + "'") + content + tag.toTag(tag.SPAN, true);
				label.insertAdjacentHTML(BEFORE_END, hint);
			}
		};

		this.preInit = function(element) {
			moveLabels(element);
		};

		/**
		 * Public for testing.
		 * @ignore
		 */
		this._convert = convertLabel;
		/**
		 * Public for testing.
		 * @ignore
		 */
		this._ajax = ajaxSubscriber;

		/**
		 * Public for testing.
		 * @ignore
		 */
		this._checkboxLabelPositionHelper = checkboxLabelPositionHelper;
	}

	/**
	 * A module which provides functionality peculiar to control labelling elements (labels and label-surrogates). This
	 * module is mainly concerned with ensuring that as controls are replaced using AJAX that any labelling components for
	 * those controls are always kept in the right state.
	 *
	 * @module
	 * @requires wc/dom/classList
	 * @requires wc/dom/initialise
	 * @requires wc/dom/shed
	 * @requires wc/dom/tag
	 * @requires wc/dom/Widget
	 * @requires wc/dom/getLabelsForElement
	 * @requires wc/ui/ajax/processResponse
	 * @requires wc/dom/role
	 * @requires wc/dom/textContent
	 * @requires wc/dom/wrappedInput
	 * @requires wc/ui/checkBox
	 * @requires wc/ui/feedback
	 */
	return initialise.register(new Label());
});
