<?php

namespace GP_Entry_Blocks\Blocks;

use GFAPI;
use GP_Entry_Blocks\GF_Queryer;
use function GP_Entry_Blocks\get_block_attributes_by_uuid;
use function GP_Entry_Blocks\get_current_url;

/**
 * Block that renders out user-facing filters.
 *
 * @since 1.0
 */
class Filters extends Block {
	/**
	 * @var null|\WP_Block Used for storing the current block in runtime as a means for passing around the block instance to filter callbacks.
	 */
	private static $_current_block = null;

	/**
	 * @var array An array of unsupported field and input types.
	 */
	public static $unsupported_input_types = array( 'form' );

	/**
	 * @var array Gravity Forms forces integers for field IDs. This means any psuedo-fields we add need to follow this rule and this is our way of mapping
	 *  the submitted IDs to the associated meta.
	 */
	const FIELD_ID_META_MAP = array(
		99999 => 'all',
	);

	public function __construct( $path ) {
		parent::__construct( $path );

		// We use 'wp' here that way $wp_query is populated which allows us to fetch block attributes.
		add_action( 'wp', array( $this, 'maybe_process_filter_submission' ) );

		add_filter( 'gpeb_filter_form', array( $this, 'filter_form_add_meta_fields' ) );
		add_filter( 'gpeb_filter_form', array( $this, 'filter_form_exclude_display_only_fields' ) );
		add_filter( 'gpeb_filter_form', array( $this, 'filter_form_exclude_unsupported_fields' ) );
		add_filter( 'gpeb_filter_form', array( $this, 'filter_form_only_include_selected_fields' ) );
		add_filter( 'gpeb_filter_form', array( $this, 'filter_form_set_button' ) );
		add_filter( 'gpeb_filter_form', array( $this, 'filter_form_sort_fields_by_order' ) );

		add_filter( 'gpeb_filter_form_field', array( $this, 'filter_form_field_maybe_convert_to_text' ), 10, 2 );
		add_filter( 'gpeb_filter_form_field', array( $this, 'filter_form_field_add_placeholder' ), 10, 2 );
		add_filter( 'gpeb_filter_form_field', array( $this, 'filter_form_field_remove_default_value' ), 10, 2 );
		add_filter( 'gpeb_filter_form_field', array( $this, 'filter_form_field_remove_conditional_logic' ), 10, 2 );
		add_filter( 'gpeb_filter_form_field', array( $this, 'filter_form_field_prepare_for_population' ), 10, 2 );
		add_filter( 'gpeb_filter_form_field', array( $this, 'filter_form_field_use_custom_label' ), 10, 2 );
		add_filter( 'gpeb_filter_form_field', array( $this, 'filter_form_field_disable_read_only' ), 10, 2 );
		add_filter( 'gpeb_filter_form_field', array( $this, 'filter_form_field_disable_gpld_inline' ), 10, 2 );
		add_filter( 'gpeb_filter_form_field', array( $this, 'filter_form_field_disable_required' ), 10, 2 );
		add_filter( 'gpeb_filter_form_field', array( $this, 'filter_form_field_enable_administrative' ), 10, 2 );
	}

	/**
	 * Extend render_with_hooks to add in a class depending on the orientation attribute.
	 *
	 * @param array $attributes Block attributes.
	 * @param string $content Block content.
	 * @param \WP_Block $block Block instance.
	 *
	 * @return string
	 */
	public function render_with_hooks( $attributes, $content, $block ) {
		$attributes['wrapperExtraAttributes'] = array(
			'class' => rgar( $attributes, 'orientation' ) === 'vertical' ? 'is-orientation-vertical' : 'is-orientation-horizontal',
		);

		return parent::render_with_hooks( $attributes, $content, $block );
	}

