<?php

namespace GravityKit\GravityActions\Actions;

use GF_Field;
use GF_Field_Address;
use GF_Field_Checkbox;
use GF_Field_Date;
use GF_Field_Email;
use GF_Field_Hidden;
use GF_Field_MultiSelect;
use GF_Field_Name;
use GF_Field_Number;
use GF_Field_Phone;
use GF_Field_Radio;
use GF_Field_Select;
use GF_Field_Tag;
use GF_Field_Text;
use GF_Field_Textarea;
use GF_Field_Time;
use GF_Field_Website;
use GFAPI;
use GF_Entry_List_Table;
use GFCommon;

use GFFormsModel;

use GravityKit\GravityActions\Assets;
use GravityKit\GravityActions\Plugin;
use GravityKit\GravityActions\Admin;
use GravityKit\GravityActions\Template;
use GravityKit\GravityActions\Utils\Arrays;
use GravityKit\GravityActions\Utils;
use function GravityKit\GravityActions\get_request_var;
use function GravityKit\GravityActions\is_truthy;

class EditAction extends ActionAbstract {
	const TEXT_FIELDS = [
		GF_Field_Text::class,
		GF_Field_Textarea::class,
		GF_Field_Hidden::class,
		GF_Field_Name::class,
		GF_Field_Date::class,
		GF_Field_Time::class,
		GF_Field_Phone::class,
		GF_Field_Address::class,
		GF_Field_Website::class,
		GF_Field_Email::class,
	];

	/**
	 * @since 1.0.2
	 */
	const NUMBER_FIELDS = [
		GF_Field_Number::class,
	];

	const SELECT_FIELDS = [
		GF_Field_Select::class,
		GF_Field_Checkbox::class,
		GF_Field_Radio::class,
		GF_Field_MultiSelect::class,
	];

	const REDUCED_FIELDS = [
		GF_Field_Time::class,
	];

	protected $modifiable_field_count = 0;

	/**
	 * @inheritDoc
	 */
	public static function get_key() {
		return 'gk-bulk-edit-entries';
	}

	/**
	 * @inheritDoc
	 */
	public static function get_title() {
		return esc_attr__( 'Bulk Edit Entries', 'gk-gravityactions' );
	}

	/**
	 * @inheritDoc
	 */
	public function get_subtitle( Template $template ) {
		return $template->render( 'edit/modal-subtitle', [], false );
	}

	/**
	 * @inheritDoc
	 */
	public static function has_view() {
		return true;
	}

	/**
	 * @inheritDoc
	 */
	public static function is_ajax() {
		return true;
	}

	/**
	 * @inheritDoc
	 */
	public function hook() {
		parent::hook();
		add_action( 'gk/gravityactions/before_modal_render:edit/step-processing', [ $this, 'process_modifications' ] );
		add_action( 'gk/gravityactions/before_modal_render:edit/step-calculating', [ $this, 'recalculate_formula_fields' ] );
	}

	/**
	 * {@inheritDoc}
	 */
	public function enqueue_assets() {
		if ( ! Assets::instance()->should_enqueue_admin() ) {
			return;
		}
		wp_enqueue_script( 'gk-gravityactions-edit' );
	}

