// Utility variables
var GF_CONDITIONAL_INSTANCE = false;
var GF_CONDITIONAL_INSTANCES_COLLECTION = [];
var FOCUSABLE_ELEMENTS      = [ 'a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', 'iframe', 'object', 'embed', '[contenteditable]', '[tabindex]:not([tabindex^="-"])' ];
var TAB_KEY                 = 9;
var ESCAPE_KEY              = 27;
var FOCUSED_BEFORE_DIALOG   = null;
var FOCUSED_BEFORE_RENDER = null;

/**
 * Set the focus to the first focusable child of the given element
 *
 * @param {Element} node The element to focus within.
 * @param {Event} event Passed event from some handlers
 */
function setFocusToFirstItem( node, event ) {
	if ( event && event.target && ! gform.tools.getClosest( event.target, '#' + node.id ) ) {
		return;
	}
	var focusableChildren = getFocusableChildren( node );

	if ( focusableChildren.length ) {
		focusableChildren[ 0 ].focus();
	}
}

/**
 * Get the focusable children for the provided node.
 *
 * @param {Element} node The element to search within.
 *
 * @return {Element[]}
 */
function getFocusableChildren( node ) {
	return $$( FOCUSABLE_ELEMENTS.join( ',' ), node ).filter( function( child ) {
		return !!(child.offsetWidth || child.offsetHeight || child.getClientRects().length);
	} );
}

/**
 * Trap the focus inside the given element
 *
 * @param {Element} node  The node to trap within.
 * @param {Event}   event The JS event.
 */
function trapTabKey( node, event ) {
	var focusableChildren = getFocusableChildren( node );
	var focusedItemIndex  = focusableChildren.indexOf( document.activeElement );

	// If the SHIFT key is being pressed while tabbing (moving backwards) and
	// the currently focused item is the first one, move the focus to the last
	// focusable item from the dialog element
	if ( event.shiftKey && focusedItemIndex === 0 ) {
		focusableChildren[ focusableChildren.length - 1 ].focus();
		event.preventDefault();
		// If the SHIFT key is not being pressed (moving forwards) and the currently
		// focused item is the last one, move the focus to the first focusable item
		// from the dialog element
	} else if ( !event.shiftKey && focusedItemIndex === focusableChildren.length - 1 ) {
		focusableChildren[ 0 ].focus();
		event.preventDefault();
	}
}

/**
 * Query the DOM for nodes matching the given selector, scoped to context (or
 * the whole document)
 *
 * @param {String}  selector             The selector to use.
 * @param {Element} [context = document] The context to search within.
 *
 * @return {Array<Element>}
 */
function $$( selector, context ) {
	return gform.tools.convertElements( (context || document).querySelectorAll( selector ) );
}

/**
 * Render the given view HTML with the provided token replacement configuration.
 *
 * @param {string}  html      The HTML the view should render.
 * @param {Element} container The container in which to render the view.
 * @param {object}  config    An object representing key/value pairs to use for token replacement.
 * @param {bool}    echo      Whether to echo the resulting markup - will return the markup if set to false.
 *
 * @return {boolean|string}
 */
function renderView( html, container, config, echo ) {
	FOCUSED_BEFORE_RENDER = document.activeElement;

	var parsed = html;
	for ( var key in config ) {
		var val       = config[ key ];
		var search    = '{{ ' + key + ' }}';
		var searchRgx = new RegExp( search, 'g' );
		parsed        = parsed.replace( searchRgx, val );
	}

	if ( !echo ) {
		return parsed;
	}

	container.innerHTML = parsed;

	if ( FOCUSED_BEFORE_RENDER.id ) {
		window.setTimeout( function() {
			if ( document.getElementById( FOCUSED_BEFORE_RENDER.id ) == null ) {
				return;
			}

			document.getElementById( FOCUSED_BEFORE_RENDER.id ).focus();
		}, 10 );
	}

	return true;
}

/**
 * Get a field object from the given ID.
 *
 *
 * @param fieldId
 * @return {boolean|*|T}
 */
function getFieldById( fieldId ) {
	var found = this.form.fields.filter( function( field ) {
		return field.id == fieldId;
	} );

	if ( !found.length ) {
		return false;
	}

	return found[ 0 ];
}

/**
 * Get the correct field ID to use as a default value when adding a new rule:
 *
 * - If the field has no child inputs, return the field ID
 * - If the field has child inputs, but all are set to be hidden, return field ID
 * - Otherwise, return the ID of the first non-hidden child input.
 *
 * @param {object} field The field being rendered.
 *
 * @return {string|integer}
 */
function getCorrectDefaultFieldId( field ) {
	if ( ! field ) {
		return null;
	}

	if ( field.type === 'checkbox' || field.type === 'radio' || field.inputType === 'checkbox' || field.inputType === 'radio' || ! field.inputs || ! field.inputs.length ) {
		return field.id;
	}

	var options = field.inputs.filter( function( input ) {
		return ! input.isHidden;
	} );

	if ( ! options.length ) {
		return field.id;
	}

	return options[0].id;
}

