/*
 * JavaScript tracker for Snowplow: forms.js
 * 
 * Significant portions copyright 2010 Anthon Pang. Remainder copyright 
 * 2012-2014 Snowplow Analytics Ltd. All rights reserved. 
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are 
 * met: 
 *
 * * Redistributions of source code must retain the above copyright 
 *   notice, this list of conditions and the following disclaimer. 
 *
 * * Redistributions in binary form must reproduce the above copyright 
 *   notice, this list of conditions and the following disclaimer in the 
 *   documentation and/or other materials provided with the distribution. 
 *
 * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the
 *   names of their contributors may be used to endorse or promote products
 *   derived from this software without specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

var forEach = require('lodash/forEach'),
	filter = require('lodash/filter'),
	find = require('lodash/find'),
	helpers = require('./lib/helpers'),
	object = typeof exports !== 'undefined' ? exports : this;

/**
 * Object for handling automatic form tracking
 *
 * @param object core The tracker core
 * @param string trackerId Unique identifier for the tracker instance, used to mark tracked elements
 * @param function contextAdder Function to add common contexts like PerformanceTiming to all events
 * @return object formTrackingManager instance
 */
object.getFormTrackingManager = function (core, trackerId, contextAdder) {

	// Tag names of mutable elements inside a form
	var innerElementTags = ['textarea', 'input', 'select'];

	// Used to mark elements with event listeners
	var trackingMarker = trackerId + 'form';

	// Filter to determine which forms should be tracked
	var formFilter = function () { return true };

	// Filter to determine which form fields should be tracked
	var fieldFilter = function () { return true };

	// Default function applied to all elements, optionally overridden by transform field
	var fieldTransform = function (x) { return x };

	/*
	 * Get an identifier for a form, input, textarea, or select element
	 */
	function getFormElementName(elt) {
		return elt[find(['name', 'id', 'type', 'nodeName'], function (propName) {

			// If elt has a child whose name is "id", that element will be returned
			// instead of the actual id of elt unless we ensure that a string is returned
			return elt[propName] && typeof elt[propName] === 'string';
		})];
	}

	/*
	 * Identifies the parent form in which an element is contained
	 */
	function getParentFormName(elt) {
		while (elt && elt.nodeName && elt.nodeName.toUpperCase() !== 'HTML' && elt.nodeName.toUpperCase() !== 'FORM') {
			elt = elt.parentNode;
		}
		if (elt && elt.nodeName && elt.nodeName.toUpperCase() === 'FORM') {
			return getFormElementName(elt);
		}
	}

	/*
	 * Returns a list of the input, textarea, and select elements inside a form along with their values
	 */
	function getInnerFormElements(elt) {
		var innerElements = [];
		forEach(innerElementTags, function (tagname) {

			var trackedChildren = filter(elt.getElementsByTagName(tagname), function (child) {
				return child.hasOwnProperty(trackingMarker);
			});

			forEach(trackedChildren, function (child) {
				if (child.type === 'submit') {
					return;
				}
				var elementJson = {
					name: getFormElementName(child),
					value: child.value,
					nodeName: child.nodeName
				};
				if (child.type && child.nodeName.toUpperCase() === 'INPUT') {
					elementJson.type = child.type;
				}

				if ((child.type === 'checkbox' || child.type === 'radio') && !child.checked) {
					elementJson.value = null;
				}
				innerElements.push(elementJson);
			});
		});

		return innerElements;
	}

	/*
	 * Return function to handle form field change event
	 */
	function getFormChangeListener(event_type, context) {
		return function (e) {
			var elt = e.target;
			var type = (elt.nodeName && elt.nodeName.toUpperCase() === 'INPUT') ? elt.type : null;
			var value = (elt.type === 'checkbox' && !elt.checked) ? null : fieldTransform(elt.value);
			if (event_type === 'change_form' || (type !== 'checkbox' && type !== 'radio')) {
				core.trackFormFocusOrChange(event_type, getParentFormName(elt), getFormElementName(elt), elt.nodeName, type, helpers.getCssClasses(elt), value, contextAdder(helpers.resolveDynamicContexts(context, elt, type, value)));
			}
		};
	}

	/*
	 * Return function to handle form submission event
	 */
	function getFormSubmissionListener(context) {
		return function (e) {
			var elt = e.target;
			var innerElements = getInnerFormElements(elt);
			forEach(innerElements, function (innerElement) {
				innerElement.value = fieldTransform(innerElement.value);
			});
			core.trackFormSubmission(getFormElementName(elt), helpers.getCssClasses(elt), innerElements, contextAdder(helpers.resolveDynamicContexts(context, elt, innerElements)));
		};
	}

	return {

		/*
		 * Configures form tracking: which forms and fields will be tracked, and the context to attach
		 */
		configureFormTracking: function (config) {
			if (config) {
				formFilter = helpers.getFilter(config.forms, true);
				fieldFilter = helpers.getFilter(config.fields, false);
				fieldTransform = helpers.getTransform(config.fields);
			}
		},

		/*
		 * Add submission event listeners to all form elements
		 * Add value change event listeners to all mutable inner form elements
		 */
		addFormListeners: function (context) {
			forEach(document.getElementsByTagName('form'), function (form) {
				if (formFilter(form) && !form[trackingMarker]) {

					forEach(innerElementTags, function (tagname) {
						forEach(form.getElementsByTagName(tagname), function (innerElement) {
							if (fieldFilter(innerElement) && !innerElement[trackingMarker] && innerElement.type.toLowerCase() !== 'password') {
								helpers.addEventListener(innerElement, 'focus', getFormChangeListener('focus_form', context), false);
								helpers.addEventListener(innerElement, 'change', getFormChangeListener('change_form', context), false);
								innerElement[trackingMarker] = true;
							}
						});
					});

					helpers.addEventListener(form, 'submit', getFormSubmissionListener(context));
					form[trackingMarker] = true;
				}
			});
		}
	};
};
