<?php

namespace WPForms\Pro\Admin\Entries;

use WP_List_Table;
use WPForms\Admin\Notice;
use WPForms\Db\Payments\ValueValidator;
use WPForms\Pro\Admin\DashboardWidget;
use WPForms\Pro\Admin\Entries\Table\Facades\Columns;

// IMPORTANT NOTICE:
// This line is needed to prevent fatal errors in the third-party plugins.
// We know about Jetpack (probably others also) can load WP classes during cron jobs or something similar.
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';

/**
 * Generate the table on the entries' overview page.
 *
 * @since 1.8.6
 */
class ListTable extends WP_List_Table {

	// @todo: remove those phpcs lines when deprecated version will be set as a real number, it is a bug in phpcs.
	// phpcs:disable WPForms.Comments.DeprecatedTag.InvalidDeprecatedVersion
	/**
	 * The ID of the table column called "Entry ID".
	 *
	 * @since 1.8.6
	 * @deprecated 1.8.6. Use \WPForms\Pro\Admin\Entries\Table\Facades\Columns::COLUMN_ENTRY_ID instead.
	 *
	 * @var int
	 */
	const COLUMN_ENTRY_ID = -1;

	/**
	 * The ID of the table column called "Entry Notes".
	 *
	 * @since 1.8.6
	 * @deprecated 1.8.6. Use \WPForms\Pro\Admin\Entries\Table\Facades\Columns::COLUMN_NOTES_COUNT instead.
	 *
	 * @var int
	 */
	const COLUMN_NOTES_COUNT = -2;
	// phpcs:enable WPForms.Comments.DeprecatedTag.InvalidDeprecatedVersion

	/**
	 * Number of entries to show per page.
	 *
	 * @since 1.8.6
	 *
	 * @var int
	 */
	public $per_page;

	/**
	 * Form data as an array.
	 *
	 * @since 1.8.6
	 *
	 * @var array
	 */
	public $form_data;

	/**
	 * Form id.
	 *
	 * @since 1.8.6
	 *
	 * @var string|integer
	 */
	public $form_id;

	/**
	 * Number of different entry types.
	 *
	 * @since 1.8.6
	 *
	 * @var int
	 */
	public $counts;

	/**
	 * Date and time format.
	 *
	 * @since 1.8.6
	 *
	 * @var array
	 */
	private $datetime_format;

	/**
	 * Primary class constructor.
	 *
	 * @since 1.8.6
	 */
	public function __construct() {

		// Use the parent constructor to build the main class properties.
		parent::__construct(
			[
				'singular' => 'entry',
				'plural'   => 'entries',
				'ajax'     => false,
				'screen'   => 'entries',
			]
		);

		// Default number of forms to show per page.
		$this->per_page = wpforms()->obj( 'entry' )->get_count_per_page();

		// Date and time formats.
		$this->datetime_format = [
			'date' => get_option( 'date_format' ),
			'time' => get_option( 'time_format' ),
		];

		$this->hooks();
	}

	/**
	 * Register hooks.
	 *
	 * @since 1.8.6
	 */
	private function hooks() {

		// Add trashed views.
		add_filter( 'wpforms_entries_table_views', [ $this, 'add_trashed_views' ] );
	}

