define(["wc/i18n/i18n",
	"wc/ajax/triggerManager",
	"wc/dom/uid",
	"wc/dom/event",
	"wc/dom/initialise",
	"wc/dom/serialize",
	"wc/dom/isSuccessfulElement",
	"lib/sprintf",
	"wc/dom/Widget",
	"wc/dom/formUpdateManager",
	"wc/dom/focus",
	"wc/ui/ajax/processResponse"],
function(i18n, triggerManager, uid, event, initialise, serialize, isSuccessfulElement, sprintf, Widget, formUpdateManager, focus, processResponse) {
	"use strict";
	var instance;

	/*
	 * TODO: we have a lot of form ID testing. Obviously if the form does not have an id then the whole
	 * unsaved changes registry malarkey will fail. All forms created by WApplication have an id, so maybe we do not
	 * need these tests?*/

	/**
	 * @constructor
	 * @private
	 * @alias module:wc/ui/cancelUpdate~CancelUpdateControl
	 */
	function CancelUpdateControl() {
		var loading = false,  // if cancel button && unsavedOnServer() get dialog twice without this
			buttonClicked,
			FORM = new Widget("FORM"),
			CANCEL_TITLE,
			CANCEL_MESSAGE,
			FORM_UNSAVED,
			// SUBMIT_CONTROL = new Widget("BUTTON", "", {"type":"submit"}),
			CANCEL_BUTTON,
			registry = {},
			RECALC = "-recalc";

		/**
		 * Get the current (not stored) state of a form.
		 *
		 * @function
		 * @private
		 * @param {Element} form The form whise state we want.
		 * @returns {String} The serialized state of the form.
		 */
		function getCurrentState(form) {
			return serialize.serialize(form, true, true, isDirty);
		}

		/**
		 * Filters out "clean" elements from the serialization.
		 * @param {Element} element A state field.
		 * @returns {Boolean} false if the element should be vetoed.
		 */
		function isDirty(element) {
			return !element.hasAttribute("data-wc-clean");
		}

		/**
		 * Determines if the form has unsaved changes.
		 *
		 * @function
		 * @private
		 * @param {Element} form The form we are going to test for unsaved changes.
		 * @returns {boolean} True if the form state has changed, false if the state has not changed or has never
		 *    been calculated.
		 */
		function hasUnsavedChanges(form) {
			var formId, result = false;
			FORM_UNSAVED = FORM_UNSAVED || FORM.extend("wc_unsaved");
			if (!(result = !!FORM_UNSAVED.findDescendant(document.body))) {
				formId = form.id;
				if (!(formId && registry[formId])) {  // the form was never stored so we can assume it has not changed.
					return false;
				}
				formUpdateManager.clean(form);  // this clears out any previously written states (such as from an AJAX update) which will always make the form appear to have changed
				result = serialize.areDifferent(registry[formId], getCurrentState(form));
			}
			return result;
		}

		/**
		 * Stores the state of a form for later comparison. This is called after AJAX.
		 *
		 * @function
		 * @private
		 * @param {Element} form The form of which we are going to save state.
		 */
		function storeFormState(form) {
			var formId;
			if (form && (formId = form.id)) {  // if the form does not have an ID then the initial state has not been set
				registry[formId] = getCurrentState(form);
			}
		}

		/**
		 * Determines if an element is a cancel button.
		 *
		 * @function
		 * @private
		 * @param {Element} element The element to test.
		 * @returns {boolean} true if the element is the kind of button or link that triggers a cancelUpdate check.
		 */
		function isCancelUpdateButton(element) {
			var result, control;
			if (element && triggerManager.getTrigger(element)) {
				return false;
			}
			CANCEL_BUTTON = CANCEL_BUTTON || new Widget("BUTTON", "wc_btn_cancel");  // do not extend SUBMIT_CONTROL else navigation link buttons cease to trigger unsaved changes warnings
			if ((control = CANCEL_BUTTON.findAncestor(element))) {
				result = control.id;
			}
			return result;
		}

		/**
		 * Click event listener to store the last clicked element in case we need to use it to determine if we have
		 * a cancel button when we are inside a submit event or other function which calls cancelSubmission.
		 *
		 * @function
		 * @private
		 * @param {Event} $event
		 */
		function clickEvent($event) {
			var element = $event.target, form, id;
			if (!$event.defaultPrevented) {
				if ((id = isCancelUpdateButton(element))) {
					buttonClicked = id;
					if ((form = FORM.findAncestor(element)) && instance.cancelSubmission(form)) {
						buttonClicked = null;
						$event.preventDefault();
					}
				} else if (buttonClicked) {
					buttonClicked = null;
				}
			}
		}

		/**
		 * Cancels a form submission based on user response to an unsaved changes warning.
		 *
		 * @function
		 * @private
		 * @param {Element} element Any element within a form.
		 * @param {Element} submitter The element which originated the submission event.
		 * @returns {Boolean} true if the user wants to keep their unsaved changes and cancel the submission, false
		 *    to continue with the submission/navigation.
		 */
		function cancelSubmit(element, submitter) {
			var title = CANCEL_TITLE,
				message = CANCEL_MESSAGE,
				keep = true,
				result,
				form,
				formTitle,
				msg;
			if (!loading) {
				msg = (submitter ? submitter.getAttribute("data-wc-btnmsg") : "");
				if (!msg) {
					if ((form = FORM.findAncestor(element)) && (formTitle = form.getAttribute("title"))) {
						title = formTitle;
					}
					msg = (sprintf.sprintf(message, title));
				}
				keep = window.confirm(msg);
			}
			// if they didn't mean the change, cancel the "cancel" event.
			result = !keep;
			loading = keep;
			return result;
		}

		/**
		 * This AJAX subscriber runs before any content is added to the DOM and tests
		 * all forms in the page to determine if we have to recalculate the initial
		 * state of a form after the ajax action finishes. If the form ancestor of
		 * the ajax target element does not have unsaved changes prior to the AJAX
		 * action then we set a flag to recalculate the 'initial' state allowing for
		 * the changes made by the AJAX action.
		 *
		 * This is to cover the situation where an AJAX transaction occurs which
		 * adds or removes form fields. This will always cause an unsavedChanges warning
		 * because the serialization is different, even if the user does not actually
		 * change anything. This will occur, for example, if a WCancelButton is
		 * triggered in a WDialog before the user makes any changes.
		 *
		 * @function
		 * @private
		 * @param {Element} element The AJAX target element in the DOM prior to the ajax action.
		 */
		function ajaxSubscriber(element/* , documentFragment, action */) {
			var form, key;
			if (element && (form = FORM.findAncestor(element))) {
				if (!form.id) { // not likely, but not serialized as the serialize routine will add an id to the form.
					return;
				}

				key = form.id;
				if (!registry[key]) { // not yet serialized, so no need to recalculate the initial state
					return;
				}

				key = form.id + RECALC;
				if (hasUnsavedChanges(form)) {
					if (registry[key]) {
						delete registry[key];
					}
				} else {
					registry[key] = true;
				}
			}
		}

		/**
		 * This AJAX subscriber fires after the AJAX action has added components to
		 * the DOM. If a RECALC flag has been set for a form then recalculate the
		 * 'initial' state to allow for the changes made by the AJAX action.
		 *
		 * @function
		 * @private
		 * @param {Element} element The AJAX target element in the DOM prior to the AJAX action.
		 */
		function postAjaxSubscriber(element/* , action */) {
			var form, key;
			if (element && (form = FORM.findAncestor(element))) {
				key = form.id + RECALC;
				if (registry[key]) {
					formUpdateManager.clean(form);  // clear the write state info left over from the ajax request
					storeFormState(form);
					delete registry[key];
				} else {
					instance.addElements(element);
				}
			}
		}

		/**
		 * Iterator function for resetAllFormState's forEach. Resets and re-stores the "initial" state of a form if
		 * it has previously been stored.
		 * @function
		 * @private
		 * @param {Element} form A HTML form element.
		 */
		function _resetForm(form) {
			var key = form.id;
			if (!key) {  // form never serialized
				return;
			}
			if (registry[key]) {
				storeFormState(form);
			}
		}

		/**
		 * Stores the form state on page load. A specialisation of storeFormState
		 * which only triggers once per form on page load and checks if the state has
		 * been stored already before storing. This test will prevent the form state
		 * being updated on AJAX initiated postInit without the necessary tests
		 * incorporated in the AJAX subscribers.
		 *
		 * @function
		 * @private
		 * @param {Element} form The form (or form segment) we need to store.
		 */
		function storeInitialFormState(form) {
			var formId;
			if (form) {
				formId = form.id || (form.id = uid());
				if (!registry[formId]) {
					storeFormState(form);
				}
			}
		}

		/**
		 * Allow any other class to do a forced reset of the initial form state. This is required if the initialise
		 * or post-init functions of a class cause an update to the form state before the user interacts with the
		 * form. See {@link module:wc/ui/dateField~processNow}.
		 *
		 * @function
		 * @alias module:wc/ui/cancelUpdate.resetAllFormState
		 */
		this.resetAllFormState = function() {
			Array.prototype.forEach.call(FORM.findDescendants(document), _resetForm);
		};

		/**
		 * Adds these elements to the "initial" state of the form.
		 * Call this carefully - it does not replace existing elements with the same name.
		 *
		 * @param {Element} element  A form control or container.
		 */
		this.addElements = function(element) {
			var i, elements = isSuccessfulElement.getAll(element, true);
			for (i = 0; i < elements.length; i++) {
				this.addElement(elements[i]);
			}
		};

		/**
		 * Adds this element to the "initial" state of the form.
		 * Call this carefully - it does not replace existing elements with the same name.
		 *
		 * @param {Element} element A form control.
		 */
		this.addElement = function (element) {
			var form, nodeList, oldState, newState, newKeys, next, i;
			if (element && (form = element.form) && form.id && (oldState = registry[form.id])) {
				nodeList = [element];
				newState = serialize.serialize(nodeList, true, true, isDirty);
				newKeys = Object.keys(newState);
				for (i = 0; i < newKeys.length; i++) {
					next = newKeys[i];
					if (oldState.hasOwnProperty(next)) {
						oldState[next] = oldState[next].concat(newState[next]);
					} else {
						oldState[next] = newState[next];
					}
				}
			} else {
				console.log("Could not add state for element", element);
			}
		};

		/**
		 * Remove these elements from the "initial" state of the form.
		 *
		 * @param {Element} element A form control or container..
		 */
		this.removeElements = function(element) {
			var i, elements = isSuccessfulElement.getAll(element, true);
			for (i = 0; i < elements.length; i++) {
				this.removeElement(elements[i]);
			}
		};

		/**
		 * Removes this element's current state from the "initial" state of the form.
		 *
		 * @param {Element} element A form control.
		 */
		this.removeElement = function (element) {
			var form, nodeList, oldState, delState, newKeys, next, i, nextVal, delIdx;
			if (element && (form = element.form) && form.id && (oldState = registry[form.id])) {
				nodeList = [element];
				delState = serialize.serialize(nodeList, true, true, isDirty);
				newKeys = Object.keys(delState);
				for (i = 0; i < newKeys.length; i++) {
					next = newKeys[i];
					if (oldState.hasOwnProperty(next)) {
						while (delState[next].length > 0) {
							nextVal = delState[next].pop();
							delIdx = oldState[next].indexOf(nextVal);
							if (delIdx > -1) {
								oldState[next].splice(delIdx, 1);
							}
						}
					}
				}
			} else {
				console.log("Could not remove state for element", element);
			}
		};

		/**
		 * Set up the cancel update controller.
		 * @function
		 * @alias module:wc/ui/cancelUpdate.initialise
		 * @param {Element} element The element being initialised, usually document.body.
		 */
		this.initialise = function(element) {
			event.add(element, "click", clickEvent, -100);
			return i18n.translate(["cancel_title", "cancel_message"]).then(function(translations) {
				CANCEL_TITLE = translations[0];
				CANCEL_MESSAGE = translations[1];
			});
		};

		/**
		 * Late initialisation to store the initial state of all forms in a document and set up any subscribers.
		 * @function
		 * @alias module:wc/ui/cancelUpdate.postInit
		 */
		this.postInit = function() {
			Array.prototype.forEach.call(FORM.findDescendants(document), storeInitialFormState);
			processResponse.subscribe(ajaxSubscriber);  // when ajax occurs, but before stuff is added to the DOM, determine if we need to recalculate the 'initial' state
			processResponse.subscribe(postAjaxSubscriber, true);  // listen for ajax completion and determine if we need to recalculate 'initial' state
		};

		/**
		 * Unsubscribes event listeners etc.
		 * @param {Element} The element being deinitialised, usually document.body.
		 */
		this.deinit = function(element) {
			event.remove(element, "click", clickEvent, -100);
			processResponse.unsubscribe(ajaxSubscriber);
			processResponse.unsubscribe(postAjaxSubscriber, true);
		};

		/**
		 * Determines if we should cancel a form submission (not a submit event). If a cancelUpdateButton has been
		 * clicked this function checks to see if the form has been changed and if so it will confirm with the user
		 * that they wish to continue.
		 *
		 * @function
		 * @alias module:wc/ui/cancelUpdate.cancelSubmission
		 * @param {Element} container An element which is, or is within, a FORM element.
		 * @returns {Boolean} true if the user wishes to cancel or if the form is not valid.
		 */
		this.cancelSubmission = function(container) {
			var submitter, form;
			if (buttonClicked &&
					(submitter = document.getElementById(buttonClicked)) &&
					focus.canFocus(submitter) &&
					(form = FORM.findAncestor(container)) &&
					isCancelUpdateButton(submitter) &&
					hasUnsavedChanges(form) && cancelSubmit(form, submitter)) {
				formUpdateManager.clean(container);
				focus.setFocusRequest(submitter);
				return true;
			}
			return false;
		};
	}

	/**
	 * Provides a mechanism to warn a user of cancel invocation which may result in user initiated changes from being lost or discarded.
	 *
	 * I suggest erring on the side of NOT nagging. Yes, the user may lose work if we get it wrong this way but the alternative is that they get
	 * used to seeing the warning message and ignoring it because it is wrong. The user needs to know that if we show that dialog we really mean
	 * it.
	 *
	 * @todo sort out the method order.
	 * @todo to a large extent we could probably use Element.defaultValue instead.
	 *
	 * @module
	 * @requires module:wc/i18n/i18n
	 * @requires module:wc/ajax/triggerManager
	 * @requires module:wc/dom/uid
	 * @requires module:wc/dom/event
	 * @requires module:wc/dom/initialise
	 * @requires module:wc/dom/serialize
	 * @requires module:wc/dom/isSuccessfulElement
	 * @requires external:lib/sprintf
	 * @requires module:wc/dom/Widget
	 * @requires module:wc/dom/formUpdateManager
	 * @requires module:wc/dom/focus
	 */
	instance = new CancelUpdateControl();
	return initialise.register(instance);
});