/**
 * Get the available options for a given select field.
 *
 * @param {object} field The field being rendered.
 * @param {mixed}  value The currently-selected value.
 *
 * @return {[]}
 */
function getOptionsFromSelect( field, value ) {
	var options = [];

	var emptyLabel = gf_vars.emptyChoice;

	if ( field.placeholder ) {
		emptyLabel = field.placeholder;
	}

	var emptyChoiceConfig = {
		label: emptyLabel,
		value: '',
		selected: '' === value ? 'selected="selected"' : '',
	};

	options.push( emptyChoiceConfig );

	for ( var i = 0; i < field.choices.length; i++ ) {
		var choice = field.choices[ i ];
		var config = {
			label: choice.text,
			value: choice.value,
			selected: choice.value == value ? 'selected="selected"' : '',
		};

		options.push( config );

	}

	return options;
}

/**
 * Get the available post category options.
 *
 * @param {object} field The field being rendered.
 * @param {mixed}  value The currently-selected value.
 *
 * @return {[]}
 */
function getCategoryOptions( field, value ) {
	var cats    = gf_vars.conditionalLogic.categories;
	var options = [];

	for ( var i = 0; i < cats.length; i++ ) {
		var cat    = cats[ i ];
		var config = {
			label: cat.label,
			value: cat.term_id,
			selected: cat.term_id == value ? 'selected="selected"' : '',
		}

		options.push( config );
	}

	return options;
}

/**
 * Get the available post category options.
 *
 * @param {object} field   The field being rendered.
 * @param {string} inputId The inputId of the current field.
 * @param {mixed}  value   The currently-selected value.
 *
 * @return {[]}
 */
function getAddressOptions( field, inputId, value ) {
	var options        = [];
	var addressOptions = gf_vars.conditionalLogic.addressOptions;

	if ( !field.inputs ) {
		return options;
	}

	if ( !addressOptions[ field.addressType ] ) {
		return [];
	}

	var fieldAddressOptions = addressOptions[ field.addressType ];

	// Associative arrays are expected to have alphanumeric keys (country codes).
	// If asort() is used in the gform_countries filter, the resulting array will be
	// associative even if the original array was plain, we only need the values.
	if ( ! Array.isArray( fieldAddressOptions ) ) {
		var allNumericKeys = true;
		for ( var key in fieldAddressOptions ) {
			if ( isNaN( key ) ) {
				allNumericKeys = false;
				break;
			}
		}
		if ( allNumericKeys ) {
			fieldAddressOptions = Object.values( fieldAddressOptions );
		}
	}

	// True associative arrays (country codes) are handled here.
	if ( ! Array.isArray( fieldAddressOptions ) ) {

		for ( var locale in fieldAddressOptions ) {
			var group = fieldAddressOptions[ locale ];
			var config;

			if( Array.isArray( group ) ) {
				// Address options are grouped by a key; parse them as sub-items.
				for (var i = 0; i < group.length; i++) {
					var option = group[i];

					config = {
						label: option,
						value: option,
						selected: option == value ? 'selected="selected"' : '',
					}
					options.push(config);
				}
			} else {
				config = {
					label: group,
					value: locale,
					selected: locale == value ? 'selected="selected"' : '',
				}
				options.push(config);
			}
		}

		return options;
	}

	// Address options are just a single-level array; loop through them.
	for ( var i = 0; i < fieldAddressOptions.length; i++ ) {
		var option = fieldAddressOptions[ i ];

		var config = {
			label: option,
			value: option,
			selected: option == value ? 'selected="selected"' : '',
		}

		options.push( config );
	}

	return options;
}

/**
 * Generate a GFConditionalLogic instance from the given field ID and object type.
 *
 * @param {int}    fieldId    The ID for the current field.
 * @param {string} objectType The object type of the current field.
 */
function generateGFConditionalLogic( fieldId, objectType ) {
	if ( GF_CONDITIONAL_INSTANCE && GF_CONDITIONAL_INSTANCE.fieldId != fieldId  ) {
		GF_CONDITIONAL_INSTANCES_COLLECTION.forEach( function( instance, instanceIndex ) {
			instance.hideFlyout();
			instance.removeEventListeners();
			instance.deactivated = true;
		});
	}

	GF_CONDITIONAL_INSTANCE = new GFConditionalLogic( fieldId, objectType );

	GF_CONDITIONAL_INSTANCES_COLLECTION = GF_CONDITIONAL_INSTANCES_COLLECTION.filter( function( instance ) {
		return instance.deactivated !== true;
	});

	GF_CONDITIONAL_INSTANCES_COLLECTION.push( GF_CONDITIONAL_INSTANCE );
}

/**
 * Determine whether a click event is from a valid flyout element.
 *
 * @param {Event} e The Event object.
 *
 * @return {boolean}
 */
