<?php

namespace GP_Entry_Blocks;

use GFCommon;
use GFFormsModel;

/**
 * Traverse a list of blocks recursively and flatten the blocks. Also, add availableContext to the innerBlocks from parent block attributes/context
 * as they're flattened.
 *
 * @param array Array of blocks provided by parse_blocks()
 *
 * @return array Flattened array of blocks.
 */
function flatten_blocks( $blocks, $available_context = null ) {
	$flattened_blocks = array();

	if ( $available_context === null ) {
		$available_context = array();
	}

	foreach ( $blocks as $block ) {
		$block['availableContext'] = $available_context;

		$flattened_blocks[] = $block;

		if ( ! empty( $block['innerBlocks'] ) ) {
			$flattened_blocks     = array_merge( $flattened_blocks, flatten_blocks( $block['innerBlocks'], $block['availableContext'] + $block['attrs'] ) );
			$block['innerBlocks'] = 'FLATTENED';
		}
	}

	return $flattened_blocks;
}

/**
 * @return string The current URL of the page being viewed.
 */
function get_current_url() {
	return 'http' . ( is_ssl() ? 's' : '' ) . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
}

/**
 * @return string The current URL with all query params pertaining to Entry Blocks removed.
 */
function cleaned_current_url( $include_edit_and_view = true ) {
	$url = remove_query_arg( array(
		'delete_entry',
		'duplicate_entry',
		'_wpnonce',
		'order',
		'order_by',
		'filters',
		'filters_form_id',
	), get_current_url() );

	if ( $include_edit_and_view ) {
		$url = remove_query_arg( array(
			'edit_entry',
			'view_entry',
		), $url );
	}

	/**
	 * Filter the current URL after it has been cleaned.
	 *
	 * This filter is useful when you need to remove custom query params from the current URL.
	 *
	 * @since 1.0-alpha-2.12
	 *
	 * @param string $url                   The cleaned current URL.
	 * @param bool   $include_edit_and_view Whether the edit and view query params have been removed from the URL.
	 */
	return apply_filters( 'gpeb_cleaned_current_url', $url, $include_edit_and_view );
}

/**
 * @param array $entry Gravity Forms entry
 *
 * @return string Edit URL to edit the provided entry.
 */
function get_edit_url( $entry ) {
	return add_query_arg( array(
		'edit_entry' => $entry['id'],
	), cleaned_current_url() );
}

/**
 * @param array $entry Gravity Forms entry
 * @param string Custom link text to pass. Used by block attributes.
 *
 * @return string Hyperlink to edit to the current entry. Empty string is user is not permitted to edit the entry.
 */
function get_edit_link( $entry, $link_text = null ) {
	$edit_url = get_edit_url( $entry );

	if ( ! gp_entry_blocks()->permissions->can_current_user_edit_entry( $entry ) ) {
		return '';
	}

	if ( ! $link_text ) {
		$link_text = __( 'Edit', 'gp-entry-blocks' );
	}

	/**
	 * Filter the edit link to a specific entry.
	 *
	 * @param string $edit_link The edit link HTML.
	 * @param string $edit_url URL used in edit link.
	 * @param array $entry The entry being edited.
	 * @param string $link_text Custom link text. May not always be available, in which case it defaults to 'Edit'.
	 *
	 * @since 1.0-alpha-2.3
	 */
	return apply_filters( 'gpeb_edit_link', "<a href='{$edit_url}'>" . esc_html( $link_text ) . '</a>', $edit_url, $entry, $link_text );
}


/**
 * @param array $entry Gravity Forms entry
 *
 * @return string Delete URL to delete the provided entry.
 */
function get_delete_url( $entry ) {
	return wp_nonce_url( add_query_arg( array(
		'delete_entry' => $entry['id'],
	), cleaned_current_url( false ) ), "delete_entry_{$entry['id']}" );
}

/**
 * @param array $entry Gravity Forms entry
 * @param string Custom link text to pass. Used by block attributes.
 *
 * @return string Hyperlink with a confirmation to delete the current entry.
 */