	public static function get_supported_form_meta(): array {
		$metas = [
			[
				'id' => 'date_created',
				'label' => __( 'Date Created', 'gk-gravityactions' ),
				'type' => 'datetime',
				'format' => 'Y-m-d H:i:s',
			],
			[
				'id' => 'date_updated',
				'label' => __( 'Date Updated', 'gk-gravityactions' ),
				'type' => 'datetime',
				'format' => 'Y-m-d H:i:s',
			],
			[
				'id' => 'is_starred',
				'label' => __( 'Is Starred', 'gk-gravityactions' ),
				'type' => 'boolean',
			],
			[
				'id' => 'is_read',
				'label' => __( 'Is Read', 'gk-gravityactions' ),
				'type' => 'boolean',
			],
			[
				'id' => 'ip',
				'label' => __( 'IP', 'gk-gravityactions' ),
				'type' => 'text',
			],
			[
				'id' => 'source_url',
				'label' => __( 'Source URL', 'gk-gravityactions' ),
				'type' => 'text',
			],
			[
				'id' => 'user_agent',
				'label' => __( 'User Agent', 'gk-gravityactions' ),
				'type' => 'text',
			],
			[
				'id' => 'is_fulfilled',
				'label' => __( 'Is Fulfilled', 'gk-gravityactions' ),
				'type' => 'boolean',
			],
			[
				'id' => 'created_by',
				'label' => __( 'Created By', 'gk-gravityactions' ),
				'type' => 'user',
			],
			[
				'id' => 'status',
				'label' => __( 'Status', 'gk-gravityactions' ),
				'type' => 'select',
				'allow_empty' => false,
				'choices' => [
					[
						'value' => 'active',
						'text' => __( 'Active', 'gk-gravityactions' ),
					],
					[
						'value' => 'spam',
						'text' => __( 'Spam', 'gk-gravityactions' ),
					],
					[
						'value' => 'trash',
						'text' => __( 'Trash', 'gk-gravityactions' ),
					],
				],
			],
		];

		/**
		 * Filter to allow short-circuiting the modal rendering.
		 *
		 * @since 1.3
		 *
		 * @param array $metas
		 */
		$metas = apply_filters( 'gk/gravityactions/edit_action/supported_form_meta', $metas );

		$metas = array_map( [ static::class, 'prepare_supported_form_meta' ], $metas );

		return $metas;
	}

	/**
	 * Prepares the form supported meta.
	 *
	 * @since 1.3
	 *
	 * @param array $meta
	 *
	 * @return object
	 */
	protected static function prepare_supported_form_meta( array $meta ): object {
		return (object) $meta;
	}

	/**
	 * Fetches a particular meta field.
	 *
	 * @since 1.3
	 *
	 * @param $id
	 *
	 * @return object|null
	 */
	public static function get_meta_field( $id ): ?object {
		if ( ! static::is_valid_meta( $id ) ) {
			return null;
		}

		$metas = static::get_supported_form_meta();
		$meta  = wp_list_filter( $metas, [ 'id' => $id ] );

		if ( empty( $meta ) ) {
			return null;
		}

		return reset( $meta );
	}

	/**
	 * Determine if a given field ID is a valid meta.
	 *
	 * @since 1.3
	 *
	 * @param $id
	 *
	 * @return bool
	 */
	public static function is_valid_meta( $id ): bool {
		$ids = wp_list_pluck( static::get_supported_form_meta(), 'id' );

		return in_array( $id, $ids, true );
	}

	/**
	 * @inheritDoc
	 */
	public function prepare_template_vars( array $template_vars = [] ) {
		$template_vars = parent::prepare_template_vars( $template_vars );

		if ( ! empty( $template_vars['selected_fields'] ) ) {
			$template_vars['selected_fields'] = array_unique( $template_vars['selected_fields'] );
			$template_vars['selected_fields'] = array_filter( $template_vars['selected_fields'], [ static::class, 'is_valid_selected_field' ] );
		}

		return $template_vars;
	}

	/**
	 * Process the modifications required by this action.
	 *
	 * @since 1.0
	 *
	 * @param array $template_vars Which template variables were passed down.
	 *
	 * @return array
	 */
	public function process_modifications( $template_vars ) {
		global $wpdb;

		$form_id = absint( $template_vars['form_id'] );

		$selected_fields = $template_vars['selected_fields'];
		$selected_fields = array_unique( $selected_fields );

		$metas = array_filter( $selected_fields, [ static::class, 'is_valid_meta' ] );
		$metas = array_map( [ static::class, 'get_meta_field' ], $metas );

		$fields = array_filter( $selected_fields, static function ( $field_id ) use ( $form_id ) {
			if ( static::is_valid_meta( $field_id ) ) {
				return false;
			}

			$field = GFAPI::get_field( $form_id, $field_id );

			// Fields that combine multiple inputs into a single row should only have the parent field ID included.
			if ( static::is_reduced_field( $field ) ) {
				return static::is_not_sub_field( $field_id );
			}

			if ( ! static::is_sub_field( $field_id ) ) {

				// Fields that have their subfields selected don't get value.
				if ( ! empty( $field->inputs ) ) {
					return false;
				}
			}

			return true;
		} );

		$field_values = $this->prepare_field_values( $form_id, $fields, $template_vars['field_values'] );

		$return = [];

		if ( ! empty( $fields ) ) {
			$insert_sqls = $this->get_fields_insert_sql( $form_id, $template_vars['entries'], $fields, $field_values );

			// There will be an insert per entry to avoid reaching the limit for the database.
			foreach ( $insert_sqls as $sql ) {
				$return['insert'][] = $wpdb->query( $sql );
			}

			$sql = $this->get_fields_update_sql( $template_vars['form_id'], $template_vars['entries'], $fields, $field_values );

			$return['update'] = $wpdb->query( $sql );
		}

		if ( ! empty( $metas ) ) {
			$meta_values = $this->prepare_meta_values( $form_id, $metas, $template_vars['field_values'] );

			$update_metas_sql = $this->get_metas_update_sql( $template_vars['form_id'], $template_vars['entries'], $metas, $meta_values );

			$return['update_metas'] = $wpdb->query( $update_metas_sql );
		}

		return $return;
	}