function isValidFlyoutClick( e ) {
	var isValidFlyoutClick = (
		'jsConditonalToggle' in e.target.dataset ||
		'jsAddRule' in e.target.dataset ||
		'jsDeleteRule' in e.target.dataset ||
		e.target.classList.contains( 'gform-field__toggle-input' ) ||
		e.target.closest( '.gform-dialog__mask' ) !== null
	);
	return gform.applyFilters( 'gform_conditional_logic_is_valid_flyout_click', isValidFlyoutClick, e );
}

/**
 * Determine whether a given rule needs to present a text input for the value.
 *
 * @param {object} e The rule object.
 *
 * @return {boolean}
 */
function ruleNeedsTextValue( rule ) {
	return ['contains', 'starts_with', 'ends_with', '<', '>' ].indexOf ( rule.operator ) !== -1;
}

/**
 * Class GFConditionalLogic
 *
 * A JS class encapsulating all of the logic and state for a conditional flyout.
 *
 * @param {int}    fieldId    The ID for the current field.
 * @param {string} objectType The object type of the current field.
 *
 * @constructor
 */
function GFConditionalLogic( fieldId, objectType ) {

	// State and Flyout data
	this.fieldId    = fieldId;
	this.form       = form;
	this.objectType = objectType;
	this.els        = this.gatherElements();
	this.state      = this.getStateForField( fieldId );
	this.visible    = document.querySelector( '.editor-sidebar .conditional_logic_flyout_container.anim-in-active' ) ? true : false;

	// Prebind event listener callbacks to maintain references
	this._handleToggleClick    = this.handleToggleClick.bind( this );
	this._handleFlyoutChange   = this.handleFlyoutChange.bind( this );
	this._handleBodyClick      = this.handleBodyClick.bind( this );
	this._handleAccordionClick = this.handleAccordionClick.bind( this );
	this._handleSidebarClick   = this.handleSidebarClick.bind( this );
	this._maintainFocus        = this._maintainFocus.bind( this );
	this._bindKeypress         = this._bindKeypress.bind( this );

	this.init();
}

/**
 * Render the sidebar view.
 */
GFConditionalLogic.prototype.renderSidebar = function() {
	var config = {
		title: this.getAccordionTitle(),
		toggleText: gf_vars.configure + ' ' + gf_vars.conditional_logic_text,
		active_class: this.isEnabled() ? 'gform-status--active' : '',
		active_text: this.isEnabled() ? 'Active' : 'Inactive',
		desc_class: GetFirstRuleField() <= 0 ? 'active' : '',
		toggle_class: GetFirstRuleField() <= 0 ? '' : 'active',
		desc: gf_vars.conditionalLogic.conditionalLogicHelperText,
	}

	var html = gf_vars.conditionalLogic.views.sidebar;

	renderView( html, this.els[ this.objectType ], config, true );
};

/**
 * Render the flyout view.
 */
GFConditionalLogic.prototype.renderFlyout = function() {
	var config = {
		objectType: this.objectType,
		fieldId: this.fieldId,
		checked: this.state.enabled ? 'checked' : '',
		activeClass: this.visible ? 'active' : 'inactive',
		enabledText: this.state.enabled ? gf_vars.enabled : gf_vars.disabled,
		configure: gf_vars.configure,
		conditionalLogic: gf_vars.conditional_logic_text,
		enable: gf_vars.enable,
		desc: gf_vars.conditional_logic_desc,
		main: this.renderMainControls( false ),
	};

	var html = gf_vars.conditionalLogic.views.flyout;

	renderView( html, this.els.flyouts[ this.objectType ], config, true );

	gform.tools.trigger( 'gform_render_simplebars' );
};

/**
 * Render the main controls.
 *
 * @param {boolean} echo
 *
 * @return {boolean|string}
 */
GFConditionalLogic.prototype.renderLogicDescription = function() {

	var config = {
		actionType: this.state.actionType,
		logicType: this.state.logicType,
		objectTypeText: this.getObjectTypeText(),
		objectShowText: this.getObjectShowText(),
		objectHideText: this.getObjectHideText(),
		matchText: gf_vars.ofTheFollowingMatch,
		allText: gf_vars.all,
		anyText: gf_vars.any,
		hideSelected: this.state.actionType === 'hide' ? 'selected="selected"' : '',
		showSelected: this.state.actionType === 'show' ? 'selected="selected"' : '',
		allSelected: this.state.logicType === 'all' ? 'selected="selected"' : '',
		anySelected: this.state.logicType === 'any' ? 'selected="selected"' : '',
	};

	var html = gf_vars.conditionalLogic.views.logicDescription;

	var markup = renderView( html, this.els.flyouts[ this.objectType ], config, false );

	/**
	 * @filter gform_conditional_logic_description
	 *
	 * Allows add-ons to modify the markup returned for the Conditional Logic description area.
	 *
	 * @since unknown
	 * @since 2.5 descPieces passed as empty array
	 *
	 * @param {string} markup The current markup HTML for the description
	 * @param {array} descPieces The individual markup pieces which make up the final markup (empty here)
	 * @param {string} objectType The current object type
	 * @param {object} this The current object
	 *
	 * @return {string}
	 */
	return gform.applyFilters( 'gform_conditional_logic_description', markup, [], this.objectType, this );
};