function get_delete_link( $entry, $link_text = null ) {
	$delete_url = get_delete_url( $entry );

	if ( ! gp_entry_blocks()->permissions->can_current_user_edit_entry( $entry ) ) {
		return '';
	}

	if ( ! $link_text ) {
		$link_text = __( 'Delete', 'gp-entry-blocks' );
	}

	/**
	 * Filter the delete link to a specific entry.
	 *
	 * @param string $delete_link The delete link HTML.
	 * @param string $delete_url URL used in delete link.
	 * @param array $entry The entry being deleted.
	 * @param string $link_text Custom link text. May not always be available, in which case it defaults to 'Delete'.
	 *
	 * @since 1.0-alpha-2.3
	 */
	return apply_filters( 'gpeb_delete_link', "<a
			href='{$delete_url}'
			onclick=\"if (!confirm('" . esc_js( esc_html__( 'Are you sure you wish to delete this entry?', 'gp-entry-blocks' ) ) . "')) { return false; }\"
			style='color: red;'>" . esc_html( $link_text ) . '</a>', $delete_url, $entry, $link_text );
}

/**
 * @param array $entry Gravity Forms entry
 * @param string Custom link text to pass. Used by block attributes.
 *
 * @return string Hyperlink with a confirmation to delete the current entry.
 */
function get_duplicate_link( $entry, $link_text = null ) {
	$duplicate_url = get_duplicate_url( $entry );

	if ( ! gp_entry_blocks()->permissions->can_current_user_edit_entry( $entry ) ) {
		return '';
	}

	if ( ! $link_text ) {
		$link_text = __( 'Duplicate', 'gp-entry-blocks' );
	}

	/**
	 * Filter the duplicate link to a specific entry.
	 *
	 * @param string $duplicate_link The duplicate link HTML.
	 * @param string $duplicate_url URL used in duplicate link.
	 * @param array $entry The entry being duplicated.
	 * @param string $link_text Custom link text. May not always be available, in which case it defaults to 'Duplicate'.
	 *
	 * @since 1.0-alpha-2.10
	 */
	return apply_filters( 'gpeb_duplicate_link', "<a href='{$duplicate_url}'>" . esc_html( $link_text ) . '</a>', $duplicate_url, $entry, $link_text );
}

/**
 * @param array $entry Gravity Forms entry
 *
 * @return string URL to duplicate the entry.
 */
function get_duplicate_url( $entry ) {
	return wp_nonce_url( add_query_arg( array(
		'duplicate_entry' => $entry['id'],
	), cleaned_current_url( false ) ), "duplicate_entry_{$entry['id']}" );
}

/**
 * @param array $entry Gravity Forms entry
 *
 * @return string URL to view the entry.
 */
function get_view_url( $entry ) {
	return add_query_arg( array(
		'view_entry' => $entry['id'],
	), cleaned_current_url() );
}

/**
 * @param array $entry Gravity Forms entry
 * @param string Custom link text to pass. Used by block attributes.
 *
 * @return string Hyperlink to view the entry.
 */
function get_view_link( $entry, $link_text = null ) {
	$view_url = get_view_url( $entry );

	if ( ! $link_text ) {
		$link_text = __( 'View', 'gp-entry-blocks' );
	}

	/**
	 * Filter the view link to a specific entry.
	 *
	 * @param string $view_link The view link HTML.
	 * @param string $view_url URL used in view link.
	 * @param array $entry The entry being linked to.
	 * @param string $link_text Custom link text. May not always be available, in which case it defaults to 'View'.
	 *
	 * @since 1.0-alpha-2.3
	 */
	return apply_filters( 'gpeb_view_link', "<a href='{$view_url}'>" . esc_html( $link_text ) . '</a>', $view_url, $entry, $link_text );
}

/**
 * @param array $array
 *
 * @return boolean Whether or not the array is associative.
 */
function is_assoc_array( array $array ) {
	return ( array_values( $array ) !== $array );
}

/**
 * @param $value string
 *
 * @return array Merge tag matches
 */
function parse_merge_tags( $value ) {
	preg_match_all( '/{(.*?)(?::(.+?))?}/', $value, $matches, PREG_SET_ORDER );

	return $matches;
}

/**
 * @param string $content   The content of the block being parsed for merge tags.
 * @param array $form       Current form.
 * @param array $entry      Current entry. (optional)
 *
 * @todo Make each of these replacements a callback for a filter so replacements can be enabled/disabled as needed. Also probably should pass the block in here.
 *
 * @return mixed
 */
function replace_merge_tags( $content, $form, $entry = null ) {
	/*
	 * Replace merge tags prefixed with a protocol if the merge tag contains a protocol.
	 *
	 * This is mostly needed due to Gutenberg being inconsistent with prependHTTP() and not having available JS filters to stop it.
	 */
	preg_match_all( '/https?:\/\/({((.*?):?(.+?))})/mi', $content, $protocol_prefixed_matches, PREG_SET_ORDER );

	foreach ( $protocol_prefixed_matches as $protocol_prefixed_match ) {
		$full_match            = $protocol_prefixed_match[0];
		$merge_tag             = $protocol_prefixed_match[1];
		$merge_tag_replacement = replace_merge_tags( $merge_tag, $form, $entry );

		if ( preg_match( '/^https?:\/\//', $merge_tag_replacement ) ) {
			$content = str_replace( $full_match, $merge_tag_replacement, $content );
		}
	}

	$go_back_url = cleaned_current_url();
	$content     = str_replace( '{go_back_link}', "<p><a href='{$go_back_url}'>&larr; " . esc_html__( 'Go Back to Entries', 'gp-entry-blocks' ) . '</a></p>', $content );

	if ( $entry ) {
		$delete_link    = get_delete_link( $entry );
		$edit_link      = get_edit_link( $entry );
		$view_link      = get_view_link( $entry );
		$duplicate_link = get_duplicate_link( $entry );

		$delete_url    = get_delete_url( $entry );
		$edit_url      = get_edit_url( $entry );
		$view_url      = get_view_url( $entry );
		$duplicate_url = get_duplicate_url( $entry );

		$content = str_replace( '{delete_link}', $delete_link, $content );
		$content = str_replace( '{edit_link}', $edit_link, $content );
		$content = str_replace( '{view_link}', $view_link, $content );
		$content = str_replace( '{duplicate_link}', $duplicate_link, $content );

		$content = str_replace( '{go_back_url}', $go_back_url, $content );
		$content = str_replace( '{delete_url}', $delete_url, $content );
		$content = str_replace( '{edit_url}', $edit_url, $content );
		$content = str_replace( '{view_url}', $view_url, $content );
		$content = str_replace( '{duplicate_url}', $duplicate_url, $content );
	}

	add_filter( 'gform_pre_replace_merge_tags', 'GP_Entry_Blocks\replace_html_fields', 10, 7 );

	$content = GFCommon::replace_variables( $content, $form, $entry, false, true, false );

	/* Replace URL-escaped variables if possible. */
	preg_match_all( '/(?:%7B)[^{]*?%3A(\d+(\.\d+)?)(%3A(.*?))?(?:%7D)/mi', $content, $url_escaped_matches, PREG_SET_ORDER );

	foreach ( $url_escaped_matches as $url_escaped_match ) {
		$merge_tag             = urldecode( $url_escaped_match[0] );
		$merge_tag_replacement = GFCommon::replace_variables( $merge_tag, $form, $entry, false, true, false );

		if ( $merge_tag_replacement ) {
			$content = str_replace( $url_escaped_match[0], urlencode( $merge_tag_replacement ), $content );
		}
	}

	remove_filter( 'gform_pre_replace_merge_tags', 'GP_Entry_Blocks\replace_html_fields' );

	return $content;
}

/**
 * In the context of Entry Blocks, allow HTML fields to be replaced with their content rather than a blank string.
 *
 * @param string $text       The text which may contain merge tags to be processed.
 * @param array  $form       The current form.
 * @param array  $entry      The current entry.
 * @param bool   $url_encode Indicates if the replacement value should be URL encoded.
 * @param bool   $esc_html   Indicates if HTML found in the replacement value should be escaped.
 * @param bool   $nl2br      Indicates if newlines should be converted to html <br> tags.
 * @param string $format     Determines how the value should be formatted. HTML or text.
 *
 * @return string
 */
function replace_html_fields( $text, $form, $entry, $url_encode, $esc_html, $nl2br, $format ) {
	preg_match_all( '/{[^{]*?:(\d+(\.\d+)?)(:(.*?))?}/mi', $text, $field_variable_matches, PREG_SET_ORDER );

	foreach ( $field_variable_matches as $match ) {
		$input_id = $match[1];
		$field    = GFFormsModel::get_field( $form, $input_id );

		if ( rgar( $field, 'type' ) !== 'html' ) {
			continue;
		}

		$replacement = $field->content;

		add_filter( 'gppa_allow_all_lmts', '__return_true', 99595 );
		$replacement = GFCommon::replace_variables( $replacement, $form, $entry, $url_encode, $esc_html, $nl2br, $format );
		remove_filter( 'gppa_allow_all_lmts', '__return_true', 99595 );

		$replacement = $field->do_shortcode( $replacement );

		$text = str_replace( $match[0], $replacement, $text );
	}

	return $text;
}

/**
 * Get the quantity of a product field for the given entry.
 *
 * Extracted from GP Conditional Pricing and modified for Nested Forms and then Entry Blocks. Used when generating the display value for
 * Product fields in the Nested Entries table.
 *
 * @param \GF_Field $product_field
 * @param array $entry
 * @param array $form
 *
 * @return int
 */
function get_product_quantity( $product_field, $entry, $form ) {
	$product_value = GFFormsModel::get_lead_field_value( $entry, $product_field );
	$qty_field     = GFCommon::get_product_fields_by_type( $form, array( 'quantity' ), $product_field->id );
	$has_qty_field = ! empty( $qty_field );

	if ( $has_qty_field ) {
		$qty_field = $qty_field[0];
	}

	$is_qty_field_valid = $has_qty_field && ! GFFormsModel::is_field_hidden( $form, $qty_field, array(), $entry );

	if ( $is_qty_field_valid ) {
		$quantity = GFFormsModel::get_lead_field_value( $entry, $qty_field );
	} else {
		if ( is_array( $product_value ) && ! $product_field->disableQuantity ) {
			$quantity = rgar( $product_value, "{$product_field->id}.3" );
		} else {
			$quantity = 1;
		}
	}

	return (int) ( ! $quantity ? 0 : $quantity );
}

/**
 * Converts a field's value to a value suitable for display. This is needed so other plugins such as Populate Anything can filter how the values
 * get displayed.
 *
 * Adapted from Nested Forms.
 *
 * @param \GF_Field $field Field that is having its value displayed.
 * @param number|string $input_id Input ID to retrieve from the value.
 * @param array $entry Current entry.
 * @param array $form Current form.
 *
 * @return mixed The value ready to be displayed.
 */
function get_field_display_value( $field, $input_id, $entry, $form ) {
	$raw_value = GFFormsModel::get_lead_field_value( $entry, $field );

	/**
	 * Check if multi-input product fields (e.g. Single Product, Calculation) and have a quantity. Without this,
	 * unselected products will still return their Name and Price creating a confusing UX - and - products with
	 * a separate quantity field will not display their correct quantity.
	 */
	if ( $field->type === 'product' && is_array( $raw_value ) ) {
		$quantity = get_product_quantity( $field, $entry, $form );
		if ( empty( $quantity ) ) {
			$raw_value = array();
		} else {
			$raw_value[ "{$field->id}.3" ] = $quantity;
		}
	}

	// If the field ID is actually an input ID, we need to get the value differently.
	if ( (float) $input_id !== (float) $field->id && ! empty( $field->inputs ) ) {
		return rgar( $raw_value, $input_id );
	} else {
		$value = GFCommon::get_lead_field_display( $field, $raw_value, $entry['currency'], true );

		// Run $value through same filter GF uses before displaying on the entry detail view.
		$value = apply_filters( 'gform_entry_field_value', $value, $field, $entry, $form );

		if ( is_array( $value ) ) {
			ksort( $value );
			$value = implode( ' ', $value );
		}
	}

	/**
	 * Filter the value to be displayed with Entry Blocks.
	 *
	 * @since 1.0-alpha-1.3
	 *
	 * @param mixed     $value The field value to be displayed.
	 * @param \GF_Field $field The current field.
	 * @param array     $form  The current form.
	 * @param array     $entry The current entry.
	 */
	$value = gf_apply_filters( array( 'gpeb_display_value', $form['id'], $field->id ), $value, $field, $form, $entry );
	$value = gf_apply_filters( array( "gpeb_{$field->get_input_type()}_display_value", $form['id'] ), $value, $field, $form, $entry );

	return $value;
}

/**
 * Gets the Entries blocks that exist in a given post.
 *
 * @param \WP_Post $post Post to get the Entries blocks in.
 *
 * @return \WP_Block[] Entries blocks in the post.
 */
function get_post_blocks( $post ) {
	$blocks         = flatten_blocks( parse_blocks( $post->post_content ) );
	$entries_blocks = array();

	foreach ( $blocks as $block ) {
		if ( rgar( $block, 'blockName' ) === 'gp-entry-blocks/entries' ) {
			$entries_blocks[] = $block;
		}
	}

	return $entries_blocks;
}

/**
 * Gets the Entries blocks that exist in the current post.
 *
 * @return \WP_Block[] Entries blocks in the current post.
 */
function get_current_post_blocks() {
	global $post;

	return get_post_blocks( $post );
}

/**
 * Get a block's attributes by UUID from blocks on the current post/page.
 *
 * @return array|null
 */
function get_block_attributes_by_uuid( $uuid, $block_type = null ) {
	$parsed_block = get_parsed_block_by_uuid( $uuid, $block_type );

	if ( ! $parsed_block ) {
		return null;
	}

	return $parsed_block['attrs'] + $parsed_block['availableContext'];
}

/**
 * Get a block by UUID from blocks on the current post/page.
 *
 * @return array|null
 */
function get_parsed_block_by_uuid( $uuid, $block_type = null ) {
	global $wp_query;

	if ( ! isset( $wp_query->posts ) || ! is_array( $wp_query->posts ) ) {
		return null;
	}

	foreach ( $wp_query->posts as $post ) {
		if ( ! $post instanceof \WP_Post ) {
			continue;
		}

		$blocks = flatten_blocks( parse_blocks( $post->post_content ) );

		/* Get blocks from Reusable Blocks Extended */
		preg_match_all( '/\[reblex id=[\'"]?(\d+)[\'"]?\]/i', $post->post_content, $reblex_match, PREG_SET_ORDER );

		if ( rgars( $reblex_match, '0/1' ) ) {
			$reusable_block = get_post( $reblex_match[0][1] );

			if ( $reusable_block ) {
				$blocks = $blocks + flatten_blocks( parse_blocks( $reusable_block->post_content ) );
			}
		}

		/* Get blocks from [GRAVITY_CHOICE reblexID] shortcode. */
		preg_match_all( '/\[GRAVITY_CHOICE reblexID=[\'"]?(\d+)[\'"]?.*?\]/i', $post->post_content, $gravity_choice_reblex_match, PREG_SET_ORDER );

		if ( rgars( $gravity_choice_reblex_match, '0/1' ) ) {
			$reusable_block = get_post( $gravity_choice_reblex_match[0][1] );

			if ( $reusable_block ) {
				$blocks = $blocks + flatten_blocks( parse_blocks( $reusable_block->post_content ) );
			}
		}

		foreach ( $blocks as $block ) {
			if (
				( rgar( $block, 'blockName' ) === $block_type || $block_type === null )
				&& ( rgars( $block, 'availableContext/uuid' ) === $uuid || rgars( $block, 'attrs/uuid' ) === $uuid )
			) {
				return $block;
			}
		}
	}

	return null;
}