	/**
	 * Given a Form and Which fields we are trying to recalculate, infinitely loop through the fields and recalculate.
	 *
	 * @since 1.4
	 *
	 * @param int|string $form_id            Which forms we are recalculating for.
	 * @param array      $recalculate_fields Fields that initiated the recalculation.
	 * @param array      $entries            Entries IDs to recalculate.
	 *
	 * @return array|mixed
	 */
	protected function recursively_recalculate_fields( $form_id, array $recalculate_fields, array $entries ) {
		// Retrieve the form object by its ID.
		$form = \GFAPI::get_form( $form_id );

		// Get all formula fields in the form.
		$formula_fields = Utils\Form::get_formula_fields( $form );
		$results        = [];

		while ( ! empty( $recalculate_fields ) ) {
			// Calculate the intersection of fields and formula fields.
			$selected_formula_fields = array_intersect( $formula_fields, $recalculate_fields );

			// Initialize an array to store calculation fields involved in the selected formula fields.
			$calculation_fields_involved = [];

			// Iterate over the selected formula fields.
			foreach ( $selected_formula_fields as $field ) {
				// Get calculation fields for each formula field and merge them into the array.
				$calculation_fields_involved = array_merge( $calculation_fields_involved, Utils\Form::get_calculation_fields_by_formula_field( $form, $field ) );
			}

			// Remove duplicate calculation fields from the array.
			$recalculate_fields = $calculation_fields_involved = array_unique( $calculation_fields_involved );

			$results = array_merge( $results, $this->batch_update_fields( $form_id, $entries, $calculation_fields_involved ) );
		}

		return $results;
	}

	/**
	 * Given a Form and Which fields we are trying to recalculate, update the fields using the batch update method from Gravity Form.
	 *
	 * @since 1.4
	 *
	 * @param int   $form_id                    Which forms we are recalculating for.
	 * @param array $entries                    Entries IDs to recalculate.
	 * @param array $calculation_fields_involved Calculation fields involved in the selected formula fields.
	 *
	 * @return array
	 */
	protected function batch_update_fields( $form_id, $entries, $calculation_fields_involved ): array {
		// Retrieve the form object by its ID.
		$form = \GFAPI::get_form( $form_id );

		// Get the current fields with their entry information.
		$current_fields_by_entry = $this->get_current_fields_by_entry( $form_id, $entries );

		// Begin a batch of field operations.
		GFFormsModel::begin_batch_field_operations();

		// Iterate over the current fields by entry.
		foreach ( $current_fields_by_entry as $entry_id => $current_fields ) {
			// Get the entry object by its ID.
			$entry = GFAPI::get_entry( $entry_id );

			// Iterate over the calculation fields involved.
			foreach ( $calculation_fields_involved as $field_id ) {
				// Get the field object by its ID.
				$field = GFAPI::get_field( $form_id, $field_id );

				// Get the input elements associated with the field.
				$inputs = $field->get_entry_inputs();

				// Check if the field has multiple inputs.
				if ( is_array( $inputs ) ) {
					// Save each input in the batch of field operations.
					foreach ( $inputs as $input ) {
						GFFormsModel::save_input( $form, $field, $entry, $current_fields, $input['id'] );

						// Clear transient cache for the entry.
						\GFCache::delete( 'GFFormsModel::get_lead_field_value_' . $entry_id . '_' . $input['id'] );
					}
				} else {
					// Save the field in the batch of field operations.
					GFFormsModel::save_input( $form, $field, $entry, $current_fields, $field->id );

					// Clear transient cache for the entry.
					\GFCache::delete( 'GFFormsModel::get_lead_field_value_' . $entry_id . '_' . $field->id );
				}
			}
		}
		// Commit the batch of field operations and store the results.
		return GFFormsModel::commit_batch_field_operations();
	}