/**
 * Render the main controls.
 *
 * @param {boolean} echo
 *
 * @return {boolean|string}
 */
GFConditionalLogic.prototype.renderMainControls = function( echo ) {

	var config = {
		enabledClass: this.state.enabled ? 'active' : '',
		logicDescription: this.renderLogicDescription(),
		a11yWarning: this.objectType === 'button' ? gf_vars.conditionalLogic.views.a11yWarning : '',
		a11yWarningText: gf_vars.conditional_logic_a11y,
	};

	var html = gf_vars.conditionalLogic.views.main;

	if ( ! echo ) {
		return renderView( html, this.els.flyouts[ this.objectType ], config, false );
	}

	renderView( html, this.els.flyouts[ this.objectType ].querySelector( '.conditional_logic_flyout__main' ), config, true );
};

/**
 * Render the field options for the given rule.
 *
 * @param {object} rule The rule data to render.
 *
 * @return {string}
 */
GFConditionalLogic.prototype.renderFieldOptions = function( rule ) {
	var html     = '';
	var template = gf_vars.conditionalLogic.views.option;
	var options  = [];

	for ( var i = 0; i < form.fields.length; i++ ) {

		var field = form.fields[ i ];

		if ( !IsConditionalLogicField( field ) ) {
			continue;
		}

		if ( field.inputs && jQuery.inArray( GetInputType( field ), [ 'checkbox', 'email', 'consent' ] ) == -1 && GetInputType( field ) !== 'radio' ) {
			for ( var j = 0; j < field.inputs.length; j++ ) {
				var input = field.inputs[ j ];

				if ( input.isHidden ) {
					continue;
				}

				var config = {
					label: GetLabel( field, input.id ),
					value: input.id,
					selected: input.id == rule.fieldId ? 'selected="selected"' : '',
				};

				options.push( config );
			}
		} else {
			var config = {
				label: GetLabel( field ),
				value: field.id,
				selected: field.id == rule.fieldId ? 'selected="selected"' : '',
			};

			options.push( config );
		}
	}

	options = gform.applyFilters( 'gform_conditional_logic_fields', options, form, rule.fieldId );

	for ( var i = 0; i < options.length; i++ ) {
		var config = options[ i ];

		if ( ! config.selected ) {
			config.selected = config.value == rule.fieldId ? 'selected="selected"' : '';
		}

		html += renderView( template, null, config, false );
	}

	return html;
};

/**
 * Render operator options for the given rule.
 *
 * @param {object} rule The rule data to render.
 *
 * @return {string}
 */
GFConditionalLogic.prototype.renderOperatorOptions = function( rule ) {
	var html      = '';
	var template  = gf_vars.conditionalLogic.views.option;
	var operators = {
		is: gf_vars.is,
		isnot: gf_vars.isNot,
		'>': gf_vars.greaterThan,
		'<': gf_vars.lessThan,
		contains: gf_vars.contains,
		starts_with: gf_vars.startsWith,
		ends_with: gf_vars.endsWith,
	};

	operators = gform.applyFilters( 'gform_conditional_logic_operators', operators, this.objectType, rule.fieldId );

	for ( key in operators ) {
		var label  = operators[ key ];
		var config = {
			label: label,
			value: key,
			selected: key == rule.operator ? 'selected="selected"' : '',
		};

		html += renderView( template, null, config, false );
	}

	return html;
};

/**
 * Render value options for the given rule.
 *
 * @param {object} rule The rule data to render.
 *
 * @return {string}
 */
GFConditionalLogic.prototype.renderValueOptions = function( rule, idx ) {
	var field    = getFieldById( rule.fieldId );
	var html     = '';
	var template = gf_vars.conditionalLogic.views.option;
	var options  = [];

	// Field is actually a sub-field (such as the First Name or Country field), get the correct field from its ID.
	if ( rule.fieldId.toString().indexOf( '.' ) !== -1 ) {
		var parts   = rule.fieldId.toString().split( '.' );
		var fieldId = parts[ 0 ];
		field       = getFieldById( fieldId );
	}

	// Something went wrong; bail.
	if ( !field && !IsAddressSelect( rule.fieldId, field ) ) {
		return html;
	}

	// We're dealing with an Address field - get the correct values for it.
	if ( IsAddressSelect( rule.fieldId, field ) ) {
		options = getAddressOptions( field, rule.fieldId, rule.value );
	}

	// We're dealing with a category field - get all post categories as options.
	if ( field && field[ 'type' ] == 'post_category' && field[ 'displayAllCategories' ] ) {
		options = getCategoryOptions( field, rule.value );
	}

	// We're dealing with a normal select field - get the options from it.
	if ( field && field.choices && field[ 'type' ] != 'post_category' ) {
		options = getOptionsFromSelect( field, rule.value );
	}

	for ( var i = 0; i < options.length; i++ ) {
		var config = options[ i ];
		html += renderView( template, null, config, false );
	}

	return html;
};

