<?php

if ( ! defined( 'ABSPATH' ) ) {
	die( 'You are not allowed to call this page directly.' );
}

class FrmProLookupFieldsController {

	/**
	 * @since 6.10.1
	 *
	 * @var array
	 */
	private static $lookup_field_options = array();

	/**
	 * Get the data types for Lookup fields
	 *
	 * @return array Data types.
	 */
	public static function get_lookup_field_data_types() {
		return array(
			'select'   => __( 'Dropdown', 'formidable' ),
			'radio'    => __( 'Radio Buttons', 'formidable' ),
			'checkbox' => __( 'Checkboxes', 'formidable' ),
			'text'     => __( 'Single Line Text', 'formidable-pro' ),
			'data'     => __( 'List', 'formidable-pro' ),
		);
	}

	/**
	 * Clean a Lookup field's options before updating in the database
	 * Necessary when switching from another field type to a Lookup
	 *
	 * @since 2.01.02
	 *
	 * @param array $values
	 *
	 * @return array Values.
	 */
	public static function clean_field_options_before_update( $values ) {
		if ( $values['type'] !== 'lookup' ) {
			return $values;
		}

		if ( ! empty( $values['options'] ) ) {
			$values['options'] = array();
		}

		return FrmProFieldsHelper::map_dropdown_data_type_to_select( $values );
	}

	/**
	 * Add Autopopulate Values options for certain field types
	 * Used on front and back end. Either $values or $field could be false :/
	 *
	 * @since 2.01.0
	 *
	 * @param array $values
	 * @param object $field
	 * @param array $opts
	 *
	 * @return void
	 */
	public static function add_autopopulate_value_field_options( $values, $field, &$opts ) {
		$field_type               = $field ? ( $field->field_options['original_type'] ?? $field->type ) : $values['type'];
		$autopopulate_field_types = self::get_autopopulate_field_types();

		if ( ! in_array( $field_type, $autopopulate_field_types ) ) {
			return;
		}

		$opts['get_values_form']            = '';
		$opts['get_values_field']           = '';
		$opts['watch_lookup']               = array();
		$opts['get_most_recent_value']      = '';
		$opts['lookup_filter_current_user'] = false;
	}

	/**
	 * Get the field types that should have the "Autopopulate Value" section
	 *
	 * @since 2.01.02
	 *
	 * @return array
	 */
	public static function get_autopopulate_field_types() {
		return array(
			'text',
			'email',
			'url',
			'time',
			'user_id',
			'number',
			'phone',
			'date',
			'select',
			'hidden',
			'textarea',
			'virtual',
		);
	}

	/**
	 * Show the "Autopopulate Value" option/section in the form builder
	 *
	 * @since 2.01.0
	 *
	 * @param array $field
	 * @param array $args
	 */
	public static function show_autopopulate_value_section_in_form_builder( $field, $args = array() ) {
		if ( ! isset( $field['data_type'] ) ) {
			$field['data_type'] = 'text';
		}

		$lookup_fields = self::get_lookup_fields_for_watch_row( $field );
		$field_obj     = FrmFieldFactory::get_field_type( 'lookup', $field );
		$class_attr    = $args['class_attr'] ?? '';

		if ( class_exists( 'FrmTextToggleStyleComponent' ) ) {
			require FrmProAppHelper::plugin_path() . '/classes/views/frmpro-fields/back-end/autopopulate-values.php';
		} else {
			// Backwards compatibility. @since 6.24
			include FrmProAppHelper::plugin_path() . '/classes/views/frmpro-fields/back-end/backwards-compatibility/autopopulate-values.php';
		}
	}

	/**
	 * Get the lookup fields that will appear in the "Watch" option
	 *
	 * @since 2.01.0
	 *
	 * @param array $field
	 *
	 * @return array Lookup fields.
	 */
	public static function get_lookup_fields_for_watch_row( $field ) {
		$parent_form_id = $field['parent_form_id'] ?? $field['form_id'];
		return self::get_limited_lookup_fields_in_form( $parent_form_id, $field['form_id'] );
	}

	/**
	 * Get the dropdown options for the "Get Options/Value From" Field option
	 *
	 * @since 2.01.0
	 */
	public static function ajax_get_options_for_get_values_field() {
		check_ajax_referer( 'frm_ajax', 'nonce' );

		$form_id    = FrmAppHelper::get_post_param( 'form_id', '', 'absint' );
		$field_type = FrmAppHelper::get_post_param( 'field_type', '', 'sanitize_text_field' );
		$field_obj  = FrmFieldFactory::get_field_type( 'lookup' );
		$fields     = $field_obj->get_fields_for_get_values_field_dropdown( $form_id, $field_type );

		self::show_options_for_get_values_field( $fields );
		wp_die();
	}

	/**
	 * Show the dropdown options for the "Get Options/Values From" Field option
	 *
	 * @since 2.01.0
	 *
	 * @param array $form_fields
	 * @param array $field ($field is not empty on page load)
	 */
	public static function show_options_for_get_values_field( $form_fields, $field = array() ) {
		$select_field_text = __( 'Select Field', 'formidable-pro' );
		echo '<option value="">' . esc_html( $select_field_text ) . '</option>';

		$selected_value = ! $field || ! isset( $field['get_values_field'] ) ? '' : $field['get_values_field'];

		foreach ( $form_fields as $field_option ) {
			if ( FrmField::is_no_save_field( $field_option->type ) ) {
				continue;
			}

			$field_name = FrmAppHelper::truncate( $field_option->name, 30 );
			echo '<option value="' . esc_attr( $field_option->id ) . '"' . selected( $selected_value, $field_option->id, false ) . '>' . esc_html( $field_name ) . '</option>';
		}
	}