	/**
	 * Process the recalculations required when Formula Fields are involved on the Bulk Edit..
	 *
	 * @since 1.4
	 *
	 * @param array $template_vars Which template variables were passed down.
	 *
	 * @return array
	 */
	public function recalculate_formula_fields( $template_vars ) {

		$form_id = (int) $template_vars['form_id'];

		$selected_fields = $template_vars['selected_fields'];
		$selected_fields = array_unique( $selected_fields );

		// Ensure that the fields from here on out are the ones we allow for this operation.
		$fields = array_filter( $selected_fields, static function ( $field_id ) use ( $form_id ) {
			if ( static::is_valid_meta( $field_id ) ) {
				return false;
			}

			$field = GFAPI::get_field( $form_id, $field_id );

			// Fields that combine multiple inputs into a single row should only have the parent field ID included.
			if ( static::is_reduced_field( $field ) ) {
				return static::is_not_sub_field( $field_id );
			}

			if ( ! static::is_sub_field( $field_id ) ) {

				// Fields that have their subfields selected don't get value.
				if ( ! empty( $field->inputs ) ) {
					return false;
				}
			}

			return true;
		} );

		$this->recursively_recalculate_fields( $form_id, $fields, $template_vars['entries'] );
	}

	/**
	 * Retrieves the current fields with their entry information for the given form ID and entries.
	 *
	 * @since 1.4
	 *
	 * @param int   $form_id The ID of the form.
	 * @param array $entries An array of entry IDs to filter the fields.
	 *
	 * @return array An array of objects containing the id, entry_id, meta_key, and item_index of the current fields.
	 */
	protected function get_current_fields_by_entry( int $form_id, array $entries ): array {
		global $wpdb;
		$entry_meta_table = GFFormsModel::get_entry_meta_table_name();

		$entry_ids     = array_map( 'intval', $entries );
		$entry_ids_sql = implode( ', ', $entry_ids );

		$sql[] = "SELECT id, entry_id, meta_key, item_index FROM `{$entry_meta_table}`";
		$sql[] = 'WHERE';
		$sql[] = '1=1';
		$sql[] = $wpdb->prepare( 'AND `form_id` = %d', $form_id );
		$sql[] = "AND `entry_id` IN ( {$entry_ids_sql} )";
		$sql   = implode( " \n ", $sql );

		// Get the data from the DB.
		$current_fields_with_entry = $wpdb->get_results( $sql );
		$current_fields_by_entry   = [];

		// Organize the current fields by their entry IDs.
		foreach ( $current_fields_with_entry as $current_field ) {
			if ( ! isset( $current_fields[ $current_field->entry_id ] ) ) {
				$current_fields_by_entry[ $current_field->entry_id ] = [];
			}

			$current_fields_by_entry[ $current_field->entry_id ][] = $current_field;
		}

		return $current_fields_by_entry;
	}

	/**
	 * Modify field values before they are saved to the database.
	 *
	 * @param int   $form_id
	 * @param array $fields
	 * @param array $field_values
	 *
	 * @return array
	 */
	protected function prepare_field_values( $form_id, $fields, $field_values ) {

		foreach ( $fields as $field_id ) {
			// skip any meta.
			if ( static::is_valid_meta( $field_id ) ) {
				continue;
			}

			$field = GFAPI::get_field( $form_id, $field_id );

			switch ( $field->get_input_type() ) {

				// Time fields convert from multiple inputs to a single value.
				case 'time':
					$value = static::get_all_input_values_for_field_id( $field_values, $field_id );

					/** @var GF_Field_Time $field */
					$field_values[ $field->id ] = $field->get_value_save_entry( $value, [], '', 0, [] );
					break;
			}
		}

		return $field_values;
	}