	/**
	 * @param array $attributes Block attributes.
	 * @param string $content Block content.
	 * @param \WP_Block $block Block instance.
	 *
	 * @return string
	 */
	public function render( $attributes, $content, $block ) {
		$queryer = GF_Queryer::attach( $block->context );

		if ( ! $queryer || ! $queryer->is_entries() || empty( $attributes['filters'] ) ) {
			return '';
		}

		$form_id = $queryer->form_id;

		self::$_current_block = $block;

		add_filter( 'gform_pre_render_' . $form_id, array( $this, 'convert_form_to_filter_form' ) );
		add_filter( 'gform_field_value', array( $this, 'map_get_params_to_value' ), 10, 3 );
		add_filter( 'gform_submit_button', array( $this, 'add_clear_filters_button' ), 10, 2 );
		add_filter( 'gform_form_tag_' . $form_id, array( $this, 'add_hidden_inputs' ), 10, 2 );
		add_filter( 'gform_form_args', array( $this, 'force_non_ajax_mode' ), 99955 );
		add_filter( 'gppt_is_enabled', '__return_false', 99955 ); // Disable GPPT
		add_filter( 'gpld_has_limit_dates_enabled', '__return_false', 99955 ); // Disable GPLD

		/**
		 * Replace with block style and use actual CSS
		 */
		$form_output = gravity_form( $form_id, false, false, false, rgars( $_GET, 'filters' ), false, 0, false );

		remove_filter( 'gform_pre_render_' . $form_id, array( $this, 'convert_form_to_filter_form' ) );
		remove_filter( 'gform_field_value', array( $this, 'map_get_params_to_value' ) );
		remove_filter( 'gform_submit_button', array( $this, 'add_clear_filters_button' ) );
		remove_filter( 'gform_form_tag_' . $form_id, array( $this, 'add_hidden_inputs' ) );
		remove_filter( 'gform_form_args', array( $this, 'force_non_ajax_mode' ), 99955 );
		remove_filter( 'gppt_is_enabled', '__return_false', 99955 );
		remove_filter( 'gpld_has_limit_dates_enabled', '__return_false', 99955 );

		self::$_current_block = null;

		/* Remove hidden inputs in the form footer for submission and replace it with a hidden input to signal it to be processed as filters. */
		return $this->convert_hidden_inputs( $form_output, $form_id );
	}

	/**
	 * Remove hidden inputs in the form footer for submission and replace it with a hidden input to signal it to be processed as filters.
	 *
	 * @param string $form_html The form HTML.
	 * @param number $form_id The form ID. Used for populating gpeb_filters_form_id hidden input.
	 *
	 * @return string
	 */
	public function convert_hidden_inputs( $form_html, $form_id ) {
		return preg_replace(
			'/(?![\s\S]*gform_footer[\s\S]*)(?:<input type=[\'"]hidden[\'"][^>]*>)+/',
			'<input type="hidden" name="gpeb_filters_form_id" value="' . $form_id . '">',
			$form_html
		);
	}

	/**
	 * Converts a Gravity Form to be used for filtering entries. It performs various
	 * actions on the form including (but not limited to):
	 *    • Only showing the selected fields
	 *    • Converting field types such as paragraph to single line text
	 *    • Setting all default values to an empty string
	 *    • Filling in the field values with the submitted filters
	 *    • Changing the submit button text
	 *    • Setting placeholder for all choice-based fields
	 *
	 * @param array $form Gravity Form to convert to a filters form.
	 *
	 * @return array Form converted to be used for filters.
	 */
	public function convert_form_to_filter_form( $form ) {
		/**
		 * Filters the form object that will be used to build the filters form.
		 *
		 * @param array $form The form object.
		 */
		$form = apply_filters( 'gpeb_filter_form', $form );

		/** @var \GF_Field $field */
		foreach ( $form['fields'] as &$field ) {
			/**
			 * Filter the field object that will be used to build the filters form.
			 *
			 * @param array $field The field object.
			 * @param array $form The form object.
			 */
			$field = apply_filters( 'gpeb_filter_form_field', $field, $form );
		}

		return $form;
	}

	/**
	 * Add psuedo-fields for meta search fields such as all.
	 *
	 * @param array $form
	 *
	 * @return array
	 */
	public function filter_form_add_meta_fields( $form ) {
		$attributes = $this->get_current_block_attrs();

		// If there are no attributes, return the form as is during submission to prevent malformed values.
		if ( ! $attributes ) {
			return $form;
		}

		foreach ( $attributes['filters'] as $filter ) {
			if ( rgars( $filter, 'meta/fieldId' ) ) {
				continue;
			}

			switch ( $filter['type'] ) {
				case 'all':
					$form['fields'][] = new \GF_Field_Text( array(
						'id'     => self::get_meta_map_field_id( 'all' ),
						'label'  => __( 'Search All', 'gp-entry-blocks' ),
						'type'   => 'text',
						'size'   => 'large',
						'inputs' => null,
						'formId' => $form['id'],
					) );
					break;
			}
		}

		return $form;
	}