	/**
	 * Show a Lookup Field on the form builder page
	 *
	 * @since 2.01.0
	 *
	 * @param array $field
	 */
	public static function show_lookup_field_input_on_form_builder( $field ) {
		if ( 'dropdown' === $field['data_type'] ) {
			// Map back the data type.
			// POST data sends "dropdown" to avoid "select" which gets blocked by security tools.
			$field['data_type'] = 'select';
		}

		if ( $field['data_type'] === 'text' ) {
			// Set up width string
			if ( FrmField::is_option_true( $field, 'size' ) && ! FrmAppHelper::is_admin_page( 'formidable' ) ) {
				$width_string = ' style="width:' . $field['size'] . ( is_numeric( $field['size'] ) ? 'px' : '' ) . ';"';
			} else {
				$width_string = '';
			}
		} else {
			// Get the field options
			$field['options'] = self::get_initial_lookup_field_options( $field );
		}

		// Generate field name and HTML id
		$field_name = 'item_meta[' . $field['id'] . ']';

		if ( 'checkbox' === $field['data_type'] ) {
			$field_name .= '[]';
		}

		$html_id = 'field_' . $field['field_key'];

		// Make sure field value is set
		if ( ! isset( $field['value'] ) ) {
			$field['value'] = '';
		}

		$saved_value_array = (array) $field['value'];

		require FrmProAppHelper::plugin_path() . '/classes/views/lookup-fields/back-end/input.php';
	}

	/**
	 * If current field is repeating, get lookup fields in repeating section and parent form
	 * Otherwise, only get lookup fields in parent form
	 *
	 * @since 2.01.0
	 *
	 * @param int $parent_form_id
	 * @param int $current_form_id
	 *
	 * @return array
	 */
	private static function get_limited_lookup_fields_in_form( $parent_form_id, $current_form_id ) {
		// If the current field's form ID matches $form_id, only get fields in that form (not embedded or repeating)
		// If current field is repeating, get lookup fields in repeating section and outside of it
		$inc_repeating = $parent_form_id == $current_form_id ? 'exclude' : 'include';

		return self::get_lookup_fields_for_form( $parent_form_id, $inc_repeating );
	}

	/**
	 * Get field options for a target form and $inc_repeating option.
	 * These fields are stored in memory for re-use as an optimization.
	 * On a form with a lot of over 1000 fields, this optimization saves several seconds
	 * when loading the form builder page.
	 *
	 * @since 6.10.1
	 *
	 * @param int    $parent_form_id
	 * @param string $inc_repeating
	 *
	 * @return array
	 */
	private static function get_lookup_fields_for_form( $parent_form_id, $inc_repeating ) {
		$key = $parent_form_id . $inc_repeating;

		if ( ! isset( self::$lookup_field_options[ $key ] ) ) {
			self::$lookup_field_options[ $key ] = FrmField::get_all_types_in_form( $parent_form_id, 'lookup', '', $inc_repeating );
		}

		return self::$lookup_field_options[ $key ];
	}

	/**
	 * Add a "Watch Lookup" row in the field options (when the + or 'Watch Lookup Fields' link is clicked)
	 *
	 * @since 2.01.0
	 */
	public static function add_watch_lookup_row() {
		check_ajax_referer( 'frm_ajax', 'nonce' );

		$row_key        = FrmAppHelper::get_post_param( 'row_key', '', 'absint' );
		$field_id       = FrmAppHelper::get_post_param( 'field_id', '', 'absint' );
		$form_id        = FrmAppHelper::get_post_param( 'form_id', '', 'absint' );
		$selected_field = '';
		$current_field  = FrmField::getOne( $field_id );// Maybe (for efficiency) change this to a specific database call
		$lookup_fields  = self::get_limited_lookup_fields_in_form( $form_id, $current_field->form_id );

		include FrmProAppHelper::plugin_path() . '/classes/views/lookup-fields/back-end/watch-row.php';
		wp_die();
	}

	/**
	 * Get the values from a Lookup Field for conditional logic
	 *
	 * @since 2.01.0
	 *
	 * @param object $field
	 *
	 * @return array
	 */
	public static function get_lookup_field_values_for_conditional_logic( $field ) {
		$linked_field_id = $field->field_options['get_values_field'] ?? '';

		if ( ! is_numeric( $linked_field_id ) ) {
			return array( __( 'No options available: please check this field\'s options', 'formidable-pro' ) );
		}

		$field_array = array(
			'lookup_filter_current_user' => false,
			'lookup_option_order'        => $field->field_options['lookup_option_order'],
		);
		$all_values  = self::get_independent_lookup_field_values( $linked_field_id, $field_array );

		// Only show 300 options
		return array_slice( $all_values, 0, 300 );
	}

	/**
	 * Get the initial options for a non-text Lookup field on page load
	 * Used on front and back-end
	 *
	 * @since 2.01.0
	 *
	 * @param array $values
	 */
	public static function maybe_get_initial_lookup_field_options( &$values ) {
		if ( $values['data_type'] !== 'text' ) {
			$values['options'] = self::get_initial_lookup_field_options( $values );
		}
	}

	/**
	 * Get the options for a lookup field on initial page load
	 * Used on front and back-end
	 *
	 * @since 2.01.0
	 *
	 * @param array $values
	 *
	 * @return array Options.
	 */
	private static function get_initial_lookup_field_options( $values ) {
		if ( self::is_lookup_field_independent( $values['watch_lookup'] ) ) {
			return self::get_independent_lookup_field_options( $values );
		}

		return self::get_initial_dependent_lookup_field_options( $values );
	}

	/**
	 * Check if dependent on another lookup field
	 *
	 * @since 2.01.0
	 *
	 * @param array $watch_lookup
	 *
	 * @return bool Independent.
	 */
	private static function is_lookup_field_independent( $watch_lookup ) {
		return ! array_filter( $watch_lookup );
	}

