<?php
/**
 * GravityView Extension -- DataTables -- Server side data
 *
 * @since     1.0.4
 * @license   GPL2+
 * @author    GravityKit <hello@gravitykit.com>
 * @link      https://www.gravitykit.com
 * @copyright Copyright 2014, Katz Web Services, Inc.
 *
 * @package   GravityView
 */

use GV\Entry_Collection;
use GV\Entry_DataTable_Template;
use GV\Template_Context;
use GV\View;

if ( ! defined( 'ABSPATH' ) ) {
	return;
}

class GV_Extension_DataTables_Data {

	/**
	 * @var bool True: the file is currently being accessed directly by an AJAX request or otherwise. False: Normal WP load.
	 */
	static $is_direct_access = false;

	public function __construct() {

		$this->add_hooks();
		$this->trigger_ajax();
	}

	/**
	 * Trigger the AJAX response
	 *
	 * @since 1.3
	 */
	private function trigger_ajax() {

		// Reduce query load for DT calls
		add_action( 'admin_init', array( $this, 'reduce_query_load' ) );

		// enable ajax
		add_action( 'wp_ajax_gv_datatables_data', array( $this, 'get_datatables_data' ) );
		add_action( 'wp_ajax_nopriv_gv_datatables_data', array( $this, 'get_datatables_data' ) );
	}

	/**
	 * If this file is being accessed directly, then set up WP so we can handle the AJAX request
	 *
	 * @since      1.3
	 * @deprecated Remove direct access altogether.
	 */
	function maybe_bootstrap_wp() {

		gravityview()->log->notice( 'Accessing the DataTables data directly is no longer possible. Use the WordPress AJAX API.' );
	}

	/**
	 * Create required globals for minimal bootstrap
	 *
	 * @since      1.3
	 * @deprecated Remove direct access altogether.
	 */
	function bootstrap_setup_globals() {

		gravityview()->log->notice( 'Accessing the DataTables data directly is no longer possible. Use the WordPress AJAX API.' );
	}

	/**
	 * Include Gravity Forms, GravityView, and GravityView Extensions
	 *
	 * @since      1.3
	 * @deprecated Remove direct access altogether.
	 */
	function bootstrap_gv() {

		gravityview()->log->notice( 'Accessing the DataTables data directly is no longer possible. Use the WordPress AJAX API.' );
	}

	/**
	 * Include only the WP files needed
	 *
	 * This brilliant piece of code (cough) is from the dsIDXpress plugin.
	 *
	 * @since      1.3
	 * @deprecated Remove direct access altogether.
	 */
	function bootstrap_wp_for_direct_access() {

		gravityview()->log->notice( 'Accessing the DataTables data directly is no longer possible. Use the WordPress AJAX API.' );
	}

	/**
	 * @since 1.3
	 */
	function add_hooks() {

		/**
		 * Don't fetch entries inline when rendering the View, since we're using AJAX requests to do that.
		 *
		 * Only affects GravityView 1.3+
		 *
		 * @since 1.1
		 */
		add_filter( 'gravityview_get_view_entries_table-dt', '__return_false' );

		// Add template path
		add_filter( 'gravityview_template_paths', array( $this, 'add_template_path' ) );

		// Override the template classes
		add_filter( 'gravityview/template/view/class', array( $this, 'set_view_template_class' ), 10, 2 );
		add_filter( 'gravityview/template/entry/class', array( $this, 'set_entry_template_class' ), 10, 3 );

		if ( ! is_admin() ) {
			// Enqueue scripts and styles
			add_action( 'gravityview/template/after', array( $this, 'add_scripts_and_styles' ) );
		}

		// Extend DataTables view as needed
		add_action( 'gravityview/template/after', array( $this, 'extend_view' ) );

		add_filter( 'gravityview-inline-edit/js-settings', array( $this, 'maybe_modify_inline_edit_settings' ), 10, 2 );
	}

