<?php
/**
 * GravityView template tags API
 *
 * @package   GravityView
 * @license   GPL2+
 * @author    GravityKit <hello@gravitykit.com>
 * @link      http://www.gravitykit.com
 * @copyright Copyright 2014, Katz Web Services, Inc.
 *
 * @since 1.0.0
 */

if ( ! class_exists( 'GravityView_API', false ) ) :

	/**
	 * Legacy API class alias for backward compatibility.
	 *
	 * Keeps the historical GravityView_API class name available while delegating
	 * behavior to the PSR-4 API implementation.
	 *
	 * @since 3.0.0
	 */
	class GravityView_API extends \GravityKit\GravityView\API\API {
	}

endif; // class_exists guard.

/**
 * Returns query parameters from $_GET with reserved internal GravityView keys removed
 *
 * @uses stripslashes_deep() $_GET is passed through stripslashes_deep().
 * @uses urldecode_deep() $_GET is passed through urldecode_deep().
 *
 * Important: The return value of gv_get_query_args() is not escaped by default. Output should be
 * late-escaped with esc_url() or similar to help prevent vulnerability to cross-site scripting
 * (XSS) attacks.
 *
 * @since 2.10
 *
 * @return array
 */
function gv_get_query_args() { // phpcs:ignore Universal.Files.SeparateFunctionsFromOO.Mixed -- The legacy class alias and the template tag functions ship together for backward compatibility.

	$passed_get = isset( $_GET ) ? $_GET : array(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only access to build query args; callers are documented as responsible for late escaping.

	$passed_get = stripslashes_deep( $passed_get );
	$passed_get = urldecode_deep( $passed_get );

	if ( empty( $passed_get ) ) {
		return array();
	}

	$query_args = $passed_get;

	$reserved_args = array(
		'entry',
		'gvid',
		'status',
		'action',
		'view_id',
		'entry_id',
		'pagenum',
		'gv_updated',
	);

	// Per-View pagination keys are internal state, like the base `pagenum` above. Registering them
	// before the filter runs lets callbacks see them and un-reserve them, same as the base key.
	$scoped_pagination_keys = \GravityKit\GravityView\Pagination\PaginationKeys::scoped_keys_in( $query_args );

	$reserved_args = array_merge( $reserved_args, $scoped_pagination_keys );

	/**
	 * Modify the URL arguments that should not be used because they are internal to GravityView.
     *
	 * @since 2.10
	 * @since 3.0.0 The list includes any `pagenum_{View ID}` per-View pagination keys present in the request.
	 * @param array $reserved_args Array of URL query keys that should not be used except internally.
	 */
	$reserved_args = apply_filters( 'gravityview/api/reserved_query_args', $reserved_args );

	foreach ( $reserved_args as $reserved_arg ) {
		unset( $query_args[ $reserved_arg ] );
	}

	return $query_args;
}


// inside loop functions.

/**
 * Returns the label for a field, generated by GravityView_API::field_label().
 *
 * @deprecated 3.0.0 \GV\Field::get_label()
 *
 * @param array      $field GravityView field array.
 * @param array|null $entry Gravity Forms entry array.
 */
function gv_label( $field, $entry = null ) {
	return GravityView_API::field_label( $field, $entry );
}

/**
 * Returns the CSS class for a field, generated by GravityView_API::field_class().
 *
 * @param array      $field GravityView field array.
 * @param array|null $form  Gravity Forms form array.
 * @param array      $entry Gravity Forms entry array.
 */
function gv_class( $field, $form = null, $entry = array() ) {
	return GravityView_API::field_class( $field, $form, $entry );
}

/**
 * Generate a CSS class to be added to the wrapper <div> of a View
 *
 * @since 1.5.4
 * @since 1.16 Added $echo parameter.
 * @since 2.0 Added $context parameter.
 *
 * @param string               $passed_css_class Default: `gv-container gv-container-{view id}`. If View is hidden until search, adds ` hidden`.
 * @param boolean              $echo Whether to echo the output. Default: true.
 * @param \GV\Template_Context $context The template context.
 *
 * @return string CSS class, sanitized by gravityview_sanitize_html_class()
 */
function gv_container_class( $passed_css_class = '', $echo = true, $context = null ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.echoFound -- Renaming the parameter could break PHP 8 named-argument callers of this template tag.
	$view = null;

	if ( $context instanceof \GV\Template_Context ) {
		$hide          = false;
		$total_entries = 0;
		$view_id       = 0;
		if ( $context->view ) {
			$view    = $context->view;
			$view_id = $context->view->ID;
			if ( $context->view->settings->get( 'hide_until_searched' ) ) {
				$hide = ( empty( $context->entry ) && ! $context->request->is_search( $context->view ) );
			}
		}
		if ( $context->entries ) {
			$total_entries = $context->entries->total();
		} elseif ( $context->entry ) {
			$total_entries = 1;
		}
	} else {
		/** @deprecated 3.0.0 execution path */
		$view_id       = GravityView_View::getInstance()->getViewId();
		$hide          = GravityView_View::getInstance()->isHideUntilSearched();
		$total_entries = GravityView_View::getInstance()->getTotalEntries();

		// A theme template override routes every View through this branch, so
		// the View has to be resolved here too or its settings are all lost.
		$view = $view_id ? gravityview()->views->get( $view_id ) : null;
	}

	$passed_css_class = trim( $passed_css_class );

	$default_css_class = ! empty( $view_id ) ? sprintf( 'gv-container gv-container-%d', $view_id ) : 'gv-container';

	// `gv-themed` is stamped on the View container here, and on the
	// per-View `gv-template-*` wrapper by
	// `Frontend::add_theme_class_to_wrapper()`, so both same-element
	// compounds (`.gv-table-container.gv-themed`) and descendant rules
	// (`.gv-themed .gv-widget-search`) resolve per-View. Added BEFORE the
	// `gravityview/render/container/class` filter fires so sites that
	// strip `gv-container` still keep the theme marker.
	//
	// Two paths qualify a View for the marker:
	// 1. The View's `theme` setting is a non-legacy theme (normal frontend).
	// 2. The `gk/gravityview/theme/force-active` filter returns
	// true: surfaces that need the theme stack to apply during
	// transient state hook this so token / grid / Custom CSS edits
	// reflect before the customer hits Save.
	$theme          = $view
		? (string) $view->settings->get( 'theme', 'legacy' )
		: 'legacy';
	$is_themed_view = 'legacy' !== $theme;

	/**
	 * Force the theme stack onto a View regardless of its saved
	 * `theme` setting.
	 *
	 * Returning true stamps `.gv-themed` on the View container AND
	 * enqueues the theme stylesheet (the `Frontend.php` enqueue gate
	 * reads the same filter). Returning false defers to the saved
	 * setting (the default).
	 *
	 * @since 3.0.0
	 *
	 * @param bool                       $force_active Default false.
	 * @param \GV\Template_Context|null  $context      Render context, or null.
	 */
	$force_active = (bool) apply_filters(
		'gk/gravityview/theme/force-active',
		false,
		$context instanceof \GV\Template_Context ? $context : null
	);

	if ( $is_themed_view || $force_active ) {
		$theme_slug         = 'legacy' !== $theme ? $theme : 'vantage';
		$default_css_class .= ' gv-themed gv-theme-' . $theme_slug;
	}

	if ( 0 === $total_entries ) {
		$default_css_class .= ' gv-container-no-results';

		if (
			$view
			&& ! gravityview()->request->is_search( $view )
			&& 3 === (int) $view->settings->get( 'no_entries_options', '0' )
		) {
			$hide = true;
		}
	}

	if ( $hide ) {
		$default_css_class .= ' gv-hidden';
	}

	if ( $view ) {
		$default_css_class .= ' ' . $view->settings->get( 'class', '' );
	}

	$css_class = trim( $passed_css_class . ' ' . $default_css_class );

	/**
	 * Modify the CSS class to be added to the wrapper div of a View.
     *
	 * @since 1.5.4
	 * @param string $css_class Default: `gv-container gv-container-{view id}`. If View is hidden until search, adds ` hidden`. If the View has no results, adds `gv-container-no-results`
	 * @since 2.0
	 * @param \GV\Template_Context $context The context.
	 */
	$css_class = apply_filters( 'gravityview/render/container/class', $css_class, $context );

	$css_class = gravityview_sanitize_html_class( $css_class );

	if ( $echo ) {
		echo $css_class; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Sanitized with gravityview_sanitize_html_class() immediately above.
	}

	return $css_class;
}

/**
 * Checks whether a non-legacy theme applies to a View.
 *
 * Returns true when the View's `theme` setting is a non-legacy theme or when
 * the `gk/gravityview/theme/force-active` filter forces the stack
 * on. This is the supported detection API for themes and integrations that
 * need to branch on the themed front-end, and it matches the gate
 * `gv_container_class()` uses to stamp the `gv-themed` marker class on the
 * View container.
 *
 * When called without a View on a page that renders multiple Views, returns
 * true if any of those Views qualifies.
 *
 * @api
 * @since 3.0.0
 *
 * @param \GV\View|int|string|null $view The View, a View ID, or null to detect
 *                                       the current View from context.
 *
 * @return bool True when a non-legacy theme applies.
 */
function gravityview_is_themed( $view = null ) {
	if ( ! $view instanceof \GV\View ) {
		$view = gravityview()->views->get( $view );
	}

	$is_themed = false;

	if ( $view instanceof \GV\View_Collection ) {
		foreach ( $view->all() as $single_view ) {
			if ( $single_view instanceof \GV\View && 'legacy' !== (string) $single_view->settings->get( 'theme', 'legacy' ) ) {
				$is_themed = true;
				break;
			}
		}

		$view = null;
	} elseif ( $view instanceof \GV\View ) {
		$is_themed = 'legacy' !== (string) $view->settings->get( 'theme', 'legacy' );
	} else {
		$view = null;
	}

	if ( $is_themed ) {
		return true;
	}

	/** This filter is documented in src/Frontend/Frontend.php */
	return (bool) apply_filters( 'gk/gravityview/theme/force-active', false, $view );
}

/**
 * Returns the field value for an entry, generated by GravityView_API::field_value().
 *
 * @deprecated 3.0.0 \GV\Field_Template::render()
 *
 * @param array $entry Gravity Forms entry array.
 * @param array $field GravityView field array.
 */
function gv_value( $entry, $field ) {

	$value = GravityView_API::field_value( $entry, $field );

	if ( '' === $value ) {
		/**
		 * What to display when a field is empty.
		 *
		 * @param string $value (empty string)
		 */
		$value = apply_filters( 'gravityview_empty_value', '' );
	}

	return $value;
}

/**
 * Returns the URL to the Multiple Entries (directory) context of a View.
 *
 * @see GravityView_API::directory_link()
 *
 * @param int|null                  $post           Post ID to link to. Defaults to the current post.
 * @param bool                      $add_pagination Whether to include pagination in the URL.
 * @param \GV\Template_Context|null $context        The template context.
 */
function gv_directory_link( $post = null, $add_pagination = true, $context = null ) {
	return GravityView_API::directory_link( $post, $add_pagination, $context );
}

/**
 * Returns the URL to the Single Entry context of an entry.
 *
 * @see GravityView_API::entry_link()
 *
 * @param array    $entry   Gravity Forms entry array.
 * @param int|null $post_id Post ID to link to. Defaults to the current post.
 */
function gv_entry_link( $entry, $post_id = null ) {
	return GravityView_API::entry_link( $entry, $post_id );
}

/**
 * Returns the "no results" text for a View.
 *
 * @see GravityView_API::no_results()
 *
 * @param bool                      $wpautop Whether to apply wpautop() to the output.
 * @param \GV\Template_Context|null $context The template context.
 */
function gv_no_results( $wpautop = true, $context = null ) {
	return GravityView_API::no_results( $wpautop, $context );
}

/**
 * Generate HTML for the back link from single entry view
 *
 * @since 1.0.1
 * @since 2.0
 * @param \GV\Template_Context $context The context this link is being displayed from.
 * @return string|null      If no GV post exists, null. Otherwise, HTML string of back link.
 */
function gravityview_back_link( $context = null ) {

	$href = gv_directory_link( null, true, $context );

	/**
	 * Modify the back link URL.
     *
	 * @since 1.17.5
	 * @see gv_directory_link() Generated the original back link
	 * @param string $href Existing label URL
	 */
	$href = GravityView_Deprecated_Hook_Notices::apply_filters( 'gravityview_go_back_url', [ $href ], '2.55', 'gravityview/template/links/back/url' );

	/**
	 * Modify the back link URL.
     *
	 * @since 2.0
	 * @see gv_directory_link() Generated the original back link
	 * @param string $href Existing label URL
	 * @param \GV\Template_Context The context.
	 */
	$href = apply_filters( 'gravityview/template/links/back/url', $href, $context );

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

	if ( $context instanceof \GV\Template_Context ) {
		// Legacy templates hand-build a context whose `view` is the template object, not a View.
		$view_id    = $context->view instanceof \GV\View ? $context->view->ID : gravityview_get_view_id();
		$view_label = $context->template->get_back_label();
	} else {
		/** @deprecated 3.0.0 path */
		$gravityview_view = GravityView_View::getInstance();
		$view_id          = $gravityview_view->getViewId();
		$view_label       = $gravityview_view->getBackLinkLabel() ? $gravityview_view->getBackLinkLabel() : false;
	}

	/** Default */
	$label = $view_label ? $view_label : __( '&larr; Go back', 'gk-gravityview' );

	/**
	 * Modify the back link text.
     *
	 * @since 1.0.9
	 * @param string $label Existing label text
	 */
	$label = GravityView_Deprecated_Hook_Notices::apply_filters( 'gravityview_go_back_label', [ $label ], '2.55', 'gravityview/template/links/back/label' );

	/**
	 * Modify the back link text.
     *
	 * @since 2.0
	 * @see gv_directory_link() Generated the original back link
	 * @param string $label Existing label text
	 * @param \GV\Template_Context The context.
	 */
	$label = apply_filters( 'gravityview/template/links/back/label', $label, $context );

	/**
	 * Modify the attributes used on the back link anchor tag.
     *
	 * @since 2.1
	 * @param array $atts Original attributes, default: [ data-viewid => $view_id ]
	 * @param \GV\Template_Context The context.
	 */
	$atts = apply_filters( 'gravityview/template/links/back/atts', array( 'data-viewid' => $view_id ), $context );

	$link = gravityview_get_link( $href, esc_html( $label ), $atts );

	return $link;
}

/**
 * Handle getting values for complex Gravity Forms fields
 *
 * If the field is complex, like a product, the field ID, for example, 11, won't exist. Instead,
 * it will be 11.1, 11.2, and 11.3. This handles being passed 11 and 11.2 with the same function.
 *
 * @since 1.0.4
 * @param  array  $entry    GF entry array.
 * @param  string $field_id The field ID, which may include an input part (for example, 11 or 11.2).
 * @param  string $display_value The value generated by Gravity Forms.
 * @return string                Value
 */
function gravityview_get_field_value( $entry, $field_id, $display_value ) {

	if ( floatval( $field_id ) === floor( floatval( $field_id ) ) ) {

		// For the complete field value as generated by Gravity Forms.
		return $display_value;

	} else {

		// For one part of the address (City, ZIP, etc.).
		return isset( $entry[ $field_id ] ) ? $entry[ $field_id ] : '';

	}
}

/**
 * Take a passed CSV of terms and generate a linked list of terms
 *
 * Gravity Forms passes categories as "Name:ID" so we handle that using the ID, which
 * is more accurate than checking the name, which is more likely to change.
 *
 * @param  string $value    Existing value.
 * @param  string $taxonomy Type of term (`post_tag` or `category`).
 * @return string                CSV of linked terms
 */
function gravityview_convert_value_to_term_list( $value, $taxonomy = 'post_tag' ) {

	$output = array();

	if ( is_array( $value ) ) {
		$terms = array_filter( array_values( $value ), 'strlen' );
	} else {
		$terms = explode( ', ', $value );
	}

	foreach ( $terms as $term_name ) {

		// If we're processing a category.
		if ( 'category' === $taxonomy ) {

			// Use rgexplode to prevent errors if : doesn't exist.
			list( $term_name, $term_id ) = rgexplode( ':', $term_name, 2 );

			// The explode was succesful; we have the category ID.
			if ( ! empty( $term_id ) ) {
				$term = get_term_by( 'id', $term_id, $taxonomy );
			} else {
				// We have to fall back to the name.
				$term = get_term_by( 'name', $term_name, $taxonomy );
			}
		} else {
			// Use the name of the tag to get the full term information.
			$term = get_term_by( 'name', $term_name, $taxonomy );
		}

		// There's still a tag/category here.
		if ( $term ) {

			$term_link = get_term_link( $term, $taxonomy );

			// If there was an error, continue to the next term.
			if ( is_wp_error( $term_link ) ) {
			    continue;
			}

			$output[] = gravityview_get_link( $term_link, esc_html( $term->name ) );
		}
	}

	return implode( ', ', $output );
}

/**
 * Get the links for post_tags and post_category output based on post ID
 *
 * @param  int     $post_id  The ID of the post.
 * @param  boolean $link     Whether to add links to the terms.
 * @param  string  $taxonomy Taxonomy of term to fetch.
 * @return string                String with terms
 */
function gravityview_get_the_term_list( $post_id, $link = true, $taxonomy = 'post_tag' ) {

	$output = get_the_term_list( $post_id, $taxonomy, null, ', ' );

	if ( empty( $link ) ) {
		return wp_strip_all_tags( $output );
	}

	return $output;
}


/**
 * Get all views processed so far for the current page load
 *
 * @see  GravityView_View_Data::add_view()
 * @return array Array of View data, each View data with `id`, `view_id`, `form_id`, `template_id`, `atts`, `fields`, `widgets`, `form` keys.
 */
function gravityview_get_current_views() {

	$fe = GravityView_frontend::getInstance();

	// Solve problem when loading content via admin-ajax.php.
	if ( ! $fe->getGvOutputData() ) {

		\gravityview()->log->debug( 'gv_output_data not defined; parsing content.' );

		$fe->parse_content();
	}

	// Make 100% sure that we're dealing with a properly called situation.
	if ( ! is_a( $fe->getGvOutputData(), 'GravityView_View_Data' ) ) {

		\gravityview()->log->debug( 'gv_output_data not an object or get_view not callable.', array( 'data' => $fe->getGvOutputData() ) );

		return array();
	}

	return $fe->getGvOutputData()->get_views();
}

/**
 * Get data for a specific view
 *
 * @deprecated 3.0.0 \GV\View API instead
 * @since 2.5
 *
 * @see  GravityView_View_Data::get_view()
 *
 * @param int $view_id The View ID. Default: 0 (uses the current View).
 *
 * @return array View data with `id`, `view_id`, `form_id`, `template_id`, `atts`, `fields`, `widgets`, `form` keys.
 */
function gravityview_get_current_view_data( $view_id = 0 ) {
	if ( $view_id ) {
		$view = \GV\View::by_id( $view_id );

		if ( $view ) {
			return $view; // Implements ArrayAccess.
		}

		return array();
	}

	$fe = GravityView_frontend::getInstance();

	// If not set, grab the current view ID.
	if ( empty( $view_id ) ) {
		$view_id = $fe->get_context_view_id();
	}

	if ( ! $fe->getGvOutputData() ) {
		return array(); }

	return $fe->getGvOutputData()->get_view( $view_id );
}

// Templates' hooks.

/**
 * Builds a Template_Context for template-tag calls that received none.
 *
 * Deprecated template overrides call gravityview_before()/header()/footer()/after() without
 * arguments; the modern template actions pass a \GV\Template_Context, so one is synthesized from
 * the View being rendered. The request is included because listeners call $context->request methods.
 *
 * @since 3.1.0
 *
 * @return \GV\Template_Context
 */
function _gravityview_template_tag_context() {
	// An object, not an array: it becomes $context->template, and listeners reach through it
	// (for example GravityMath reads $context->template->view in its footer callback).
	return \GV\Template_Context::from_template(
		(object) [
			'view'    => \GV\View::by_id( gravityview_get_view_id() ),
			'request' => gravityview()->request,
		]
	);
}

/**
 * Fires the `gravityview/template/before` action and the deprecated `gravityview_before` action.
 *
 * Accepts an optional \GV\Template_Context object as the first argument, passed by template files.
 */
function gravityview_before() {
	/**
	 * Append content to the view.
     *
	 * @param object $gravityview The $gravityview object available in templates.
	 */
	$args = func_get_args();

	if ( count( $args ) ) {
		$gravityview = reset( $args );
		if ( $gravityview instanceof \GV\Template_Context ) {
			/**
			 * Prepend content to the View.
			 *
			 * @param \GV\Template_Context $gravityview The $gravityview object available in templates.
			 */
			do_action( 'gravityview/template/before', $gravityview );

			return GravityView_Deprecated_Hook_Notices::do_action( 'gravityview_before', [ $gravityview->view->ID ], '2.55', 'gravityview/template/before' );
		}
	}

	$view_id = gravityview_get_view_id();

	if ( $view_id ) {
		/* Documented elsewhere; fired here too so modern listeners run under deprecated template overrides. */
		do_action( 'gravityview/template/before', _gravityview_template_tag_context() );
	}

	/**
	 * Prepend content to the View container div.
     *
	 * @param int $view_id The ID of the View being displayed
	 */
	GravityView_Deprecated_Hook_Notices::do_action( 'gravityview_before', [ $view_id ], '2.55', 'gravityview/template/before' );
}

/**
 * Fires the `gravityview/template/header` action and the deprecated `gravityview_header` action.
 *
 * Accepts an optional \GV\Template_Context object as the first argument, passed by template files.
 */
function gravityview_header() {
	/**
	 * Append content to the view.
     *
	 * @param object $gravityview The $gravityview object available in templates.
	 */
	$args = func_get_args();

	if ( count( $args ) ) {
		$gravityview = reset( $args );
		if ( $gravityview instanceof \GV\Template_Context ) {
			/**
			 * Prepend content to the View container div.
			 *
			 * @param \GV\Template_Context $gravityview The $gravityview object available in templates.
			 */
			do_action( 'gravityview/template/header', $gravityview );

			return GravityView_Deprecated_Hook_Notices::do_action( 'gravityview_header', [ $gravityview->view->ID ], '2.55', 'gravityview/template/header' );
		}
	}

	$view_id = gravityview_get_view_id();

	if ( $view_id ) {
		/* Documented elsewhere; fired here too so modern listeners run under deprecated template overrides. */
		do_action( 'gravityview/template/header', _gravityview_template_tag_context() );
	}

	/**
	 * Prepend content to the View container div.
     *
	 * @param int $view_id The ID of the View being displayed
	 */
	GravityView_Deprecated_Hook_Notices::do_action( 'gravityview_header', [ $view_id ], '2.55', 'gravityview/template/header' );
}

/**
 * Fires the `gravityview/template/footer` action and the deprecated `gravityview_footer` action.
 *
 * Accepts an optional \GV\Template_Context object as the first argument, passed by template files.
 */
function gravityview_footer() {
	/**
	 * Append content to the view.
     *
	 * @param object $gravityview The $gravityview object available in templates.
	 */
	$args = func_get_args();

	if ( count( $args ) ) {
		$gravityview = reset( $args );
		if ( $gravityview instanceof \GV\Template_Context ) {
			/**
			 * Prepend outside the View container div.
			 *
			 * @param \GV\Template_Context $gravityview The $gravityview object available in templates.
			 */
			do_action( 'gravityview/template/footer', $gravityview );

			/* Documented elsewhere. */
			return GravityView_Deprecated_Hook_Notices::do_action( 'gravityview_footer', [ $gravityview->view->ID ], '2.55', 'gravityview/template/footer' );
		}
	}

	$view_id = gravityview_get_view_id();

	if ( $view_id ) {
		/* Documented elsewhere; fired here too so modern listeners run under deprecated template overrides. */
		do_action( 'gravityview/template/footer', _gravityview_template_tag_context() );
	}

	/**
	 * Display content after a View. Used to render footer widget areas. Rendered outside the View container div.
     *
	 * @param int $view_id The ID of the View being displayed
	 */
	GravityView_Deprecated_Hook_Notices::do_action( 'gravityview_footer', [ $view_id ], '2.55', 'gravityview/template/footer' );
}

/**
 * Fires the `gravityview/template/after` action and the deprecated `gravityview_after` action.
 *
 * Accepts an optional \GV\Template_Context object as the first argument, passed by template files.
 */
function gravityview_after() {
	$args = func_get_args();

	if ( count( $args ) ) {
		$gravityview = reset( $args );
		if ( $gravityview instanceof \GV\Template_Context ) {
			/**
			 * Append content to the View.
			 *
			 * @param \GV\Template_Context $gravityview The $gravityview object available in templates.
			 * @since 2.0
			 */
			do_action( 'gravityview/template/after', $gravityview );

			/* Documented elsewhere. */
			GravityView_Deprecated_Hook_Notices::do_action( 'gravityview_after', [ $gravityview->view->ID ], '2.55', 'gravityview/template/after' );

			return;
		}
	}

	$view_id = gravityview_get_view_id();

	if ( $view_id ) {
		/* Documented elsewhere; fired here too so modern listeners run under deprecated template overrides. */
		do_action( 'gravityview/template/after', _gravityview_template_tag_context() );
	}

	/**
	 * Append content to the View container div.
     *
	 * @param int $view_id The ID of the View being displayed
	 */
	GravityView_Deprecated_Hook_Notices::do_action( 'gravityview_after', [ $view_id ], '2.55', 'gravityview/template/after' );
}

/**
 * Get the current View ID being rendered
 *
 * Prefers the rendering stack, which tracks the View actually being rendered
 * (including embedded and nested Views on multi-View pages); falls back to
 * the legacy GravityView_View singleton for code paths that predate the
 * renderers.
 *
 * @global GravityView_View $gravityview_view
 *
 * @return int View ID, if exists. `0` if `GravityView_View` doesn't exist, like in the admin, or no View is set.
 */
function gravityview_get_view_id() {

	$rendering_view_id = \GravityKit\GravityView\View\View::get_current_rendering();

	if ( $rendering_view_id ) {
		return (int) $rendering_view_id;
	}

	if ( ! class_exists( \GravityKit\GravityView\View\ViewLegacy::class ) ) {
		return 0;
	}

	return GravityView_View::getInstance()->getViewId();
}

/**
 * Returns the current GravityView context, or empty string if not GravityView
 *
 * - Returns empty string on GravityView archive pages
 * - Returns empty string on archive pages containing embedded Views
 * - Returns empty string for embedded Views, not 'directory'
 * - Returns empty string for embedded entries (oEmbed or [gventry]), not 'single'
 * - Returns 'single' when viewing a [gravityview] shortcode-embedded single entry
 *
 * @global GravityView_View $gravityview_view
 * @deprecated 3.0.0 2.0.6.2 Use `gravityview()->request`
 * @return string View context "directory", "single", "edit", or empty string if not GravityView
 */
function gravityview_get_context() {
	global $wp_query;

	if ( isset( $wp_query ) && $wp_query->post_count > 1 ) {
		return '';
	}

	if ( gravityview()->request->is_edit_entry() ) {
		return 'edit';
	} elseif ( gravityview()->request->is_entry() ) {
		return 'single';
	} elseif ( gravityview()->request->is_view( false ) ) {
		return 'directory';
	} elseif ( gravityview()->views->get() ) {
		return 'directory';
	}

	return '';
}


/**
 * Return an array of files prepared for output. Wrapper for GravityView_Field_FileUpload::get_files_array()
 *
 * Processes files by file type and generates unique output for each.
 *
 * Returns array for each file, with the following keys:
 *
 * `file_path` => The file path of the file, with a line break
 * `html` => The file output HTML formatted
 *
 * @see GravityView_Field_FileUpload::get_files_array()
 *
 * @since  1.2
 * @param  string               $value    Field value passed by Gravity Forms. String of file URL, or serialized string of file URL array.
 * @param  string               $gv_class Field class to add to the output HTML.
 * @since  2.0
 * @param  \GV\Template_Context $context The context.
 * @return array           Array of file output, with `file_path` and `html` keys (see comments above)
 */
function gravityview_get_files_array( $value, $gv_class = '', $context = null ) {
	/** @define "GRAVITYVIEW_DIR" "../" */

	if ( is_null( $context ) ) {
		_doing_it_wrong( __FUNCTION__, '2.0', 'Please pass an \GV\Template_Context object as the 3rd parameter' );
	}

	return GravityView_Field_FileUpload::get_files_array( $value, $gv_class, $context );
}

/**
 * Generate a mapping link from an address
 *
 * The address should be plain text with new line (`\n`) or `<br />` line breaks separating sections
 *
 * @since 2.14.1 Added $atts parameter
 *
 * @todo use GF's field get_export_value() instead
 *
 * @see https://docs.gravitykit.com/article/59-modify-the-map-it-address-link Read how to modify the link
 * @param  string $address Address.
 * @param  array  $atts    Attributes to add to the anchor tag. Default: empty array.
 * @return string          URL of link to map of address
 */
function gravityview_get_map_link( $address, $atts = array() ) {
	$address_qs = str_replace( array( '<br />', "\n" ), ' ', $address ); // Replace \n with spaces.
	$address_qs = urlencode( $address_qs ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode -- Switching to rawurlencode() would change the generated map URLs.

	$url = "https://maps.google.com/maps?q={$address_qs}";

	$link_text = esc_html__( 'Map It', 'gk-gravityview' );

	$atts = array_merge(
		array(
			'class' => 'map-it-link',
		),
		$atts
	);

	$link = gravityview_get_link( $url, $link_text, $atts );

	/**
	 * Modify the map link generated. You can use a different mapping service, for example.
     *
	 * @param  string $link Map link
	 * @param string $address Address to generate link for
	 * @param string $url URL generated by the function
	 */
	$link = apply_filters( 'gravityview_map_link', $link, $address, $url );

	return $link;
}


/**
 * Output field based on a certain html markup
 *
 * Markup - string to be used on a sprintf statement.
 *      Use:
 *         {{label}} - field label
 *         {{value}} - entry field value
 *         {{class}} - field class
 *
 *   wpautop - true will filter the value using wpautop function
 *
 * @since  1.1.5
 * @param  array                $passed_args Associative array with field data. `field` and `form` are required.
 * @since  2.0
 * @param  \GV\Template_Context $context The template context.
 * @return string Field output. If empty value and hide empty is true, return empty.
 */
function gravityview_field_output( $passed_args, $context = null ) {
	$defaults = array(
		'entry'        => null,
		'field'        => null,
		'form'         => null,
		'hide_empty'   => true,
		'markup'       => '<div id="{{ field_id }}" class="{{ class }}"{{ style_attr }}>{{ label }}{{ value }}</div>',
		'label_markup' => '',
		'wpautop'      => false,
		'zone_id'      => null,
	);

	$args = wp_parse_args( $passed_args, $defaults );

	/**
	 * Modify the args before generation begins.
     *
	 * @since 1.7
	 * @param array $args Associative array; `field` and `form` is required.
	 * @param array $passed_args Original associative array with field data. `field` and `form` are required.
	 * @since 2.0
	 * @param \GV\Template_Context $context The context.
	 * @deprecated 3.0.0
	 */
	$args = GravityView_Deprecated_Hook_Notices::apply_filters( 'gravityview/field_output/args', [ $args, $passed_args, $context ], '2.55', 'gravityview/template/field_output/context', 'To modify field values, use gravityview/field_output/context/{$tag}. To modify the final HTML, use gravityview/field_output/html.' );

	/**
	 * Modify the context before generation begins.
     *
	 * @since 2.0
	 * @param \GV\Template_Context $context The context.
	 * @param array $args The sanitized arguments, these should not be trusted any longer.
	 * @param array $passed_args The passed arguments, these should not be trusted any longer.
	 */
	$context = apply_filters( 'gravityview/template/field_output/context', $context, $args, $passed_args );

	if ( $context instanceof \GV\Template_Context ) {
		if ( ! $context->field || ! $context->view || ! $context->view->form ) {
			\gravityview()->log->error( 'Field or form are empty.', array( 'data' => array( $context->field, $context->view->form ) ) );
			return '';
		}
	} elseif ( empty( $args['field'] ) || empty( $args['form'] ) ) {
		// @deprecated 3.0.0 path; `field` and `form` args are required.
		\gravityview()->log->error( 'Field or form are empty.', array( 'data' => $args ) );
		return '';
	}

	if ( $context instanceof \GV\Template_Context ) {
		$entry = $args['entry'] ? $args['entry'] : ( $context->entry ? $context->entry->as_entry() : array() );
		$field = $args['field'] ? $args['field'] : ( $context->field ? $context->field->as_configuration() : array() );
		$form  = $args['form'] ? $args['form'] : ( $context->view->form ? $context->view->form->form : array() );
	} else {
		// @deprecated 3.0.0
		$entry = empty( $args['entry'] ) ? array() : $args['entry'];
		$field = $args['field'];
		$form  = $args['form'];
	}

	/**
	 * Create the content variables for replacing.
     *
	 * @since 1.11
	 */
	$placeholders = [
		'value'                  => '',
		'width'                  => '',
		'width:style'            => '',
		'label'                  => '',
		'label_value'            => '',
		'label_value:esc_attr'   => '',
		'label_value:data-label' => '',
		'class'                  => '',
		'field_id'               => '',
		'rowspan'                => $args['rowspan'] ?? null,
		'row'                    => $args['row'] ?? 0,
	];

	if ( $context instanceof \GV\Template_Context && array_key_exists( 'value', $args ) ) {
		$placeholders['value'] = \GV\Utils::get( $args, 'value', '' );
	} else {
		// Falls through here when no context is set (deprecated path) or when a context is
		// provided without a pre-computed value (legacy renderZone path) — preserves the
		// original timing of `gv_value()` relative to the field-output filters.
		$placeholders['value'] = gv_value( $entry, $field );
	}

	// If the value is empty and we're hiding empty, return empty.
	if ( '' === $placeholders['value'] && ! empty( $args['hide_empty'] ) ) {
		return '';
	}

	if ( '' !== $placeholders['value'] && ! empty( $args['wpautop'] ) && 'gravityview_view' !== ( $field['id'] ?? '' ) ) {
		$placeholders['value'] = wpautop( $placeholders['value'] );
	}

	// Get width setting, if exists.
	$placeholders['width'] = GravityView_API::field_width( $field );

	// If replacing with CSS inline formatting, let's do it.
	$placeholders['width:style'] = (string) GravityView_API::field_width( $field, 'width:' . $placeholders['width'] . '%;' );

	// Grab the Class using `gv_class`.
	$placeholders['class']    = gv_class( $field, $form, $entry );
	$placeholders['field_id'] = GravityView_API::field_html_attr_id( $field, $form, $entry );

	// Per-field inline style overrides were removed; the {{ style_attr }}
	// template token always resolves to an empty attribute.
	$placeholders['style_attr'] = '';

	if ( $context instanceof \GV\Template_Context && array_key_exists( 'label', $args ) ) {
		$placeholders['label_value'] = \GV\Utils::get( $args, 'label', '' );
	} else {
		// Falls through here when no context is set (deprecated path) or when a context is
		// provided without a pre-computed label (legacy renderZone path).
		$placeholders['label_value'] = gv_label( $field, $entry );
	}

	$placeholders['label_value:data-label'] = trim( esc_attr( strip_tags( str_replace( '>&nbsp;', '>', $placeholders['label_value'] ) ) ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags -- wp_strip_all_tags() also removes script and style tag contents, which would change the data-label output.
	$placeholders['label_value:esc_attr']   = esc_attr( $placeholders['label_value'] );

	if ( empty( $placeholders['label'] ) && ! empty( $placeholders['label_value'] ) ) {
		$placeholders['label'] = '<span class="gv-field-label">{{ label_value }}</span>';
	}

	/**
	 * Allow Pre filtering of the HTML.
     *
	 * @since 1.11
	 * @param string $markup The HTML for the markup
	 * @param array $args All args for the field output
	 * @since 2.0
	 * @param \GV\Template_Context $context The context.
	 */
	$html = apply_filters( 'gravityview/field_output/pre_html', $args['markup'], $args, $context );

	$html = gravityview_field_output_guard_heading_wrapper( $html, $placeholders, $args, $context );

	if ( '' === $html ) {
		return '';
	}

	/**
	 * Modify the opening tags for the template content placeholders.
     *
	 * @since 1.11
	 * @param string $open_tag Open tag for template content placeholders. Default: `{{`
	 * @since 2.0
	 * @param \GV\Template_Context $context The context.
	 */
	$open_tag = apply_filters( 'gravityview/field_output/open_tag', '{{', $args, $context );

	/**
	 * Modify the closing tags for the template content placeholders.
     *
	 * @since 1.11
	 * @param string $close_tag Close tag for template content placeholders. Default: `}}`
	 * @since 2.0
	 * @param \GV\Template_Context $context The context.
	 */
	$close_tag = apply_filters( 'gravityview/field_output/close_tag', '}}', $args, $context );

	/**
	 * Loop through each of the tags to replace and replace both `{{tag}}` and `{{ tag }}` with the values
     *
	 * @since 1.11
	 */
	foreach ( $placeholders as $tag => $value ) {

		// If the tag doesn't exist just skip it.
		if ( false === strpos( $html, $open_tag . $tag . $close_tag ) && false === strpos( $html, $open_tag . ' ' . $tag . ' ' . $close_tag ) ) {
			continue;
		}

		// Array to search.
		$search = array(
			$open_tag . $tag . $close_tag,
			$open_tag . ' ' . $tag . ' ' . $close_tag,
		);

		/**
		 * `gravityview/field_output/context/{$tag}` Allow users to filter content on context
		 *
		 * @since 1.11
		 * @param string $value The content to be shown instead of the {{tag}} placeholder
		 * @param array $args Arguments passed to the function
		 * @since 2.0
		 * @param \GV\Template_Context $context The context.
		 */
		$value = apply_filters( 'gravityview/field_output/context/' . $tag, $value, $args, $context );

		// Finally do the replace.
		$html = str_replace( $search, (string) $value, $html );
	}

	/**
	 * Modify field HTML output.
     *
	 * @param string $html Existing HTML output
	 * @param array $args Arguments passed to the function
	 * @since 2.0
	 * @param \GV\Template_Context $context The context.
	 */
	$html = GravityView_Deprecated_Hook_Notices::apply_filters( 'gravityview_field_output', [ $html, $args, $context ], '2.55', 'gravityview/field_output/html' );

	/**
	 * Modify field HTML output.
     *
	 * @param string $html Existing HTML output
	 * @param array $args Arguments passed to the function
	 * @since 2.0
	 * @param \GV\Template_Context $context The context.
	 */
	$html = apply_filters( 'gravityview/field_output/html', $html, $args, $context );

	/** @since 2.0.8 Remove unused atts */
	$html = str_replace( array( ' style=""', ' class=""', ' id=""' ), '', $html );

	return $html;
}

/**
 * Prevent field wrappers from producing invalid or empty headings.
 *
 * Headings cannot nest, so a value containing an h1-h6 tag splits the wrapper in the browser,
 * leaving an empty heading behind; the wrapper is demoted to a div instead. A heading wrapper
 * whose rendered contents would be empty is suppressed entirely.
 *
 * Runs before placeholder substitution, so contents injected later via the
 * `gravityview/field_output/context/{$tag}` filters are not re-inspected; a heading injected
 * there falls back to the pre-guard behavior.
 *
 * @since 3.1.0
 *
 * @param string                    $markup       Field markup, after the `pre_html` filter, before placeholder substitution.
 * @param array                     $placeholders Resolved placeholder values.
 * @param array                     $args         Field output arguments.
 * @param \GV\Template_Context|null $context      The context.
 *
 * @return string Adjusted markup, or empty string to suppress the field output.
 */
function gravityview_field_output_guard_heading_wrapper( $markup, $placeholders, $args, $context ) {
	if ( ! preg_match( '/^\s*<h([1-6])(?=[\s>])([^>]*)>(.*)<\/h\1\s*>\s*$/is', $markup, $matches ) ) {
		return $markup;
	}

	/**
	 * Disable demotion and suppression of heading wrappers around invalid or empty field values.
	 *
	 * @since 3.1.0
	 *
	 * @param bool                      $fix     Whether to guard heading wrappers. Default: true.
	 * @param array                     $args    Field output arguments.
	 * @param \GV\Template_Context|null $context The context.
	 */
	if ( ! apply_filters( 'gk/gravityview/field-output/fix-heading-wrapper', true, $args, $context ) ) {
		return $markup;
	}

	// A literal heading inside the wrapper means this is not a simple single-heading wrapper; a tag swap could cross-nest it.
	if ( preg_match( '/<\/?h[1-6](?=[\s\/>])/i', $matches[3] ) ) {
		return $markup;
	}

	$contents = array();

	foreach ( array( 'value', 'label', 'label_value' ) as $token ) {
		if ( false !== strpos( $matches[3], '{{' . $token . '}}' ) || false !== strpos( $matches[3], '{{ ' . $token . ' }}' ) ) {
			$contents[] = (string) $placeholders[ 'value' === $token ? 'value' : 'label_value' ];
		}
	}

	// Without value or label tokens the heading is static template content, not field output.
	if ( ! $contents ) {
		return $markup;
	}

	$content = trim( implode( '', $contents ) );

	if ( '' === $content ) {
		return '';
	}

	if ( ! preg_match( '/<\/?h[1-6](?=[\s\/>])/i', $content ) ) {
		return $markup;
	}

	return '<div' . $matches[2] . '>' . $matches[3] . '</div>';
}

/**
 * Generate an HTML anchor tag with a list of supported attributes
 *
 * @see GVCommon::get_link_html()
 *
 * @since 1.6
 *
 * @param string       $href URL of the link.
 * @param string       $anchor_text The text or HTML inside the anchor. This is not sanitized in the function.
 * @param array|string $atts Attributes to be added to the anchor tag.
 *
 * @return string HTML output of anchor link. If empty $href, returns NULL
 */
function gravityview_get_link( $href = '', $anchor_text = '', $atts = [] ) {
	return GVCommon::get_link_html( $href, $anchor_text, $atts );
}

/**
 * Former alias for GravityView_Admin::is_admin_page()
 *
 * @param string      $hook The hook suffix of the current admin page.
 * @param null|string $page Optional. String return value of page to compare against.
 *
 * @deprecated 3.0.0 `gravityview()->request->is_admin` or `\GV\Request::is_admin`
 *
 * @return bool|string If `false`, not a GravityView page. `true` if $page is passed and is the same as current page. Otherwise, the name of the page (`single`, `settings`, or `views`)
 */
function gravityview_is_admin_page( $hook = '', $page = null ) {
	\gravityview()->log->warning( 'The gravityview_is_admin_page() function is deprecated. Use gravityview()->request->is_admin' );
	return gravityview()->request->is_admin( $hook, $page );
}