	/**
	 * Get the options for an independent Lookup field
	 *
	 * @since 2.01.01
	 *
	 * @param array $values
	 *
	 * @return array
	 */
	public static function get_independent_lookup_field_options( $values ) {
		$linked_field_id = $values['get_values_field'];

		if ( ! $linked_field_id ) {
			return array();
		}

		$options = self::get_independent_lookup_field_values( $linked_field_id, $values );

		if ( ! in_array( $values['data_type'], array( 'select', 'dropdown' ), true ) ) {
			return $options;
		}

		$placeholder    = $values['placeholder'] ?? '';
		$default_option = array( $placeholder );
		return array_merge( $default_option, $options );
	}

	/**
	 * Get the meta values for an independent lookup field
	 *
	 * @since 2.01.0
	 *
	 * @param int $linked_field_id
	 * @param array $values
	 *
	 * @return array Options.
	 */
	private static function get_independent_lookup_field_values( $linked_field_id, $values ) {
		$linked_field = FrmField::getOne( $linked_field_id );

		if ( ! $linked_field ) {
			return array();
		}

		$args = array(
			'lookup_field' => $values,
		);

		if ( self::need_to_filter_values_for_current_user( $values['id'], $values ) ) {
			$current_user = get_current_user_id();

			// If user isn't logged in, don't display any options
			if ( $current_user === 0 ) {
				return array();
			}

			$args['user_id'] = $current_user;
		}

		if ( FrmAppHelper::is_admin_page( 'formidable' ) ) {
			$args['limit'] = 500;
		}

		$options = self::get_filtered_lookup_options( $linked_field, $args );

		if ( 'label' === FrmField::get_option( $values, 'lookup_saved_value' ) || 'label' === FrmField::get_option( $values, 'lookup_displayed_value' ) ) {
			self::add_lookup_option_filters( $linked_field, $values );
		}

		self::order_values( $values['lookup_option_order'], $options, $values );

		return $options;
	}

	/**
	 * Maybe use labels instead of values.
	 *
	 * @since 6.21
	 *
	 * @param object       $linked_field
	 * @param array|object $child_field
	 *
	 * @return void
	 */
	private static function add_lookup_option_filters( $linked_field, $child_field ) {
		if ( 'data' === $linked_field->type ) {
			self::add_option_filters_for_dynamic_field( $linked_field, $child_field );
			return;
		}

		if ( empty( $linked_field->options ) || ! is_array( $linked_field->options ) ) {
			return;
		}

		$first_field_option = reset( $linked_field->options );

		if ( ! is_array( $first_field_option ) ) {
			return;
		}

		$label_by_value = array();

		foreach ( $linked_field->options as $field_option ) {
			$label_by_value[ $field_option['value'] ] = $field_option['label'];
		}

		$field_id = (int) ( is_object( $child_field ) ? $child_field->id : $child_field['id'] );
		$callback = function ( $value, $filter_field ) use ( $label_by_value, $field_id ) {
			if ( (int) $filter_field['id'] === $field_id && isset( $label_by_value[ $value ] ) ) {
				return $label_by_value[ $value ];
			}
			return $value;
		};

		if ( 'label' === FrmField::get_option( $child_field, 'lookup_saved_value' ) ) {
			add_filter( 'frm_lookup_option_value', $callback, 10, 2 );
		}

		if ( 'label' === FrmField::get_option( $child_field, 'lookup_displayed_value' ) ) {
			add_filter( 'frm_lookup_option_label', $callback, 10, 2 );
		}
	}

	/**
	 * Maybe use labels instead of values for dynamic fields.
	 *
	 * @since 6.21
	 *
	 * @param object       $linked_field
	 * @param array|object $child_field
	 *
	 * @return void
	 */
	private static function add_option_filters_for_dynamic_field( $linked_field, $child_field ) {
		$field_id = (int) ( is_object( $child_field ) ? $child_field->id : $child_field['id'] );
		$callback = function ( $value, $filter_field ) use ( $field_id, $linked_field ) {
			if ( (int) $filter_field['id'] === $field_id ) {
				return FrmProFieldsHelper::get_data_value( $value, $linked_field );
			}
			return $value;
		};

		if ( 'label' === FrmField::get_option( $child_field, 'lookup_saved_value' ) ) {
			add_filter( 'frm_lookup_option_value', $callback, 10, 2 );
		}

		if ( 'label' === FrmField::get_option( $child_field, 'lookup_displayed_value' ) ) {
			add_filter( 'frm_lookup_option_label', $callback, 10, 2 );
		}
	}

	/**
	 * Formats meta values for a lookup field.
	 *
	 * @since 3.03.03
	 *
	 * @param array  $metas
	 * @param object $linked_field
	 * @param array  $args
	 *
	 * @return array
	 */
	private static function format_field_value_for_lookup( $metas, $linked_field, $args ) {
		// Don't mess with formats for select fields since existing fields won't be selected on edit
		$lookup_type        = isset( $args['lookup_field'] ) ? FrmField::get_option( $args['lookup_field'], 'data_type' ) : '';
		$alter_lookup_value = ! $lookup_type || 'text' === $lookup_type || in_array( $linked_field->type, array( 'address', 'name' ), true );

		if ( ! $alter_lookup_value ) {
			return $metas;
		}

		$field_class = FrmFieldFactory::get_field_object( $linked_field );

		if ( ! is_callable( array( $field_class, 'prepare_field_value' ) ) ) {
			return $metas;
		}

		$result = array();

		foreach ( $metas as $value ) {
			if ( 'address' === $linked_field->type ) {
				$result[] = $field_class->format_address_for_display( $value, array( 'line_sep' => ' ' ) );
			} elseif ( 'name' === $linked_field->type ) {
				$result[] = $field_class->get_display_value( $value );
			} else {
				$result[] = $field_class->prepare_field_value( $value, array() );
			}
		}

		return $result;
	}