	/**
	 * Verify AJAX request nonce
	 *
	 * @return boolean Whether the nonce is okay or not.
	 */
	function check_ajax_nonce() {

		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'gravityview_datatables_data' ) ) {
			gravityview()->log->debug( '[DataTables] AJAX request - NONCE check failed' );

			return $this->exit_or_return( false );
		}

		return true;
	}

	/**
	 * Removes the queries caused by `widgets_init` for AJAX calls (and for generating the data)
	 *
	 * @since 2.3.1
	 *
	 * @return void
	 */
	public function reduce_query_load() {

		if ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) {
			return;
		}

		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'gravityview_datatables_data' ) ) {
			return;
		}

		remove_all_actions( 'widgets_init' );
	}

	/**
	 * Get AJAX ready by defining AJAX constants and sending proper headers.
	 *
	 * @since  1.1
	 *
	 * @param string  $content_type Type of content to be set in header.
	 * @param boolean $cache        Do you want to cache the results?
	 */
	static function do_ajax_headers( $content_type = 'text/plain', $cache = false ) {

		// Tests are running, don't output any headers.
		if ( defined( 'DOING_GRAVITYVIEW_TESTS' ) ) {
			return;
		}

		// If it's already been defined, that means we don't need to do it again.
		if ( defined( 'GV_AJAX_IS_SETUP' ) ) {
			return;
		} else {
			define( 'GV_AJAX_IS_SETUP', true );
		}

		if ( ! defined( 'DOING_AJAX' ) ) {
			define( 'DOING_AJAX', true );
		}

		// Fix errors thrown by NextGen Gallery
		if ( ! defined( 'NGG_SKIP_LOAD_SCRIPTS' ) ) {
			define( 'NGG_SKIP_LOAD_SCRIPTS', true );
		}

		// Prevent some theoretical random stuff from happening
		if ( ! defined( 'IFRAME_REQUEST' ) ) {
			define( 'IFRAME_REQUEST', true );
		}

		// Get rid of previously set headers
		if ( function_exists( 'header_remove' ) ) {
			header_remove();
		}

		// Setting the content type actually introduces 200ms of latency for some reason.
		// Give us the option to say no.
		if ( ! empty( $content_type ) ) {
			@header( 'Content-Type: ' . $content_type . '; charset=UTF-8' );
		}

		// @see send_nosniff_header()
		@header( 'X-Content-Type-Options: nosniff' );
		@header( 'Accept-Encoding: gzip, deflate' );

		if ( $cache ) {
			@header( 'Cache-Control: public, store, post-check=10000000, pre-check=100000;' );
			@header( 'Expires: Thu, 15 Apr 2030 20:00:00 GMT;' );
			@header( 'Vary: Accept-Encoding' );
			@header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', strtotime( '-2 months' ) ) . ' GMT' );

		} else {

			if ( ! defined( 'DONOTCACHEPAGE' ) ) {
				define( 'DONOTCACHEPAGE', 'true' );
			}

			@nocache_headers();
		}

		@header( 'HTTP/1.1 200 OK', true, 200 );
		@header( 'X-Robots-Tag:noindex;' );
	}

	/**
	 * main AJAX logic to retrieve DataTables data
	 */
	function get_datatables_data() {

		if ( empty( $_POST ) ) {
			gravityview()->log->debug( __METHOD__ . ': no $_POST' );

			return;
		}

		gravityview()->log->debug( __METHOD__ . ': $_POST', array( 'data' => $_POST ) );

		// Prevent error output
		ob_start();

		// Send correct headers
		$this->do_ajax_headers( 'application/javascript' );

		if ( ! $result = $this->check_ajax_nonce() ) {
			ob_end_clean();

			return $this->exit_or_return( $result );
		}

		if ( empty( $_POST['view_id'] ) ) {
			gravityview()->log->debug( '[DataTables] AJAX request - View ID check failed' );
			ob_end_clean();

			return $this->exit_or_return( false );
		}

		/**
		 * Enable or disable the Content-Length header on the AJAX JSON response
		 *
		 * @since 2.0
		 *
		 * @param boolean $has_content_length true by default
		 */
		$has_content_length = apply_filters( 'gravityview/datatables/json/header/content_length', true );

		// Prevent emails from being encrypted
		add_filter( 'gravityview_email_prevent_encrypt', '__return_true' );

		gravityview()->log->debug( '[DataTables] AJAX Request ($_POST)', array( 'data' => $_POST ) );

		// Pass $_GET variables to the View functions, since they're relied on heavily
		// for searching and filtering, for example the A-Z widget
		$_GET = ! empty( $_POST['getData'] ) ? json_decode( stripslashes( $_POST['getData'] ), true ) : array();

		$view_id = intval( $_POST['view_id'] );

		global $post;

		$post = get_post( \GV\Utils::_POST( 'post_id', null ) );

		setup_postdata( $post );

		$view_collection = \GV\View_Collection::from_post( $post );

		$atts = array();
		if ( $view = $view_collection->get( $view_id ) ) {
			$atts = $view->settings->as_atts();
			gravityview()->log->debug(
				'View #{view_id} found in $post View Collection',
				array(
					'view_id' => $view_id,
					'data'    => $atts,
				)
			);
		} elseif ( ! $view = View::by_id( $view_id ) ) {
			gravityview()->log->error( 'View #{view_id} not found', array( 'view_id' => $view_id ) );

			return $this->exit_or_return( false );
		}

		$dt_settings = get_post_meta( $view->ID, '_gravityview_datatables_settings', true );

		$is_server_side = 'serverSide' === \GV\Utils::get( $dt_settings, 'processing_mode', 'serverSide' );

		// check for order/sorting
		$atts['sort_field']     = array();
		$atts['sort_direction'] = array();
		foreach ( (array) \GV\Utils::_POST( 'order', array() ) as $i => $order ) {

			$order_index = \GV\Utils::get( $order, 'column' );

			if ( null !== $order_index ) {
				if ( ! empty( $_POST['columns'][ $order_index ]['name'] ) ) {
					// remove prefix 'gv_'
					$atts['sort_field'][]     = substr( $_POST['columns'][ $order_index ]['name'], 3 );
					$atts['sort_direction'][] = strtoupper( \GV\Utils::get( $order, 'dir', 'ASC' ) );
				}
			}
		}

		// check for search
		if ( ! empty( $_POST['search']['value'] ) ) {
			// inject DT search
			add_filter( 'gravityview_fe_search_criteria', array( $this, 'add_global_search' ), 5, 1 );
		}

		// Paging/offset
		$atts['page_size'] = isset( $_POST['length'] ) ? intval( $_POST['length'] ) : '';

		$offset = isset( $_POST['start'] ) ? intval( $_POST['start'] ) : 0;

		// check if someone requested the full filtered data (eg. TableTools print button)
		if ( $atts['page_size'] == '-1' || ! $is_server_side ) {
			$mode              = 'all';
			$atts['page_size'] = PHP_INT_MAX;
		} else {
			// regular mode - get view entries
			$mode = 'page';
		}

		// Set the pagenum in $_GET for the sequence field to work correctly with DataTables pagination.
		if ( $offset > 0 && $atts['page_size'] > 0 && $atts['page_size'] !== PHP_INT_MAX ) {
			$_GET['pagenum'] = intval( $offset / $atts['page_size'] ) + 1;
		}

		$view->settings->update( $atts );

		// Force shortcode parametrization.
		if ( $shortcode_atts = \GV\Utils::_POST( 'shortcode_atts' ) ) {
			foreach ( $shortcode_atts as $att => $value ) {
				if ( in_array( $att, array( 'search_field', 'search_value', 'search_operator' ) ) ) {
					$view->settings->update( array( $att => $value ) );
				}
			}
		}

		$atts = $view->settings->as_atts();

		foreach ( $atts as $key => $att ) {
			$atts[ $key ] = \GravityView_Merge_Tags::replace_variables( $att, ( $view->form ? $view->form->form : null ), array(), false, false, false );
		}

		gravityview()->log->debug( 'DataTables final $atts', $atts );

		// "Created By" field can be configured to display values other than the user ID,
		// in which case we need to fetch all entries and filter them manually after the fact.
		$created_by_field_columns = [];

		if ( ! empty( $_POST['columns'] ) ) {
			foreach ( $_POST['columns'] as &$column ) {
				if ( $column['name'] === 'gv_created_by' && '' !== $column['search']['value'] ) {
					$created_by_field_columns[] = $column; // Save column data for later filtering.

					$column['search']['value'] = ''; // Clear the search value to prevent it from being used in the DB query.

					// Unset the page size since we need to fetch all entries.
					$view->settings->set( 'page_size', PHP_INT_MAX );
				}
			}
		}

		$page = ( ( $offset - ( $view->settings->get( 'offset', 0 ) ?: 0 ) ) / ( $view->settings->get( 'page_size', 20 ) ?: 20 ) ) + 1;

		add_action(
			'gravityview/view/query',
			$callback = function ( $query ) use ( $page, $view ) {

				/** @var GF_Query $query */
				$query->page( $page );

				// Get an array of configured field IDs to make sure searches ONLY search available fields
				$configured_fields    = $view->fields->by_visible()->as_configuration();
				$configured_field_ids = wp_list_pluck( $configured_fields['directory_table-columns'], 'id' );

				foreach ( \GV\Utils::_POST( 'columns', array() ) as $dt_column ) {
					if ( empty( $dt_column['searchable'] ) || ! isset( $dt_column['search']['value'] ) ) {
						continue;
					}

					if ( '' === $dt_column['search']['value'] ) {
						continue;
					}

					$field_id_or_meta_name = str_replace( 'gv_', '', sanitize_text_field( $dt_column['name'] ) );

					// Field doesn't exist in View configuration; bail!
					if ( ! in_array( $field_id_or_meta_name, $configured_field_ids, true ) ) {
						continue;
					}

					if ( 'gv_entry_approval' === $dt_column['name'] ) {
						$field_id_or_meta_name = 'is_approved';
					}

					$field = GFAPI::get_field( $view->form->ID, $field_id_or_meta_name );

					// Use JSON literal for fields that are stored as JSON and where certain characters are escaped in the DB (e.g., Multi-Select field with an option that contains "ä").
					$query_literal_class = $field && 'json' === $field->storageType ? '\GF_Query_JSON_Literal' : '\GF_Query_Literal';

					$condition = new \GF_Query_Condition(
						new \GF_Query_Column( $field_id_or_meta_name ),
						\GF_Query_Condition::LIKE,
						new $query_literal_class( '%' . sanitize_text_field( $dt_column['search']['value'] ) . '%' )
					);

					$q = $query->_introspect();

					$query->where( \GF_Query_Condition::_and( $q['where'], $condition ) );
				}
			}
		);

		$entries = $view->get_entries( gravityview()->request );

		remove_action( 'gravityview/view/query', $callback );

		// wrap all
		$output = array(
			'draw'            => intval( ( isset( $_POST['draw'] ) && is_numeric( $_POST['draw'] ) ) ? $_POST['draw'] : - 1 ),
			'recordsTotal'    => $entries->total(),
			'recordsFiltered' => $entries->total(),
			'data'            => $this->get_output_data( $entries, $view, $post ),
		);

		if ( ! empty( $created_by_field_columns ) ) {
			$output = $this->filter_created_by_fields( $output, $created_by_field_columns );
		}

		/**
		 * Filter the output returned from the AJAX request
		 *
		 * @since 2.3
		 *
		 * @param array            $output  The output data array.
		 * @param View             $view    The View object.
		 * @param Entry_Collection $entries The entries collection.
		 */
		$output = apply_filters( 'gravityview/datatables/output', $output, $view, $entries );

		wp_reset_postdata();

		gravityview()->log->debug( '[DataTables] Ajax request answer', array( 'data' => $output ) );

		$json = wp_json_encode( $output );

		// End prevent error output
		$errors = ob_get_clean();

		if ( ! empty( $errors ) ) {
			gravityview()->log->error( __METHOD__ . ' Errors generated during DataTables response', array( 'data' => $errors ) );
		}

		if ( $has_content_length ) {
			$strlen = function_exists( 'mb_strlen' ) ? mb_strlen( $json ) : strlen( $json );
			// Fix strange characters before JSON response because of "Transfer-Encoding: chunked" header
			@header( 'Content-Length: ' . $strlen );
		}

		return $this->exit_or_return( $json );
	}

	/**
	 * Filter output by "Created By" field columns' search values.
	 *
	 * @param array $output
	 * @param array $created_by_field_columns
	 *
	 * @return array
	 */
	private function filter_created_by_fields( $output, $created_by_field_columns ) {
		if ( empty( $created_by_field_columns ) || empty( $output['data'] ) ) {
			return $output;
		}

		$filtered_data = $output['data'];

		foreach ( $created_by_field_columns as $column ) {
			$created_by_id = intval( $column['data'] );
			$search_value  = strtolower( $column['search']['value'] );

			if ( '' === $search_value ) {
				continue;
			}

			foreach ( $output['data'] as $key => $data ) {
				$column_value = strtolower( wp_strip_all_tags( $data[ $created_by_id ] ) );

				if ( ! preg_match( "/{$search_value}/i", $column_value ) ) {
					unset( $filtered_data[ $key ] );
				}
			}
		}


		$start  = isset( $_POST['start'] ) && is_numeric( $_POST['start'] ) ? (int) $_POST['start'] : 0;
		$length = isset( $_POST['length'] ) && is_numeric( $_POST['length'] ) ? (int) $_POST['length'] : 0;

		if ( $length === -1 ) {
			$length = null;
		}

		$output['data']            = array_slice( array_values( $filtered_data ), $start, $length );
		$output['recordsFiltered'] = count( $filtered_data );
		$output['recordsTotal']    = count( $filtered_data );

		return $output;
	}

	/**
	 * Exit with $value or return it.
	 *
	 * Behavior depends on whether tests are being run or not.
	 *
	 * @param mixed $value The value to return if tests are being run.
	 *
	 * @return mixed|null
	 */
	private function exit_or_return( $value ) {

		defined( 'DOING_GRAVITYVIEW_TESTS' ) || exit( $value );

		return $value;
	}

	/**
	 * Add the generic search to the global get_entries query
	 *
	 * @since 1.3.3
	 *
	 * @param array $search_criteria Search Criteria
	 *
	 * @return mixed
	 */
	function add_global_search( $search_criteria ) {

		if ( empty( $_POST['search']['value'] ) ) {
			return $search_criteria;
		}

		$search_all_value = stripslashes_deep( $_POST['search']['value'] );

		/**
		 * Search for each word separately or the whole phrase?
		 *
		 * @since 2.1.1
		 *
		 * @param bool $split_words True: split a phrase into words; False: search whole word only [Default: true]
		 */
		$split_words = apply_filters( 'gravityview/search-all-split-words', true );

		if ( $split_words ) {

			// Search for a piece
			$words = explode( ' ', $search_all_value );

			$words = array_filter( $words );

		} else {

			// Replace multiple spaces with one space
			$search_all_value = preg_replace( '/\s+/ism', ' ', $search_all_value );

			$words = array( $search_all_value );
		}

		foreach ( $words as $word ) {
			$search_criteria['field_filters'][] = array(
				'key'      => null, // The field ID to search
				'value'    => $word, // The value to search
				'operator' => 'contains', // What to search in. Options: `is` or `contains`
			);
		}

		if ( ! empty( $search_criteria['field_filters'] ) && empty( $search_criteria['field_filters']['mode'] ) ) {
			$search_criteria['field_filters']['mode'] = 'any';
		}

		return $search_criteria;
	}

	/**
	 * Get the array of entry data
	 *
	 * @since 1.3
	 *
	 * @param Entry_Collection $entries The collection of entries for the current search.
	 * @param View             $view    The View.
	 * @param WP_Post          $post    Current View or post/page where View is embedded.
	 *
	 * @return array
	 */
	public function get_output_data( $entries, $view, $post = null ) {
		/**
		 * Runs before generating the output data.
		 *
		 * This is helpful for adding filters that will modify the output downstream.
		 * For example, this is used by the GravityView LifterLMS integration.
		 *
		 * @action `gk/gravityview/datatables/output/before`
		 *
		 * @since 2.0
		 *
		 * @param \GV\Entry_Collection $entries The collection of entries for the current search.
		 * @param View                 $view    The View.
		 * @param \WP_Post             $post    The current View or post/page where View is embedded.
		 */
		do_action( 'gk/gravityview/datatables/output/before', $entries, $view, $post );

		if ( ! $view instanceof View ) {
			gravityview()->log->notice( '\GV_Extension_DataTables_Data::get_output_data now requires \GV\Entry_Collection and \GV\View object parameters.' );

			return array();
		}

		// build output data
		$data  = array();
		$_POST = isset( $_POST ) ? (array) $_POST : array();
		if ( $entries->total() ) {

			$fields          = $view->fields->by_position( 'directory_table-columns' )->by_visible()->all();
			$internal_source = new \GV\Internal_Source();
			$renderer        = new \GV\Field_Renderer();

			$sort_field_setting = $this->get_original_sort_field_setting( $view );
			$visible_field_ids  = [];

			foreach ( $fields as $field ) {
				if ( 'custom' == $field->type ) {
					$visible_field_ids[] = 'custom_' . $field->UID;
				} else {
					$visible_field_ids[] = $field->ID;
				}
			}

			$hidden_sort_fields = array();
			foreach ( $sort_field_setting as $sort_field ) {
				if ( ! empty( $sort_field ) && ! in_array( $sort_field, $visible_field_ids ) ) {
					// Create a \GV\Field object for this sort field.
					$hidden_field = is_numeric( $sort_field )
						? \GV\GF_Field::by_id( $view->form, $sort_field )
						: \GV\Internal_Field::by_id( $sort_field );

					if ( $hidden_field ) {
						$hidden_sort_fields[] = $hidden_field;
					}
				}
			}

			// Combine visible fields and hidden sort fields for rendering.
			$all_fields = array_merge( $fields, $hidden_sort_fields );

			// For each entry
			foreach ( $entries->all() as $entry ) {
				$temp = array();

				\GV\Mocks\Legacy_Context::push(
					array(
						'view'  => $view,
						'entry' => $entry,
						'post'  => $post,
					)
				);

				foreach ( $all_fields as $field ) {
					$form = $view->form;

					if ( is_callable( array( $entry, 'is_multi' ) ) && $entry->is_multi() ) {
						$form = \GV\GF_Form::by_id( $field->form_id );
					}

					$source = is_numeric( $field->ID ) ? $form : $internal_source;
					$temp[] = $renderer->render( $field, $view, $source, $entry, gravityview()->request );
				}

				\GV\Mocks\Legacy_Context::pop();

				/**
				 * Modify the entry output before the request is returned
				 *
				 * @since 2.3.1
				 *
				 * @param array $temp  Array of values for the entry, one item per field being rendered by \GV\Field_Renderer()
				 * @param View  $view  Current View being processed
				 * @param array $entry Current Gravity Forms entry array
				 */
				$temp = apply_filters( 'gravityview/datatables/output/entry', $temp, $view, $entry );

				// Then add the item to the output dataset
				$data[] = $temp;

			}
		}

		return $data;
	}

	/**
	 * Get column width as a % from the field setting
	 *
	 * @since 1.3
	 *
	 * @param array $field_setting Array of settings for the field
	 *
	 * @return string|null If not empty, string in "{number}%" format. Otherwise, null.
	 */
	private function get_column_width( $field_setting ) {

		$width = null;

		if ( ! empty( $field_setting['width'] ) ) {
			$width = absint( $field_setting['width'] );
			$width = $width > 100 ? 100 : $width . '%';
		}

		return $width;
	}

	/**
	 * Get the original sort_field setting from saved View meta.
	 *
	 * This retrieves the sort_field from the saved post meta rather than from
	 * runtime settings, which may have been modified by AJAX sorting requests.
	 *
	 * @since 3.7.1
	 *
	 * @param View $view The View object.
	 *
	 * @return array The sort_field setting as an array.
	 */
	private function get_original_sort_field_setting( $view ) {
		$original_settings = get_post_meta( $view->ID, '_gravityview_template_settings', true );

		return (array) ( isset( $original_settings['sort_field'] ) ? $original_settings['sort_field'] : [] );
	}

	/**
	 * Calculates the user ID and Session Token to be used when calculating the Cache Key
	 *
	 * @return string
	 */
	function get_user_session() {

		if ( ! is_user_logged_in() ) {
			return '';
		}

		if ( isset( $_GET['cache'] ) ) {
			return '';
		}

		/**
		 * @see wp_get_session_token()
		 */
		$cookie = wp_parse_auth_cookie( '', 'logged_in' );
		$token  = ! empty( $cookie['token'] ) ? $cookie['token'] : '';

		return get_current_user_id() . '_' . $token;
	}

	/**
	 * Override the template class to the correct one.
	 *
	 * @param string   $class The class to use as the template class.
	 * @param View $view  The View we're looking at.
	 *
	 * @return string The class
	 */
	public function set_view_template_class( $class, $view ) {

		if ( $view->settings->get( 'template' ) == 'datatables_table' ) {
			return '\GV\View_DataTable_Template';
		}

		return $class;
	}

	/**
	 * Override the template class to use the correct one.
	 *
	 * @param string    $class The class to use as the template class.
	 * @param \GV\Entry $entry The Entry we're looking at.
	 * @param View  $view  The View we're looking at.
	 *
	 * @return string The class
	 */
	public function set_entry_template_class( $class, $entry, $view ) {

		$template = $view->settings->get( 'template' );

		// Get the single entry template, with a fallback to the regular template for older versions of GravityView.
		$template = $view->settings->get( 'template_single_entry', $template );

		if ( 'datatables_table' === $template ) {
			return Entry_DataTable_Template::class;
		}

		return $class;
	}

	/**
	 * Include this extension templates path
	 *
	 * @param array $file_paths List of template paths ordered
	 */
	function add_template_path( $file_paths ) {

		// Index 100 is the default GravityView template path.
		// Find an empty spot afterwards and avoid overwriting other registered spots
		for ( $i = 101; ; $i++ ) {
			if ( ! empty( $file_paths[ $i ] ) ) {
				continue;
			}
			$file_paths[ $i ] = GV_DT_DIR . 'templates/deprecated';
			break;
		}

		return $file_paths;
	}

	/**
	 * Generate the values for the page length menu
	 *
	 * @filter  gravityview_datatables_lengthmenu Modify the values shown in the page length menu. Key is the # of results, value is the label for the results.
	 *
	 * @param View $view The View
	 *
	 * @return array            2D array formatted for DataTables
	 */
	function get_length_menu( $view ) {

		$page_size = 25;
		if ( ! $view instanceof View ) {
			if ( $view = View::by_id( $view['id'] ) ) {
				$page_size = $view->settings->get( 'page_size' );
			}
		} else {
			$page_size = $view->settings->get( 'page_size' );
		}

		// Create the array of values for the drop-down page menu
		$values = array(
			(int) $page_size => $page_size,
			10               => 10,
			25               => 25,
			50               => 50,
		);

		// no duplicate values
		$values = array_unique( $values );

		// Sort by the # of results per page
		ksort( $values );

		// Add the "All" option after the rest of them have been sorted by value
		$values[- 1] = _x( 'All', 'Menu label to show all results in DataTables template.', 'gv-datatables' );

		/**
		 * Modifies the values shown in the page length menu.
		 *
		 * Key is the # of results, value is the label for the results.
		 *
		 * @since 3.3.5
		 *
		 * @param array $values Array of values and labels for the page length menu.
		 * @param array $view_data The View data as an array {@see \GV\View::as_data()}.
		 */
		$values = apply_filters( 'gravityview_datatables_lengthmenu', $values, $view->as_data() );

		/**
		 * Prepare a 2D array for the dropdown.
		 *
		 * @link https://datatables.net/examples/advanced_init/length_menu.html
		 */
		$lengthMenu = array(
			array_keys( $values ),
			array_values( $values ),
		);

		return $lengthMenu;
	}

	/**
	 * Get the data for the language parameter used by DataTables
	 *
	 * @since 1.2.3
	 * @since 2.4.4 Added $view parameter; made private
	 *
	 * @param View $view The View
	 *
	 * @return array Array of strings, as used by the DataTables extension's `language` setting
	 */
	private function get_language( $view = null ) {

		$translations = $this->get_translations();

		$locale = get_locale();

		/**
		 * Change the locale used to fetch translations.
		 *
		 * @since 1.2.3
		 *
		 * @param string $locale       The current locale.
		 * @param array  $translations The translations mapping array.
		 */
		$locale = apply_filters( 'gravityview/datatables/config/locale', $locale, $translations );

		// If a translation exists
		if ( isset( $translations[ $locale ] ) ) {

			ob_start();

			// Get the JSON file
			include GV_DT_DIR . 'assets/js/third-party/datatables/translations/' . $translations[ $locale ] . '.json';

			$json_string = ob_get_clean();

			// If it exists
			if ( ! empty( $json_string ) ) {

				// Parse it into an array
				$json_array = json_decode( $json_string, true );

				// If that worked, use the array as the base
				if ( ! empty( $json_array ) ) {
					$language = $json_array;
				}
			}
		} else {

			$no_entries_text = $view->settings->get( 'no_results_text', __( 'No entries match your request.', 'gv-datatables' ) );
			$no_entries_text = ! $no_entries_text ? __( 'No entries match your request.', 'gv-datatables' ) : $no_entries_text;

			$no_results_text = $view->settings->get( 'no_search_results_text', __( 'This search returned no results.', 'gv-datatables' ) );
			$no_results_text = ! $no_results_text ? __( 'This search returned no results.', 'gv-datatables' ) : $no_results_text;

			/**
			 * Modify the text shown when DataTables is loaded
			 *
			 * @since  2.5 Added $view parameter
			 *
			 * @param string $loading_text Default: Loading data…
			 * @param View $view The View
			 */
			$loading_text = apply_filters( 'gravityview_datatables_loading_text', esc_html__( 'Loading data&hellip;', 'gv-datatables' ), $view );

			// Otherwise, load default English text with filters.
			$language = [
				'processing'          => $loading_text,
				'zeroRecords'         => esc_html( $no_entries_text ),
				'emptyTable'          => esc_html( $no_results_text ),
				'emptyServerResponse' => esc_html__( 'The server returned an empty response. Check the server logs for more details.', 'gv-datatables' ),
			];
		}

		/**
		 * Override language settings
		 * @since  1.2.2
		 *
		 * {@link https://github.com/DataTables/Plugins/blob/master/i18n/English.lang}
		 * @param array  $translations The translations mapping array from `GV_Extension_DataTables_Data::get_translations()`
		 * @param string $locale       The blog's locale, fetched from `get_locale()`
		 *                             Override language settings
		 *
		 * @param array  $language     The language settings array.\n
		 *                             [See a sample file with all available translations and their matching keys](https://github.com/DataTables/Plugins/blob/master/i18n/English.lang)
		 */
		$language = apply_filters( 'gravityview/datatables/config/language', $language, $translations, $locale );

		return $language;
	}

	/**
	 * Match the DataTables translation file to the WordPress locale setting
	 *
	 * @since 1.2.3
	 *
	 * @return array Key is the WordPress locale string; Value is the name of the file in assets/js/third-party/datatables/translations/ without .json
	 */
	private function get_translations() {
		$translations = array(
			'af'    => 'af', // Afrikaans
			'sq'    => 'sq', // Albanian
			'ar'    => 'ar', // Arabic
			'hy'    => 'hy', // Armenian
			'az'    => 'az-AZ', // Azerbaijani
			'bn_BD' => 'bn', // Bengali
			'eu'    => 'eu', // Basque
			'bel'   => 'be', // Belarusian
			'bs_BA' => 'bs-BA', // Bosnian
			'bg_BG' => 'bg', // Bulgarian
			'ca'    => 'ca', // Catalan
			'zh_CN' => 'zh', // Chinese
			'co'    => 'co', // Corsican
			'hr'    => 'hr', // Croatian
			'cs_CZ' => 'Czech', // Czech
			'da_DK' => 'da', // Danish
			'nl_NL' => 'nl-NL', // Dutch
			'en-GB' => 'en-GB', // British English
			'eo'    => 'eo', // Esperanto
			'et'    => 'et', // Estonian
			// Filipino (not in WP)
			'fi'    => 'fi', // Finnish
			'fr_FR' => 'fr-FR', // French
			'gl_ES' => 'gl', // Galician
			'ka_GE' => 'ka', // Georgian
			'de_DE' => 'de-DE', // German
			'el'    => 'el', // Greek
			'gu'    => 'gu', // Gujarati
			'he_IL' => 'he', // Hebrew
			'hi_IN' => 'hi', // Hindi
			'hu_HU' => 'hu', // Hungarian
			'is_IS' => 'is', // Icelandic
			'id_ID' => 'id', // Indonesian
			'ga'    => 'ga', // Irish
			'it_IT' => 'it-IT', // Italian
			'ja'    => 'ja', // Japanese
			'jv_ID' => 'jv', // Javanese
			'kn'    => 'kn', // Kannada
			'kk'    => 'kk', // Kazakh
			'km'    => 'km', // Khmer
			'ko_KR' => 'ko', // Korean
			'kmr'   => 'ku', // Kurdish
			'kir'   => 'ky', // Kyrgyz
			'lo'    => 'lao', // Lao
			'lv'    => 'lv', // Latvian
			'lt_LT' => 'lt', // Lithuanian
			'lug'   => 'ug', // Luganda
			'mk_MK' => 'mk', // Macedonian
			'ms_MY' => 'ms', // Malay
			'mr'    => 'mr', // Marathi
			'mn'    => 'mn', // Mongolian
			'ne_NP' => 'ne', // Nepali
			'nb_NO' => 'no-NB', // Norwegian Bokmål
			'nn_NO' => 'no-NO', // Norwegian Nynorsk
			'ps'    => 'ps', // Pashto
			'fa_IR' => 'fa', // Persian
			'pl_PL' => 'pl', // Polish
			'pt_PT' => 'pt-PT', // Portuguese
			'pt_BR' => 'pt-BR', // Brazilian Portuguese
			'pa_IN' => 'pa', // Punjabi
			'ro_RO' => 'ro', // Romanian
			'roh'   => 'rm', // Romansh
			'ru_RU' => 'ru', // Russian
			'sr_RS' => 'sr', // Serbian
			// Serbian (Latin) (not in WP)
			'sd_PK' => 'snd', // Sindhi
			'si_LK' => 'si', // Sinhala
			'sk_SK' => 'sk', // Slovak
			'sl_SI' => 'sl', // Slovenian
			'es_ES' => 'es-ES', // Spanish
			'es_AR' => 'es-AR', // Spanish (Argentina)
			'es_CL' => 'es-CL', // Spanish (Chile)
			'es_CO' => 'es-CO', // Spanish (Colombia)
			'es_MX' => 'es-MX', // Spanish (Mexico)
			'sw'    => 'sq', // Swahili
			'sv_SE' => 'sv-SE', // Swedish
			'tg'    => 'tg', // Tajik
			'ta_IN' => 'ta', // Tamil
			'te'    => 'te', // Telugu
			'th'    => 'th', // Thai
			'tr_TR' => 'tr', // Turkish
			'tuk'   => 'tk', // Turkmen
			'uk'    => 'uk', // Ukrainian
			'ur'    => 'ur', // Urdu
			'uz_UZ' => 'uz', // Uzbek
			// Uzbek - Cryllic (not in WP)
			'vi'    => 'vi', // Vietnamese
			'cy'    => 'cy', // Welsh
		);

		return $translations;
	}

	/**
	 * Get the url to the AJAX endpoint in use
	 *
	 * @return string If direct access, it's this file. otherwise, admin-ajax.php
	 */
	function get_ajax_url() {
		return admin_url( 'admin-ajax.php' );
	}

	/**
	 * Generates a unique state key for DataTables based on view configuration.
	 *
	 * This ensures that when view settings change (especially sort fields), the cached
	 * state is invalidated and a fresh state is used.
	 *
	 * @since 3.7.1
	 *
	 * @param View $view The View object
	 *
	 * @return string A unique hash representing the current view configuration
	 */
	private function generate_state_key( $view ) {
		$config_data = array(
			'view_id'        => $view->ID,
			'sort_field'     => $view->settings->get( 'sort_field', array() ),
			'sort_direction' => $view->settings->get( 'sort_direction', array() ),
			'page_size'      => $view->settings->get( 'page_size', 10 ),
			'view_modified'  => get_post_modified_time( 'U', true, $view->ID ),
		);

		// Include visible field IDs to detect column changes
		$visible_field_ids = array();
		foreach ( $view->fields->by_position( 'directory_table-columns' )->by_visible()->all() as $field ) {
			$visible_field_ids[] = ( 'custom' === $field->type ) ? 'custom_' . $field->UID : $field->ID;
		}
		$config_data['visible_fields'] = $visible_field_ids;

		return substr( md5( wp_json_encode( $config_data ) ), 0, 8 );
	}

	/**
	 * Generate the script configuration array
	 *
	 * @since 1.3.3
	 *
	 * @param WP_Post $post Current View or post/page where View is embedded
	 * @param View    $view The View
	 *
	 * @return array Array of settings formatted as DataTables options array. {@see https://datatables.net/reference/option/}
	 */
	public function get_datatables_script_configuration( $post, $view ) {
		// View DataTables settings
		$dt_settings = get_post_meta( $post->ID, '_gravityview_datatables_settings', true );

		$ajax_settings = array(
			'action'            => 'gv_datatables_data',
			'view_id'           => $view->ID,
			'post_id'           => $post->ID ?? $view->get_post()->ID,
			'nonce'             => wp_create_nonce( 'gravityview_datatables_data' ),
			'getData'           => empty( $_GET ) ? false : json_encode( (array) $_GET ), // Pass URL args to $_POST request
			'hideUntilSearched' => $view->settings->get( 'hide_until_searched' ),
			'setUrlOnSearch'    => apply_filters( 'gravityview/search/method', 'get' ) === 'get',
			'noEntriesOption'   => (int) $view->settings->get( 'no_entries_options', '0' ),
			'redirectURL'       => $view->settings->get( 'no_entries_redirect', '' ),
		);

		// apply shortcode atts
		if ( $view->settings->get( 'shortcode_atts' ) ) {
			$ajax_settings['shortcode_atts'] = $view->settings->get( 'shortcode_atts' );
		}

		// Prepare DataTables init config
		$dt_config = array(
			'processing'    => true,
			'deferRender'   => true,
			// Improves performance https://datatables.net/reference/option/deferRender
			'serverSide'    => true,
			'retrieve'      => true,
			// Only initialize each table once
			'stateSave' => ( 1 === (int) ( $dt_settings['save_state'] ?? 1 ) ) && ! isset( $_GET['cache'] ),
			// On refresh (and on single entry view, then clicking "go back"), save the page you were on.
			'stateDuration' => - 1,
			// Only save the state for the session. Use to time in seconds (like the DAY_IN_SECONDS WordPress constant) if you want to modify.
			'lengthMenu'    => $this->get_length_menu( $view ),
			// Dropdown pagination length menu
			'language'      => $this->get_language( $view ),
			'ajax'          => array(
				'url'  => $this->get_ajax_url(),
				'type' => 'POST',
				'data' => $ajax_settings,
			),
		);

		// Generates a unique state key based on view configuration to ensure cache invalidation when settings change
		if ( $dt_config['stateSave'] ) {
			$dt_config['stateKey'] = $this->generate_state_key( $view );
		}

		// page size, if defined
		$dt_config['pageLength'] = intval( $view->settings->get( 'page_size', 10 ) );

		/**
		 * Set the columns to be displayed
		 *
		 * @link https://datatables.net/reference/option/columns
		 */
		$columns = array();
		$visible_field_ids = array();

		foreach ( $view->fields->by_position( 'directory_table-columns' )->by_visible()->all() as $field ) {

			if ( 'custom' == $field->type ) {
				$field_id = 'custom_' . $field->UID;
			} else {
				$field_id = $field->ID;
			}

			$visible_field_ids[] = $field_id;

			$field_config = $field->as_configuration();

			$type       = 'string'; // See https://datatables.net/reference/option/columns.type.
			$field_type = $field->type; // This is the GF/GV field type that we use in the UI to configure filters/etc.

			if ( in_array( $field_type, [ 'date', 'date_created', 'date_updated', 'payment_date' ] ) ) {
				$field_type = 'date';
				$type       = 'num';
			}

			if ( 'number' === $field_type ) {
				$type = 'num';
			}

			$field_column = [
				'name'       => 'gv_' . $field_id,
				'width'      => $this->get_column_width( $field_config ),
				'form_id'    => rgar( $field_config, 'form_id' ),
				'className'  => gravityview_sanitize_html_class( \GV\Utils::get( $field_config, 'custom_class', '' ) ),
				'type'       => $type,
				'field_type' => $field_type,
			];

			/**
			 * Check if fields are sortable. If not, set `orderable` to false.
			 *
			 * @since 1.3.3
			 */
			if ( $view->form && class_exists( 'GravityView_Fields' ) && class_exists( 'GFFormsModel' ) ) {
				$gf_field = $field->field;
				$type     = \GV\Utils::get( $gf_field, 'type', $field->ID );
				$gv_field = GravityView_Fields::get( $type );

				// If the field does exist, use the field's sortability setting
				if ( $gv_field && ! $gv_field->is_sortable ) {
					$field_column['orderable'] = false;
				}
			}

			$columns[] = $field_column;
		}

		$sort_field_setting    = $this->get_original_sort_field_setting( $view );
		$hidden_column_indices = [];

		foreach ( $sort_field_setting as $sort_field ) {
			if ( empty( $sort_field ) || in_array( $sort_field, $visible_field_ids ) ) {
				continue;
			}

			if ( ! $view->form || ! class_exists( 'GFAPI' ) ) {
				continue;
			}

			$gf_field = GFAPI::get_field( $view->form->ID, $sort_field );

			$is_internal_field = class_exists( 'GravityView_Fields' ) && GravityView_Fields::get( $sort_field );

			if ( ! $gf_field && ! $is_internal_field ) {
				continue;
			}

			$field_type = $gf_field ? $gf_field->type : $sort_field;
			$type = 'string';

			if ( in_array( $field_type, [ 'date', 'date_created', 'date_updated', 'payment_date' ] ) ) {
				$type = 'num';
			}

			if ( 'number' === $field_type ) {
				$type = 'num';
			}

			$hidden_column_index = count( $columns );
			$hidden_column_indices[] = $hidden_column_index;

			$columns[] = [
				'name'       => 'gv_' . $sort_field,
				'width'      => null,
				'form_id'    => $view->form->ID,
				'className'  => 'gv-hidden-sort-column',
				'type'       => $type,
				'field_type' => $field_type,
				'orderable'  => true, // Hidden sort columns should be orderable.
			];
		}

		$dt_config['columns'] = $columns;
		if ( ! empty( $hidden_column_indices ) ) {
			if ( ! isset( $dt_config['columnDefs'] ) ) {
				$dt_config['columnDefs'] = array();
			}

			$dt_config['columnDefs'][] = [
				'visible' => false,
				'targets' => $hidden_column_indices,
			];
		}

		// set default order
		foreach ( $sort_field_setting as $l => $sort_field ) {
			foreach ( $columns as $k => $column ) {
				if ( $column['name'] === 'gv_' . $sort_field ) {
					$dir = (array) $view->settings->get( 'sort_direction', 'asc' );

					$dt_config['order'][] = array(
						$k,
						strtolower( \GV\Utils::get( $dir, $l, 'asc' ) ),
					);
				}
			}
		}

		/**
		 * Modify the settings used to render DataTables.
		 *
		 * @link https://datatables.net/reference/option/ Official DataTables documentation.
		 * @link https://docs.gravitykit.com/article/243-how-to-disable-the-loading-data-message How to disable the "Loading data..." message.
		 * @link https://docs.gravitykit.com/article/201-how-to-disable-the-datatables-search-filter A document on how to disable the DataTables search filter.
		 * @link https://docs.gravitykit.com/article/665-datatables-sorting Modifying and clearing the way DataTables stores sorting across sessions.
		 * @link https://docs.gravitykit.com/article/249-how-to-customize-the-csv-field-separator Converting the CSV export separator to a tab character for TSV format.
		 *
		 * @since 1.0
		 * @since 3.3 Added `$this` parameter.
		 *
		 * @param array                        $dt_config The configuration for the current View.
		 * @param int                          $view_id   The ID of the View being configured.
		 * @param WP_Post                      $post      Current View or post/page where View is embedded.
		 * @param GV_Extension_DataTables_Data $this      The current instance of the class.
		 */
		$dt_config = apply_filters( 'gravityview_datatables_js_options', $dt_config, $view->ID, $post, $this );

		return $dt_config;
	}

	/**
	 * Enqueue Scripts and Styles for DataTable View Type
	 *
	 * @filter gravityview_datatables_loading_text Modify the text shown while the DataTable is loading
	 *
	 * @since 2.5 Added $gravityview parameter
	 *
	 * @param Template_Context $gravityview The $gravityview object available in templates.
	 */
	public function add_scripts_and_styles( $gravityview ) {

		if ( empty( $gravityview ) || ! $gravityview instanceof Template_Context ) {
			return;
		}

		if ( ! $gravityview->template instanceof \GV\View_DataTable_Template ) {
			return;
		}

		$post = get_post();

		if ( ! is_a( $post, 'WP_Post' ) ) {
			return;
		}

		$script_debug = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';

		// Conditionally enqueue Entry Notes scripts.
		$has_workflow_approval_links = false;

		if ( $gravityview instanceof Template_Context && $gravityview->view instanceof View ) {
			foreach ( $gravityview->view->fields->by_position( 'directory_table-columns' )->by_visible()->all() as $field ) {
				if ( 'workflow_approval_links' === $field->ID ) {
					$has_workflow_approval_links = true;

					break;
				}
				if ( 'notes' !== $field->type ) {
					continue;
				}

				( new GravityView_Field_Notes )->register_scripts();
				( new GravityView_Field_Notes )->enqueue_scripts();

				break;
			}
		}

		/**
		 * Modify the DataTables core script used
		 *
		 * @param string $path Full URL to the jQuery DataTables file
		 */
		wp_enqueue_script(
			'gv-datatables',
			apply_filters(
				'gravityview_datatables_script_src',
				plugins_url( 'assets/js/third-party/datatables/jquery.dataTables' . $script_debug . '.js', GV_DT_FILE )
			),
			array( 'jquery', 'wp-hooks' ),
			GV_Extension_DataTables::version,
			true
		);

		/**
		 * Use your own DataTables stylesheet by using the `gravityview_datatables_style_src` filter
		 */
		wp_enqueue_style( 'gravityview_style_datatables_table' );

		// Enqueue Gravity Flow styles if Workflow Approval Links field is present.
		if ( $has_workflow_approval_links ) {
			$this->enqueue_gravityflow_styles();
		}

		/**
		 * Register the featured entries script so that if active, the Featured Entries extension can use it.
		 */
		wp_register_style( 'gv-datatables-featured-entries', plugins_url( 'assets/css/featured-entries.css', GV_DT_FILE ), array( 'gravityview_style_datatables_table' ), GV_Extension_DataTables::version, 'all' );

		// include DataTables custom script
		wp_enqueue_script( 'gv-datatables-cfg', plugins_url( 'assets/js/datatables-views' . $script_debug . '.js', GV_DT_FILE ), array( 'gv-datatables' ), GV_Extension_DataTables::version, true );

		/**
		 * Extend datatables by including other scripts and styles.
		 *
		 * @since 1.0
		 * @deprecated Will no longer give the views on the page.
		 *
		 * @param array   $empty_array_1 Empty array (deprecated).
		 * @param array   $empty_array_2 Empty array (deprecated).
		 * @param WP_Post $post          Current View or post/page where View is embedded.
		 */
		do_action( 'gravityview_datatables_scripts_styles', array(), array(), $post );
	}

	/**
	 * @deprecated 2.3
	 * @internal
	 */
	public function output_dt_config() {

		_deprecated_function( 'GV_Extension_DataTables_Data::output_dt_config', '2.3', 'This method was not intended to be called.' );
	}

	/**
	 * Extend DT view by outputting additional configuration, enqueuing scripts, etc.
	 *
	 * @since 2.3 Renamed from output_dt_config() to extend_view()
	 *
	 * @param Template_Context $gravityview The template $gravityview object.
	 *
	 * @return void
	 * @internal
	 */
	public function extend_view( $gravityview ) {
		if ( 'datatables_table' != $gravityview->view->settings->get( 'template' ) ) {
			return;
		}

		// enqueue scripts for registered/visible fields
		$fields = $gravityview->view->fields->by_position( 'directory_table-columns' );

		foreach ( $fields->by_visible()->all() as $field ) {

			wp_enqueue_script( 'gv-inline-edit-' . $field->type );

			if ( 'notes' === $field->type ) {
				do_action( 'gravityview/field/notes/scripts', $gravityview );
			}
		}

		global $post;

		if ( ! $post ) {
			return;
		}

		$script_config_json = wp_json_encode( $this->get_datatables_script_configuration( $post, $gravityview->view ) );

		?>
		<script type="text/javascript">
			(function() {
				if ( ! window.gvDTglobals ) {
					window.gvDTglobals = [];
				}

				const config = <?php echo $script_config_json; ?>;
				const configStr = JSON.stringify( config );

				// Only push if not already in the array.
				if ( ! window.gvDTglobals.some( existing => JSON.stringify( existing ) === configStr ) ) {
					window.gvDTglobals.push( config );
				}
			})();
		</script>
		<?php
	}

	/**
	 * Modify Inline Edit settings
	 *
	 * @since 2.3
	 *
	 * @param array $item_id Array with `form_id` or `view_id` set
	 *
	 * @param array $settings
	 *
	 * @return array Array with Inline Edit settings
	 */
	function maybe_modify_inline_edit_settings( $settings, $item_id = array() ) {

		if ( ! class_exists( '\GV\View' ) ) {
			return $settings;
		}

		$view_id = \GV\Utils::get( $item_id, 'view_id', false );

		if ( empty( $view_id ) ) {
			return $settings;
		}

		if ( ! $view = View::by_id( $view_id ) ) {
			return $settings;
		}

		if ( 'datatables_table' !== $view->settings->get( 'template' ) ) {
			return $settings;
		}

		$settings['disableInitOnLoad'] = false;

		return $settings;
	}

	/**
	 * Enqueues Gravity Flow styles for Workflow Approval Links field.
	 *
	 * @since 3.7.0
	 *
	 * @return void
	 */
	private function enqueue_gravityflow_styles() {
		$custom_css = '
			.gv-datatables-container .flow_approval_links,
			table.dataTable .flow_approval_links {
				padding-right: 1em;
			}
			.gv-datatables-container .fa,
			table.dataTable .fa {
				display: inline-block;
				font: normal normal normal 14px/1 GFFontAwesome;
				font-size: inherit;
				text-rendering: auto;
				-webkit-font-smoothing: antialiased;
			}
			.gv-datatables-container .fa-check:before,
			table.dataTable .fa-check:before {
				content: "\f00c";
				color: green;
			}
			.gv-datatables-container .fa-times:before,
			table.dataTable .fa-times:before {
				content: "\f00d";
				color: red;
			}
		';

		// Register our own style handle to ensure CSS is always output.
		wp_register_style(
			'gv-datatables-gravityflow-approval-links',
			false, // No external file, inline only.
			[], // No dependencies to avoid issues.
			GV_Extension_DataTables::version
		);

		wp_add_inline_style( 'gv-datatables-gravityflow-approval-links', $custom_css );
		wp_enqueue_style( 'gv-datatables-gravityflow-approval-links' );
	}

}

new GV_Extension_DataTables_Data();