	/**
	 * Modify field values before they are saved to the database.
	 *
	 * @param int   $form_id
	 * @param array $metas
	 * @param array $meta_values
	 *
	 * @return array
	 */
	protected function prepare_meta_values( $form_id, array $metas, array $meta_values ) {
		$values = [];
		foreach ( $metas as $meta ) {
			if ( ! isset( $meta_values[ $meta->id ] ) ) {
				continue;
			}

			$value = $meta_values[ $meta->id ];

			switch ( $meta->type ) {
				case 'datetime':
					$value = get_gmt_from_date( $value, 'Y-m-d H:i:s' );
					break;
				case 'boolean':
					$value = (int) $value;
					break;
			}

			$values[ $meta->id ] = $value;
		}

		return $values;
	}

	/**
	 * Get all inputs for the current field ID.
	 *
	 * @param $field_values
	 * @param $field_id
	 *
	 * @return array
	 */
	protected static function get_all_input_values_for_field_id( $field_values, $field_id ) {
		$values = [];

		foreach ( $field_values as $input_id => $input_value ) {
			// If the $input_id matches $field->id, use the value
			if ( floor( $input_id + 0 ) === (float) $field_id ) {
				$values[] = $input_value;
			}
		}

		return $values;
	}

	/**
	 * Check if the ID is a subfield.
	 *
	 * @since 1.0
	 *
	 * @param int|float|string $field_id
	 *
	 * @return bool
	 */
	public static function is_sub_field( $field_id ) {
		if ( ! is_numeric( $field_id ) ) {
			return false;
		}

		return is_float( $field_id + 0 );
	}

	/**
	 * Check if the ID is not a subfield.
	 *
	 * @since 1.0
	 *
	 * @param int|float|string $field_id
	 *
	 * @return bool
	 */
	public static function is_not_sub_field( $id ): bool {
		return ! static::is_sub_field( $id );
	}

	public static function is_valid_selected_field( $id ): bool {
		return is_numeric( $id ) || static::is_sub_field( $id ) || static::is_valid_meta( $id );
	}

	public static function is_valid_countable_field( $id ): bool {
		return ! static::is_sub_field( $id ) || static::is_valid_meta( $id );
	}

	/**
	 * Check if the field is stored as an integer ID even though it has inputs.
	 *
	 * For example, a time field with ID #1 has inputs #1.1, #1.2, and #1.3. When entry meta is stored, however, it's
	 * stored using the key "1".
	 *
	 * @param GF_Field $field
	 *
	 * @return bool
	 */
	public static function is_reduced_field( $field ) {
		return in_array( get_class( $field ), static::REDUCED_FIELDS, true );
	}

	/**
	 * Fetches the Insert SQL given a set of params required.
	 *
	 * @since 1.0
	 *
	 * @param int   $form_id
	 * @param array $entries
	 * @param array $fields
	 * @param array $field_values
	 *
	 * @return array
	 */
	protected function get_fields_insert_sql( int $form_id, array $entries, array $fields, array $field_values ): array {
		global $wpdb;
		$entry_meta_table = GFFormsModel::get_entry_meta_table_name();

		$entry_ids     = array_map( 'intval', $entries );
		$entry_ids_sql = implode( ', ', $entry_ids );

		$fields        = array_values( $fields );
		$field_ids     = array_unique( array_map( static function ( $id ) {
			return sprintf( "'%s'", esc_sql( $id ) );
		}, $fields ) );
		$field_ids_sql = implode( ', ', $field_ids );
		$all_sql       = [];

		$sql[]             = "SELECT `entry_id`, `meta_key` FROM `{$entry_meta_table}`";
		$sql[]             = 'WHERE';
		$sql[]             = '1=1';
		$sql[]             = $wpdb->prepare( 'AND `form_id` = %d', $form_id );
		$sql[]             = "AND `entry_id` IN ( {$entry_ids_sql} )";
		$sql[]             = "AND `meta_key` IN ( {$field_ids_sql} )";
		$sql               = implode( " \n ", $sql );
		$to_exclude_insert = $wpdb->get_results( $sql );
		$exclude           = [];
		foreach ( $to_exclude_insert as $result ) {
			$entry_id = (int) $result->entry_id;

			if ( ! isset( $exclude[ $entry_id ] ) ) {
				$exclude[ $entry_id ] = [];
			}
			$exclude[ $entry_id ][] = $result->meta_key;
		}

		foreach ( $entry_ids as $entry_id ) {
			$sql                = [];
			$_insert_values_sql = [];

			foreach ( $fields as $i => $field_id ) {
				if ( isset( $exclude[ $entry_id ] ) && in_array( $field_id, $exclude[ $entry_id ], false ) ) {
					continue;
				}
				$value                = isset( $field_values[ $field_id ] ) ? $field_values[ $field_id ] : '';
				$value                = stripslashes_deep( $value );
				$_insert_values_sql[] = $wpdb->prepare( "( %d, %d, %s, %s, %s )", $form_id, $entry_id, $field_id, $value, '' );
			}

			if ( empty( $_insert_values_sql ) ) {
				continue;
			}

			$sql['insert']              = "INSERT INTO `{$entry_meta_table}`";
			$sql['insert_fields']       = "(`form_id`, `entry_id`, `meta_key`, `meta_value`, `item_index`)";
			$sql['insert_values_start'] = 'VALUES';
			$sql['insert_values']       = implode( ", \n ", $_insert_values_sql );

			$all_sql[] = implode( " \n ", $sql ) . ';';
		}

		return $all_sql;
	}