	/**
	 * Get the initial options for a dependent lookup field
	 *
	 * @since 2.01.01
	 *
	 * @param array $values
	 *
	 * @return array
	 */
	private static function get_initial_dependent_lookup_field_options( $values ) {
		// If editing an entry or switching between pages, add an option for the saved value.
		$options = ! empty( $values['value'] ) ? (array) self::decode_html_entities( $values['value'] ) : array();

		if ( in_array( $values['data_type'], array( 'select', 'dropdown' ), true ) ) {
			$placeholder = array( $values['placeholder'] );
			$options     = array_merge( $placeholder, $options );
		} elseif ( ! $options ) {
			$options[] = '';
		}

		return $options;
	}

	/**
	 * Decode HTML entities recursively
	 *
	 * @since 2.04
	 *
	 * @param mixed $value
	 *
	 * @return array|string
	 */
	private static function decode_html_entities( $value ) {
		// TODO: add single, centralized function to decode entities

		if ( is_array( $value ) ) {
			foreach ( $value as $key => $single_value ) {
				$value[ $key ] = self::decode_html_entities( $single_value );
			}
		} else {
			$value = html_entity_decode( $value );
		}

		return $value;
	}

	/**
	 * Format the global lookup_fields array that will be parsed to JavaScript
	 *
	 * @since 2.01.0
	 *
	 * @param array $values
	 */
	public static function setup_lookup_field_js( $values ) {
		// If on form builder, don't set up the script
		if ( FrmAppHelper::is_admin_page( 'formidable' ) ) {
			return;
		}

		if ( $values['original_type'] !== 'lookup' && FrmField::is_option_empty( $values, 'get_values_field' ) ) {
			return;
		}

		global $frm_vars;

		// If the field has already been through this function, leave now
		// This will happen when the are multiple rows in a repeating section on page load
		if ( isset( $frm_vars['lookup_fields'][ $values['id'] ]['fieldId'] ) ) {
			return;
		}

		if ( ! isset( $frm_vars['lookup_fields'] ) ) {
			$frm_vars['lookup_fields'] = array();
		}

		self::maybe_initialize_frm_vars_lookup_fields_for_id( $values['id'], $frm_vars );

		$lookup_parents = array_filter( $values['watch_lookup'] ?? array() );
		$lookup_logic   = $frm_vars['lookup_fields'][ $values['id'] ];

		$lookup_logic['fieldId']       = $values['id'];
		$lookup_logic['fieldKey']      = $values['field_key'];
		$lookup_logic['parents']       = $lookup_parents;
		$lookup_logic['fieldType']     = $values['original_type'];
		$lookup_logic['formId']        = $values['parent_form_id'];
		$lookup_logic['inSection']     = $values['in_section'] ?? '0';
		$lookup_logic['inEmbedForm']   = $values['in_embed_form'] ?? '0';
		$lookup_logic['isRepeating']   = $values['form_id'] != $values['parent_form_id'];
		$lookup_logic['isMultiSelect'] = FrmField::is_multiple_select( $values );
		$lookup_logic['isReadOnly']    = isset( $values['read_only'] ) ? (bool) $values['read_only'] : 0;

		$lookup_logic['inputType'] = $values['original_type'] === 'lookup' ? $values['data_type'] : $values['original_type'];

		$frm_vars['lookup_fields'][ $values['id'] ] = $lookup_logic;

		// Add field to parent field's dependents, if there is a parent
		if ( ! $lookup_parents ) {
			return;
		}

		foreach ( $lookup_parents as $watch_lookup ) {
			self::maybe_initialize_frm_vars_lookup_fields_for_id( $watch_lookup, $frm_vars );
			$frm_vars['lookup_fields'][ $watch_lookup ]['dependents'][] = $values['id'];
		}
	}

	/**
	 * If an index has not been set for the current Lookup Field in $frm_vars, add it now
	 * The global $frm_vars['lookup_fields'] array is used to load Lookup Field JavaScript
	 *
	 * @since 2.01.0
	 *
	 * @param int $field_id
	 * @param array $frm_vars
	 */
	private static function maybe_initialize_frm_vars_lookup_fields_for_id( $field_id, &$frm_vars ) {
		if ( ! isset( $frm_vars['lookup_fields'][ $field_id ] ) ) {
			$frm_vars['lookup_fields'][ $field_id ] = array(
				'dependents' => array(),
			);
		}
	}

	/**
	 * Check all lookup fields that have parents when a form page is loaded
	 *
	 * @since 2.01.0
	 *
	 * @param array $frm_vars
	 *
	 * @return void
	 */
	public static function load_check_dependent_lookup_js( $frm_vars ) {
		// TODO: don't reload for ajax
		if ( empty( $frm_vars['lookup_fields'] ) ) {
			return;
		}

		$lookup_field_ids = array();

		foreach ( $frm_vars['lookup_fields'] as $l_id => $lookup_field ) {
			if ( ! empty( $lookup_field['parents'] ) ) {
				if ( $lookup_field['fieldType'] === 'lookup' ) {
					// Update all dependent Lookup fields
					$lookup_field_ids[] = $l_id;
				} elseif ( 'new' === self::get_form_action( $frm_vars ) ) {
					$lookup_field_ids[] = $l_id;
				}
			}
		}
		echo '__frmDepLookupFields=' . json_encode( $lookup_field_ids ) . ';';
	}

