<?php

namespace GP_Entry_Blocks\Blocks;

use GF_Field;
use GFAPI;
use GFCommon;
use GFFormsModel;
use GP_Entry_Blocks\GF_Queryer;
use function GP_Entry_Blocks\get_block_attributes_by_uuid;

/**
 * Block that contains a Gravity Form for editing the current entry.
 *
 * @since 1.0
 */
class Edit_Form extends Block {
	/**
	 * @var GF_Queryer
	 */
	public $queryer;

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

		add_action( 'gform_entry_id_pre_save_lead', array( $this, 'use_submitted_edit_entry_id' ), 10, 2 );
		add_action( 'gform_pre_handle_confirmation', array( $this, 'send_entry_edited_notifications' ), 10, 2 );

		add_filter( 'gform_confirmation', array( $this, 'edit_confirmation' ), 15, 4 );
		add_filter( 'gform_notification_events', array( $this, 'add_edit_notification_event' ) );
		add_filter( 'gform_disable_notification', array( $this, 'suppress_form_submission_notifications_on_edit' ), 10, 5 );

		/* Field-specific handling/fixes. */
		add_filter( 'gform_field_input', array( $this, 'rerender_signature_field_on_edit' ), 10, 5 );

		/* Population */
		add_filter( 'gpeb_edit_form', array( $this, 'prepare_form_for_population' ) );
		add_filter( 'gpeb_edit_form_entry', array( $this, 'prepare_entry_for_population' ) );

		/* GPLS */
		add_filter( 'gpls_should_apply_rules', array( $this, 'maybe_disable_gpls' ), 10, 3 );