	/**
	 * Fetches the Update SQL given a set of params required.
	 *
	 * @since 1.0
	 *
	 * @param int   $form_id
	 * @param array $entries
	 * @param array $fields
	 * @param array $field_values
	 *
	 * @return string
	 */
	protected function get_fields_update_sql( int $form_id, array $entries, array $fields, array $field_values ): string {
		global $wpdb;
		$entry_meta_table = GFFormsModel::get_entry_meta_table_name();

		$entry_ids     = array_map( 'intval', $entries );
		$entry_ids_sql = implode( ', ', $entry_ids );

		$field_ids     = array_unique( array_map( static function ( $id ) {
			return sprintf( "'%s'", esc_sql( $id ) );
		}, $fields ) );
		$field_ids_sql = implode( ', ', $field_ids );

		$_set_sql = $_where_sql = [];

		$sql['update'] = "UPDATE `{$entry_meta_table}`";

		$_set_sql[] = 'SET';
		$_set_sql[] = '`meta_value` = CASE `meta_key`';
		foreach ( $fields as $field ) {
			$value      = isset( $field_values[ $field ] ) ? $field_values[ $field ] : '';
			$value      = stripslashes_deep( $value );
			$_set_sql[] = $wpdb->prepare( 'WHEN %s THEN %s', $field, $value );
		}
		$_set_sql[] = 'END';

		$_where_sql[] = 'WHERE';
		$_where_sql[] = '1=1';
		$_where_sql[] = $wpdb->prepare( 'AND `form_id` = %d', $form_id );
		$_where_sql[] = "AND `entry_id` IN ( {$entry_ids_sql} )";
		$_where_sql[] = "AND `meta_key` IN ( {$field_ids_sql} )";

		$sql['set']   = implode( " \n ", $_set_sql );
		$sql['where'] = implode( " \n ", $_where_sql );
		$sql          = implode( " \n ", $sql ) . ';';

		return $sql;
	}

	/**
	 * Fetches the Update SQL given a set of params required.
	 *
	 * @since 1.0
	 *
	 * @param int   $form_id
	 * @param array $entries
	 * @param array $metas
	 * @param array $meta_values
	 *
	 * @return string
	 */
	protected function get_metas_update_sql( int $form_id, array $entries, array $metas, array $meta_values ): string {
		global $wpdb;
		$entry_meta_table = GFFormsModel::get_entry_table_name();

		$entry_ids     = array_map( 'intval', $entries );
		$entry_ids_sql = implode( ', ', $entry_ids );

		$_set_sql = $_where_sql = [];

		$sql['update'] = "UPDATE `{$entry_meta_table}`";

		$_set_sql = [];
		foreach ( $metas as $meta ) {
			$value = $meta_values[ $meta->id ] ?? '';
			$value = stripslashes_deep( $value );
			if ( is_int( $value ) ) {
				$_set_sql[] = $wpdb->prepare( '`' . $meta->id . '` = %d', $value );
			} elseif ( is_float( $value ) ) {
				$_set_sql[] = $wpdb->prepare( '`' . $meta->id . '` = %f', $value );
			} elseif ( is_string( $value ) ) {
				$_set_sql[] = $wpdb->prepare( '`' . $meta->id . '` = %s', $value );
			}
		}

		$_where_sql[] = 'WHERE';
		$_where_sql[] = '1=1';
		$_where_sql[] = $wpdb->prepare( 'AND `form_id` = %d', $form_id );
		$_where_sql[] = "AND `id` IN ( {$entry_ids_sql} )";

		$sql['set']   = "SET \n" . implode( ", \n ", $_set_sql );
		$sql['where'] = implode( " \n ", $_where_sql );
		$sql          = implode( " \n ", $sql ) . ';';

		return $sql;
	}