	/**
	 * Get the current action from the URL (new, create, edit, update)
	 *
	 * @since 2.02
	 *
	 * @param array $frm_vars
	 *
	 * @return string Form action.
	 */
	private static function get_form_action( $frm_vars ) {
		$action_var = isset( $_REQUEST['frm_action'] ) ? 'frm_action' : 'action';
		$action     = FrmAppHelper::get_param( $action_var, '', 'get', 'sanitize_title' );

		if ( $action ) {
			return $action;
		}

		return ! empty( $frm_vars['editing_entry'] ) ? 'edit' : 'new';
	}

	/**
	 * Handle AJAX for multiple lookup fields.
	 *
	 * @since 6.15
	 *
	 * @return void
	 */
	public static function ajax_get_dependent_lookup_field_options_arr() {
		// Don't use nonce since this is front-end.

		$data = FrmAppHelper::get_param( 'postData', array(), 'post' );

		if ( ! is_array( $data ) || ! $data ) {
			echo json_encode( array() );
			wp_die();
		}

		$response = array();

		foreach ( $data as $lookup ) {
			if ( ! isset( $lookup['unique'] ) ) {
				continue;
			}

			$unique_key  = absint( $lookup['unique'] );
			$field_id    = isset( $lookup['fieldId'] ) ? absint( $lookup['fieldId'] ) : 0;
			$parents     = $lookup['parents'] ?? '';
			$parent_vals = $lookup['parentVals'] ?? '';

			FrmAppHelper::sanitize_value( 'absint', $parents );
			FrmAppHelper::sanitize_value( 'wp_kses_post', $parent_vals );

			$parent_args = array(
				'parent_field_ids' => $parents,
				'parent_vals'      => $parent_vals,
			);

			FrmAppHelper::sanitize_value( 'wp_specialchars_decode', $parent_args['parent_vals'] );

			$child_field             = FrmField::getOne( $field_id );
			$response[ $unique_key ] = self::get_filtered_values_for_dependent_lookup_field( $parent_args, $child_field );
			$lookup_saved_value      = FrmField::get_option( $child_field, 'lookup_saved_value' );
			$lookup_displayed_value  = FrmField::get_option( $child_field, 'lookup_displayed_value' );

			if ( 'label' === $lookup_saved_value ) {
				$new_response = array();

				foreach ( $response[ $unique_key ] as $key => $value ) {
					$new_response[ $key ] = FrmProFieldLookup::filter_lookup_displayed_value( $value, (array) $child_field );
				}

				$response[ $unique_key ] = $new_response;
			} elseif ( 'value' === $lookup_saved_value && 'label' === $lookup_displayed_value ) {
				$response[ $unique_key . '_label' ] = array();

				foreach ( $response[ $unique_key ] as $value ) {
					$response[ $unique_key . '_label' ][] = FrmProFieldLookup::filter_lookup_displayed_value( $value, (array) $child_field );
				}
			}
		}

		echo json_encode( $response );
		wp_die();
	}

	/**
	 * Echo the HTML to replace a dependent Radio Lookup field's options
	 *
	 * @since 2.01.0
	 */
	public static function ajax_get_dependent_cb_radio_lookup_options() {
		// Don't use nonce since this is front-end.

		$field_id    = FrmAppHelper::get_param( 'field_id', '', 'post', 'absint' );
		$parent_args = array(
			'parent_field_ids' => FrmAppHelper::get_param( 'parent_fields', '', 'post', 'absint' ),
			'parent_vals'      => FrmAppHelper::get_param( 'parent_vals', '', 'post', 'wp_kses_post' ),
		);
		FrmAppHelper::sanitize_value( 'wp_specialchars_decode', $parent_args['parent_vals'] );

		$args = array(
			'row_index'          => FrmAppHelper::get_param( 'row_index', '', 'post', 'sanitize_text_field' ),
			'container_field_id' => FrmAppHelper::get_param( 'container_field_id', '', 'post', 'sanitize_text_field' ),
			'current_value'      => FrmAppHelper::get_param( 'current_value', '', 'post', 'sanitize_text_field' ),
			'default_value'      => FrmAppHelper::get_param( 'default_value', '', 'post', 'sanitize_text_field' ),
		);

		$child_field  = FrmField::getOne( $field_id );
		$final_values = self::get_filtered_values_for_dependent_lookup_field( $parent_args, $child_field );

		self::show_dependent_cb_radio_lookup_options( $child_field, $args, $final_values );

		wp_die();
	}

	/**
	 * Get the filtered options for a dependent lookup field
	 *
	 * @since 2.01.0
	 *
	 * @param array  $parent_args
	 * @param object $child_field
	 *
	 * @return array
	 */
	public static function get_filtered_values_for_dependent_lookup_field( $parent_args, $child_field ) {
		$entry_ids   = self::get_entry_ids_from_parent_vals( $parent_args['parent_field_ids'], $parent_args['parent_vals'], $child_field );
		$meta_values = self::get_meta_values_filtered_by_entry_ids( $entry_ids, $child_field );
		$order       = $child_field->field_options['lookup_option_order'] ?? 'ascending';

		if ( 'label' === FrmField::get_option( $child_field, 'lookup_saved_value' ) || 'label' === FrmField::get_option( $child_field, 'lookup_displayed_value' ) ) {
			$linked_field = FrmField::getOne( $child_field->field_options['get_values_field'] );

			if ( $linked_field ) {
				self::add_lookup_option_filters( $linked_field, $child_field );
			}
		}

		self::order_values( $order, $meta_values, $child_field );

		return $meta_values;
	}

