/*!
	SerializeJSON jQuery plugin.
	https://github.com/marioizquierdo/jquery.serializeJSON
	version 3.2.1 (Feb, 2021)

	Copyright (c) 2012-2021 Mario Izquierdo
	Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
	and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
*/
(function( factory ) {
	var jQuery;

	/* global define, require, module */
	if ( 'function' === typeof define && define.amd ) { // AMD. Register as an anonymous module.
		define( ['jquery'], factory );
	} else if ( 'object' === typeof exports ) { // Node/CommonJS.
		jQuery         = require( 'jquery' );
		module.exports = factory( jQuery );
	} else { // Browser globals (zepto supported).
		factory( window.jQuery || window.Zepto || window.$ ); // Zepto supported on browsers as well.
	}

}( function( $ ) {
	'use strict';

	var rCRLF           = /\r?\n/g;
	var rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i;
	var rsubmittable    = /^(?:input|select|textarea|keygen)/i;
	var rcheckableType  = /^(?:checkbox|radio)$/i;

	$.fn.serializeJSON = function( options ) {
		var f             = $.serializeJSON;
		var _this         = this; // NOTE: the set of matched elements is most likely a form, but it could also be a group of inputs.
		var opts          = f.setupOpts( options ); // Validate options and apply defaults.
		var typeFunctions = $.extend( {}, opts.defaultTypes, opts.customTypes );

		// Make a list with {name, value, el} for each input element.
		var serializedArray = f.serializeArray( _this, opts );

		// Convert the serializedArray into a serializedObject with nested keys.
		var serializedObject = {};
		$.each(
			serializedArray,
			function( _i, obj ) {
				var nameSansType = obj.name;
				var type         = $( obj.el ).attr( 'data-value-type' );
				var p;
				var typedValue;
				var keys;

				if ( ! type && ! opts.disableColonTypes ) { // Try getting the type from the input name.
					p            = f.splitType( obj.name ); // "foo:string" => ["foo", "string"].
					nameSansType = p[0];
					type         = p[1];
				}
				if ( 'skip' === type ) {
					return; // Ignore fields with type skip.
				}
				if ( ! type ) {
					type = opts.defaultType; // "string" by default
				}

				typedValue = f.applyTypeFunc( obj.name, obj.value, type, obj.el, typeFunctions ); // Parse type as string, number, etc.

				if ( ! typedValue && f.shouldSkipFalsy( obj.name, nameSansType, type, obj.el, opts ) ) {
					return; // Ignore falsy inputs if specified in the options.
				}

				keys = f.splitInputNameIntoKeysArray( nameSansType );
				f.deepSet( serializedObject, keys, typedValue, opts );
			}
		);

		return serializedObject;
	};

	// Use $.serializeJSON as namespace for the auxiliary functions
	// and to define defaults.
	$.serializeJSON = {
		defaultOptions: {}, // Reassign to override option defaults for all serializeJSON calls.

		defaultBaseOptions: { // Do not modify, use defaultOptions instead.
			checkboxUncheckedValue: undefined, // To include that value for unchecked checkboxes (instead of ignoring them).
			useIntKeysAsArrayIndex: false, // Tip: name="foo[2]" value="v" => {foo: [null, null, "v"]}, instead of {foo: ["2": "v"]}.

			skipFalsyValuesForTypes: [], // Skip serialization of falsy values for listed value types.
			skipFalsyValuesForFields: [], // Skip serialization of falsy values for listed field names.

			disableColonTypes: false, // Do not interpret ":type" suffix as a type.
			customTypes: {}, // Extends defaultTypes.
			defaultTypes: {
				'string': function( str ) {
					return String( str );
				},
				'number': function( str ) {
					return Number( str );
				},
				'boolean': function( str ) {
					var falses = ['false', 'null', 'undefined', '', '0'];
					return -1 === falses.indexOf( str );
				},
				'null': function( str ) {
					var falses = ['false', 'null', 'undefined', '', '0'];
					return -1 === falses.indexOf( str ) ? str : null;
				},
				'array': function( str ) {
					return JSON.parse( str );
				},
				'object': function( str ) {
					return JSON.parse( str );
				},
				'skip': null // Skip is a special type used to ignore fields.
			},
			defaultType: 'string'
		},

		// Validate and set defaults.
		setupOpts: function( options ) {
			var f;
			var validOpts;
			var opt;

			if ( null == options ) {
				options = {};
			}

			f = $.serializeJSON;

			// Validate.
			validOpts = [
				'checkboxUncheckedValue',
				'useIntKeysAsArrayIndex',

				'skipFalsyValuesForTypes',
				'skipFalsyValuesForFields',

				'disableColonTypes',
				'customTypes',
				'defaultTypes',
				'defaultType'
			];
			for ( opt in options ) {
				if ( validOpts.indexOf( opt ) === -1 ) {
					throw new Error( 'serializeJSON ERROR: invalid option \'' + opt + '\'. Please use one of ' + validOpts.join( ', ' ) );
				}
			}

			// Helper to get options or defaults.
			return $.extend( {}, f.defaultBaseOptions, f.defaultOptions, options );
		},

		// Just like jQuery's serializeArray method, returns an array of objects with name and value.
		// but also includes the dom element (el) and is handles unchecked checkboxes if the option or data attribute are provided.
		serializeArray: function( _this, opts ) {
			var f;
			var elements;

			if ( null == opts ) {
				opts = {};
			}

			f = $.serializeJSON;

			return _this.map(
				function() {
					elements = $.prop( this, 'elements' ); // Handle propHook "elements" to filter or add form elements.
					return elements ? $.makeArray( elements ) : this;

				}
			).filter(
				function() {
					var $el  = $( this );
					var type = this.type;

					// Filter with the standard W3C rules for successful controls: http://www.w3.org/TR/html401/interact/forms.html#h-17.13.2.
					return this.name && // Must contain a name attribute.
						! $el.is( ':disabled' ) && // Must not be disable (use .is(":disabled") so that fieldset[disabled] works).
						rsubmittable.test( this.nodeName ) && ! rsubmitterTypes.test( type ) && // only serialize submittable fields (and not buttons).
						(this.checked || ! rcheckableType.test( type ) || f.getCheckboxUncheckedValue( $el, opts ) != null); // skip unchecked checkboxes (unless using opts).

				}
			).map(
				function( _i, el ) {
					var $el  = $( this );
					var val  = $el.val();
					var type = this.type; // "input", "select", "textarea", "checkbox", etc.

					if ( null == val ) {
						return null;
					}

					if ( rcheckableType.test( type ) && ! this.checked ) {
						val = f.getCheckboxUncheckedValue( $el, opts );
					}

					if ( isArray( val ) ) {
						return $.map(
							val,
							function( val ) {
								return {name: el.name, value: val.replace( rCRLF, '\r\n' ), el: el};
							}
						);
					}

					return {name: el.name, value: val.replace( rCRLF, '\r\n' ), el: el};

				}
			).get();
		},

		getCheckboxUncheckedValue: function( $el, opts ) {
			var val = $el.attr( 'data-unchecked-value' );
			if ( null == val ) {
				val = opts.checkboxUncheckedValue;
			}
			return val;
		},

		// Parse value with type function.
		applyTypeFunc: function( name, strVal, type, el, typeFunctions ) {
			var typeFunc = typeFunctions[type];
			if ( ! typeFunc ) { // quick feedback to user if there is a typo or missconfiguration.
				throw new Error(
					'serializeJSON ERROR: Invalid type ' + type + ' found in input name \'' + name + '\', please use one of ' + objectKeys( typeFunctions )
					.join( ', ' )
				);
			}
			return typeFunc( strVal, el );
		},

		// Splits a field name into the name and the type. Examples:
		// "foo"           =>  ["foo", ""].
		// "foo:boolean"   =>  ["foo", "boolean"].
		// "foo[bar]:null" =>  ["foo[bar]", "null"].
		splitType: function( name ) {
			var parts = name.split( ':' );
			var t;

			if ( parts.length > 1 ) {
				t = parts.pop();
				return [parts.join( ':' ), t];
			} else {
				return [name, ''];
			}
		},

		// Check if this input should be skipped when it has a falsy value,
		// depending on the options to skip values by name or type, and the data-skip-falsy attribute.
		shouldSkipFalsy: function( name, nameSansType, type, el, opts ) {
			var skipFromDataAttr = $( el ).attr( 'data-skip-falsy' );
			var optForFields;
			var optForTypes;

			if ( skipFromDataAttr != null ) {
				return skipFromDataAttr !== 'false'; // any value is true, except the string "false".
			}

			optForFields = opts.skipFalsyValuesForFields;
			if ( optForFields && (optForFields.indexOf( nameSansType ) !== -1 || optForFields.indexOf( name ) !== -1) ) {
				return true;
			}

			optForTypes = opts.skipFalsyValuesForTypes;
			return ! ! ( optForTypes && optForTypes.indexOf( type ) !== -1 );
		},

		// Split the input name in programmatically readable keys.
		// Examples:
		// "foo"              => ["foo"]
		// "[foo]"            => ["foo"]
		// "foo[inn][bar]"    => ["foo", "inn", "bar"]
		// "foo[inn[bar]]"    => ["foo", "inn", "bar"]
		// "foo[inn][arr][0]" => ["foo", "inn", "arr", "0"]
		// "arr[][val]"       => ["arr", "", "val"].
		splitInputNameIntoKeysArray: function( nameWithNoType ) {
			var keys = nameWithNoType.split( '[' ); // split string into array.

			keys = $.map(
				keys,
				function( key ) {
					return key.replace( /\]/g, '' );
				}
			); // Remove closing brackets.
			if ( keys[0] === '' ) {
				keys.shift();
			} // Ensure no opening bracket ("[foo][inn]" should be same as "foo[inn]")
			return keys;
		},

		// Set a value in an object or array, using multiple keys to set in a nested object or array.
		// This is the main function of the script, that allows serializeJSON to use nested keys.
		// Examples:
		//
		// deepSet(obj, ["foo"], v)               // obj["foo"] = v
		// deepSet(obj, ["foo", "inn"], v)        // obj["foo"]["inn"] = v // Create the inner obj["foo"] object, if needed
		// deepSet(obj, ["foo", "inn", "123"], v) // obj["foo"]["arr"]["123"] = v //
		//
		// deepSet(obj, ["0"], v)                                   // obj["0"] = v
		// deepSet(arr, ["0"], v, {useIntKeysAsArrayIndex: true})   // arr[0] = v
		// deepSet(arr, [""], v)                                    // arr.push(v)
		// deepSet(obj, ["arr", ""], v)                             // obj["arr"].push(v)
		//
		// arr = [];
		// deepSet(arr, ["", v]          // arr => [v]
		// deepSet(arr, ["", "foo"], v)  // arr => [v, {foo: v}]
		// deepSet(arr, ["", "bar"], v)  // arr => [v, {foo: v, bar: v}]
		// deepSet(arr, ["", "bar"], v)  // arr => [v, {foo: v, bar: v}, {bar: v}].
		deepSet: function( o, keys, value, opts ) {
			if ( null == opts ) {
				opts = {};
			}
			var f = $.serializeJSON;
			if ( isUndefined( o ) ) {
				throw new Error( 'ArgumentError: param \'o\' expected to be an object or array, found undefined' );
			}
			if ( ! keys || 0 === keys.length ) {
				throw new Error( 'ArgumentError: param \'keys\' expected to be an array with least one element' );
			}

			var key = keys[0];

			// Only one key, then it's not a deepSet, just assign the value in the object or add it to the array.
			if ( 1 === keys.length ) {
				if ( key === '' ) { // Push values into an array (o must be an array).
					o.push( value );
				} else {
					o[key] = value; // Keys can be object keys (strings) or array indexes (numbers).
				}
				return;
			}

			var nextKey  = keys[1]; // Nested key.
			var tailKeys = keys.slice( 1 ); // List of all other nested keys (nextKey is first).

			if ( key === '' ) { // Push nested objects into an array (o must be an array).
				var lastIdx = o.length - 1;
				var lastVal = o[lastIdx];

				// if the last value is an object or array, and the new key is not set yet.
				if ( isObject( lastVal ) && isUndefined( f.deepGet( lastVal, tailKeys ) ) ) {
					key = lastIdx; // then set the new value as a new attribute of the same object.
				} else {
					key = lastIdx + 1; // otherwise, add a new element in the array.
				}
			}

			if ( nextKey === '' ) { // "" is used to push values into the nested array "array[]".
				if ( isUndefined( o[key] ) || ! isArray( o[key] ) ) {
					o[key] = []; // define (or override) as array to push values.
				}
			} else {
				if ( opts.useIntKeysAsArrayIndex && isValidArrayIndex( nextKey ) ) { // if 1, 2, 3 ... then use an array, where nextKey is the index.
					if ( isUndefined( o[key] ) || ! isArray( o[key] ) ) {
						o[key] = []; // Define (or override) as array, to insert values using int keys as array indexes.
					}
				} else { // nextKey is going to be the nested object's attribute.
					if ( isUndefined( o[key] ) || ! isObject( o[key] ) ) {
						o[key] = {}; // Define (or override) as object, to set nested properties.
					}
				}
			}

			// Recursively set the inner object.
			f.deepSet( o[key], tailKeys, value, opts );
		},

		deepGet: function( o, keys ) {
			var f = $.serializeJSON;
			var tailKeys;

			if ( isUndefined( o ) || isUndefined( keys ) || keys.length === 0 || ( ! isObject( o ) && ! isArray( o ) ) ) {
				return o;
			}
			var key = keys[0];
			if ( '' === key ) { // "" means next array index (used by deepSet)
				return undefined;
			}
			if ( 1 === keys.length ) {
				return o[key];
			}

			tailKeys = keys.slice( 1 );
			return f.deepGet( o[key], tailKeys );
		}
	};

	// Polyfill Object.keys to get option keys in IE<9.
	var objectKeys = function( obj ) {
		if ( Object.keys ) {
			return Object.keys( obj );
		} else {
			var key, keys = [];
			for ( key in obj ) {
				keys.push( key );
			}
			return keys;
		}
	};

	var isObject = function( obj ) {
		return obj === Object( obj );
	}; // true for Objects and Arrays.

	var isUndefined = function( obj ) {
		return obj === void 0;
	}; // safe check for undefined values.

	var isValidArrayIndex = function( val ) {
		return /^[0-9]+$/.test( String( val ) );
	}; // 1,2,3,4 ... are valid array indexes.

	var isArray = Array.isArray || function( obj ) {
		return '[object Array]' === Object.prototype.toString.call( obj );
	};
} ) );