	/**
	 * Exclude display-only fields such as sections, pages, HTML, etc. This shouldn't happen thanks to Filters::filter_form_only_include_selected_fields(),
	 * but it can still be a possibility if field IDs are changed.
	 *
	 * @param array $form
	 *
	 * @return array
	 */
	public function filter_form_exclude_display_only_fields( $form ) {
		/** @var \GF_Field $field */
		foreach ( $form['fields'] as $field_index => $field ) {
			if ( rgar( $field, 'displayOnly' ) ) {
				unset( $form['fields'][ $field_index ] );
			}
		}

		return $form;
	}

	/**
	 * Exclude unsupported field types.
	 *
	 * @param array $form
	 *
	 * @return array
	 */
	public function filter_form_exclude_unsupported_fields( $form ) {
		/** @var \GF_Field $field */
		foreach ( $form['fields'] as $field_index => $field ) {
			if ( in_array( $field->get_input_type(), self::$unsupported_input_types ) ) {
				unset( $form['fields'][ $field_index ] );
			}
		}

		return $form;
	}

	/**
	 * Gravity Forms submission is handled prior to the blocks being rendered out. As such, we need to extract the
	 * Filters block attributes from the block that was used to filter the entries. This way, we can get various
	 * settings such as the field IDs to filter by.
	 *
	 * @return array|null
	 */
	public function get_submitted_filter_form_attrs() {
		// UUID for tracking which block was responsible for submitting the edit, so we can fetch its attributes.
		$submitted_uuid = rgpost( 'gpeb_filters_block_uuid' );

		if ( ! $submitted_uuid ) {
			return null;
		}

		return get_block_attributes_by_uuid( $submitted_uuid, 'gp-entry-blocks/filters' );
	}

	/**
	 * Gets the current block attributes either from render or submission. Returns null if unable to get both.
	 */
	public function get_current_block_attrs() {
		// If we are in the submission process, pull the attributes from the submitted block.
		if ( self::$_current_block ) {
			$attributes = self::$_current_block->attributes;
		} else {
			$attributes = $this->get_submitted_filter_form_attrs();
		}

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

		return $attributes;
	}

	/**
	 * Only include the selected fields in the block attributes.
	 *
	 * @param array $form
	 *
	 * @return array
	 */
	public function filter_form_only_include_selected_fields( $form ) {
		$attributes = $this->get_current_block_attrs();

		// If there are no attributes, return the form as is during submission to prevent malformed values.
		if ( ! $attributes ) {
			return $form;
		}

		$field_ids = array();

		foreach ( $attributes['filters'] as $filter ) {
			if ( ! rgars( $filter, 'meta/fieldId' ) ) {
				$field_ids[] = self::get_meta_map_field_id( $filter['type'] );

				continue;
			}

			$field_ids[] = $filter['meta']['fieldId'];
		}

		/** @var \GF_Field $field */
		foreach ( $form['fields'] as $field_index => $field ) {
			// phpcs:ignore WordPress.PHP.StrictInArray.FoundNonStrictFalse
			if ( ! in_array( $field->id, $field_ids, false ) ) {
				unset( $form['fields'][ $field_index ] );
			}
		}

		// The GF RECAPTCHA add-on adds a special input outside the normal rendering flow. Let's remove it.
		if ( is_callable( array( 'Gravity_Forms\Gravity_Forms_RECAPTCHA\GF_RECAPTCHA', 'get_instance' ) ) ) {
			remove_filter( 'gform_form_tag', array( \Gravity_Forms\Gravity_Forms_RECAPTCHA\GF_RECAPTCHA::get_instance(), 'add_recaptcha_input' ), 50, 2 );
		}

		return $form;
	}