/**
 * Render an input using the given data.
 *
 * @param {object} rule The rule data to render.
 * @param {int}    idx  The index of the rule.
 *
 * @return {string}
 */
GFConditionalLogic.prototype.renderInput = function( rule, idx ) {
	var config = {
		ruleIdx: idx,
		value: rule.value,
	};

	var html = gf_vars.conditionalLogic.views.input;

	return renderView( html, null, config, false );
};

/**
 * Render a select using the given data.
 *
 * @param {object} rule The rule data to render.
 * @param {int}    idx  The index of the rule.
 *
 * @return {string}
 */
GFConditionalLogic.prototype.renderSelect = function( rule, idx ) {
	var config = {
		ruleIdx: idx,
		fieldValueOptions: this.renderValueOptions( rule, idx ),
	};

	var html = gf_vars.conditionalLogic.views.select;

	return renderView( html, null, config, false );
};

/**
 * Render a rule value using the given data.
 *
 * @param {object} rule The rule data to render.
 * @param {int}    idx  The index of the rule.
 *
 * @return {string}
 */
GFConditionalLogic.prototype.renderRuleValue = function( rule, idx ) {
	var fieldValueOptions = this.renderValueOptions( rule, idx );
	var isSelect          = fieldValueOptions.length;
	var html              = '';
	var needsTextInput    = ruleNeedsTextValue( rule );

	if ( ! isSelect || needsTextInput ) {
		html = this.renderInput( rule, idx );
	} else {
		html = this.renderSelect( rule, idx );
	}

	html = gform.applyFilters( 'gform_conditional_logic_values_input', html, this.objectType, idx, rule.fieldId, rule.value );

	var el = gform.tools.htmlToElement( html );

	if ( ! el.classList.contains( 'active' ) ) {
		el.classList.add( 'active' );
	}

	if ( ! el.hasAttribute( 'data-js-rule-input' ) ) {
		el.setAttribute( 'data-js-rule-input', 'value' );
	}

	return gform.tools.elementToHTML( el );
};

/**
 * Render a rule using the given data.
 *
 * @param {object} rule The rule data to render.
 * @param {int}    idx  The index of the rule.
 *
 * @return {string}
 */
GFConditionalLogic.prototype.renderRule = function( rule, idx ) {
	var field = getFieldById( rule.fieldId );

	if ( ! field ) {
		field = {
			choices: '',
		};
	}

	var config = {
		rule_idx: idx,
		fieldOptions: this.renderFieldOptions( rule ),
		operatorOptions: this.renderOperatorOptions( rule ),
		deleteClass: this.state.rules.length > 1 ? 'active' : '',
		value: rule.value,
		valueMarkup: this.renderRuleValue( rule, idx ),
		addRuleText: gf_vars.conditionalLogic.addRuleText,
		removeRuleText: gf_vars.conditionalLogic.removeRuleText,
	};

	var html = gf_vars.conditionalLogic.views.rule;

	return renderView( html, null, config, false );
}

/**
 * Render a list of rules.
 *
 * @return {string}
 */
GFConditionalLogic.prototype.renderRules = function() {
	var container = this.els.flyouts[ this.objectType ].querySelector( '.conditional_logic_flyout__logic' );

	var html = '';
	for ( var i = 0; i < this.state.rules.length; i++ ) {
		html += this.renderRule( this.state.rules[ i ], i );
	}

	renderView( html, container, {}, true );
}

/**
 * Update the visibility of the conditional logic icon in compact view.
 */
GFConditionalLogic.prototype.updateCompactView = function() {
	if( this.objectType == 'next_button' ) {
		return;
	}

	const icon = document.querySelector( '#gfield_' + this.fieldId + '-conditional-logic-icon' );
	if ( ! icon ) {
		return;
	}

	if ( this.state.enabled ) {
		icon.style.display = 'block';
	} else {
		icon.style.display = 'none';
	}
}

/**
 * Gather an object populated with the DOM elements we'll be interacting with.
 *
 * @return {object}
 */
GFConditionalLogic.prototype.gatherElements = function() {
	return {
		field: document.querySelector( '.conditional_logic_field_setting' ),
		page: document.querySelector( '.conditional_logic_page_setting' ),
		next_button: document.querySelector( '.conditional_logic_nextbutton_setting' ),
		button: document.querySelector( '.conditional_logic_submit_setting' ),
		flyouts: {
			page: document.getElementById( 'conditional_logic_flyout_container' ),
			field: document.getElementById( 'conditional_logic_flyout_container' ),
			next_button: document.getElementById( 'conditional_logic_next_button_flyout_container' ),
			button: document.getElementById( 'conditional_logic_submit_flyout_container' ),
		},
	};
};

/**
 * Get the default rule to show if none exist.
 *
 * @return {{value: string, operator: string, fieldId: number}}
 */