	/**
	 * Show the refreshed options in a Radio Lookup field
	 *
	 * @since 2.01.0
	 *
	 * @param object $child_field
	 * @param array $args
	 * @param array $final_values
	 */
	private static function show_dependent_cb_radio_lookup_options( $child_field, $args, $final_values ) {
		$field             = self::initialize_dependent_cb_radio_field_array( $child_field, $final_values, $args );
		$saved_value_array = (array) $args['current_value'];
		$html_id           = 'field_' . $child_field->field_key . '-' . $args['row_index'];
		$field_name        = self::generate_field_name_for_radio_inputs( $child_field, $args );
		$disabled          = FrmField::is_read_only( $child_field ) && ! FrmAppHelper::is_admin() ? ' disabled="disabled"' : '';

		if ( 'checkbox' === $field['data_type'] ) {
			$field_name .= '[]';
			require FrmProAppHelper::plugin_path() . '/classes/views/lookup-fields/front-end/checkbox-rows.php';
		} else {
			require FrmProAppHelper::plugin_path() . '/classes/views/lookup-fields/front-end/radio-rows.php';
		}
	}

	/**
	 * Initialize a refreshed Radio Lookup field array
	 *
	 * @since 2.01.0
	 *
	 * @param object $child_field
	 * @param array $final_values
	 * @param array $args
	 *
	 * @return array Field.
	 */
	private static function initialize_dependent_cb_radio_field_array( $child_field, $final_values, $args ) {
		$field_options = $child_field->field_options;
		$field         = get_object_vars( $child_field ) + $field_options;
		unset( $field['field_options'] );

		$field['original_type'] = 'lookup';
		$field['options']       = $final_values ? $final_values : array( '' );
		$field['default_value'] = $args['default_value'];

		return $field;
	}

	/**
	 * Generate the field input name for a repeating, embedded, or standard field
	 *
	 * @since 2.01.0
	 *
	 * @param object $field
	 * @param array $args
	 *
	 * @return string Field name.
	 */
	private static function generate_field_name_for_radio_inputs( $field, $args ) {
		if ( $args['row_index'] != '' ) {
			$i = str_replace( '-', '', $args['row_index'] );
			return 'item_meta[' . $args['container_field_id'] . '][' . $i . '][' . $field->id . ']';
		}
		return 'item_meta[' . $field->id . ']';
	}

	/**
	 * Get the values for a text field that is dependent on Lookup Fields
	 *
	 * @since 2.01.0
	 */
	public static function ajax_get_text_field_lookup_value() {
		// Don't use nonce since this is front-end.

		$parent_field_ids = FrmAppHelper::get_param( 'parent_fields', '', 'post', 'absint' );
		$parent_vals      = FrmAppHelper::get_param( 'parent_vals', '', 'post', 'wp_kses_post' );
		FrmAppHelper::sanitize_value( 'wp_specialchars_decode', $parent_vals );

		$field_id    = FrmAppHelper::get_param( 'field_id', '', 'post', 'absint' );
		$child_field = FrmField::getOne( $field_id );
		$entry_ids   = self::get_entry_ids_from_parent_vals( $parent_field_ids, $parent_vals, $child_field );
		$meta_values = self::get_meta_values_filtered_by_entry_ids( $entry_ids, $child_field );
		$meta_value  = implode( ', ', $meta_values );

		echo wp_kses_post( $meta_value );

		wp_die();
	}

	/**
	 * Get the entry IDs in common for all parent Lookup fields/values
	 *
	 * @since 2.01.0
	 *
	 * @param array        $parent_field_ids
	 * @param array|string $selected_values
	 * @param object       $child_field
	 *
	 * @return array Entry ids.
	 */
	private static function get_entry_ids_from_parent_vals( $parent_field_ids, $selected_values, $child_field ) {
		$entry_ids = array();
		$args      = array();

		if ( ! is_array( $selected_values ) ) {
			// Exit early if $selected_values is a string. We expect it to be an array.
			return $entry_ids;
		}

		// TODO: Maybe add current user filter here, or maybe add it in final call
		if ( self::need_to_filter_values_for_current_user( $child_field->id, $child_field->field_options ) ) {
			$args['user_id'] = get_current_user_id();
		}

		foreach ( $parent_field_ids as $i => $p_field_id ) {
			$parent_field = FrmField::getOne( $p_field_id );

			if ( ! $parent_field ) {
				continue;
			}

			if ( ! array_key_exists( $i, $selected_values ) ) {
				continue;
			}

			$parent_val   = $selected_values[ $i ];
			$linked_field = FrmField::getOne( $parent_field->field_options['get_values_field'] );

			$args['comparison_type'] = apply_filters( 'frm_set_comparison_type_for_lookup', 'equals', $parent_field, $child_field );
			$args['and_or']          = apply_filters( 'frm_set_and_or_for_lookup', 'and', $parent_field, $child_field );
			self::apply_current_user_filter_for_non_lookup( $child_field, $parent_field, $args );

			$entry_ids = self::get_entry_ids_for_parent_field_and_value( $linked_field, $parent_val, $args );

			if ( ! $entry_ids ) {
				break;
			}

			self::append_child_entry_ids( $entry_ids );

			$args['entry_ids'] = $entry_ids;
		}

		return $entry_ids;
	}

	/**
	 * @param stdClass $child_field
	 * @param stdClass $parent_field
	 * @param array $args
	 */
	private static function apply_current_user_filter_for_non_lookup( $child_field, $parent_field, &$args ) {
		if ( $child_field->type !== 'lookup' && self::need_to_filter_values_for_current_user( $parent_field->id, $parent_field->field_options ) ) {
			$args['user_id'] = get_current_user_id();
		}
	}