	/**
	 * Change the submit button to show "Apply Filters"
	 *
	 * @param array $form
	 *
	 * @return array
	 */
	public function filter_form_set_button( $form ) {
		$form['button'] = array(
			'type'     => 'text',
			'text'     => __( 'Apply Filters', 'gp-entry-blocks' ),
			'width'    => 'auto',
			'location' => 'bottom',
		);

		return $form;
	}

	/**
	 * Sorts the fields in the form according to the order set in the block settings.
	 *
	 * @param array $form
	 *
	 * @return array
	 */
	public function filter_form_sort_fields_by_order( $form ) {
		$attributes = $this->get_current_block_attrs();

		// If there are no attributes, return the form as is during submission to prevent malformed values.
		if ( ! $attributes ) {
			return $form;
		}

		$field_ids = array();

		foreach ( $attributes['filters'] as $filter ) {
			if ( ! rgars( $filter, 'meta/fieldId' ) ) {
				continue;
			}

			$field_ids[] = $filter['meta']['fieldId'];
		}

		/** @var \GF_Field $field */
		usort( $form['fields'], function ( $a, $b ) use ( $field_ids ) {
			$a = array_search( $a->id, $field_ids, true );
			$b = array_search( $b->id, $field_ids, true );

			if ( $a === $b ) {
				return 0;
			}

			return ( $a < $b ) ? - 1 : 1;
		} );

		return $form;
	}

	/**
	 * Some field types may not make sense in the context of filtering or simply not be usable. In these cases, we'll
	 * convert them to a Single Line Text field.
	 *
	 * @param \GF_Field $field The current field.
	 * @param array $form The current form.
	 *
	 * @return \GF_Field $field
	 */
	public function filter_form_field_maybe_convert_to_text( $field, $form ) {
		/**
		 * Filter the input types that should be preserved.
		 *
		 * For example, Email fields are not preserved by default so they would be converted to a Single Line Text field.
		 * Checkbox fields are preserved, so they will be rendered as checkboxes.
		 *
		 * @param array $input_types_to_preserve The input types to preserve.
		 */
		$input_types_to_preserve = apply_filters( 'gpeb_filter_fields_preserve_type', array(
			'text',
			'checkbox',
			'radio',
			'select',
			'number',
			'date',
		) );

		if ( in_array( $field->get_input_type(), $input_types_to_preserve, true ) ) {
			return $field;
		}

		$text_field              = new \GF_Field_Text( $field );
		$text_field->type        = 'text';
		$text_field->inputs      = null;
		$text_field->choices     = array();
		$text_field->placeholder = '';

		return $text_field;
	}

	/**
	 * Add placeholders to choice-based fields that way a choice isn't forcefully submitted for dropdowns, etc.
	 *
	 * @param \GF_Field $field
	 * @param array $form
	 *
	 * @return \GF_Field
	 */
	public function filter_form_field_add_placeholder( $field, $form ) {
		if ( ! empty( $field->choices ) ) {
			$field['placeholder'] = __( '&mdash;', 'gp-entry-blocks' );
		}

		return $field;
	}

	/**
	 * Remove all default values from fields as it will likely not make sense for most filter use-cases.
	 *
	 * @param \GF_Field $field
	 * @param array $form
	 *
	 * @return \GF_Field
	 */
	public function filter_form_field_remove_default_value( $field, $form ) {
		$field['defaultValue'] = '';

		// Remove default values from inputs.
		if ( is_array( $field['inputs'] ) ) {
			$inputs = $field['inputs'];

			foreach ( $inputs as &$input ) {
				$input['defaultValue'] = '';
			}

			$field['inputs'] = $inputs;
		}

		// Remove default values from choices.
		if ( is_array( $field['choices'] ) ) {
			$choices = $field['choices'];

			foreach ( $choices as &$choice ) {
				$choice['isSelected'] = false;
			}

			$field['choices'] = $choices;
		}

		return $field;
	}

	public function filter_form_field_remove_conditional_logic( $field, $form ) {
		$field['conditionalLogic'] = '';

		return $field;
	}