	/**
	 * Determines which template we should include for this field.
	 *
	 * @since 1.0
	 *
	 * @param GF_Field|false $field
	 *
	 * @return string
	 */
	public function get_template_for_field( $field ) {

		if ( $this->is_complex_field( $field->type ) ) {
			return $this->get_complex_field_template( $field->type );
		}

		return $this->get_simple_field_template( $field->type );
	}

	/**
	 * Which simple fields alongside their template.
	 *
	 * @since 1.0
	 *
	 * @return array[string,string]
	 */
	public function get_simple_fields() {
		$fields = [
			'text' => 'components/fields/text',
			'hidden' => 'components/fields/text',
			'website' => 'components/fields/text',
			'phone' => 'components/fields/text',
			'date' => 'components/fields/date',
			'datetime' => 'components/fields/datetime',
			'textarea' => 'components/fields/textarea',
			'number' => 'components/fields/number',
			'email' => 'components/fields/email',
			'checkbox' => 'components/fields/checkbox',
			'radio' => 'components/fields/radio',
			'select' => 'components/fields/select',
			'boolean' => 'components/fields/boolean',
		];

		return $fields;
	}

	/**
	 * Determines if a given field is simple and requires it's own template.
	 *
	 * @since 1.0
	 *
	 * @param string $field
	 *
	 * @return bool
	 */
	public function is_simple_field( $field ) {
		return isset( $this->get_simple_fields()[ $field ] );
	}

	/**
	 * Gets the template associated with a given type of field.
	 *
	 * @since 1.0
	 *
	 * @param string $field
	 *
	 * @return string
	 */
	public function get_simple_field_template( $field ) {
		if ( ! $this->is_simple_field( $field ) ) {
			return null;
		}

		return $this->get_simple_fields()[ $field ];
	}

	/**
	 * Which fields are complex, which means they will need a unique template.
	 *
	 * @since 1.0
	 *
	 * @return array[string,string]
	 */
	public function get_complex_fields() {
		$fields = [
			'name' => 'components/fields/complex/default',
			'address' => 'components/fields/complex/default',
			'time' => 'components/fields/complex/time',
			'user' => 'components/fields/complex/user',
		];

		return $fields;
	}

	/**
	 * Determines if a given field is complex and requires its own template.
	 *
	 * @since 1.0
	 *
	 * @param string $field
	 *
	 * @return bool
	 */
	public function is_complex_field( $field ) {
		return isset( $this->get_complex_fields()[ $field ] );
	}

	/**
	 * Gets the template associated with a given type of field.
	 *
	 * @since 1.0
	 *
	 * @param string $field
	 *
	 * @return string
	 */
	public function get_complex_field_template( $field ) {
		if ( ! $this->is_complex_field( $field ) ) {
			return null;
		}

		return $this->get_complex_fields()[ $field ];
	}

	/**
	 * @param GF_Field $field
	 *
	 * @return bool
	 */
	public static function is_valid_field_type( $field ) {
		$available_field_classes = array_merge( static::TEXT_FIELDS, static::NUMBER_FIELDS, static::SELECT_FIELDS );

		return in_array( get_class( $field ), $available_field_classes, true );
	}

	/**
	 * @param GF_Field $field
	 *
	 * @return bool
	 */
	public static function is_select_field_type( $field ) {
		return in_array( get_class( $field ), static::SELECT_FIELDS, true );
	}
}