	/**
	 * Get the entry IDs for a given field and value
	 *
	 * @since 2.01.01
	 *
	 * @param object $linked_field
	 * @param array|string $parent_val
	 * @param array $args
	 *
	 * @return array
	 */
	private static function get_entry_ids_for_parent_field_and_value( $linked_field, $parent_val, $args ) {
		if ( ! is_array( $parent_val ) ) {
			self::get_save_value_from_display( $linked_field, $parent_val );
			return FrmProEntryMeta::get_entry_ids_for_field_and_value( $linked_field, $parent_val, $args );
		}

		$entry_ids = array( 'first' => true );

		foreach ( $parent_val as $p_val ) {
			self::get_save_value_from_display( $linked_field, $p_val );
			$new_entry_ids = FrmProEntryMeta::get_entry_ids_for_field_and_value( $linked_field, $p_val, $args );
			$entry_ids     = self::filter_or_merge_entry_ids( $entry_ids, $new_entry_ids, $args['and_or'] );
		}

		return $entry_ids;
	}

	/**
	 * Get the value in the format saved to the database in order to
	 * correctly compare with an SQL call
	 *
	 * @since 3.03.03
	 *
	 * @param object $field
	 * @param string $value
	 *
	 * @return void
	 */
	private static function get_save_value_from_display( $field, &$value ) {
		$field_obj = FrmFieldFactory::get_field_object( $field );
		$value     = $field_obj->set_value_before_save( $value );
	}

	/**
	 * Either combine the results, or get only those in common.
	 * and/or depends on the frm_set_and_or_for_lookup filter
	 *
	 * @since 2.03.08
	 *
	 * @param array $entry_ids
	 * @param array $new_entry_ids
	 * @param string $and_or
	 *
	 * @return array
	 */
	private static function filter_or_merge_entry_ids( $entry_ids, $new_entry_ids, $and_or ) {
		if ( isset( $entry_ids['first'] ) ) {
			return $new_entry_ids;
		}

		return $and_or === 'or' ? array_intersect( $entry_ids, $new_entry_ids ) : array_merge( $entry_ids, $new_entry_ids );
	}

	/**
	 * Append child entry IDs, if there are any, to an array of entry IDs
	 *
	 * @since 2.02.13
	 *
	 * @param array $entry_ids
	 *
	 * @return void
	 */
	private static function append_child_entry_ids( &$entry_ids ) {
		$child_entry_ids = self::get_child_entry_ids( $entry_ids );

		if ( is_array( $child_entry_ids ) && $child_entry_ids ) {
			$entry_ids = array_merge( $entry_ids, $child_entry_ids );
		}
	}

	/**
	 * @since 6.28
	 *
	 * @param array $entry_ids
	 *
	 * @return array
	 */
	private static function get_child_entry_ids( $entry_ids ) {
		if ( count( $entry_ids ) < FrmProEntryMeta::max_entry_ids_in_query() ) {
			// If there are not many entry IDs, it's fine to just query by ID.
			return FrmDb::get_col( 'frm_items', array( 'parent_item_id' => $entry_ids ), 'id' );
		}

		$lowest_entry_id  = min( $entry_ids );
		$highest_entry_id = max( $entry_ids );
		$all_entry_data   = FrmDb::get_results(
			'frm_items',
			array(
				'parent_item_id !' => 0,
				'parent_item_id >' => $lowest_entry_id - 1,
				'parent_item_id <' => $highest_entry_id + 1,
			),
			'id, parent_item_id'
		);

		if ( ! $all_entry_data ) {
			return array();
		}

		$indexed_entry_ids = array_combine( $entry_ids, $entry_ids );
		$child_entry_ids   = array();

		foreach ( $all_entry_data as $entry ) {
			if ( $entry->parent_item_id && isset( $indexed_entry_ids[ $entry->parent_item_id ] ) ) {
				$child_entry_ids[] = $entry->id;
			}
		}

		return $child_entry_ids;
	}

	/**
	 * Get meta values for a specific field, filtered by an array of entry IDs
	 *
	 * @since 2.01.0
	 *
	 * @param array $entry_ids
	 * @param object $child_field
	 *
	 * @return array Meta values.
	 */
	private static function get_meta_values_filtered_by_entry_ids( $entry_ids, $child_field ) {
		if ( ! $entry_ids ) {
			return array();
		}

		$args = array(
			'entry_ids'    => $entry_ids,
			'lookup_field' => $child_field,
		);

		if ( FrmField::is_option_true_in_object( $child_field, 'get_most_recent_value' ) ) {
			$args['order_by'] = 'e.id DESC';
			$args['limit']    = '1';
		}

		$linked_field = FrmField::getOne( $child_field->field_options['get_values_field'] );

		return self::get_filtered_lookup_options( $linked_field, $args );
	}

	/**
	 * @since 3.0.06
	 *
	 * @param object $field
	 * @param array $args
	 */
	private static function get_filtered_lookup_options( $field, $args ) {
		$options = FrmProEntryMeta::get_all_metas_for_field( $field, $args );
		$options = self::format_field_value_for_lookup( $options, $field, $args );
		$options = self::flatten_and_unserialize_meta_values( $options );
		self::get_unique_values( $options );

		/**
		 * @since 3.0.06
		 */
		return apply_filters( 'frm_filtered_lookup_options', $options, compact( 'field', 'args' ) );
	}

	/**
	 * Check if the values need to be filtered for the current user
	 *
	 * @since 2.01.0
	 *
	 * @param int|string $field_id
	 * @param array $field_options
	 *
	 * @return bool
	 */
	private static function need_to_filter_values_for_current_user( $field_id, $field_options ) {
		$is_filter_needed = FrmField::is_option_true_in_array( $field_options, 'lookup_filter_current_user' ) && ! current_user_can( 'administrator' ) && ! FrmAppHelper::is_admin();
		return apply_filters( 'frm_lookup_is_current_user_filter_needed', $is_filter_needed, $field_id, $field_options );
	}