	/**
	 * Prepares fields/inputs for pre-population so the fields show the current filter values.
	 *
	 * @param \GF_Field $field
	 * @param array $form
	 *
	 * @return \GF_Field
	 */
	public function filter_form_field_prepare_for_population( $field, $form ) {
		$field['allowsPrepopulate'] = true;

		if ( is_array( $field['inputs'] ) ) {
			$inputs = $field['inputs'];

			foreach ( $inputs as &$input ) {
				$input['name'] = (string) $input['id'];
			}

			$field['inputs'] = $inputs;
		}

		$field['inputName'] = $field['id'];

		return $field;
	}

	/**
	 * Changes the field to use the supplied label in block attributes if present.
	 *
	 * @param \GF_Field $field
	 * @param array $form
	 *
	 * @return \GF_Field
	 */
	public function filter_form_field_use_custom_label( $field, $form ) {
		$attributes = $this->get_current_block_attrs();

		// If there are no attributes, return the field as is as we won't have a custom label to use.
		if ( ! $attributes ) {
			return $field;
		}

		$custom_label = null;

		foreach ( $attributes['filters'] as $filter ) {
			if ( empty( $filter['meta']['label'] )
				|| ( rgars( $filter, 'meta/fieldId' ) !== $field->id && self::get_meta_map_field_id( $filter['type'] ) !== $field->id )
			) {
				continue;
			}

			$custom_label = $filter['meta']['label'];
		}

		if ( ! empty( $custom_label ) ) {
			$field->label = $custom_label;
		}

		return $field;
	}

	/**
	 * Prevent GP Read Only from making filter fields read only.
	 *
	 * @param \GF_Field $field
	 * @param array $form
	 *
	 * @return \GF_Field
	 */
	public function filter_form_field_disable_read_only( $field, $form ) {
		if ( rgar( $field, 'gwreadonly_enable' ) ) {
			unset( $field['gwreadonly_enable'] );
		}

		return $field;
	}

	/**
	 * Disable GP Limit Dates Inline Datepicker.
	 *
	 * @param \GF_Field $field
	 * @param array $form
	 *
	 * @return \GF_Field
	 */
	public function filter_form_field_disable_gpld_inline( $field, $form ) {
		if ( rgar( $field, 'gpLimitDatesinlineDatepicker' ) ) {
			unset( $field['gpLimitDatesinlineDatepicker'] );
		}

		return $field;
	}

	/**
	 * Changes the field to make sure required rule is not displayed for filter.
	 *
	 * @param \GF_Field $field
	 * @param array $form
	 *
	 * @return \GF_Field
	 */
	public function filter_form_field_disable_required( $field, $form ) {
		if ( rgar( $field, 'isRequired' ) ) {
			unset( $field['isRequired'] );
		}

		return $field;
	}

	/**
	 * Changes the administrative field to visible.
	 *
	 * @param \GF_Field $field
	 * @param array $form
	 *
	 * @return \GF_Field
	 */
	public function filter_form_field_enable_administrative( $field, $form ) {
		if ( rgar( $field, 'visibility' ) === 'administrative' ) {
			$field['visibility'] = 'visible';
		}

		return $field;
	}

	/**
	 * @param string $button Gravity Forms submit button.
	 * @param array $form Current form.
	 *
	 * @returns string Filtered submit button with the Clear Filters button if necessary.
	 */
	public function add_clear_filters_button( $button, $form ) {
		if ( rgget( 'filters' ) ) {
			$label   = esc_html__( 'Clear Filters', 'gp-entry-blocks' );
			$button .= "<button class='gpeb_clear_filters' name='gpeb_clear_filters' value='1'>{$label}</button>";
		}

		return $button;
	}

	/**
	 * Maps the filter value in the GET params to the field.
	 *
	 * @param mixed     $value Field value.
	 * @param \GF_Field $field Current field.
	 * @param string    $name  Name of the current input.
	 */
	public function map_get_params_to_value( $value, $field, $name ) {
		if ( empty( $_GET['filters'] ) ) {
			return $value;
		}

		$_value       = rgars( $_GET, "filters/{$name}" );
		$meta_map_key = self::get_meta_map_from_field_id( $name );

		if ( $_value ) {
			$value = $_value;
		}

		/* If no immediate match with field ID, try the meta map. */
		if ( ! $value && $meta_map_key && isset( $_GET['filters'][ $meta_map_key ] ) ) {
			$value = $_GET['filters'][ $meta_map_key ];
		}

		/* Build array if there are multiple values for the input (e.g. checkboxes). */
		if ( ! $value && is_array( $_GET['filters'] ) ) {
			foreach ( $_GET['filters'] as $filter_key => $filter_value ) {
				if ( strpos( $filter_key, '_' ) === false ) {
					continue;
				}

				$input_id = str_replace( '_', '.', $filter_key );

				if ( (int) $input_id !== $field->id ) {
					continue;
				}

				if ( ! is_array( $value ) ) {
					$value = array();
				}

				$value[ $input_id ] = $filter_value;
			}
		}

		return $value;
	}