GFConditionalLogic.prototype.getDefaultRule = function() {
	var fieldId = GetFirstRuleField();
	var field   = GetFieldById( fieldId );
	var fieldId = getCorrectDefaultFieldId( field );

	return {
		fieldId: fieldId,
		operator: 'is',
		value: '',
	};
};

/**
 * Get the default state for a new field.
 *
 * @return {{actionType: string, logicType: string, rules: [*], enabled: boolean}}
 */
GFConditionalLogic.prototype.getDefaultState = function() {
	return {
		enabled: false,
		actionType: 'show',
		logicType: 'all',
		rules: [
			this.getDefaultRule(),
		]
	};
};

/**
 * Get the correct state for the given field ID.
 *
 * @param {int} fieldId The ID of the field for which the state should be gathered.
 *
 * @return {obj}
 */
GFConditionalLogic.prototype.getStateForField = function( fieldId ) {
	// The submit field in the editor has a non-numeric ID.
	if( 'submit' === fieldId ) {
		var logic = form.button.conditionalLogic;
		if( logic ) {
			logic.enabled = true;
		} else {
			return this.getDefaultState();
		}
		return logic;
	}

	var field = getFieldById( fieldId );

	if ( field === false ) {
		return this.getDefaultState();
	}

	var logic = this.objectType === 'next_button' ? field.nextButton.conditionalLogic : field.conditionalLogic;

	if ( !logic || !logic.actionType ) {
		return this.getDefaultState();
	}

	// pre 2.5 forms dont have the enabled key in this object. If we have logic but no key, lets enable the ui
	if ( !( 'enabled' in logic ) ) {
		logic.enabled = true;
	}

	return logic;
};

/**
 * Determine whether the current conditional logic is enabled for this field.
 *
 * @return {boolean}
 */
GFConditionalLogic.prototype.isEnabled = function() {
	return this.state.enabled && GetFirstRuleField() > 0;
}

GFConditionalLogic.prototype.getAccordionTitle = function() {
	var prefix = '';
	switch ( this.objectType ) {
		case 'page':
			prefix = gf_vars.page + ' ';
			break;
		case 'next_button':
			prefix = gf_vars.next_button + ' ';
			break;
		case 'button':
			prefix = gf_vars.button + ' ';
		case 'field':
		default:
			break;
	}

	return prefix + gf_vars.conditional_logic_text;
};

/**
 * Get the correctly-translated text for the object type.
 *
 * @return {string}
 */
GFConditionalLogic.prototype.getObjectTypeText = function() {
	switch ( this.objectType ) {
		case 'section':
			return gf_vars.thisSectionIf;
		case 'field':
			return gf_vars.thisFieldIf;
		case 'page':
			return gf_vars.thisPage;
		case 'confirmation':
			return gf_vars.thisConfirmation;
		case 'notification':
			return gf_vars.thisNotification;
		default:
			return gf_vars.thisFormButton;
	}
}

/**
 * Get the correctly-translated text for the show text.
 *
 * @return {string}
 */
GFConditionalLogic.prototype.getObjectShowText = function() {
	if ( this.objectType === "next_button" ) {
		return gf_vars.enable;
	} else {
		return gf_vars.show;
	}
}

/**
 * Get the correctly-translated text for the hide text.
 *
 * @return {string}
 */
GFConditionalLogic.prototype.getObjectHideText = function() {
	if ( this.objectType === "next_button" ) {
		return gf_vars.disable;
	} else {
		return gf_vars.hide;
	}
}

/**
 * Hide the flyout.
 */
GFConditionalLogic.prototype.hideFlyout = function() {
	var thisFlyout = this.els.flyouts[ this.objectType ];
	if ( ! thisFlyout.classList.contains( 'anim-in-active' ) ) {
		return;
	}

	thisFlyout.classList.remove( 'anim-in-ready' );
	thisFlyout.classList.remove( 'anim-in-active' );
	thisFlyout.classList.add( 'anim-out-ready' );

	window.setTimeout( function() {
		thisFlyout.classList.add( 'anim-out-active' );
	}, 25 );

	window.setTimeout( function() {
		thisFlyout.classList.remove( 'anim-out-ready' );
		thisFlyout.classList.remove( 'anim-out-active' );
	}, 215 );
};

/**
 * Show the flyout.
 */
GFConditionalLogic.prototype.showFlyout = function() {
	for ( type in this.els.flyouts ) {
		var flyout = this.els.flyouts[ type ];
		flyout.classList.remove( 'anim-in-ready' );
		flyout.classList.remove( 'anim-in-active' );
		flyout.classList.remove( 'anim-out-ready' );
		flyout.classList.remove( 'anim-out-active' );
	}

	var thisFlyout = this.els.flyouts[ this.objectType ];

	thisFlyout.classList.add( 'anim-in-ready' );
	window.setTimeout( function() {
		thisFlyout.classList.add( 'anim-in-active' );
	}, 25 );
}

/**
 * Toggle the flyout when button is clicked.
 */