	/**
	 * If meta values are arrays (checkboxes, repeating fields, etc), flatten the values to a single-dimensional array
	 *
	 * @since 2.01.0
	 *
	 * @param array $meta_values
	 *
	 * @return array Final values.
	 */
	private static function flatten_and_unserialize_meta_values( $meta_values ) {
		$final_values = array();

		foreach ( $meta_values as $meta_val ) {
			FrmAppHelper::unserialize_or_decode( $meta_val );

			if ( is_array( $meta_val ) ) {
				$final_values = array_merge( $final_values, $meta_val );
			} else {
				$meta_val       = self::decode_html_entities( $meta_val );
				$final_values[] = $meta_val;
			}
		}

		return $final_values;
	}

	/**
	 * Only get unique values in Lookup Fields
	 *
	 * @since 2.01.0
	 *
	 * @param array $final_values
	 */
	private static function get_unique_values( &$final_values ) {
		$final_values = array_unique( $final_values );
		$final_values = array_values( $final_values );
	}

	/**
	 * Order the values in a Lookup Field
	 *
	 * @since 2.01.0
	 *
	 * @param string       $order
	 * @param array        $options
	 * @param array|object $field
	 *
	 * @return void
	 */
	private static function order_values( $order, &$options, $field ) {
		$displayed_value_setting = FrmField::get_option( $field, 'lookup_displayed_value' );
		$map_to_labels           = 'label' === $displayed_value_setting;

		if ( $map_to_labels ) {
			// Sort the options as labels.
			$options          = array_reduce(
				$options,
				function ( $total, $option ) use ( $field ) {
					$total[ $option ] = FrmProFieldLookup::filter_lookup_displayed_value( $option, (array) $field );
					return $total;
				},
				array()
			);
			$options_by_label = array_flip( $options );
		}

		$options = FrmProFieldsController::order_values( $options, array( 'dynamic_field' => array( 'option_order' => $order ) ) );
		$options = array_values( $options );

		if ( $map_to_labels ) {
			// Change the sorted option labels back to their values.
			$options = array_map(
				function ( $option ) use ( $options_by_label ) {
					return $options_by_label[ $option ];
				},
				$options
			);
		}

		/**
		 * Allow custom code to change the order of the options.
		 *
		 * @since 6.22.1 Added the $field parameter.
		 *
		 * @param array        $options
		 * @param string       $order
		 * @param array|object $field
		 */
		$options = apply_filters( 'frm_order_lookup_options', $options, $order, $field );
	}

	/**
	 * Get the HTML for a Lookup Field on the front-end
	 *
	 * @since 2.01.0
	 *
	 * @param array $field
	 * @param string $field_name
	 * @param string $html_id
	 */
	public static function get_front_end_lookup_field_html( $field, $field_name, $html_id ) {
		$disabled          = self::get_disabled_input_string( $field );
		$saved_value_array = (array) $field['value'];
		$saved_value_array = self::decode_html_entities( $saved_value_array );

		if ( 'checkbox' === $field['data_type'] ) {
			$field_name .= '[]';
		}

		require FrmProAppHelper::plugin_path() . '/classes/views/lookup-fields/front-end/input.php';
	}

	/**
	 * Get the disabled="disabled" string if a field input should be disabled/readonly
	 *
	 * @since 2.01.0
	 *
	 * @param array $field
	 *
	 * @return string Disabled.
	 */
	private static function get_disabled_input_string( $field ) {
		$disabled = '';

		if ( ! FrmField::is_read_only( $field ) || FrmAppHelper::is_admin() ) {
			return $disabled;
		}

		global $frm_vars;

		if ( isset( $frm_vars['readonly'] ) && $frm_vars['readonly'] === 'disabled' ) {
			$disabled = '';
		} elseif ( $field['data_type'] === 'text' ) {
			$disabled = ' readonly="readonly"';
		} else {
			$disabled = ' disabled="disabled"';
		}

		return $disabled;
	}

	/**
	 * Add the data-placeholder attribute to lookup fields with the autocomplete option.
	 *
	 * @since 2.01.0
	 *
	 * @param array  $field
	 * @param string $add_html
	 *
	 * @return void
	 */
	public static function maybe_add_lookup_input_html( $field, &$add_html ) {
		if ( $field['type'] === 'lookup' && $field['data_type'] === 'select' && FrmField::is_option_true( $field, 'autocom' ) ) {
			// If autocomplete is selected, add a blank data-placeholder so chosen's default isn't used
			$add_html .= ' data-placeholder=" "';
		}
	}

	/**
	 * Get the options for a dependent Lookup Field based on the parent Lookup field values
	 *
	 * @since 2.01.0
	 * @deprecated 6.15 This has been replaced with ajax_get_dependent_lookup_field_options_arr. As of v6.15 lookup requests are batched.
	 */
	public static function ajax_get_dependent_lookup_field_options() {
		_deprecated_function( __METHOD__, '6.15', 'FrmProLookupFieldsController::ajax_get_dependent_lookup_field_options_arr' );

		$field_id    = FrmAppHelper::get_param( 'field_id', '', 'post', 'absint' );
		$parent_args = array(
			'parent_field_ids' => FrmAppHelper::get_param( 'parent_fields', '', 'post', 'absint' ),
			'parent_vals'      => FrmAppHelper::get_param( 'parent_vals', '', 'post', 'wp_kses_post' ),
		);
		FrmAppHelper::sanitize_value( 'wp_specialchars_decode', $parent_args['parent_vals'] );

		$child_field  = FrmField::getOne( $field_id );
		$final_values = self::get_filtered_values_for_dependent_lookup_field( $parent_args, $child_field );

		echo json_encode( $final_values );
		wp_die();
	}
}