	/**
	 * List of CSS classes for the "WP_List_Table" table tag.
	 *
	 * @global string $mode List table view mode.
	 *
	 * @since 1.8.6
	 *
	 * @return array
	 */
	protected function get_table_classes() {

		global $mode;

		// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
		$mode       = get_user_setting( 'posts_list_mode', 'list' );
		$mode_class = esc_attr( 'table-view-' . $mode );
		$classes    = [
			'widefat',
			'striped',
			$mode_class,
		];

		// For styling purposes, we'll add a dedicated class name for determining the number of visible columns.
		// The ideal threshold for applying responsive styling is set at "5" columns based on the need for "Tablet" view.
		$columns_class = $this->get_column_count() > 5 ? 'many' : 'few';

		$classes[] = "has-{$columns_class}-columns";

		// Add trash class.
		if ( $this->is_trash_list() ) {
			$classes[] = 'wpforms-entries-table-trash';
		}

		/**
		 * Filters the list of CSS classes for the WP_List_Table table tag.
		 *
		 * @since 1.8.3
		 *
		 * @param string[] $classes   An array of CSS classes for the table tag.
		 * @param array    $form_data Form data.
		 */
		return apply_filters( 'wpforms_entries_table_get_table_classes', $classes, $this->form_data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
	}

	/**
	 * Get the entry counts for various types of entries.
	 *
	 * @since 1.8.6
	 */
	public function get_counts() {

		$this->counts = [];
		$entry_obj    = wpforms()->obj( 'entry' );

		$this->counts = $entry_obj->get_counts( [ 'form_id' => $this->form_id ] );

		/**
		 * Filters the array of entries counts in different views.
		 *
		 * @since 1.3.3
		 *
		 * @param int[] $counts    An array of entries' counts.
		 * @param array $form_data Form data.
		 */
		$this->counts = (array) apply_filters( 'wpforms_entries_table_counts', $this->counts, $this->form_data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName

		$defaults = [
			'total'   => 0,
			'unread'  => 0,
			'payment' => 0,
			'starred' => 0,
			'spam'    => 0,
			'trash'   => 0,
		];

		$this->counts = wp_parse_args( $this->counts, $defaults );
	}

	/**
	 * Retrieve the view types.
	 *
	 * @since 1.8.6
	 *
	 * @noinspection HtmlUnknownAttribute
	 * @noinspection HtmlUnknownTarget
	 */
	public function get_views() {

		$base = remove_query_arg( [ 'type', 'status', 'paged', 'message' ] );

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		$current = isset( $_GET['type'] ) ? sanitize_key( wp_unslash( $_GET['type'] ) ) : '';
		$total   = '&nbsp;<span class="count">(<span class="total-num">' . $this->counts['total'] . '</span>)</span>';
		$unread  = '&nbsp;<span class="count">(<span class="unread-num">' . $this->counts['unread'] . '</span>)</span>';
		$starred = '&nbsp;<span class="count">(<span class="starred-num">' . $this->counts['starred'] . '</span>)</span>';
		$all     = empty( $_GET['status'] ) && ( $current === 'all' || empty( $current ) ) ? ' class="current"' : '';
		// phpcs:enable WordPress.Security.NonceVerification.Recommended

		$views = [
			'all'    => sprintf(
				'<a href="%s"%s>%s</a>',
				esc_url( $base ),
				$all,
				esc_html__( 'All', 'wpforms' ) . $total
			),
			'unread' => sprintf(
				'<a href="%s"%s>%s</a>',
				esc_url( add_query_arg( 'type', 'unread', $base ) ),
				$current === 'unread' ? ' class="current"' : '',
				esc_html__( 'Unread', 'wpforms' ) . $unread
			),
		];

		// Only show the payment view if the form has a payment field.
		// Add the "payment" view after the "unread" view.
		if ( isset( $this->counts['payment'] ) ) {
			$payment          = '&nbsp;<span class="count">(<span class="payment-num">' . $this->counts['payment'] . '</span>)</span>';
			$views['payment'] = sprintf(
				'<a href="%s"%s>%s</a>',
				esc_url( add_query_arg( 'type', 'payment', $base ) ),
				$current === 'payment' ? ' class="current"' : '',
				_n( 'Payment', 'Payments', $this->counts['payment'], 'wpforms' ) . $payment
			);
		}

		$views['starred'] = sprintf(
			'<a href="%s"%s>%s</a>',
			esc_url( add_query_arg( 'type', 'starred', $base ) ),
			$current === 'starred' ? ' class="current"' : '',
			esc_html__( 'Starred', 'wpforms' ) . $starred
		);

		$views = $this->add_published_view( $views, $base );

		/**
		 * Filters the array of entries counts in different views.
		 *
		 * @since 1.3.3
		 *
		 * @param array $views     An array of views.
		 * @param array $form_data Form data.
		 * @param int[] $counts    An array of entries' counts.
		 *
		 * @return array
		 */
		return apply_filters( 'wpforms_entries_table_views', $views, $this->form_data, $this->counts ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
	}

	/**
	 * Add Published views to the list of views.
	 *
	 * @since 1.9.8.3
	 *
	 * @param array  $views Entries table views.
	 * @param string $base  Base URL for the views.
	 *
	 * @return array $views Array of all the list table views.
	 */
	private function add_published_view( array $views, string $base ): array {

		if ( $this->counts['total'] === $this->counts['published'] ) {
			return $views;
		}

		$published = '&nbsp;<span class="count">(<span class="published-num">' . ( $this->counts['published'] ) . '</span>)</span>';

		$views['published'] = sprintf(
			'<a href="%s"%s>%s</a>',
			esc_url( add_query_arg( 'status', 'published', $base ) ),
			isset( $_GET['status'] ) && $_GET['status'] === 'published' ? ' class="current"' : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			esc_html__( 'Published', 'wpforms' ) . $published
		);

		return $views;
	}

	/**
	 * Add Trashed views to the list of views.
	 * We've separated it to use the filter because we need it to be after spam, which uses the filter.
	 *
	 * @since 1.8.5
	 *
	 * @param array $views Entries table views.
	 *
	 * @return array $views Array of all the list table views.
	 * @noinspection HtmlUnknownTarget
	 * @noinspection HtmlUnknownAttribute
	 */
	public function add_trashed_views( $views ) {

		if (
			! $this->counts['trash'] &&
			( ! isset( $_GET['status'] ) || $_GET['status'] !== 'trash' ) && // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			! wpforms()->obj( 'entry' )->get_trash_count( $this->form_id )
		) {
			return $views;
		}

		$trashed = '&nbsp;<span class="count">(<span class="trashed-num">' . $this->counts['trash'] . '</span>)</span>';
		$base    = remove_query_arg( [ 'type', 'status', 'paged', 'message' ] );

		$views['trashed'] = sprintf(
			'<a href="%1s"%2s>%3s</a>',
			esc_url( add_query_arg( 'status', Page::TRASH_ENTRY_STATUS, $base ) ),
			$this->is_trash_list() ? ' class="current"' : '',
			esc_html__( 'Trash', 'wpforms' ) . $trashed
		);

		return $views;
	}

	/**
	 * Retrieve the table columns.
	 *
	 * @since 1.8.6
	 *
	 * @return array Array of all the list table columns.
	 */
	public function get_columns(): array {

		return Columns::get_list_table_columns( $this );
	}

	/**
	 * Retrieve the table's sortable columns.
	 *
	 * @since 1.8.6
	 *
	 * @return array Array of all the sortable columns
	 */
	public function get_sortable_columns() {

		$sortable = [
			'entry_id'    => [ 'id', false ],
			'notes_count' => [ 'notes_count', false ],
			'id'          => [ 'title', false ],
			'date'        => [ 'date', false ],
		];

		/**
		 * Filters the Entries list table sortable columns.
		 *
		 * @since 1.3.3
		 *
		 * @param array $sortable  Sortable columns.
		 * @param array $form_data Form data.
		 *
		 * @return bool
		 */
		return apply_filters( 'wpforms_entries_table_sortable', $sortable, $this->form_data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
	}

	/**
	 * Get the list of fields, that are disallowed to be displayed as column in a table.
	 *
	 * @since 1.8.6
	 *
	 * @return array
	 */
	public static function get_columns_form_disallowed_fields() {

		/**
		 * Filter the list of the disallowed fields in the entries' table.
		 *
		 * @since 1.4.4
		 *
		 * @param array $fields Field types list.
		 */
		return (array) apply_filters( 'wpforms_entries_table_fields_disallow', [ 'captcha', 'divider', 'entry-preview', 'html', 'pagebreak', 'layout' ] ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
	}

	/**
	 * Render the checkbox column.
	 *
	 * @since 1.8.6
	 *
	 * @param object $entry Entry data from DB.
	 *
	 * @return string
	 */
	public function column_cb( $entry ) {

		return '<input type="checkbox" name="entry_id[]" value="' . absint( $entry->entry_id ) . '" />';
	}

	/**
	 * Display "Type" column.
	 *
	 * @since 1.8.6
	 *
	 * @param object $entry       Current entry data.
	 * @param string $column_name Current column name.
	 *
	 * @return string
	 * @noinspection PhpMissingParamTypeInspection
	 * @noinspection PhpUnusedParameterInspection
	 */
	public function column_type_field( $entry, $column_name ) {

		// Show the original type if is trash.
		if ( isset( $_GET['status'] ) && $_GET['status'] === Page::TRASH_ENTRY_STATUS ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$meta = wpforms()->obj( 'entry_meta' )->get_meta(
				[
					'entry_id' => $entry->entry_id,
					'type'     => 'status_prev',
				]
			);

			if ( isset( $meta[0] ) && ! empty( $meta[0]->status ) ) {
				return ucwords( sanitize_text_field( $meta[0]->status ) );
			}
		}

		// If the entry has a status, show it.
		if ( ! empty( $entry->status ) && $entry->type !== 'payment' && $entry->status !== Page::TRASH_ENTRY_STATUS ) {
			return ucwords( sanitize_text_field( $entry->status ) );
		}

		// Otherwise, show "Completed" as a placeholder.
		return esc_html__( 'Completed', 'wpforms' );
	}

	/**
	 * Display payment status and total amount.
	 *
	 * @since 1.8.6
	 *
	 * @param object $entry Current entry data.
	 *
	 * @return string
	 * @noinspection HtmlUnknownTarget
	 */
	private function column_payment_field( $entry ) {

		list( $status_label, $status_slug, $payment ) = $this->get_payment_status_by_entry_id( (int) $entry->entry_id );

		// If payment data is not found, return customized N/A.
		if ( ! $payment ) {
			return sprintf(
				'<span class="payment-status-%s">%s</span>',
				$status_slug,
				$status_label
			);
		}

		if ( ! $payment->is_published ) {
			return sprintf(
				'<span class="payment-status-%s" title="%s">%s</a>',
				sanitize_html_class( $status_slug ),
				esc_html__( 'The payment in the Trash.', 'wpforms' ),
				wpforms_format_amount( $payment->total_amount, true, $payment->currency )
			);
		}

		$payment_url = '';

		if ( wpforms_current_user_can() ) {
			// Generate the single payment URL.
			$payment_url = add_query_arg(
				[
					'page'       => 'wpforms-payments',
					'view'       => 'payment',
					'payment_id' => absint( $payment->id ),
				],
				admin_url( 'admin.php' )
			);
		}

		return sprintf(
			'<a href="%s" class="payment-status-%s" title="%s">%s</a>',
			esc_url( $payment_url ),
			sanitize_html_class( $status_slug ),
			esc_html( $status_label ),
			wpforms_format_amount( $payment->total_amount, true, $payment->currency )
		);
	}

	/**
	 * Show specific form fields.
	 *
	 * @since 1.8.6
	 *
	 * @param object $entry       Entry data from DB.
	 * @param string $column_name Column unique name.
	 *
	 * @return string
	 */
	public function column_form_field( $entry, $column_name ) {

		if ( strpos( $column_name, 'wpforms_field_' ) === false ) {
			/**
			 * Filters the entry table column value in case it is not a form field.
			 *
			 * @since 1.8.6
			 *
			 * @param string $value       Value.
			 * @param object $entry       Current entry data.
			 * @param string $column_name Current column name.
			 *
			 * @return string
			 */
			return apply_filters( 'wpforms_pro_admin_entries_list_table_column_form_field_meta_field_value', '', $entry, $column_name );
		}

		$field_id     = (int) str_replace( 'wpforms_field_', '', $column_name );
		$entry_fields = (array) wpforms_decode( $entry->fields );
		$value        = $entry_fields[ $field_id ]['value'] ?? '';

		if ( ! wpforms_is_empty_string( $value ) ) {
			$value = wp_strip_all_tags( trim( $value ) );
		}

		if ( ! wpforms_is_empty_string( $value ) ) {

			$field_type = $entry_fields[ $field_id ]['type'] ?? '';
			$value      = $this->truncate_long_value( $value, $field_type );
			$value      = nl2br( $value );

			/** This filter is documented in src/SmartTags/SmartTag/FieldHtmlId.php.*/
			return apply_filters( 'wpforms_html_field_value', $value, $entry_fields[ $field_id ], $this->form_data, 'entry-table' ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
		}

		return '-';
	}

	/**
	 * Render the columns.
	 *
	 * @since 1.8.6
	 * @since 1.5.7 Added an `Entry Notes` column.
	 *
	 * @param object $entry       Current entry data.
	 * @param string $column_name Current column name.
	 *
	 * @return string
	 */
	public function column_default( $entry, $column_name ): string { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		$field_type = $this->get_field_type( $entry, $column_name );

		switch ( strtolower( $column_name ) ) {
			case 'entry_id':
			case 'id':
				$value = absint( $entry->entry_id );
				break;

			case 'notes_count':
				$value = absint( $entry->notes_count );
				break;

			case 'date':
				$value = sprintf(
					'<span class="date">%1$s</span> <span class="time">%3$s %2$s</span>',
					wpforms_datetime_format( $entry->date, $this->datetime_format['date'], true ),
					wpforms_datetime_format( $entry->date, $this->datetime_format['time'], true ),
					esc_html__( /* translators: date and time separator. */ 'at', 'wpforms' )
				);
				break;

			case 'type':
				$value = $this->column_type_field( $entry, $column_name );
				break;

			case 'payment':
				$value = $this->column_payment_field( $entry );
				break;

			case 'user_ip':
				$value = esc_html( $entry->ip_address );
				break;

			case 'user_agent':
				$value = esc_html( $entry->user_agent );
				break;

			case 'user_uuid':
				$value = esc_html( $entry->user_uuid );
				break;

			default:
				$value = $this->column_form_field( $entry, $column_name );
		}

		// Adds a wrapper with a field type in data attribute.
		if ( ! empty( $value ) && ! empty( $field_type ) ) {
			$value = sprintf( '<div data-field-type="%s">%s</div>', esc_attr( $field_type ), $value );
		}

		/**
		 * Allow filtering entry table column value.
		 *
		 * @since 1.0.0
		 * @since 1.7.0 Added Field type.
		 *
		 * @param string $value       Value.
		 * @param object $entry       Current entry data.
		 * @param string $column_name Current column name.
		 * @param string $field_type  Field type.
		 */
		return apply_filters( 'wpforms_entry_table_column_value', $value, $entry, $column_name, $field_type ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
	}

	/**
	 * Retrieve a field type.
	 *
	 * @since 1.8.6
	 *
	 * @param object $entry       Current entry data.
	 * @param string $column_name Current column name.
	 *
	 * @return string
	 */
	public function get_field_type( $entry, $column_name ) {

		$field_id     = str_replace( 'wpforms_field_', '', $column_name );
		$entry_fields = wpforms_decode( $entry->fields );
		$field_type   = '';

		if (
			! empty( $entry_fields[ $field_id ] ) &&
			isset( $entry_fields[ $field_id ]['type'] ) &&
			! wpforms_is_empty_string( $entry_fields[ $field_id ]['type'] )
		) {
			$field_type = $entry_fields[ $field_id ]['type'];
		}

		return $field_type;
	}

	/**
	 * Render the indicators' column.
	 *
	 * @since 1.8.6
	 *
	 * @param object $entry Entry data from DB.
	 *
	 * @return string
	 */
	public function column_indicators( $entry ) {

		// Stars.
		$star_action = ! empty( $entry->starred ) ? 'unstar' : 'star';
		$star_title  = ! empty( $entry->starred ) ? esc_html__( 'Unstar entry', 'wpforms' ) : esc_html__( 'Star entry', 'wpforms' );
		$star_icon   = '<a href="#" class="indicator-star ' . $star_action . '" data-id="' . absint( $entry->entry_id ) . '" data-form-id="' . absint( $entry->form_id ) . '" title="' . esc_attr( $star_title ) . '"><span class="dashicons dashicons-star-filled"></span></a>';

		// Viewed.
		$read_action = ! empty( $entry->viewed ) ? 'unread' : 'read';
		$read_title  = ! empty( $entry->viewed ) ? esc_html__( 'Mark entry unread', 'wpforms' ) : esc_html__( 'Mark entry read', 'wpforms' );
		$read_icon   = '<a href="#" class="indicator-read ' . $read_action . '" data-id="' . absint( $entry->entry_id ) . '" data-form-id="' . absint( $entry->form_id ) . '" title="' . esc_attr( $read_title ) . '"></a>';

		return $star_icon . $read_icon;
	}

	/**
	 * Render the actions' column.
	 *
	 * @since 1.8.6
	 *
	 * @param object $entry Entry data from DB.
	 *
	 * @return string
	 * @noinspection HtmlUnknownTarget
	 */
	public function column_actions( $entry ) {

		$actions = [];

		// Show the delete action only on trash and spam page.
		if ( $this->should_delete( $entry->entry_id ) ) {
			if ( wpforms_current_user_can( 'delete_entries_form_single', $this->form_id ) ) {
				// Restore.
				$actions['restore'] = sprintf(
					'<a href="%s" title="%s" class="restore">%s</a>',
					esc_url(
						wp_nonce_url(
							add_query_arg(
								[
									'view'     => 'list',
									'action'   => 'restore',
									'form_id'  => $this->form_id,
									'entry_id' => $entry->entry_id,
								]
							),
							'bulk-entries'
						)
					),
					esc_attr__( 'Restore Form Entry', 'wpforms' ),
					esc_html__( 'Restore', 'wpforms' )
				);
				// Delete.
				$actions['delete'] = sprintf(
					'<a href="%s" title="%s" class="delete">%s</a>',
					esc_url(
						wp_nonce_url(
							add_query_arg(
								[
									'view'     => 'list',
									'action'   => 'delete',
									'form_id'  => $this->form_id,
									'entry_id' => $entry->entry_id,
								]
							),
							'bulk-entries'
						)
					),
					esc_attr__( 'Delete Form Entry', 'wpforms' ),
					esc_html__( 'Delete', 'wpforms' )
				);

			}
		} else {
			// View.
			$actions['view'] = sprintf(
				'<a href="%s" title="%s" class="view">%s</a>',
				esc_url(
					add_query_arg(
						[
							'view'     => 'details',
							'entry_id' => $entry->entry_id,
						],
						admin_url( 'admin.php?page=wpforms-entries' )
					)
				),
				esc_attr__( 'View Form Entry', 'wpforms' ),
				esc_html__( 'View', 'wpforms' )
			);

			if (
				wpforms_current_user_can( 'edit_entries_form_single', $this->form_id ) &&
				wpforms()->obj( 'entry' )->has_editable_fields( $entry )
			) {
				// Edit.
				$actions['edit'] = sprintf(
					'<a href="%s" title="%s" class="edit">%s</a>',
					esc_url(
						add_query_arg(
							[
								'view'     => 'edit',
								'entry_id' => $entry->entry_id,
							],
							admin_url( 'admin.php?page=wpforms-entries' )
						)
					),
					esc_attr__( 'Edit Form Entry', 'wpforms' ),
					esc_html__( 'Edit', 'wpforms' )
				);
			}

			if ( wpforms_current_user_can( 'delete_entries_form_single', $this->form_id ) ) {
				// Trash can share the same capabilities as deleting.
				$actions['trash'] = sprintf(
					'<a href="%s" title="%s" class="trash">%s</a>',
					esc_url(
						wp_nonce_url(
							add_query_arg(
								[
									'view'     => 'list',
									'action'   => 'trash',
									'form_id'  => $this->form_id,
									'entry_id' => $entry->entry_id,
								]
							),
							'bulk-entries'
						)
					),
					esc_attr__( 'Trash Form Entry', 'wpforms' ),
					esc_html__( 'Trash', 'wpforms' )
				);
			}
		}

		/**
		 * Filters the list of actions available for each entry in the table.
		 *
		 * @since 1.0.0
		 *
		 * @param array $actions An array of actions.
		 * @param array $entry   Entry data.
		 */
		$actions = apply_filters( 'wpforms_entry_table_actions', $actions, $entry ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName

		return implode( ' <span class="sep">|</span> ', $actions );
	}

	/**
	 * Extra controls to be displayed between bulk actions and pagination.
	 *
	 * @since 1.8.6
	 *
	 * @param string $which Either top or bottom of the page.
	 */
	protected function extra_tablenav( $which ) {

		if ( $which === 'top' ) {
			$this->display_date_range_filter();
		}

		$this->display_entry_trash_button();

		/**
		 * Fires after the filter controls, before the table.
		 *
		 * @since 1.8.3
		 */
		do_action( 'wpforms_entries_table_extra_tablenav' ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
	}

	/**
	 * Display Empty Trash Button.
	 *
	 * @since 1.8.6
	 */
	private function display_entry_trash_button() {

		if ( ! $this->is_trash_list() ) {
			return;
		}

		$base = add_query_arg(
			[
				'page'    => 'wpforms-entries',
				'view'    => 'list',
				'form_id' => absint( $this->form_id ),
			],
			admin_url( 'admin.php' )
		);

		?>

		<a href="<?php echo esc_url( wp_nonce_url( $base, 'bulk-entries' ) ); ?>" class="button delete-all form-details-actions-removeall" data-page="trash">
			<?php esc_html_e( 'Empty Trash', 'wpforms' ); ?>
		</a>

		<?php
	}

	/**
	 * Display date range filter.
	 *
	 * @since 1.8.6
	 */
	private function display_date_range_filter() {

		/**
		 * Filter to disable date range filter.
		 *
		 * @since 1.8.3
		 *
		 * @param bool $disable_date_range_filter Whether to disable date range filter.
		 */
		if ( apply_filters( 'wpforms_entries_table_display_date_range_filter_disable', false ) ) { // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
			return;
		}

		?>

		<div class="alignleft actions wpforms-filter-date">

			<input type="text" name="date" class="regular-text wpforms-filter-date-selector"
				placeholder="<?php esc_attr_e( 'Select a date range', 'wpforms' ); ?>"
				style="cursor: pointer">

			<button type="submit" name="action" value="filter_date" class="button">
				<?php esc_html_e( 'Filter', 'wpforms' ); ?>
			</button>

		</div>

		<?php
	}

	/**
	 * Define bulk actions available for our table listing.
	 *
	 * @since 1.8.6
	 *
	 * @return array
	 */
	public function get_bulk_actions() {

		$bulk_actions = [
			'read'   => esc_html__( 'Mark as Read', 'wpforms' ),
			'unread' => esc_html__( 'Mark as Unread', 'wpforms' ),
			'star'   => esc_html__( 'Star', 'wpforms' ),
			'unstar' => esc_html__( 'Unstar', 'wpforms' ),
			'print'  => esc_html__( 'Print', 'wpforms' ),
		];

		$bulk_actions = $this->get_addtional_bulk_actions( $bulk_actions );

		/**
		 * Filters bulk actions.
		 *
		 * @since 1.8.3
		 *
		 * @param array $bulk_actions Bulk actions.
		 *
		 * @return array
		 */
		return apply_filters( 'wpforms_entries_table_get_bulk_actions', $bulk_actions ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
	}

	/**
	 * Define additional bulk actions available for our table listing.
	 * Additional settings are all related to the delete/restore action.
	 *
	 * @since 1.8.5
	 *
	 * @param array $bulk_actions Bulk actions.
	 *
	 * @return array
	 */
	private function get_addtional_bulk_actions( $bulk_actions ) {

		if ( ! wpforms_current_user_can( 'delete_entries_form_single', $this->form_id ) ) {
			return $bulk_actions;
		}

		$bulk_actions['null'] = esc_html__( '----------', 'wpforms' );

		if ( wpforms()->obj( 'spam_entry' )->is_spam_list() ) {
			$bulk_actions['unspam'] = esc_html__( 'Mark as Not Spam', 'wpforms' );
		} else {
			$bulk_actions['spam'] = esc_html__( 'Mark as Spam', 'wpforms' );
		}

		if ( $this->is_trash_list() ) {
			$bulk_actions['restore'] = esc_html__( 'Restore', 'wpforms' );
		} else {
			$bulk_actions['trash'] = esc_html__( 'Move to Trash', 'wpforms' );
		}

		$bulk_actions['delete'] = esc_html__( 'Delete', 'wpforms' );

		return $bulk_actions;
	}

	/**
	 * Process the bulk actions.
	 *
	 * @since 1.8.6
	 */
	public function process_bulk_actions() {

		$this->display_bulk_action_message();

		if ( empty( $_REQUEST['_wpnonce'] ) ) {
			return;
		}

		if (
			! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'bulk-entries' ) &&
			! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'bulk-entries-nonce' )
		) {
			return;
		}

		$this->process_bulk_action_single();
	}

	/**
	 * Process single bulk action.
	 *
	 * @since 1.8.6
	 */
	protected function process_bulk_action_single() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		$doaction = $this->current_action();
		$status   = '';

		if ( empty( $doaction ) || $doaction === 'filter_date' ) {
			return;
		}

		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended
		$ids = isset( $_GET['entry_id'] ) ? wp_unslash( $_GET['entry_id'] ) : false;

		if ( ! is_array( $ids ) ) {
			$ids = [ $ids ];
		}

		$ids = array_map( 'absint', $ids );

		if ( empty( $ids ) ) {
			return;
		}

		// Check if it is a Trash list.
		if ( $this->is_trash_list() ) {
			$status = Page::TRASH_ENTRY_STATUS;
		}

		$spam_entry = wpforms()->obj( 'spam_entry' );

		// Check if it is a Spam list.
		if ( $spam_entry->is_spam_list() ) {
			$status = $spam_entry::ENTRY_STATUS;
		}

		$args = [
			'entry_id'    => $ids,
			'is_filtered' => true,
			'number'      => $this->get_items_per_page( 'wpforms_entries_per_page', $this->per_page ),
			'status'      => $status,
		];

		// Get entries, that would be affected.
		$entries_list = wpforms()->obj( 'entry' )->get_entries( $args );

		/**
		 * Filter entries list.
		 *
		 * @since 1.8.3
		 *
		 * @param array $entries_list List of entries.
		 * @param array $args         Arguments.
		 */
		$entries_list = apply_filters( 'wpforms_entries_table_process_actions_entries_list', $entries_list, $args ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName

		$sendback = remove_query_arg( [ 'read', 'unread', 'spam', 'unspam', 'starred', 'unstarred', 'print', 'deleted', 'trashed', 'restored', 'paged' ], wp_get_referer() );

		switch ( $doaction ) {
			// Mark as read.
			case 'read':
				$sendback = $this->process_bulk_action_single_read( $entries_list, $ids, $sendback );
				break;

			// Mark as unread.
			case 'unread':
				$sendback = $this->process_bulk_action_single_unread( $entries_list, $ids, $sendback );
				break;

			// Star entry.
			case 'star':
				$sendback = $this->process_bulk_action_single_star( $entries_list, $ids, $sendback );
				break;

			// Unstar entry.
			case 'unstar':
				$sendback = $this->process_bulk_action_single_unstar( $entries_list, $ids, $sendback );
				break;

			// Print entries.
			case 'print':
				$this->process_bulk_action_single_print( $ids );
				break;

			// Trash entries.
			case 'trash':
				$sendback = $this->process_bulk_action_single_trash( $ids, $sendback );
				break;

			// Delete entries.
			case 'delete':
				$sendback = $this->process_bulk_action_single_delete( $ids, $sendback );
				break;

			// Restore entries.
			case 'restore':
				$sendback = $this->process_bulk_action_single_restore( $ids, $sendback );
				break;

			// Mark as Spam.
			case 'spam':
				$sendback = $this->process_bulk_action_single_spam( $entries_list, $ids, $sendback );
				break;

			// Mark as Not Spam.
			case 'unspam':
				$sendback = $this->process_bulk_action_single_unspam( $entries_list, $ids, $sendback );
				break;
		}

		$sendback = remove_query_arg( [ 'action', 'action2', 'entry_id' ], $sendback );

		wp_safe_redirect( $sendback );
		exit();
	}

	/**
	 * Process the bulk action read.
	 *
	 * @since 1.5.7
	 *
	 * @param array  $entries_list Filtered entries list.
	 * @param array  $ids          IDs to process.
	 * @param string $sendback     URL query string.
	 *
	 * @return string
	 */
	protected function process_bulk_action_single_read( $entries_list, $ids, $sendback ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$form_id = ! empty( $_GET['form_id'] ) ? absint( $_GET['form_id'] ) : false;

		if ( empty( $form_id ) ) {
			return $sendback;
		}

		$user_id = get_current_user_id();
		$entries = wp_list_pluck( $entries_list, 'viewed', 'entry_id' );
		$read    = 0;

		foreach ( $ids as $id ) {

			if ( ! array_key_exists( $id, $entries ) ) {
				continue;
			}

			if ( $entries[ $id ] === '1' ) {
				continue;
			}

			$success = wpforms()->obj( 'entry' )->update(
				$id,
				[
					'viewed' => '1',
				]
			);

			if ( $success ) {

				wpforms()->obj( 'entry_meta' )->add(
					[
						'entry_id' => $id,
						'form_id'  => $form_id,
						'user_id'  => $user_id,
						'type'     => 'log',
						'data'     => wpautop( sprintf( '<em>%s</em>', esc_html__( 'Entry read.', 'wpforms' ) ) ),
					],
					'entry_meta'
				);

				++$read;
			}
		}

		return add_query_arg( 'read', $read, $sendback );
	}

	/**
	 * Process the bulk action unread.
	 *
	 * @since 1.8.6
	 *
	 * @param array  $entries_list Filtered entries list.
	 * @param array  $ids          IDs to process.
	 * @param string $sendback     URL query string.
	 *
	 * @return string
	 */
	protected function process_bulk_action_single_unread( $entries_list, $ids, $sendback ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$form_id = ! empty( $_GET['form_id'] ) ? absint( $_GET['form_id'] ) : false;

		if ( empty( $form_id ) ) {
			return $sendback;
		}

		$user_id = get_current_user_id();
		$entries = wp_list_pluck( $entries_list, 'viewed', 'entry_id' );
		$unread  = 0;

		foreach ( $ids as $id ) {

			if ( ! array_key_exists( $id, $entries ) ) {
				continue;
			}

			if ( $entries[ $id ] === '0' ) {
				continue;
			}

			$success = wpforms()->obj( 'entry' )->update(
				$id,
				[
					'viewed' => '0',
				]
			);

			if ( $success ) {
				wpforms()->obj( 'entry_meta' )->add(
					[
						'entry_id' => $id,
						'form_id'  => $form_id,
						'user_id'  => $user_id,
						'type'     => 'log',
						'data'     => wpautop( sprintf( '<em>%s</em>', esc_html__( 'Entry unread.', 'wpforms' ) ) ),
					],
					'entry_meta'
				);

				++$unread;
			}
		}

		return add_query_arg( 'unread', $unread, $sendback );
	}

	/**
	 * Process the bulk action spam.
	 *
	 * @since 1.8.9
	 *
	 * @param array  $entries_list Filtered entries list.
	 * @param array  $ids          IDs to process.
	 * @param string $sendback     URL query string.
	 */
	protected function process_bulk_action_single_spam( $entries_list, $ids, $sendback ) {

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$form_id = ! empty( $_GET['form_id'] ) ? absint( $_GET['form_id'] ) : false;

		if ( empty( $form_id ) ) {
			return $sendback;
		}

		$user       = get_user_by( 'id', get_current_user_id() );
		$entries    = wp_list_pluck( $entries_list, 'status', 'entry_id' );
		$spam_entry = wpforms()->obj( 'spam_entry' );
		$spam       = 0;

		foreach ( $ids as $id ) {

			if ( ! array_key_exists( $id, $entries ) ) {
				continue;
			}

			if ( $entries[ $id ] === $spam_entry::ENTRY_STATUS ) {
				continue;
			}

			$spam_entry->set_as_spam( $id, $form_id, $user->display_name );

			++$spam;
		}

		return add_query_arg( 'spam', $spam, $sendback );
	}

	/**
	 * Process the bulk action unspam.
	 *
	 * @since 1.8.9
	 *
	 * @param array  $entries_list Filtered entries list.
	 * @param array  $ids          IDs to process.
	 * @param string $sendback     URL query string.
	 *
	 * @return string
	 */
	protected function process_bulk_action_single_unspam( $entries_list, $ids, $sendback ) {

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$form_id = ! empty( $_GET['form_id'] ) ? absint( $_GET['form_id'] ) : false;

		if ( empty( $form_id ) ) {
			return $sendback;
		}

		$entries    = wp_list_pluck( $entries_list, 'status', 'entry_id' );
		$spam_entry = wpforms()->obj( 'spam_entry' );
		$unspam     = 0;

		foreach ( $ids as $id ) {

			if ( ! array_key_exists( $id, $entries ) ) {
				continue;
			}

			if ( $entries[ $id ] !== $spam_entry::ENTRY_STATUS ) {
				continue;
			}

			$entry = wpforms()->obj( 'entry' )->get( $id );

			$spam_entry->set_as_not_spam( $entry );

			++$unspam;
		}

		return add_query_arg( 'unspam', $unspam, $sendback );
	}

	/**
	 * Process the bulk action star.
	 *
	 * @since 1.8.6
	 *
	 * @param array  $entries_list Filtered entries list.
	 * @param array  $ids          IDs to process.
	 * @param string $sendback     URL query string.
	 *
	 * @return string
	 */
	protected function process_bulk_action_single_star( $entries_list, $ids, $sendback ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity

		$form_id = ! empty( $_GET['form_id'] ) ? absint( $_GET['form_id'] ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

		if ( empty( $form_id ) ) {
			return $sendback;
		}

		$user_id = get_current_user_id();
		$entries = wp_list_pluck( $entries_list, 'starred', 'entry_id' );
		$starred = 0;

		foreach ( $ids as $id ) {

			if ( ! array_key_exists( $id, $entries ) ) {
				continue;
			}

			if ( $entries[ $id ] === '1' ) {
				continue;
			}

			$success = wpforms()->obj( 'entry' )->update(
				$id,
				[
					'starred' => '1',
				]
			);

			if ( $success ) {
				wpforms()->obj( 'entry_meta' )->add(
					[
						'entry_id' => $id,
						'form_id'  => $form_id,
						'user_id'  => $user_id,
						'type'     => 'log',
						'data'     => wpautop( sprintf( '<em>%s</em>', esc_html__( 'Entry starred.', 'wpforms' ) ) ),
					],
					'entry_meta'
				);

				++$starred;
			}
		}

		return add_query_arg( 'starred', $starred, $sendback );
	}

	/**
	 * Process the bulk action unstar.
	 *
	 * @since 1.8.6
	 *
	 * @param array  $entries_list Filtered entries list.
	 * @param array  $ids          IDs to process.
	 * @param string $sendback     URL query string.
	 *
	 * @return string
	 */
	protected function process_bulk_action_single_unstar( $entries_list, $ids, $sendback ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$form_id = ! empty( $_GET['form_id'] ) ? absint( $_GET['form_id'] ) : false;

		if ( empty( $form_id ) ) {
			return $sendback;
		}

		$user_id   = get_current_user_id();
		$entries   = wp_list_pluck( $entries_list, 'starred', 'entry_id' );
		$unstarred = 0;

		foreach ( $ids as $id ) {

			if ( ! array_key_exists( $id, $entries ) ) {
				continue;
			}

			if ( $entries[ $id ] === '0' ) {
				continue;
			}

			$success = wpforms()->obj( 'entry' )->update(
				$id,
				[
					'starred' => '0',
				]
			);

			if ( $success ) {
				wpforms()->obj( 'entry_meta' )->add(
					[
						'entry_id' => $id,
						'form_id'  => $form_id,
						'user_id'  => $user_id,
						'type'     => 'log',
						'data'     => wpautop( sprintf( '<em>%s</em>', esc_html__( 'Entry unstarred.', 'wpforms' ) ) ),
					],
					'entry_meta'
				);

				++$unstarred;
			}
		}

		return add_query_arg( 'unstarred', $unstarred, $sendback );
	}

	/**
	 * Process the bulk action print.
	 *
	 * @since 1.8.6
	 *
	 * @param array $ids IDs to process.
	 *
	 * @return void
	 */
	private function process_bulk_action_single_print( $ids ) {

		$print_url = add_query_arg(
			[
				'page'     => 'wpforms-entries',
				'view'     => 'print',
				'entry_id' => implode( ',', $ids ),
			],
			admin_url( 'admin.php' )
		);

		wp_safe_redirect( $print_url );
		exit();
	}

	/**
	 * Process the bulk delete action.
	 *
	 * @since 1.8.5
	 *
	 * @param array  $ids      IDs to process.
	 * @param string $sendback URL query string.
	 *
	 * @return string
	 */
	private function process_bulk_action_single_trash( $ids, $sendback ) {

		$trashed = 0;
		$form_id = ! empty( $_GET['form_id'] ) ? absint( $_GET['form_id'] ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$user_id = get_current_user_id();

		foreach ( $ids as $id ) {
			// Get the entry first.
			$entry = wpforms()->obj( 'entry' )->get( $id );

			if ( ! $entry ) {
				continue;
			}

			$status = $entry->status;

			/**
			 * TODO :: After the support for PHP 7 ends,
			 * we can update the following code to use named arguments and skip the optional params.
			 */
			$success = wpforms()->obj( 'entry' )->update(
				$id,
				[ 'status' => Page::TRASH_ENTRY_STATUS ],
				'',
				'',
				[ 'cap' => 'delete_entry_single' ] // Force the cap to trash the entry, since we cant provide edit cap here.
			);

			// If it didn't work, continue.
			if ( ! $success ) {
				continue;
			}

			if ( $status !== '' ) {
				wpforms()->obj( 'entry_meta' )->add(
					[
						'entry_id' => $id,
						'form_id'  => $form_id,
						'user_id'  => $user_id,
						'type'     => 'status_prev',
						'data'     => '',
						'status'   => $status,
					],
					'entry_meta'
				);
			}

			++$trashed;
		}

		// If trashed entries are more than 1, then clear widget cache.
		if ( $trashed >= 1 ) {
			DashboardWidget::clear_widget_cache();
		}

		return add_query_arg(
			[
				'trashed' => $trashed,
				'view'    => 'list', // force the list view to make sure we go to the right page.
				'form_id' => $form_id,
			],
			$sendback
		);
	}

	/**
	 * Process the bulk action restores.
	 *
	 * @since 1.8.5
	 *
	 * @param array  $ids      IDs to process.
	 * @param string $sendback URL query string.
	 *
	 * @return string
	 */
	private function process_bulk_action_single_restore( $ids, $sendback ) {

		$restored = 0;

		foreach ( $ids as $id ) {
			// Reset or set initial status.
			$status = '';

			// Get the entry first.
			$entry = wpforms()->obj( 'entry' )->get( $id );

			if ( ! $entry ) {
				continue;
			}

			$meta = wpforms()->obj( 'entry_meta' )->get_meta(
				[
					'entry_id' => $id,
					'type'     => 'status_prev',
				]
			);

			// check meta for status log to restore the status.
			if ( $meta ) {
				$status = $meta[0]->status;

				// After taking status from meta, delete the meta.
				wpforms()->obj( 'entry_meta' )->delete_by( 'id', $meta[0]->id );
			}

			/**
			 * TODO :: After the support for PHP 7 ends,
			 * we can update the following code to use named arguments and skip the optional params.
			 */
			$success = wpforms()->obj( 'entry' )->update(
				$id,
				[ 'status' => $status ],
				'',
				'',
				[ 'cap' => 'delete_entry_single' ] // Force the cap to trash the entry, since we cant provide edit cap here.
			);

			// If it didn't work, continue.
			if ( ! $success ) {
				continue;
			}

			++$restored;
		}

		$trash_count = wpforms()->obj( 'entry' )->get_entries(
			[
				'form_id' => $this->form_id,
				'status'  => Page::TRASH_ENTRY_STATUS,
			],
			true
		);

		// If trash is emptied.
		if ( $trash_count < 1 ) {
			$sendback = remove_query_arg( 'status', $sendback );
		}

		// If restored entries are more than 1, then clear widget cache.
		if ( $restored >= 1 ) {
			DashboardWidget::clear_widget_cache();
		}

		return add_query_arg( 'restored', $restored, $sendback );
	}

	/**
	 * Process the bulk delete action.
	 *
	 * @since 1.8.6
	 *
	 * @param array  $ids      IDs to process.
	 * @param string $sendback URL query string.
	 *
	 * @return string
	 */
	protected function process_bulk_action_single_delete( $ids, $sendback ) {

		$deleted = 0;
		$form_id = ! empty( $_GET['form_id'] ) ? absint( $_GET['form_id'] ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

		foreach ( $ids as $id ) {
			if ( wpforms()->obj( 'entry' )->delete( $id ) ) {
				++$deleted;
			}
		}

		return add_query_arg(
			[
				'deleted' => $deleted,
				'view'    => 'list', // force the list view to make sure we go to the right page.
				'form_id' => $form_id,
			],
			$sendback
		);
	}

	/**
	 * Display a bulk action result message.
	 *
	 * @since 1.8.6
	 */
	protected function display_bulk_action_message() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		$bulk_counts = [
			'read'      => isset( $_REQUEST['read'] ) ? absint( $_REQUEST['read'] ) : 0,
			'unread'    => isset( $_REQUEST['unread'] ) ? absint( $_REQUEST['unread'] ) : 0,
			'spam'      => isset( $_REQUEST['spam'] ) ? absint( $_REQUEST['spam'] ) : 0,
			'unspam'    => isset( $_REQUEST['unspam'] ) ? absint( $_REQUEST['unspam'] ) : 0,
			'starred'   => isset( $_REQUEST['starred'] ) ? absint( $_REQUEST['starred'] ) : 0,
			'unstarred' => isset( $_REQUEST['unstarred'] ) ? absint( $_REQUEST['unstarred'] ) : 0,
			'deleted'   => isset( $_REQUEST['deleted'] ) ? (int) $_REQUEST['deleted'] : 0,
			'trashed'   => isset( $_REQUEST['trashed'] ) ? (int) $_REQUEST['trashed'] : 0,
			'restored'  => isset( $_REQUEST['restored'] ) ? (int) $_REQUEST['restored'] : 0,
		];
		// phpcs:enable WordPress.Security.NonceVerification.Recommended

		$bulk_messages = [
			/* translators: %d - number of processed entries. */
			'read'      => _n( '%d entry was successfully marked as read.', '%d entries were successfully marked as read.', $bulk_counts['read'], 'wpforms' ),
			/* translators: %d - number of processed entries. */
			'unread'    => _n( '%d entry was successfully marked as unread.', '%d entries were successfully marked as unread.', $bulk_counts['unread'], 'wpforms' ),
			/* translators: %d - number of processed entries. */
			'spam'      => _n( '%d entry was successfully marked as spam.', '%d entries were successfully marked as spam.', $bulk_counts['spam'], 'wpforms' ),
			/* translators: %d - number of processed entries. */
			'unspam'    => _n( '%d entry was successfully marked as not spam.', '%d entries were successfully marked as not spam.', $bulk_counts['unspam'], 'wpforms' ),
			/* translators: %d - number of processed entries. */
			'starred'   => _n( '%d entry was successfully starred.', '%d entries were successfully starred.', $bulk_counts['starred'], 'wpforms' ),
			/* translators: %d - number of processed entries. */
			'unstarred' => _n( '%d entry was successfully unstarred.', '%d entries were successfully unstarred.', $bulk_counts['unstarred'], 'wpforms' ),
			/* translators: %d - number of processed entries. */
			'deleted'   => _n( '%d entry was successfully deleted.', '%d entries were successfully deleted.', $bulk_counts['deleted'], 'wpforms' ),
			/* translators: %d - number of processed entries. */
			'trashed'   => _n( '%d entry was successfully trashed.', '%d entries were successfully trashed.', $bulk_counts['trashed'], 'wpforms' ),
			/* translators: %d - number of processed entries. */
			'restored'  => _n( '%d entry was successfully restored.', '%d entries were successfully restored.', $bulk_counts['restored'], 'wpforms' ),
		];

		if ( $bulk_counts['deleted'] === -1 ) {
			$bulk_messages['deleted'] = esc_html__( 'All entries for the currently selected form were successfully deleted.', 'wpforms' );
		}

		if ( $bulk_counts['trashed'] === -1 ) {
			$bulk_messages['trashed'] = esc_html__( 'All entries for the currently selected form were successfully trashed.', 'wpforms' );
		}

		// Leave only non-zero counts, so only those that were processed are left.
		$bulk_counts = array_filter( $bulk_counts );

		// If we have bulk messages to display.
		$messages = [];

		foreach ( $bulk_counts as $type => $count ) {
			if ( isset( $bulk_messages[ $type ] ) ) {
				$messages[] = sprintf( $bulk_messages[ $type ], $count );
			}
		}

		if ( $messages ) {
			Notice::success( implode( '<br>', array_map( 'esc_html', $messages ) ) );
		}
	}

	/**
	 * Message to be displayed when there are no entries.
	 *
	 * @since 1.8.6
	 */
	public function no_items() {

		printf(
			'<div class="wpforms-no-entries-found">%1$s</div>',
			esc_html__( 'No entries found.', 'wpforms' )
		);
	}

	/**
	 * Entries list form search.
	 *
	 * @since 1.8.6
	 *
	 * @param string $text     The 'submit' button label.
	 * @param string $input_id ID attribute value for the search input field.
	 */
	public function search_box( $text, $input_id ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity

		$input_id .= '-search-input';

		/**
		 * Fires before output the search box on the Entries list page.
		 *
		 * @since 1.4.4
		 *
		 * @param array $forms_data Form data.
		 */
		do_action( 'wpforms_entries_list_form_filters_before', $this->form_data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName

		$filter_fields = [];

		if ( ! empty( $this->form_data['fields'] ) ) {
			foreach ( $this->form_data['fields'] as $id => $field ) {
				if ( in_array( $field['type'], self::get_columns_form_disallowed_fields(), true ) ) {
					continue;
				}

				$filter_fields[ $id ] = ! empty( $field['label'] ) ? wp_strip_all_tags( $field['label'] ) : esc_html__( 'Field', 'wpforms' );
			}
		}

		/**
		 * Filters fields displayed in the search box.
		 *
		 * @since 1.5.5.1
		 *
		 * @param array     $fields Search box fields.
		 * @param ListTable $fields Instance of the ListTable class.
		 *
		 * @return array
		 */
		$filter_fields = (array) apply_filters( 'wpforms_entries_list_form_filters_search_fields', $filter_fields, $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName

		$cur_field = 'any';

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		if ( isset( $_GET['search']['field'] ) ) {
			if ( is_numeric( $_GET['search']['field'] ) ) {
				$cur_field = (int) $_GET['search']['field'];
			} else {
				$cur_field = sanitize_key( $_GET['search']['field'] );
			}
		}

		$advanced_options = Helpers::get_search_fields_advanced_options();
		$cur_comparison   = ! empty( $_GET['search']['comparison'] ) ? sanitize_key( $_GET['search']['comparison'] ) : 'contains';
		$cur_term         = '';

		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
		if ( isset( $_GET['search']['term'] ) && ! wpforms_is_empty_string( $_GET['search']['term'] ) ) {
			$cur_term = sanitize_text_field( wp_unslash( $_GET['search']['term'] ) );
			$cur_term = empty( $cur_term ) ? htmlspecialchars( wp_unslash( $_GET['search']['term'] ) ) : $cur_term; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		}

		// phpcs:enable WordPress.Security.NonceVerification.Recommended
		$this->search_box_output( $text, $input_id, $filter_fields, $advanced_options, $cur_field, $cur_comparison, $cur_term );

		/**
		 * Allows developers output some HTML after the filter forms on the entries' list page.
		 *
		 * @since 1.4.4
		 *
		 * @param array $form_data Form data.
		 */
		do_action( 'wpforms_entries_list_form_filters_after', $this->form_data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
	}

	/**
	 * Entries list form search.
	 *
	 * @since 1.8.6
	 *
	 * @param string $text                    The 'submit' button label.
	 * @param string $input_id                ID attribute value for the search input field.
	 * @param array  $filter_fields           Filter fields options.
	 * @param array  $search_advanced_options Advanced options.
	 * @param mixed  $cur_field               Current (selected) field or advanced option.
	 * @param string $cur_comparison          Current comparison.
	 * @param string $cur_term                Current search term.
	 *
	 * @noinspection HtmlUnknownAttribute*/
	private function search_box_output( $text, $input_id, $filter_fields, $search_advanced_options, $cur_field, $cur_comparison, $cur_term ) {

		?>
		<p class="search-box wpforms-form-search-box">

			<select name="search[field]" class="wpforms-form-search-box-field">
				<optgroup label="<?php esc_attr_e( 'Form fields', 'wpforms' ); ?>">
					<option value="any" <?php selected( 'any', $cur_field ); ?>><?php esc_html_e( 'Any form field', 'wpforms' ); ?></option>
					<?php
					if ( ! empty( $filter_fields ) ) {
						foreach ( $filter_fields as $id => $name ) {
							printf(
								'<option value="%1$s" %2$s>%3$s</option>',
								esc_attr( $id ),
								selected( $id, $cur_field, false ),
								esc_html( $name )
							);
						}
					}
					?>
				</optgroup>
				<?php if ( ! empty( $search_advanced_options ) ) : ?>
					<optgroup label="<?php esc_attr_e( 'Advanced Options', 'wpforms' ); ?>">
						<?php
						foreach ( $search_advanced_options as $val => $name ) {
							printf(
								'<option value="%1$s" %2$s>%3$s</option>',
								esc_attr( $val ),
								selected( $val, $cur_field, false ),
								esc_html( $name )
							);
						}
						?>
					</optgroup>
				<?php endif; // Advanced options group. ?>
			</select>

			<select name="search[comparison]" class="wpforms-form-search-box-comparison">
				<option value="contains" <?php selected( 'contains', $cur_comparison ); ?>>
					<?php esc_html_e( 'contains', 'wpforms' ); ?>
				</option>
				<option value="contains_not" <?php selected( 'contains_not', $cur_comparison ); ?>>
					<?php esc_html_e( 'does not contain', 'wpforms' ); ?>
				</option>
				<option value="is" <?php selected( 'is', $cur_comparison ); ?>>
					<?php esc_html_e( 'is', 'wpforms' ); ?>
				</option>
				<option value="is_not" <?php selected( 'is_not', $cur_comparison ); ?>>
					<?php esc_html_e( 'is not', 'wpforms' ); ?>
				</option>
			</select>

			<label class="screen-reader-text" for="<?php echo esc_attr( $input_id ); ?>">
				<?php echo esc_html( $text ); ?>:
			</label>
			<input type="search" name="search[term]" class="wpforms-form-search-box-term" value="<?php echo esc_attr( wp_unslash( $cur_term ) ); ?>" id="<?php echo esc_attr( $input_id ); ?>">

			<button type="submit" class="button"><?php echo esc_html( $text ); ?></button>
		</p>
		<?php
	}

	/**
	 * Fetch and set up the final data for the table.
	 *
	 * @since 1.8.6
	 */
	public function prepare_items() {

		// Retrieve the count.
		$this->get_counts();

		// Set up the columns.
		$columns = $this->get_columns();

		// Hidden columns (none).
		$hidden = [];

		// Define which columns can be sorted.
		$sortable = $this->get_sortable_columns();

		// Get a primary column. It will be a 3rd column.
		$primary = key( array_slice( $columns, 2, 1 ) );

		// Set column headers.
		$this->_column_headers = [ $columns, $hidden, $sortable, $primary ];

		// Get entries.
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		$total_items = $this->counts['total'];
		$page        = $this->get_pagenum();
		$order       = isset( $_GET['order'] ) ? sanitize_key( $_GET['order'] ) : 'DESC';
		$orderby     = isset( $_GET['orderby'] ) ? sanitize_key( $_GET['orderby'] ) : 'entry_id';
		$per_page    = $this->get_items_per_page( 'wpforms_entries_per_page', $this->per_page );
		$data_args   = [
			'form_id' => $this->form_id,
			'number'  => $per_page,
			'offset'  => $per_page * ( $page - 1 ),
			'order'   => $order,
			'orderby' => $orderby,
		];

		if ( ! empty( $_GET['type'] ) && $_GET['type'] === 'starred' ) {
			$data_args['starred'] = '1';
			$total_items          = $this->counts['starred'];
		}
		if ( ! empty( $_GET['type'] ) && $_GET['type'] === 'unread' ) {
			$data_args['viewed'] = '0';
			$total_items         = $this->counts['unread'];
		}

		if ( ! empty( $_GET['type'] ) && $_GET['type'] === 'payment' ) {
			$data_args['type'] = 'payment';
			$total_items       = $this->counts['payment'];
		}

		if ( ! empty( $_GET['status'] ) ) {
			$status      = sanitize_text_field( $_GET['status'] ); // phpcs:ignore WordPress.Security
			$total_items = ! empty( $this->counts[ $status ] ) ? $this->counts[ $status ] : 0;

			$data_args['status'] = $status === 'published' ? [ '' ] : $status;
		}
		// phpcs:enable WordPress.Security.NonceVerification.Recommended

		if ( array_key_exists( 'notes_count', $columns ) ) {
			$data_args['notes_count'] = true;
		}

		// Reset the orderby if the parameter does not exist in columns.
		if ( ! array_key_exists( $orderby, $columns ) ) {
			$data_args['orderby'] = 'entry_id';
		}

		/**
		 * Filters get entries arguments array.
		 *
		 * @since 1.4.0
		 *
		 * @param array $args Arguments.
		 *
		 * @return array
		 */
		$data_args = apply_filters( 'wpforms_entry_table_args', $data_args ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
		$data      = wpforms()->obj( 'entry' )->get_entries( $data_args );

		// Giddy up.
		$this->items = $data;

		// Finalize pagination.
		$this->set_pagination_args(
			[
				'total_items' => $total_items,
				'total_pages' => (int) ceil( $total_items / $per_page ),
				'per_page'    => $per_page,
			]
		);
	}

	/**
	 * Extending the `display_rows()` method to add hooks.
	 *
	 * @since 1.8.6
	 */
	public function display_rows() {

		/**
		 * Fires before displaying the table rows.
		 *
		 * @since 1.5.6.2
		 *
		 * @param ListTable $list_table_obj ListTable instance.
		 */
		do_action( 'wpforms_admin_entries_before_rows', $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName

		parent::display_rows();

		/**
		 * Fires after displaying the table rows.
		 *
		 * @since 1.5.6.2
		 *
		 * @param ListTable $list_table_obj ListTable instance.
		 */
		do_action( 'wpforms_admin_entries_after_rows', $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
	}

	/**
	 * Truncate long text value to X lines and Y characters.
	 *
	 * @since 1.8.6
	 *
	 * @param string $value      The value to truncate, if needed.
	 * @param string $field_type Field type.
	 *
	 * @return string
	 */
	private function truncate_long_value( $value, $field_type ) {

		// Limit multiline text to 4 lines, 5 for Address field, and overall length to 75 characters.
		$lines_limit = $field_type === 'address' ? 5 : 4;
		$chars_limit = 75;

		// Decode HTML entities to avoid truncating on &euro; and similar.
		$value = html_entity_decode( $value, ENT_COMPAT, 'UTF-8' );

		$lines = preg_split( '/\r\n|\r|\n/', $value );
		$value = array_slice( $lines, 0, $lines_limit );
		$value = implode( PHP_EOL, $value );

		// Encode HTML entities back to prevent XSS.
		$value = htmlentities( $value, ENT_COMPAT, 'UTF-8' );

		if ( strlen( $value ) > $chars_limit ) {
			return mb_substr( $value, 0, $chars_limit ) . '&hellip;';
		}

		// Ellipsis should be on a new line if the value is multiline, and extra lines were truncated.
		if ( count( $lines ) > $lines_limit ) {
			return $value . PHP_EOL . '&hellip;';
		}

		return $value;
	}

	/**
	 * Returns payment status label, slug and payment object by given entry ID.
	 * The returned data includes:
	 * - label: payment status label.
	 * - slug: payment status slug.
	 * - payment: payment object.
	 *
	 * @since 1.8.6
	 *
	 * @param int $entry_id Entry ID.
	 *
	 * @return array
	 */
	private function get_payment_status_by_entry_id( $entry_id ) {

		// Get payment data.
		$payment = wpforms()->obj( 'payment' )->get_by( 'entry_id', $entry_id );

		// If payment data is not found, return N/A.
		if ( ! $payment ) {
			return [
				__( 'N/A', 'wpforms' ),
				'n-a',
				null,
			];
		}

		$allowed_statuses = ValueValidator::get_allowed_statuses();
		$payment_status   = ! empty( $payment->subscription_id ) ? $payment->subscription_status : $payment->status;
		$status_slug      = ! empty( $payment_status ) ? $payment_status : 'n-a';
		$status_label     = $allowed_statuses[ $payment_status ] ?? __( 'N/A', 'wpforms' );

		return [ $status_label, $status_slug, $payment ];
	}

	/**
	 * Check the status of entries to check if they are trashable.
	 *
	 * @since 1.8.5
	 *
	 * @param int $entry_id Entry id to check.
	 *
	 * @return boolean
	 */
	private function should_delete( $entry_id ) {

		$entry = wpforms()->obj( 'entry' )->get( $entry_id );

		if ( ! $entry ) {
			return false;
		}

		return $entry->status === Page::TRASH_ENTRY_STATUS;
	}

	/**
	 * Check if the current page is a trash list.
	 *
	 * @since 1.8.5
	 *
	 * @return bool
	 */
	public function is_trash_list(): bool {

		$status = isset( $_GET['status'] ) ? sanitize_key( $_GET['status'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

		return $status === Page::TRASH_ENTRY_STATUS;
	}

	/**
	 * Displays the table.
	 *
	 * @since 1.8.6
	 */
	public function display() {

		$singular = $this->_args['singular'];

		$this->display_tablenav( 'top' );

		$this->screen->render_screen_reader_content( 'heading_list' );
		?>
		<div class="wpforms-table-container">
			<table class="wp-list-table <?php echo esc_attr( implode( ' ', $this->get_table_classes() ) ); ?>">
				<?php $this->print_table_description(); ?>
				<thead>
				<tr>
					<?php $this->print_column_headers(); ?>
				</tr>
				</thead>

				<tbody id="the-list"
					<?php
					if ( $singular ) {
						echo ' data-wp-lists="list:' . esc_attr( $singular ) . '"';
					}
					?>
				>
				<?php $this->display_rows_or_placeholder(); ?>
				</tbody>

				<tfoot>
				<tr>
					<?php $this->print_column_headers( false ); ?>
				</tr>
				</tfoot>
			</table>
		</div>
		<?php
		$this->display_tablenav( 'bottom' );
	}

	/**
	 * Print column headers but without the `actions` column if this is bottom table header.
	 *
	 * @since 1.8.6
	 *
	 * @param bool $with_id Whether to print the `<tr>` element with an `id` attribute.
	 */
	public function print_column_headers( $with_id = true ) {

		if ( $with_id ) {
			parent::print_column_headers();
		} else {
			$column_headers = $this->_column_headers[0];

			if ( isset( $column_headers['actions'] ) ) {
				$column_headers['actions'] = esc_html__( 'Actions', 'wpforms' );

				$this->_column_headers[0] = $column_headers;
			}

			parent::print_column_headers( false );
		}
	}
}