GFConditionalLogic.prototype.toggleFlyout = function( restoreFocus ) {
	this.renderFlyout();
	this.renderRules();

	if ( this.visible ) {
		this.hideFlyout();
	} else {
		this.showFlyout();
	}

	this.visible = !this.visible;

	var self = this;

	if ( ! restoreFocus ) {
		return;
	}

	window.setTimeout( function() {
		self.handleFocus();
	}, 325 );
};

/**
 * Update the current state with the corresponding key/value pair.
 *
 * @param {string} stateKey   The key in the state to update.
 * @param {*}      stateValue The value to update with.
 *
 * @return void
 */
GFConditionalLogic.prototype.updateState = function( stateKey, stateValue ) {
	this.state[ stateKey ] = stateValue;
	this.updateForm();

	if ( stateKey === 'enabled' ) {
		this.renderSidebar();
		this.renderMainControls( true );
		this.renderRules();
		this.updateCompactView();
	}
};

/**
 * Update the given rule by its index with the provided values.
 *
 * @param {string} key   The key in the state to update.
 * @param {*}      value The value to update with.
 * @param {int}    idx   The index of the rule to update.
 *
 * @return void
 */
GFConditionalLogic.prototype.updateRule = function( key, value, idx ) {
	this.state.rules[ idx ][ key ] = value;
	this.renderRules();
	this.updateForm();
}

/**
 * Add a rule.
 *
 * @return void
 */
GFConditionalLogic.prototype.addRule = function() {
	this.state.rules.push( this.getDefaultRule() );
	this.renderRules();
	this.updateForm();
}

/**
 * Delete a rule at the provided index.
 *
 * @param {int} idx The index of the rule to delete.
 *
 * @return void
 */
GFConditionalLogic.prototype.deleteRule = function( idx ) {
	this.state.rules.splice( idx, 1 );
	this.renderRules();
	this.updateForm();
};

/**
 * Update the form conditional data at the provided index with the given data.
 *
 * @param {int} index The index of the data to update.
 * @param {obj} data  The conditional data to update.
 *
 * @return void
 */
GFConditionalLogic.prototype.updateFormConditionalData = function( index, data ) {
	if ( this.objectType === 'next_button' ) {
		form.fields[ index ].nextButton.conditionalLogic = data;
		return;
	}

	if ( this.objectType === 'button' ) {
		form.button.conditionalLogic = data;
		return;
	}

	form.fields[ index ].conditionalLogic = data;
}

/**
 * Update the global form object so that data saves correctly.
 */
GFConditionalLogic.prototype.updateForm = function() {

	if ( 'submit' === this.fieldId ) {
		this.updateFormButtonConditionalData( this.state );
	}

	for ( var i = 0; i < form.fields.length; i++ ) {
		var field = form.fields[ i ];

		if ( field.id != this.fieldId ) {
			continue;
		}

		if ( !this.isEnabled() ) {
			this.updateFormConditionalData( i, '' )
			return;
		}

		this.updateFormConditionalData( i, this.state );
		return;
	}
}

/**
 * Update the submit button in the global form object so that data saves correctly.
 *
 * @since 2.6
 *
 * @params {array} data
 */
GFConditionalLogic.prototype.updateFormButtonConditionalData = function( data ) {
	if ( !this.isEnabled() ) {
		form.button.conditionalLogic = '';
		return;
	}
	form.button.conditionalLogic = data;
}

/**
 * Handle clicks of the toggle button.
 *
 * @param {Event} e
 */
GFConditionalLogic.prototype.handleToggleClick = function( e ) {
	if ( e.target.classList.contains( 'conditional_logic_accordion__toggle_button' ) || e.target.classList.contains( 'conditional_logic_accordion__toggle_button_icon' ) ) {
		this.toggleFlyout( true );
	}
};

/**
 * Handle clicks within the sidebar.
 *
 * @param {Event} e
 */
GFConditionalLogic.prototype.handleSidebarClick = function( e ) {
	if ( ('jsConditonalToggle' in e.target.dataset) ) {
		this.updateState( 'enabled', e.target.checked );
	}

	if ( ('jsAddRule' in e.target.dataset) ) {
		this.addRule();
	}

	if ( ('jsDeleteRule' in e.target.dataset) ) {
		var parent = gform.tools.getClosest( e.target, '[data-js-rule-idx]' );
		this.deleteRule( parent.dataset.jsRuleIdx );
	}

	if ( ('jsCloseFlyout' in e.target.dataset) ) {
		this.toggleFlyout( true );
	}
};

/**
 * Handle changes within the flyout container.
 *
 * @param {Event} e
 */
GFConditionalLogic.prototype.handleFlyoutChange = function( e ) {
	if ( ('jsStateUpdate' in e.target.dataset) ) {
		var key = e.target.dataset.jsStateUpdate;
		var val = e.target.value;

		this.updateState( key, val );
	}

	if ( ('jsRuleInput' in e.target.dataset) ) {
		var parent = e.target.parentNode;
		var key    = e.target.dataset.jsRuleInput;
		var val    = e.target.value;

		this.updateRule( key, val, parent.dataset.jsRuleIdx );
	}
};