		/* Edit entry with no duplicate field setting */
		add_filter( 'gform_is_duplicate', array( $this, 'check_for_duplicate' ), 10, 4 );
	}

	public function render( $attributes, $content, $block ) {
		$this->queryer = GF_Queryer::attach( $block->context );

		if ( ! $this->queryer->is_edit_entry() ) {
			return '';
		}

		$entry = $this->queryer->entry;

		if ( ! $entry ) {
			return __( 'Oops! We can\'t locate that entry.', 'gp-entry-blocks' );
		}

		$form_id = $entry['form_id'];

		if ( ! gp_entry_blocks()->permissions->can_current_user_edit_entry( $entry ) ) {
			return __( 'Oops! You don\'t have permission to edit this entry.', 'gp-entry-blocks' );
		}

		add_filter( 'gppt_is_enabled', '__return_false', 99995 );
		add_filter( 'gform_pre_render_' . $form_id, array( $this, 'add_edit_form_filter' ) );
		add_filter( 'gform_form_tag_' . $form_id, array( $this, 'add_hidden_inputs' ), 10, 2 );
		add_filter( 'gform_savecontinue_link_' . $form_id, array( $this, 'hide_savecontinue_link' ) );

		/**
		 * Filter the entry that is used when populating the Edit Form.
		 *
		 * @param array $entry The entry being edited.
		 *
		 * @since 1.0-alpha-2.3
		 */
		$entry = apply_filters( 'gpeb_edit_form_entry', $entry );

		$form = gravity_form( $form_id, false, false, false, $entry, false, 0, false );

		remove_filter( 'gppt_is_enabled', '__return_false', 99995 );
		remove_filter( 'gform_pre_render_' . $form_id, array( $this, 'add_edit_form_filter' ) );
		remove_filter( 'gform_form_tag_' . $form_id, array( $this, 'add_hidden_inputs' ) );
		remove_filter( 'gform_savecontinue_link_' . $form_id, array( $this, 'hide_savecontinue_link' ) );

		$form = $this->escape_merge_tags( $form );

		return $form;
	}

	/**
	 * Hide save and continue while editing due to draft submissions not working in this context.
	 */
	public function hide_savecontinue_link() {
		return null;
	}

	/**
	 * Set allowsPrepopulate on fields so we can fill in the field's with their values from the desired entry.
	 *
	 * @param array $form The current form.
	 *
	 * @return array
	 */
	public function prepare_form_for_population( $form ) {

		foreach ( $form['fields'] as &$field ) {

			$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 $form;
	}

	/**
	 * Makes it easier to filter an Edit form without needing to use gform_pre_render.
	 *
	 * @param array $form Gravity Form to use as an Edit form.
	 *
	 * @return array Edit form with filters added.
	 */
	public function add_edit_form_filter( $form ) {
		/**
		 * Filter the form that is used when populating the Edit Form.
		 *
		 * @param array $form The form used in the Edit Form.
		 *
		 * @since 1.0-alpha-2.3
		 */
		$form = apply_filters( 'gpeb_edit_form', $form );

		/** @var \GF_Field $field */
		foreach ( $form['fields'] as &$field ) {
			/**
			 * Filter individual fields that are used when populating the Edit Form.
			 *
			 * @param \GF_Field $field The current field being filtered.
			 * @param array $form The form used in the Edit Form.
			 *
			 * @since 1.0-alpha-2.3
			 */
			$field = apply_filters( 'gpeb_edit_form_field', $field, $form );
		}

		return $form;
	}

	/**
	 * Append hidden inputs to the form to signal that the submission should be processed as an edit instead of adding a new entry.
	 *
	 * @param string $form_tag The form opening tag.
	 * @param array $form The current form.
	 *
	 * @return string
	 */
	public function add_hidden_inputs( $form_tag, $form ) {
		if ( empty( $this->queryer->entry ) ) {
			return $form_tag;
		}

		$form_tag .= '<input type="hidden" value="' . esc_attr( $this->queryer->entry['id'] ) . '" name="gpeb_entry_id" />';
		$form_tag .= '<input type="hidden" value="' . esc_attr( $this->queryer->block_context['gp-entry-blocks/uuid'] ) . '" name="gpeb_block_uuid" />';

		return $form_tag;
	}

	/**
	 * Overrides the form's confirmation to use whatever is set in the Edit Form block settings.
	 *
	 * @param array $confirmation The confirmation details.
	 * @param array $form         The Form Object that the confirmation is being run for.
	 * @param array $entry        The Entry Object associated with the submission.
	 * @param bool  $ajax         If the submission was done using AJAX.
	 *
	 * @return array|string The confirmation details.
	 */
	public function edit_confirmation( $confirmation, $form, $entry, $ajax ) {
		if ( ! $this->has_submitted_edited_entry() ) {
			return $confirmation;
		}

		$edit_form_block_attrs = $this->get_submitted_edit_form_attrs();

		// Replace all form confirmations with a single confirmation so it's always selected.
		$form['confirmations'] = array(
			/**
			 * Filter the confirmation shown when editing an entry. Useful for changing behavior to a redirect or
			 * programmatically changing the message.
			 *
			 * @param array $confirmation The edit confirmation.
			 * @param array $form The current form.
			 * @param array $entry The entry being edited.
			 *
			 * @since 1.0-alpha-2.4
			 */
			gf_apply_filters( array( 'gpeb_edit_confirmation', $form['id'] ), array(
				'type'    => 'message',
				'message' => rgar( $edit_form_block_attrs, 'confirmationMessage', __( 'Edit successful.', 'gp-entry-blocks' ) ),
			), $form, $entry ),
		);

		// Prevent infinite loop
		remove_filter( 'gform_confirmation', array( $this, 'edit_confirmation' ), 15 );
		$confirmation = \GFFormDisplay::handle_confirmation( $form, $entry );
		add_filter( 'gform_confirmation', array( $this, 'edit_confirmation' ), 15, 4 );

		return $confirmation;
	}

	/**
	 * @param GF_Field $field
	 * @param array $entry
	 *
	 * @return array|mixed
	 */
	public function get_field_values_from_entry( $field, $entry ) {
		$values = array();

		foreach ( $entry as $input_id => $value ) {
			$fid = intval( $input_id );
			if ( $fid === (int) $field['id'] ) {
				$values[] = $value;
			}
		}

		return count( $values ) <= 1 ? $values[0] : $values;
	}

	/**
	 * Prepare individual values for population into the form for editing.
	 *
	 * @see \GP_Nested_Forms::prepare_entry_for_population
	 *
	 * @param $entry array Entry being prepared for population.
	 *
	 * @return array The entry prepared for population.
	 */
	public function prepare_entry_for_population( $entry ) {

		$form = GFFormsModel::get_form_meta( $entry['form_id'] );

		foreach ( $form['fields'] as $field ) {

			switch ( GFFormsModel::get_input_type( $field ) ) {

				case 'checkbox':
					$values = $this->get_field_values_from_entry( $field, $entry );

					// Ensure single value checkbox field are still stored as Arrays.
					if ( is_string( $values ) ) {
						$values = array( $values );
					}

					$entry[ $field['id'] ] = $values;
					break;

				case 'list':
					$value       = maybe_unserialize( rgar( $entry, $field->id ) );
					$list_values = array();

					if ( is_array( $value ) ) {
						foreach ( $value as $vals ) {
							if ( is_array( $vals ) ) {
								// Escape commas so the value is not split into multiple inputs.
								$vals = implode( '|', array_map( function( $value ) {
									$value = str_replace( ',', '&#44;', $value );
									return $value;
								}, $vals ) );
							}
							array_push( $list_values, $vals );
						}
						$entry[ $field->id ] = implode( ',', $list_values );
					}

					break;

				case 'multiselect':
					$value                 = GFCommon::maybe_decode_json( rgar( $entry, $field->id ) );
					$entry[ $field['id'] ] = $value;
					break;

				case 'fileupload':
					$is_multiple = $field->multipleFiles;
					$value       = rgar( $entry, $field->id );
					$return      = array();

					if ( $is_multiple ) {
						$files = json_decode( $value );
					} else {
						$files = array( $value );
					}

					if ( is_array( $files ) ) {
						foreach ( $files as $file ) {

							$path_info = pathinfo( $file );

							// Check if file has been "deleted" via form UI.
							$upload_files = json_decode( rgpost( 'gform_uploaded_files' ), ARRAY_A );
							$input_name   = "input_{$field->id}";

							if ( is_array( $upload_files ) && array_key_exists( $input_name, $upload_files ) && ! $upload_files[ $input_name ] ) {
								continue;
							}

							if ( $is_multiple ) {
								$return[] = array(
									'uploaded_filename' => $path_info['basename'],
								);
							} else {
								$return[] = $path_info['basename'];
							}
						}
					}

					// if $uploaded_files array is not set for this form at all, init as array
					if ( ! isset( GFFormsModel::$uploaded_files[ $form['id'] ] ) ) {
						GFFormsModel::$uploaded_files[ $form['id'] ] = array();
					}

					// check if this field's key has been set in the $uploaded_files array, if not add this file (otherwise, a new image may have been uploaded so don't overwrite)
					if ( ! isset( GFFormsModel::$uploaded_files[ $form['id'] ][ "input_{$field->id}" ] ) ) {
						GFFormsModel::$uploaded_files[ $form['id'] ][ "input_{$field->id}" ] = $is_multiple ? $return : reset( $return );
					}
			}

			switch ( $field->type ) {
				case 'post_category':
					$value = rgar( $entry, $field->id );

					if ( ! empty( $value ) ) {
						$categories = array();

						foreach ( explode( ',', $value ) as $cat_string ) {
							$categories[] = GFCommon::format_post_category( $cat_string, true );
						}

						$entry[ $field['id'] ] = 'multiselect' === $field->get_input_type() ? $categories : implode( ',', $categories );
					}
					break;
			}
		}

		return $entry;
	}

	/**
	 * Handle file upload fields when editing entries. Without this, existing uploads can be dropped off.
	 *
	 * @param array $form
	 * @param int $entry_id
	 *
	 * @return void
	 */
	public function handle_existing_files_submission( $form, $entry_id ) {
		global $_gf_uploaded_files;

		$entry = GFAPI::get_entry( $entry_id );
		if ( ! $entry ) {
			return;
		}

		// get all fileupload fields
		// loop through and see if the image has been:
		//  - resubmitted:         populate the existing image data into the $_gf_uploaded_files
		//  - deleted:             do nothing
		//  - new image submitted: do nothing

		if ( empty( $_gf_uploaded_files ) ) {
			$_gf_uploaded_files = array();
		}

		foreach ( $entry as $input_id => $value ) {

			if ( ! is_numeric( $input_id ) ) {
				continue;
			}

			$field      = GFFormsModel::get_field( $form, $input_id );
			$input_name = "input_{$field['id']}";

			if ( $field->get_input_type() != 'fileupload' ) {
				continue;
			}

			// Handle multi-file uploads.
			if ( $field->multipleFiles ) {

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

				$posted = wp_list_pluck( rgar( json_decode( rgpost( 'gform_uploaded_files' ), true ), $input_name ), 'uploaded_filename' );
				$count  = count( $value );

				// Remove any files that have been removed via the UI.
				for ( $i = $count - 1; $i >= 0; $i-- ) {
					$path = pathinfo( $value[ $i ] );
					if ( ! in_array( $path['basename'], $posted ) ) {
						unset( $value[ $i ] );
					}
				}
			} elseif ( self::is_prepopulated_file_upload( $form['id'], $input_name ) ) {
				// Handle single file uploads.
				$_gf_uploaded_files[ $input_name ] = $value;
			}
		}

	}

	/**
	 * Check for newly updated file. Only applies to single file uploads.
	 *
	 * @param $form_id
	 * @param $input_name
	 *
	 * @return bool
	 */
	public function is_new_file_upload( $form_id, $input_name ) {

		$file_info     = GFFormsModel::get_temp_filename( $form_id, $input_name );
		$temp_filepath = GFFormsModel::get_upload_path( $form_id ) . '/tmp/' . $file_info['temp_filename'];

		// check if file has already been uploaded by previous step
		if ( $file_info && file_exists( $temp_filepath ) ) {
			return true;
		} elseif ( ! empty( $_FILES[ $input_name ]['name'] ) ) {
			// check if file is uploaded on current step
			return true;
		}

		return false;
	}

	public function is_prepopulated_file_upload( $form_id, $input_name, $is_multiple = false ) {

		// prepopulated files will be stored in the 'gform_uploaded_files' field
		$uploaded_files = json_decode( rgpost( 'gform_uploaded_files' ), ARRAY_A );

		// file is prepopulated if it is present in the 'gform_uploaded_files' field AND is not a new file upload
		$in_uploaded_files = is_array( $uploaded_files ) && array_key_exists( $input_name, $uploaded_files ) && ! empty( $uploaded_files[ $input_name ] );
		$is_prepopulated   = $in_uploaded_files && ! $this->is_new_file_upload( $form_id, $input_name );

		return $is_prepopulated;
	}

	/**
	 * Get the ID of the entry that's being edited.
	 *
	 * @return int|null
	 */
	public function get_submitted_edit_entry_id() {
		$edit_entry_id = rgpost( 'gpeb_entry_id' );

		if ( $edit_entry_id && gp_entry_blocks()->permissions->can_current_user_edit_entry( $edit_entry_id ) ) {
			return (int) $edit_entry_id;
		}

		return null;
	}

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

		if ( ! $submitted_uuid ) {
			return null;
		}

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

	/**
	 * @return bool Whether an entry edit is being processed.
	 */
	public function has_submitted_edited_entry() {
		return ! ! $this->get_submitted_edit_entry_id();
	}

	/**
	 * Update the ID that Gravity Forms is saving to match the entry being edited so Gravity Forms doesn't create a new entry.
	 *
	 * @param $entry_id
	 *
	 * @return mixed
	 */
	public function use_submitted_edit_entry_id( $entry_id, $form ) {
		$edit_entry_id = $this->get_submitted_edit_entry_id();

		if ( $edit_entry_id ) {
			$this->handle_existing_files_submission( $form, $edit_entry_id );

			return $edit_entry_id;
		}

		return $entry_id;
	}

	/**
	 * Add Entry Edited as an available Notification event for Gravity Forms.
	 *
	 * @param array $events
	 *
	 * @return array Events with Edit event added.
	 */
	public function add_edit_notification_event( $events ) {
		$events['gpeb_edit'] = __( 'Entry Edited via Entry Blocks', 'gp-entry-blocks' );

		return $events;
	}

	/**
	 * Suppress Form Submitted notifications on edit.
	 *
	 * @param bool  $disabled     Determines if the notification will be disabled. Set to true to disable the notification.
	 * @param array $notification The notification.
	 * @param array $form         The Form Object that triggered the notification event.
	 * @param array $entry        The Entry Object that triggered the notification event.
	 * @param array $data         Array of data which can be used in the notifications via the generic {object:property} merge tag. Defaults to empty array.
	 */
	public function suppress_form_submission_notifications_on_edit( $disabled, $notification, $form, $entry, $data = array() ) {
		if ( ! $this->has_submitted_edited_entry() ) {
			return $disabled;
		}

		if ( rgar( $notification, 'event' ) === 'form_submission' ) {
			return true;
		}

		return $disabled;
	}

	/**
	 * Send notifications that are attached to the Entry Edited via Entry Blocks event.
	 *
	 * @param array $entry The current entry.
	 * @param array $form The current form.
	 */
	public function send_entry_edited_notifications( $entry, $form ) {
		if ( ! $this->has_submitted_edited_entry() ) {
			return;
		}

		$notifications = GFCommon::get_notifications_to_send( 'gpeb_edit', $form, $entry );

		GFCommon::send_notifications( wp_list_pluck( $notifications, 'id' ), $form, $entry, true, 'gpeb_edit' );
	}

	/**
	 * Re-render the Signature field when editing Nested Entries with the value of the entry being edited.
	 *
	 * This is similar to the method in Nested Forms.
	 *
	 * Ticket #35147
	 */
	public function rerender_signature_field_on_edit( $markup, $field, $value, $entry_id, $form_id ) {
		static $_processing_signature;

		if ( $field->type !== 'signature' ) {
			return $markup;
		}

		/**
		 * Prevent recursion
		 */
		if ( $_processing_signature === true ) {
			return $markup;
		}

		if ( empty( $this->queryer->entry ) ) {
			return $markup;
		}

		$entry = $this->queryer->entry;
		$form  = GFAPI::get_form( $form_id );

		$_processing_signature = true;
		$markup                = GFCommon::get_field_input( $field, rgar( $entry, $field->id ), $entry_id, $form_id, $form );
		$_processing_signature = false;

		return $markup;
	}

	public function is_editing_entry( $form_id ) {
		if (
			$this->queryer
			&& method_exists( $this->queryer, 'attach_to_current_block' )
			&& $this->queryer->is_edit_entry()
			&& $this->queryer->form_id == $form_id
		) {
			return true;
		}

		return $this->has_submitted_edited_entry();
	}

	public function maybe_disable_gpls( $should_apply_rules, $form_id, $gpls_rules ) {
		return $this->is_editing_entry( $form_id ) ? false : $should_apply_rules;
	}

	/*
	 * Check if the No Duplicates option is not validating entry against itself.
	 * Since we're editing a stored entry, it would also assume it's a duplicate.
	 */
	public function check_for_duplicate( $count, $form_id, $field, $value ) {

		if ( ! empty( $field->noDuplicates ) ) {
			$entry_id = $this->get_submitted_edit_entry_id();

			// Not found entry id, or not on the edit entry blocks page.
			if ( ! $entry_id ) {
				return $count;
			}

			$entry = GFAPI::get_entry( $entry_id );

			// If the value of the entry is the same as the stored value
			// Then we can assume it's not a duplicate, it's the same.
			if ( $value == rgar( $entry, $field->id ) ) {

				// If value submitted was not changed, then don't validate.
				$field->failed_validation = false;

				return 0;
			}
		}
		return $count;
	}
}