	/**
	 * Intercepts form submission and converts submitted values to be
	 * search params (GET params) and redirects.
	 *
	 * @todo Can this be handled via JS to avoid the redirect? $field->get_value_save_entry() would need to be used in GF_Queryer
	 *
	 * @return void
	 */
	public function maybe_process_filter_submission() {
		if ( ! rgpost( 'gpeb_filters_form_id' ) ) {
			return;
		}

		if ( rgpost( 'gpeb_clear_filters' ) ) {
			wp_safe_redirect( remove_query_arg( 'filters', get_current_url() ) );
			exit;
		}

		$values = array();
		$form   = $this->convert_form_to_filter_form( GFAPI::get_form( rgpost( 'gpeb_filters_form_id' ) ) );

		if ( ! $form ) {
			return;
		}

		foreach ( $_POST as $key => $value ) {
			if ( strpos( $key, 'input_' ) === false || rgblank( $value ) ) {
				continue;
			}

			$key   = str_replace( 'input_', '', $key );
			$field = GFAPI::get_field( $form, absint( $key ) );

			/*
			 * The field may not be present if it's a pseudo-field such as "Search All." Filtering the form with `gpeb_filter_form` doesn't work in this case
			 * since the block hasn't rendered yet thus the attributes property isn't available.
			 */
			if ( $field ) {
				$values[ $key ] = $field->get_value_save_entry( $value, $form, $key, -1, array() );
			} else {
				if ( self::get_meta_map_from_field_id( $key ) ) {
					$key = self::get_meta_map_from_field_id( $key );
				}

				$values[ $key ] = $value;
			}
		}

		wp_safe_redirect( add_query_arg( array(
			'filters'         => $values,
			'filters_form_id' => $form['id'],
		), get_current_url() ) );
		exit;
	}

	/*
	 * Fetches the pseudo-field ID for the given search meta.
	 *
	 * @param string $search_meta The meta key to get the pseudo-field ID for.
	 *
	 * @see Filters::FIELD_ID_META_MAP
	 * @returns null|int
	 */
	public static function get_meta_map_field_id( $search_meta ) {
		foreach ( self::FIELD_ID_META_MAP as $field_id => $meta ) {
			if ( $search_meta === $meta ) {
				return $field_id;
			}
		}

		return null;
	}

	/*
	 * Fetches the meta field given the pseudo field ID.
	 *
	 * @param int $field_id Psuedo-field ID such as 99999.
	 *
	 * @see Filters::FIELD_ID_META_MAP
	 * @returns null|string
	 */
	public static function get_meta_map_from_field_id( $search_field_id ) {
		foreach ( self::FIELD_ID_META_MAP as $field_id => $meta ) {
			if ( (int) $search_field_id === (int) $field_id ) {
				return $meta;
			}
		}

		return null;
	}

	/**
	 * Append hidden inputs to the form to include the Filters block UUID, so we can pull attributes off of it
	 * during submission.
	 *
	 * @param string $form_tag The form opening tag.
	 * @param array $form The current form.
	 *
	 * @return string
	 */
	public function add_hidden_inputs( $form_tag, $form ) {
		$block = self::$_current_block;

		if ( ! $block ) {
			return $form_tag;
		}

		$form_tag .= '<input type="hidden" value="' . esc_attr( $block->context['gp-entry-blocks/uuid'] ) . '" name="gpeb_filters_block_uuid" />';

		return $form_tag;
	}

	/**
	 * Prevent filter form from using AJAX as it will not work.
	 */
	public function force_non_ajax_mode( $form_args ) {
		$form_args['ajax'] = false;

		return $form_args;
	}
}