/**
 * Handle clicks outside the flyout container.
 *
 * @param {Event} e
 */
GFConditionalLogic.prototype.handleBodyClick = function( e ) {
	if ( isValidFlyoutClick( e ) ) {
		return;
	}

	if ( this.visible && !this.els.flyouts[ this.objectType ].contains( e.target ) ) {
		this.toggleFlyout( true );
	}

};

/**
 * Handle clicks on sidebar accordion items.
 *
 * @param {Event} e
 */
GFConditionalLogic.prototype.handleAccordionClick = function( e ) {
	if (
		this.visible &&
		! e.target.classList.contains( 'conditional_logic_accordion__toggle_button') &&
		! e.target.classList.contains( 'conditional_logic_accordion__toggle_button_icon' )
	) {
		this.toggleFlyout( false );
	}
};

/**
 * Add all event listeners to flyout.
 */
GFConditionalLogic.prototype.addEventListeners = function() {
	this.els[ this.objectType ].addEventListener( 'click', this._handleToggleClick );
	this.els.flyouts[ this.objectType ].addEventListener( 'click', this._handleSidebarClick );
	this.els.flyouts[ this.objectType ].addEventListener( 'change', this._handleFlyoutChange );
	document.body.addEventListener( 'click', this._handleBodyClick );
	gform.addAction( 'formEditorNullClick', this._handleAccordionClick );
}

/**
 * Remove all event listeners from flyout.
 */
GFConditionalLogic.prototype.removeEventListeners = function() {
	this.els[ this.objectType ].removeEventListener( 'click', this._handleToggleClick );
	this.els.flyouts[ this.objectType ].removeEventListener( 'click', this._handleSidebarClick );
	this.els.flyouts[ this.objectType ].removeEventListener( 'change', this._handleFlyoutChange );
	document.body.removeEventListener( 'click', this._handleBodyClick );
}

/**
 * Private event handler used when listening to some specific key presses
 * (namely ESCAPE and TAB)
 *
 * @param {Event} event
 */
GFConditionalLogic.prototype._bindKeypress = function( event ) {
	// If the dialog is shown and the ESCAPE key is being pressed, prevent any
	// further effects from the ESCAPE key and hide the dialog
	if ( this.visible && event.which === ESCAPE_KEY ) {
		event.preventDefault();
		this.toggleFlyout( true );
	}

	// If the dialog is shown and the TAB key is being pressed, make sure the
	// focus stays trapped within the dialog element
	if ( this.visible && event.which === TAB_KEY ) {
		trapTabKey( this.els.flyouts[ this.objectType ], event );
	}
};

/**
 * Add focus to the flyout.
 */
GFConditionalLogic.prototype.addFocusToFlyout = function() {
	// Keep a reference to the currently focused element to be able to restore
	// it later, then set the focus to the first focusable child of the dialog
	// element
	FOCUSED_BEFORE_DIALOG = document.activeElement;

	setFocusToFirstItem( this.els.flyouts[ this.objectType ] );

	// Bind a focus event listener to the body element to make sure the focus
	// stays trapped inside the dialog while open, and start listening for some
	// specific key presses (TAB and ESC)
	document.body.addEventListener( 'focus', this._maintainFocus, true );
	document.addEventListener( 'keydown', this._bindKeypress );
};

/**
 * Remove focus from the flyout.
 */
GFConditionalLogic.prototype.removeFocusFromFlyout = function() {
	// If their was a focused element before the dialog was opened, restore the
	// focus back to it
	if ( FOCUSED_BEFORE_DIALOG ) {
		FOCUSED_BEFORE_DIALOG.focus();
	}

	// Remove the focus event listener to the body element and stop listening
	// for specific key presses
	document.body.removeEventListener( 'focus', this._maintainFocus, true );
	document.removeEventListener( 'keydown', this._bindKeypress );
};

/**
 * Handle the focus event callback.
 */
GFConditionalLogic.prototype.handleFocus = function() {
	if ( this.visible ) {
		this.addFocusToFlyout();
	} else {
		this.removeFocusFromFlyout();
	}
};

/**
 * Private event handler used when making sure the focus stays within the
 * currently open dialog
 *
 * @param {Event} event
 *
 * @return void
 */
GFConditionalLogic.prototype._maintainFocus = function( event ) {
	// If the dialog is shown and the focus is not within the dialog element,
	// move it back to its first focusable child
	if ( this.visible && !this.els.flyouts[ this.objectType ].contains( event.target ) ) {
		setFocusToFirstItem( this.els.flyouts[ this.objectType ], event );
	}
};

/**
 * Render the markup for this Conditional Flyout.
 */
GFConditionalLogic.prototype.render = function() {
	this.renderSidebar();
	this.renderFlyout();
	this.renderRules();

	this.updateForm();
};

/**
 * Initialize the Conditional Flyout.
 */
GFConditionalLogic.prototype.init = function() {
	this.addEventListeners();

	this.renderSidebar();
};
