<?php
/**
 * Geo Location functionality
 *
 * @package Tribe\Events\Pro\Geo_Loc
 *
 * @since 4.4.0
 */

use Tribe\Events\Pro\Views\V2\Views\Map_View;
use Tribe__Events__Main as TEC;

class Tribe__Events__Pro__Geo_Loc { // phpcs:ignore -- legacy class name

	/**
	 * Meta key for the venue Latitude.
	 */
	const LAT = '_VenueLat';

	/**
	 * Meta key for the venue Longitude.
	 */
	const LNG = '_VenueLng';

	/**
	 * Meta key for if the venue has overwritten coordinates.
	 */
	const OVERWRITE = '_VenueOverwriteCoords';

	/**
	 * The meta key for the full address, either from user input that then gets
	 * sent to the geocoder or, if geocoding is successful, this field gets
	 * overwritten with that full address result. Therefore, it's used as a
	 * cache to avoid sending geocoder requests unless this field changes (i.e.
	 * does not match the value saved from the last geocoding result).
	 */
	const ADDRESS = '_VenueGeoAddress';

	/**
	 * Option key for the Geoloc settings.
	 */
	const OPTIONNAME = 'tribe_geoloc_options';

	/**
	 * Cache key for the geo point at the center of all site's venues.
	 */
	const ESTIMATION_CACHE_KEY = 'geoloc_center_point_estimation';

	/**
	 * Earth radius in Kms. Used for the distance math.
	 *
	 * @deprecated 6.1.0
	 */
	const EARTH_RADIO = 6371;

	/**
	 * Earth radius in Kms. Used for the distance math.
	 */
	const EARTH_RADIUS = 6371;

	/**
	 * Deprecated once we reverted the geolocalization changes to this class.
	 *
	 * @since      4.4.24
	 * @deprecated 4.4.24.2
	 */
	const GEODATE = '_VenueGeoDateUTC';

	/**
	 * Deprecated once we reverted the geolocalization changes to this class.
	 *
	 * @since      4.4.24
	 * @deprecated 4.4.24.2
	 */
	const SITE_GEO_FIXED_OPTIONNAME = '_tribe_geoloc_fixed';

	/**
	 * Settings.
	 *
	 * @var array
	 */
	protected static $options;

	/**
	 * Slug of the map view.
	 *
	 * @var mixed|void
	 */
	public $rewrite_slug;

	/**
	 * Limit for the distance search.
	 *
	 * @var int
	 */
	private $selected_geofence;

	/**
	 * Cache of how many venues we "fixed" (ie: generated geopoints for).
	 *
	 * @var int
	 */
	private $last_venues_fixed_count = 0;

	/**
	 * Singleton instance of this class.
	 *
	 * @var Tribe__Events__Pro__Geo_Loc
	 */
	private static $instance;

	/**
	 * Whether or not the OVER_QUERY_LIMIT notification has been displayed.
	 *
	 * @var boolean
	 */
	private $over_query_limit_displayed = false;

	/**
	 * Class constructor.
	 */
	public function __construct() {
		$this->rewrite_slug = 'map'; // Converted to translated string on init, below.

		add_action( 'tribe_events_venue_updated', [ $this, 'save_venue_geodata' ], 10, 2 );
		add_action( 'tribe_events_venue_created', [ $this, 'save_venue_geodata' ], 10, 2 );
		add_action( 'tribe_settings_after_save', [ $this, 'clear_min_max_coords_cache' ] );

		add_action( 'tribe_events_after_venue_metabox', [ $this, 'setup_overwrite_geoloc' ], 10 );
		add_action( 'tribe_events_filters_create_filters', [ $this, 'setup_geoloc_filter_in_filters' ], 1 );
		add_action( 'wp_enqueue_scripts', [ $this, 'scripts' ] );
		add_action( 'admin_init', [ $this, 'maybe_generate_geopoints_for_all_venues' ] );
		add_action( 'admin_init', [ $this, 'maybe_offer_generate_geopoints' ] );
		add_filter( 'tribe-events-bar-views', [ $this, 'setup_view_for_bar' ], 25, 1 );
		add_filter( 'tec_events_settings_display_maps_section', [ $this, 'inject_settings' ], 10 );
		add_filter( 'tribe-events-bar-filters', [ $this, 'setup_geoloc_filter_in_bar' ], 1, 1 );

		add_filter( 'tribe_events_rewrite_rules_custom', [ $this, 'add_routes' ], 10, 3 );
		add_action( 'tribe_events_pre_get_posts', [ $this, 'setup_geoloc_in_query' ] );
		add_action( 'tribe_repository_events_query', [ $this, 'setup_geoloc_in_query' ] );
		add_filter( 'tribe_events_list_inside_before_loop', [ $this, 'add_event_distance' ] );

		add_filter( 'tribe_events_importer_venue_array', [ $this, 'filter_aggregator_add_overwrite_geolocation_value' ], 10, 4 );
		add_filter( 'tribe_events_importer_venue_column_names', [ $this, 'filter_aggregator_add_overwrite_geolocation_column' ] );

		add_action( 'admin_notices', [ $this, 'maybe_notify_about_google_over_limit' ] );
		add_filter( 'tribe_events_google_map_link', [ $this, 'google_map_link' ], 10, 2 );
		add_action( 'init', [ $this, 'init' ] );
	}

	/**
	 * Initialize the class.
	 *
	 * @since 7.5.0
	 *
	 * @return void
	 */
	public function init(): void {
		$this->rewrite_slug = Tribe__Settings_Manager::get_option( 'geoloc_rewrite_slug', __( 'map', 'tribe-events-calendar-pro' ) );
	}

	/**
	 * If the "Filters bar" add-on is active, set up the distance filter.
	 */
	public function setup_geoloc_filter_in_filters() {
		if ( ! tribe_get_option( 'hideLocationSearch', false ) ) {
			new Tribe__Events__Pro__Geo_Loc_Filter( __( 'Distance', 'tribe-events-calendar-pro' ), 'geofence' );
		}
	}

	/**
	 * Enqueue Google Maps Geolocation JS in all archive views if needed: 1) if the "Near" search is present
	 * in Tribe Bar (if Hide Location Search is unchecked), 2) if we are rendering Map View.
	 */
	public function scripts() {
		if (
			tribe_is_event_query()
			&& ! is_single()
			&& (
				! tribe_get_option( 'hideLocationSearch', false )
				|| tribe_is_map()
			)
		) {
			tribe_asset_enqueue( 'tribe-events-pro-map' );
		}
	}

	/**
	 * Inject the GeoLoc settings into the general TEC settings screen.
	 *
	 * @since 3.0
	 * @since 5.3.0 Change tooltip text.
	 *
	 * @param array $args The current settings array.
	 *
	 * @return array
	 */
	public function inject_settings( $args ) {
		if ( tribe_is_using_basic_gmaps_api() ) {
			// If the user is using the default Google Maps API key, then don't show the button to fix Venues at all.
			return $args;
		}

		$needs_geo_fix = get_transient( '_tribe_geoloc_fix_needed' );

		// We want to inject the map default distance and unit into the map section directly after "enable Google Maps".
		$args = Tribe__Main::array_insert_after_key(
			'embedGoogleMaps',
			$args,
			[
				'geoloc_default_geofence' => [
					'type'            => 'text',
					'label'           => __( 'Map view search distance limit', 'tribe-events-calendar-pro' ),
					'size'            => 'small',
					'tooltip'         => __( 'Set the distance that the location search covers (find events within X distance units of location search input).', 'tribe-events-calendar-pro' ),
					'default'         => '25',
					'class'           => '',
					'validation_type' => 'number_or_percent',
				],
				'geoloc_default_unit'     => [
					'type'            => 'dropdown',
					'label'           => __( 'Distance unit', 'tribe-events-calendar-pro' ),
					'validation_type' => 'options',
					'size'            => 'small',
					'default'         => 'miles',
					'options'         => apply_filters(
						'tribe_distance_units',
						[
							'miles' => __( 'Miles', 'tribe-events-calendar-pro' ),
							'kms'   => __( 'Kilometers', 'tribe-events-calendar-pro' ),
						]
					),
				],
				'geoloc_fix_venues'       => [
					'type'        => 'html',
					'html'        => $this->get_fieldset_html(),
					'conditional' => ! empty( $needs_geo_fix ),
				],
			]
		);

		$args = Tribe__Main::array_insert_after_key(
			'tribeDisableTribeBar',
			$args,
			[
				'hideLocationSearch' => [
					'type'            => 'checkbox_bool',
					'label'           => __( 'Hide location search', 'tribe-events-calendar-pro' ),
					'tooltip'         => __( 'Removes location search field from the Events Bar on all views.', 'tribe-events-calendar-pro' ),
					'default'         => false,
					'validation_type' => 'boolean',
				],
			]
		);

		return $args;
	}

	/**
	 * Generates the settings helper text that accompanies the "Fix venues data" button.
	 *
	 * @since 4.4.34
	 *
	 * @return string
	 */
	public function get_fieldset_html() {
		ob_start();
		include Tribe__Events__Pro__Main::instance()->pluginPath . 'src/admin-views/geolocation-fix-text.php';
		return ob_get_clean();
	}

	/**
	 * @param bool $full_data Whether to return full data or limit to 1.
	 *
	 * @return WP_Query
	 */
	protected function get_venues_without_geoloc_info( $full_data = false ) {
		$default_args = [];
		$venues       = [];
		$count_target = 250;

		// When not full data limit the Query to 1.
		if ( ! $full_data ) {
			$default_args['fields']         = 'ids';
			$default_args['posts_per_page'] = 1;
			$count_target                   = 1;
		}

		$per_page = $count_target;

		foreach ( [ static::ADDRESS, static::LAT, static::LNG ] as $required_meta ) {
			if ( 0 === $per_page ) {
				break;
			}

			// phpcs:disable - WordPress.DB.SlowDBQuery.slow_db_query_meta_query
			$query_args = wp_parse_args(
				[
					'post_type'      => TEC::VENUE_POST_TYPE,
					'post_status'    => 'publish',
					'posts_per_page' => $per_page,
					'meta_query'     => [
						[
							'has-an-address-string'   => [
								'key'     => '_VenueAddress',
								'compare' => '!=',
								'value'   => '',
							],
							'relation'                => 'AND',
							'has-empty-geoloc-fields' => [
								'empty-' . $required_meta  => [
									'key'     => $required_meta,
									'compare' => '=',
									'value'   => '',
								],
								'relation'                => 'OR',
								$required_meta . '-not-exists' => [
									'key'     => $required_meta,
									'compare' => 'NOT EXISTS',
								],
							],
						],
					],
				],
				$default_args
			);
			// phpcs:enable

			$posts    = get_posts( $query_args );
			$per_page = max( $count_target - count( $posts ), 0 );
			$venues[] = $posts;
		}

		$found = array_unique( array_merge( ...$venues ), SORT_REGULAR );

		$query              = new WP_Query();
		$query->found_posts = count( $found );
		$query->posts       = $found;

		return $query;
	}

	/**
	 * Add the Map View to the view switcher in the Tribe Bar.
	 *
	 * @param array $views The current views in the Tribe Bar.
	 *
	 * @return array
	 */
	public function setup_view_for_bar( $views ) {
		$views[] = [
			'displaying'     => 'map',
			'event_bar_hook' => 'tribe_events_list_the_title',
			'anchor'         => __( 'Map', 'tribe-events-calendar-pro' ),
			'url'            => tribe_get_mapview_link(),
		];

		return $views;
	}

	/**
	 * Filter of the Values for Venues and add Overwrite Coordinates.
	 *
	 * @since  4.4.18
	 *
	 * @param  array                                         $venue     Venue Array for creating the Post.
	 * @param  WP_Post                                       $record    Aggregator Record Post.
	 * @param  int                                           $venue_id  Which Venue ID this belongs to.
	 * @param  Tribe__Events__Importer__File_Importer_Venues $importer  Importer with the CSV data.
	 *
	 * @return array
	 */
	public function filter_aggregator_add_overwrite_geolocation_value( $venue, $record, $venue_id, $importer ) {
		$record_value = $importer->get_value_by_key( $record, 'venue_overwrite_coords' );
		$from_venue   = Tribe__Utils__Array::get( $venue, 'OverwriteCoords', '' );

		$venue['OverwriteCoords'] = '' === $record_value && '' !== $from_venue ? $from_venue : $record_value;

		return $venue;
	}

	/**
	 * Filter of the Columns for Venues and add Overwrite Coordinates.
	 *
	 * @since  4.4.18
	 *
	 * @param  array $columns  Previous columns.
	 *
	 * @return array
	 */
	public function filter_aggregator_add_overwrite_geolocation_column( $columns ) {
		$columns['venue_overwrite_coords'] = esc_html__( 'Venue Overwrite Coordinates', 'tribe-events-calendar-pro' );

		return $columns;
	}

	/**
	 * Add the location filter in the Tribe Bar.
	 *
	 * @param array $filters The current filters in the Tribe Bar.
	 *
	 * @return array
	 */
	public function setup_geoloc_filter_in_bar( $filters ) {

		if ( true == tribe_get_option( 'tribeDisableTribeBar', false ) ) {
			return $filters;
		}

		if ( tribe_is_using_basic_gmaps_api() ) {
			return $filters;
		}


		if ( tribe_is_map() || ! tribe_get_option( 'hideLocationSearch', false ) ) {

			$value = tribe_get_request_var( 'tribe-bar-geoloc', '' );
			$lat   = tribe_get_request_var( 'tribe-bar-geoloc-lat', '' );
			$lng   = tribe_get_request_var( 'tribe-bar-geoloc-lng', '' );

			$filters['tribe-bar-geoloc'] = [
				'name'    => 'tribe-bar-geoloc',
				'caption' => esc_html__( 'Near', 'tribe-events-calendar-pro' ),
				'html'    => '<input type="hidden" name="tribe-bar-geoloc-lat" id="tribe-bar-geoloc-lat" value="' . esc_attr( $lat ) . '" /><input type="hidden" name="tribe-bar-geoloc-lng" id="tribe-bar-geoloc-lng" value="' . esc_attr( $lng ) . '" /><input type="text" name="tribe-bar-geoloc" id="tribe-bar-geoloc" value="' . esc_attr( $value ) . '" placeholder="' . __( 'Location', 'tribe-events-calendar-pro' ) . '">',
			];

		}

		return $filters;
	}

	/**
	 * Returns whether the user made a location search in the Tribe Bar.
	 *
	 * @return bool
	 */
	public function is_geoloc_query() {
		return ( ! empty( $_REQUEST['tribe-bar-geoloc-lat'] ) && ! empty( $_REQUEST['tribe-bar-geoloc-lng'] ) );
	}

	public function setup_overwrite_geoloc( $post ) {
		if ( $post->post_type != TEC::VENUE_POST_TYPE ) {
			return;
		}
		$overwrite_coords = (bool) get_post_meta( $post->ID, self::OVERWRITE, true );

		$_lat = get_post_meta( $post->ID, self::LAT, true );
		$_lng = get_post_meta( $post->ID, self::LNG, true );
		?>
		<tr id="overwrite_coordinates">
			<td class="tribe-table-field-label"><?php esc_html_e( 'Use latitude + longitude', 'tribe-events-calendar-pro' ); ?>:</td>
			<td>
				<input
					tabindex="<?php tribe_events_tab_index(); ?>"
					type="checkbox"
					id="VenueOverwriteCoords"
					name="venue[OverwriteCoords]"
					value="true"
					<?php checked( $overwrite_coords ); ?>
				/>

				<input class=" " disabled title='<?php esc_attr_e( 'Latitude', 'tribe-events-calendar-pro' ); ?>' placeholder='<?php esc_attr_e( 'Latitude', 'tribe-events-calendar-pro' ); ?>' tabindex="<?php tribe_events_tab_index(); ?>" type="text" id="VenueLatitude" name="venue[Lat]" value="<?php echo esc_attr( is_numeric( $_lat ) ? (float) $_lat : '' ); ?>" />
				<input class=" " disabled title='<?php esc_attr_e( 'Longitude', 'tribe-events-calendar-pro' ); ?>' placeholder='<?php esc_attr_e( 'Longitude', 'tribe-events-calendar-pro' ); ?>' tabindex="<?php tribe_events_tab_index(); ?>" type="text" id="VenueLongitude" name="venue[Lng]" value="<?php echo esc_attr( is_numeric( $_lng ) ? (float) $_lng : '' ); ?>" />
			</td>
		</tr>

		<?php
		// Some helper text explaining the limitations of this field without a custom Google Maps API key.
		if ( tribe_is_using_basic_gmaps_api() ) :
			?>

			<?php
			$apis_page_url = $link = tribe( 'tec.main' )->settings()->get_url( [ 'tab' => 'addons' ] );
			?>

			<tr>
				<td class="tribe-table-field-label"></td>
				<td>
					<p class="description">
					<?php
						printf(
							'<a href="%1$s" target="_blank">%2$s</a> %3$s',
							esc_url( $apis_page_url ),
							esc_html__( 'Enter a custom Google Maps API key', 'tribe-events-calendar-pro' ),
							esc_html__( 'for latitude and longitude to be auto-generated; otherwise, you\'ll only be able to enter the values manually.', 'tribe-events-calendar-pro' )
						);
					?>
				</td>
			</tr>
		<?php endif; ?>

		<?php
	}

	/**
	 * Filter the main query and:
	 *  1) If the user made a Location search, get the events close to that location (inside the geo fence)
	 *  2) If the user is in the map view and didn't make a location search, only get events in venues with geo data,
	 *     so we can map them.
	 *
	 * @param WP_Query $query
	 *
	 * @return void
	 */
	public function setup_geoloc_in_query( $query ) {

		// The basic gMaps API doesn't let us get lat/long from Google, so we can't be sure there's any valid
		// GeoLoc data for venues. So don't limit the query by that possibly-nonexistent data.
		if ( tribe_is_using_basic_gmaps_api() ) {
			return;
		}

		if (
			empty( $query->query_vars['tribe_geoloc'] )
			&& (
				( ! $query->is_main_query() && ! defined( 'DOING_AJAX' ) )
				|| ! $query->get( 'post_type' ) == Tribe__Events__Main::POSTTYPE
			)
		) {
			return;
		}

		$lat = null;
		$lng = null;

		if ( ! empty( $query->query_vars['tribe_geoloc_lat'] ) ) {
			$lat = (float) $query->query_vars['tribe_geoloc_lat'];
		} elseif ( ! empty( $_REQUEST['tribe-bar-geoloc-lat'] ) ) {
			$lat = filter_var( $_REQUEST['tribe-bar-geoloc-lat'], FILTER_VALIDATE_FLOAT );
		}

		if ( ! empty( $query->query_vars['tribe_geoloc_lng'] ) ) {
			$lng = (float) $query->query_vars['tribe_geoloc_lng'];
		} elseif ( ! empty( $_REQUEST['tribe-bar-geoloc-lng'] ) ) {
			$lng = filter_var( $_REQUEST['tribe-bar-geoloc-lng'], FILTER_VALIDATE_FLOAT );
		}

		if ( empty( $lat ) && empty( $lng ) ) {
			// If there's no latitude and longitude information provided then show all events.
			return;
		}


		$force = false;

		if ( ! empty( $lat ) && ! empty( $lng ) ) {
			// Show only venues that have geolocation information.
			$force = true;

			// Get venues closest to the specified location.
			$venues = $this->get_venues_in_geofence( $lat, $lng );
		} elseif (
			'map' === Tribe__Events__Main::instance()->displaying
			|| (
				! empty( $query->query_vars['eventDisplay'] )
				&& Map_View::get_view_slug() === $query->query_vars['eventDisplay']
			)
			|| ! empty( $query->query_vars['tribe_geoloc'] )
		) {
			// Show only venues that have geolocation information.
			$force = true;

			// Set a geofence the size of the planet.
			$geofence_radius = self::EARTH_RADIUS * M_PI;

			// Get all geoloc'ed venues.
			$venues = $this->get_venues_in_geofence( 1, 1, $geofence_radius );
		}

		if ( $force ) {

			if ( empty( $venues ) ) {
				// There aren't any venues in the geofence, so let's kill the meta query, so we don't get any results.
				$venues = -1;
			} elseif ( is_array( $venues ) ) {
				// We have venues...let's make sure they are unique.
				$venues = array_unique( $venues );
			}

			$meta_query = [
				'key'     => '_EventVenueID',
				'value'   => $venues,
				'type'    => 'NUMERIC',
				'compare' => 'IN',
			];

			if ( empty( $query->query_vars['meta_query'] ) ) {
				$query->set( 'meta_query', [ $meta_query ] );
			} else {
				$query->query_vars['meta_query'][] = $meta_query;
			}
		}
	}

	/**
	 * Adds the rewrite rules to make the map view work.
	 *
	 * @param array      $rules         The current rewrite rules.
	 * @param bool       $tribe_rewrite Whether the rewrite rules are being generated by TEC.
	 * @param WP_Rewrite $wp_rewrite    The WP_Rewrite object.
	 */
	public function add_routes( $rules, $tribe_rewrite, $wp_rewrite = null ) {
		// Prevent errors if this is used on and old version of TEC plugin.
		if ( is_null( $wp_rewrite ) ) {
			unset( $wp_rewrite );
			global $wp_rewrite;
		}

		$tec = Tribe__Events__Main::instance();

		$base    = trailingslashit( $tec->getRewriteSlug() );
		$base_tax = trailingslashit( $tec->get_category_slug() );
		$base_tax = $base . $base_tax . '(?:[^/]+/)*';
		$base_tag = trailingslashit( $tec->get_tag_slug() );
		$base_tag = $base . $base_tag;

		$rewrite_slug = Tribe__Settings_Manager::get_option( 'geoloc_rewrite_slug', __( 'map', 'tribe-events-calendar-pro' ) );

		$new_rules = [];

		/**
		 * Filters the rewrite slugs used to generate the geocode based rewrite rules.
		 *
		 * @param array $rewrite_slugs An array of rewrite slugs to use; defaults to [ 'map' ], the
		 *                             default geocode-based rewrite slug.
		 */
		$rewrite_slugs = apply_filters( 'tribe_events_pro_geocode_rewrite_slugs', [ $rewrite_slug ] );

		foreach ( $rewrite_slugs as $rewrite_slug ) {
			$new_rules[ $base . $rewrite_slug ] = 'index.php?post_type=' . Tribe__Events__Main::POSTTYPE . '&eventDisplay=map';

			$new_rules[ $base_tax . '([^/]+)/' . $rewrite_slug . '/?$' ] = 'index.php?tribe_events_cat=' . $wp_rewrite->preg_index( 2 ) . '&post_type=' . Tribe__Events__Main::POSTTYPE . '&eventDisplay=map';
			$new_rules[ $base_tag . '([^/]+)/' . $rewrite_slug . '/?$' ] = 'index.php?tag=' . $wp_rewrite->preg_index( 2 ) . '&post_type=' . Tribe__Events__Main::POSTTYPE . '&eventDisplay=map';
		}

		$bases = [
			'base'     => $base,
			'base_tax' => $base_tax,
			'base_tag' => $base_tag,
		];

		/**
		 * Filters the geocode based rewrite rules.
		 *
		 * @since 4.7.9
		 *
		 * @param array $new_rules     The geocode based rewrite rules.
		 * @param array $bases         The rewrite bases used to generate the rewrite rules.
		 * @param array $rewrite_slugs The rewrite slugs used to generate the rewrite rules.
		 */
		$new_rules = apply_filters( 'tribe_events_pro_geocode_rewrite_rules', $new_rules, $bases, $rewrite_slugs );

		return $new_rules + $rules;
	}

	/**
	 *  Adds the distance of each event in the resulting list, when the user makes a location search.
	 *
	 * @param string $html The current HTML of the event.
	 *
	 * @return string
	 */
	public function add_event_distance( $html ) {
		global $post;
		if ( ! empty( $post->distance ) ) {
			$html .= '<span class="tribe-events-distance">' . tribe_get_distance_with_unit( $post->distance ) . '</span>';
		}

		return $html;
	}

	/**
	 * Hooks into the venue save and if we don't have Geo Data for that address,
	 * it calls the Google Maps API and grabs the Lat and Lng for that venue.
	 *
	 * @param int       $venue_id The Venue post ID.
	 * @param array     $data     An array of location data for the Venue as provided by WordPress.
	 * @param bool|null $force    Whether to force the re-fetch of the geolocation data for the Venue or not.
	 *
	 * @return bool Whether the Venue geolocation information was updated or not.
	 *
	 * @throws \RuntimeException If the `$throw` parameter is set to `true` and the geolocation data could not be updated.
	 */
	public function save_venue_geodata( $venue_id, $data, $force = false ) {

		$_address  = ( ! empty( $data['Address'] ) ) ? $data['Address'] : '';
		$_city     = ( ! empty( $data['City'] ) ) ? $data['City'] : '';
		$_province = ( ! empty( $data['Province'] ) ) ? $data['Province'] : '';
		$_state    = ( ! empty( $data['State'] ) ) ? $data['State'] : '';
		$_zip      = ( ! empty( $data['Zip'] ) ) ? $data['Zip'] : '';
		$_country  = ( ! empty( $data['Country'] ) ) ? $data['Country'] : '';

		$overwrite = ( ! empty( $data['OverwriteCoords'] ) ) ? 1 : 0;
		$_lat = ( ! empty( $data['Lat'] ) && is_numeric( $data['Lat'] ) ) ? (float) $data['Lat'] : false;
		$_lng = ( ! empty( $data['Lng'] ) && is_numeric( $data['Lng'] ) ) ? (float) $data['Lng'] : false;
		$reset = false;

		// Check the Overwrite data, otherwise just reset it.
		if ( $overwrite && false !== $_lat && false !== $_lng ) {
			update_post_meta( $venue_id, self::OVERWRITE, 1 );
			update_post_meta( $venue_id, self::LAT, (string) $_lat );
			update_post_meta( $venue_id, self::LNG, (string) $_lng );

			$this->clear_min_max_coords_cache();
			return true;
		} else {
			if ( 1 === (int) get_post_meta( $venue_id, self::OVERWRITE, true ) ) {
				$reset = true;
			}
			update_post_meta( $venue_id, self::OVERWRITE, 0 );
		}

		// Remove remaining spaces from any of the pieces of the address.
		$pieces  = array_map( 'trim', compact( '_address', '_province', '_city', '_state', '_zip', '_country' ) );
		$address = implode( ' ', array_filter( $pieces ) );
		// Remove any parenthesis from the address and his content as well.
		$address = preg_replace( '/\(.*\)/', '', $address );

		if ( empty( $address ) && ! empty( $data['Venue'] ) ) {
			$address = $data['Venue'];
			$force   = true;
		} elseif ( empty( $address ) ) {
			return false;
		}

		/**
		 * Allows customizing whether the Google Maps Geocode API will be used for geocoding addresses.
		 *
		 * @since 4.4.34
		 *
		 * @param boolean $geocode_addresses Whether the Geocode Address API is enabled for geocoding addresses.
		 * @param int     $venue_id           Venue post ID.
		 * @param string  $address           Address string that will be used for geocoding.
		 */
		$geocode_addresses = apply_filters( 'tribe_events_pro_geocode_addresses', true, $venue_id, $address );

		if ( false === $geocode_addresses ) {
			return false;
		}

		$all_meta_is_set = 3 === array_sum(
			[
				(bool) get_post_meta( $venue_id, static::ADDRESS, true ),
				(bool) get_post_meta( $venue_id, static::LNG, true ),
				(bool) get_post_meta( $venue_id, static::LAT, true ),
			]
		);

		// If the address didn't change, it doesn't make sense to query Google again for the geo data.
		if ( ! $force && false === $reset && $all_meta_is_set ) {
			return false;
		}

		// The basic Google Maps API key we provide doesn't support geocoding queries.
		if ( tribe_is_using_basic_gmaps_api() ) {
			return update_post_meta( $venue_id, self::ADDRESS, $address );
		}

		$resolved = $this->geocode_address( $address, $venue_id, $pieces );

		if ( false === $resolved ) {
			return false;
		}

		if ( false !== $resolved ) {
			if ( isset( $resolved['lat'] ) ) {
				update_post_meta( $venue_id, self::LAT, $resolved['lat'] );
				unset( $resolved['lat'] );
			}
			if ( isset( $resolved['lng'] ) ) {
				update_post_meta( $venue_id, self::LNG, $resolved['lng'] );
				unset( $resolved['lng'] );
			}

			/**
			 * Allow users to choose whether they want to save the geocoded address results (true)
			 * or the details they entered (false, default).
			 *
			 * @since 7.3.1
			 *
			 * @param bool $use_geocode_results
			 */
			$use_geocode_results = apply_filters( 'tec_events_pro_use_geocode_results', false );

			/**
			 * Check if the venue is the result of an import, or if the geocoded results should be used when manually
			 * creating a venue, and then update the venue details.
			 *
			 * @since 7.3.1
			 */
			if (
				'tribe_queue_ea_import_events' === tribe_get_request_var( 'action' )
				|| $use_geocode_results
			) {
				foreach ( $resolved as $key => $value ) {
					update_post_meta( $venue_id, '_Venue' . $key, sanitize_text_field( $value ) );
				}
			}
		}

		// Saving the aggregated address, so we don't need to ping google on every save.
		update_post_meta( $venue_id, self::ADDRESS, $address );

		$this->clear_min_max_coords_cache();

		return true;
	}

	/**
	 * Add notification message about the Google Maps API being over its query limit.
	 */
	public function maybe_notify_about_google_over_limit() {
		if ( ! get_transient( 'tribe-google-over-limit' ) ) {
			return;
		}

		delete_transient( 'tribe-google-over-limit' );

		$link = tribe( 'tec.main' )->settings()->get_url( [ 'tab' => 'addons' ] );
		?>
		<div class="error">
			<p>
				<?php
				printf(
					esc_html__(
						'The latitude and longitude for your venue could not be fetched. The Google Maps API daily query limit has been reached!. %1$s ',
						'tribe-events-calendar-pro'
					),
					'<a href="' . esc_url( $link ) . '">' . esc_html__( 'Enter your API Key', 'tribe-events-calendar-pro' ) . '</a>.'
				);
				?>
			</p>
		</div>
		<?php
	}

	/**
	 * Gets all settings
	 *
	 * @static
	 *
	 * @param bool $force
	 *
	 * @return mixed|void
	 */
	public static function getOptions( $force = false ) {
		if ( ! isset( self::$options ) || $force ) {
			$options       = get_option( self::OPTIONNAME, [] );
			self::$options = apply_filters( 'tribe_geoloc_get_options', $options );
		}

		return self::$options;
	}

	/**
	 * Gets a single option from the settings
	 *
	 * @param string $option_name The name of the option to get.
	 * @param string $def_value   The default value to return if the option is not set.
	 * @param bool   $force             Whether to force the retrieval of the options.
	 *
	 * @return mixed|void
	 */
	public function getOption( $option_name, $def_value = '', $force = false ) {


		if ( ! isset( self::$options ) || $force ) {
			self::getOptions( $force );
		}

		if ( isset( self::$options[ $option_name ] ) ) {
			$option = self::$options[ $option_name ];
		} else {
			$option = $def_value;
		}

		return apply_filters( 'tribe_geoloc_get_single_option', $option, $def_value );
	}

	/**
	 * Returns the geofence size in kms.
	 *
	 * @return mixed|void
	 */
	private function get_geofence_size() {
		$default  = tribe_get_option( 'geoloc_default_geofence', 25 );
		$geofence = apply_filters( 'tribe_geoloc_geofence', $default );
		$unit     = tribe_get_option( 'geoloc_default_unit', 'miles' );

		// Ensure we use the correct internal unit of measure (kilometres).
		return tribe_convert_units( $geofence, $unit, 'kms' );
	}

	/**
	 * Get a list of venues inside a given geo fence with the given geo point at the center.
	 *
	 * @param float $lat
	 * @param float $lng
	 * @param float $geofence_radius
	 *
	 * @return array|null
	 */
	public function get_venues_in_geofence( $lat, $lng, $geofence_radius = null ) {

		$lat = floatval( $lat );

		if ( empty( $geofence_radius ) ) {
			$geofence_radius = $this->get_geofence_size();
		}

		// get the limits of the geofence.

		$max_lat = $lat + rad2deg( $geofence_radius / self::EARTH_RADIUS );
		$min_lat = $lat - rad2deg( $geofence_radius / self::EARTH_RADIUS );
		$max_lng = $lng + rad2deg( $geofence_radius / self::EARTH_RADIUS / cos( deg2rad( $lat ) ) );
		$min_lng = $lng - rad2deg( $geofence_radius / self::EARTH_RADIUS / cos( deg2rad( $lat ) ) );

		$latlng = [
			'lat'    => $lat,
			'lng'    => $lng,
			'minLat' => $min_lat,
			'maxLat' => $max_lat,
			'minLng' => $min_lng,
			'maxLng' => $max_lng,
		];

		/**
		 * Allow overriding of Venues query by returning an array of Venue IDs.
		 *
		 * @since 4.4.16
		 *
		 * @param null|int[] $venues Venue IDs, default null will query database directly.
		 * @param array      $latlng {
		 *      Latitude / longitude values for geofencing
		 *
		 *      @type float $lat    Central latitude point
		 *      @type float $lng    Central longitude point
		 *      @type float $min_lat Minimum latitude constraint
		 *      @type float $max_lat Maximum latitude constraint
		 *      @type float $min_lng Minimum longitude constraint
		 *      @type float $max_lng Maximum longitude constraint
		 * }
		 * @param float      $geofence_radius Geofence size in kilometers
		 */
		$venues = apply_filters( 'tribe_geoloc_pre_get_venues_in_geofence', null, $latlng, $geofence_radius );

		if ( null === $venues ) {
			global $wpdb;

			// Get the venues inside a geofence.
			$sql = "
				SELECT DISTINCT venue_id FROM (
					SELECT coords.venue_id,
						   MAX( coords.lat ) AS lat,
						   MAX( coords.lng ) AS lng
					FROM (
						SELECT post_id AS venue_id,
							CASE
								WHEN meta_key = %s
								THEN meta_value
							END AS lat,
							CASE
								WHEN meta_key = %s
								THEN meta_value
							END AS lng
						FROM $wpdb->postmeta
						WHERE
							meta_key = %s
							OR meta_key = %s
					) AS coords
					INNER JOIN $wpdb->posts p
						ON p.id = coords.venue_id
					WHERE
						(lat > %f OR lat IS NULL)
						AND (lat < %f OR lat IS NULL)
						AND (lng > %f OR lng IS NULL)
						AND (lng < %f OR lng IS NULL)
						AND p.post_status = 'publish'
						AND p.post_type = %s
					GROUP BY venue_id
					HAVING
						lat IS NOT NULL
						AND lng IS NOT NULL
				   ) AS query
			   ";

			$sql = $wpdb->prepare(
				$sql,
				[
					self::LAT,
					self::LNG,
					self::LAT,
					self::LNG,
					$min_lat,
					$max_lat,
					$min_lng,
					$max_lng,
					Tribe__Events__Main::VENUE_POST_TYPE,
				]
			);

			$venues = $wpdb->get_col( $sql );
		}

		if ( empty( $venues ) ) {
			$venues = null;
		}

		return $venues;
	}

	/**
	 * Orders a list of posts by distance to a given geo point.
	 *
	 * @param array $posts      List of posts to order.
	 * @param float $lat_from   Latitude of the geo point.
	 * @param float $lng_from   Longitude of the geo point.
	 */
	public function assign_distance_to_posts( &$posts, $lat_from, $lng_from ) {

		// add distances.
		$num_posts = count( $posts );
		for ( $i = 0; $i < $num_posts; $i++ ) {
			$posts[ $i ]->lat      = $this->get_lat_for_event( $posts[ $i ]->ID );
			$posts[ $i ]->lng      = $this->get_lng_for_event( $posts[ $i ]->ID );
			$posts[ $i ]->distance = $this->get_distance_between_coords( $lat_from, $lng_from, $posts[ $i ]->lat, $posts[ $i ]->lng );
		}

		// no return, $posts passed by ref.
	}

	/**
	 * Implementation of the Haversine Formula to get the distance in kms between two geo points.
	 *
	 * @param float $lat_from
	 * @param float $lng_from
	 * @param float $lat_to
	 * @param float $lng_to
	 *
	 * @return float
	 */
	public function get_distance_between_coords( $lat_from, $lng_from, $lat_to, $lng_to ) {

		$delta_lat = $lat_to - $lat_from;
		$delta_lng = $lng_to - $lng_from;


		$a        = sin( deg2rad( (float) ( $delta_lat / 2 ) ) ) * sin( deg2rad( (float) ( $delta_lat / 2 ) ) ) + cos( deg2rad( (float) $lat_from ) ) * cos( deg2rad( (float) $lat_to ) ) * sin( deg2rad( (float) ( $delta_lng / 2 ) ) ) * sin( deg2rad( (float) ( $delta_lng / 2 ) ) );
		$c        = asin( min( 1, sqrt( $a ) ) );
		$distance = 2 * self::EARTH_RADIUS * $c;
		$distance = round( $distance, 4 );

		return $distance;
	}

	/**
	 * Returns the latitude of the venue for an event.
	 *
	 * @param $event_id
	 *
	 * @return mixed
	 */
	public function get_lat_for_event( $event_id ) {
		$venue = tribe_get_venue_id( $event_id );

		return get_post_meta( $venue, self::LAT, true );
	}

	/**
	 * Returns the longitude of the venue for an event.
	 *
	 * @param $event_id
	 *
	 * @return mixed
	 */
	public function get_lng_for_event( $event_id ) {
		$venue = tribe_get_venue_id( $event_id );

		return get_post_meta( $venue, self::LNG, true );
	}

	/**
	 * Get the minimum and maximum latitudes and longitudes for all published events.
	 *
	 * @return array (
	 *  Latitude / longitude values for geofencing.
	 *
	 *  @type float $max_lat Maximum latitude constraint
	 *  @type float $max_lng Maximum longitude constraint
	 *  @type float $min_lat Minimum latitude constraint
	 *  @type float $min_lng Minimum longitude constraint
	 * }
	 */
	public function get_min_max_coords() {
		global $wpdb;

		$coords = get_transient( self::ESTIMATION_CACHE_KEY );

		// We have a cached value!
		if ( is_array( $coords ) ) {
			return $coords;
		}

		/**
		 * Allow overriding of queries to get min/max coordinates by returning an array of coordinate values.
		 *
		 * @since 4.4.21
		 *
		 * @param null|array $latlng {
		 *      Latitude / longitude values for geofencing
		 *
		 *      @type float $max_lat Maximum latitude constraint
		 *      @type float $max_lng Maximum longitude constraint
		 *      @type float $min_lat Minimum latitude constraint
		 *      @type float $min_lng Minimum longitude constraint
		 * }
		 */
		$coords = apply_filters( 'tribe_geoloc_pre_get_min_max_coords', null );

		if ( null === $coords ) {
			$venues_list      = $this->get_active_venues();
			$published_venues = [];

			if ( ! empty( $venues_list ) ) {
				$published_venues = $this->filter_published_venues( $venues_list );
			}

			// Only run query if there are events.
			if ( ! empty( $published_venues ) ) {
				$venues_ids_prepared = implode( ', ', $published_venues );
				$latitude_key       = self::LAT;
				$longitude_key      = self::LNG;

				$sql = "
					SELECT
						MAX( `coords`.`lat` ) AS `max_lat`,
						MAX( `coords`.`lng` ) AS `max_lng`,
						MIN( `coords`.`lat` ) AS `min_lat`,
						MIN( `coords`.`lng` ) AS `min_lng`
					FROM (
						SELECT `post_id` AS `venue_id`,
							CASE
							WHEN `meta_key` = '{$latitude_key}'
								THEN CAST( `meta_value` AS DECIMAL( 10, 6 ) )
							END AS `lat`,
							CASE
								WHEN `meta_key` = '{$longitude_key}'
								THEN CAST( `meta_value` AS DECIMAL( 10, 6 ) )
							END AS `lng`
						FROM `{$wpdb->postmeta}`
						WHERE
							(
								`meta_key` = '{$latitude_key}'
								OR `meta_key` = '{$longitude_key}'
							)
							AND `post_id` IN ( {$venues_ids_prepared} )
					) AS `coords`
				";

				$coords = $wpdb->get_row( $sql, ARRAY_A );
			}
		}

		if ( ! empty( $coords ) ) {
			// If there is no geoloc data then each result will be null - we cannot pass null values to the Google Maps API.
			$coords = array_map( 'floatval', $coords );
		}

		set_transient( self::ESTIMATION_CACHE_KEY, $coords, DAY_IN_SECONDS );

		// If no coords found, always return an empty array with null args.
		if ( empty( $coords ) ) {
			$coords = [
				'max_lat' => null,
				'max_lng' => null,
				'min_lat' => null,
				'min_lng' => null,
			];
		}

		return $coords;
	}

	/**
	 * Get a list of IDs of all the active venues being used on events, this will allow to filter with only the active
	 * venues used at the moment.
	 *
	 * @since 4.4.24
	 *
	 * @return array
	 */
	private function get_active_venues() {
		$results = [];
		global $wpdb;
		$sql = "SELECT DISTINCT meta_value
		FROM `{$wpdb->postmeta}`
		WHERE meta_key = '_EventVenueID'";

		$query = $wpdb->get_results( $sql );
		if ( empty( $query ) || ! is_array( $query ) ) {
			return $results;
		}

		foreach ( $query as $row ) {
			$results[] = absint( $row->meta_value );
		}

		return $results;
	}

	/**
	 * Makes sure the list of venues are published.
	 *
	 * @since 4.4.24
	 *
	 * @param array $list List of Venues IDs
	 *
	 * @return array
	 */
	private function filter_published_venues( $list = [] ) {
		$query = new WP_Query(
			[
				'post_type'      => Tribe__Events__Venue::POSTTYPE,
				'posts_per_page' => -1,
				'post_status'    => 'publish',
				'fields'         => 'ids',
				'post__in'       => $list,
				'no_found_rows'  => true,
			]
		);
		return $query->posts;
	}

	/**
	 * Deletes the cached value for the min/max lat/lng.
	 *
	 * @return bool Indicates success.
	 */
	public function clear_min_max_coords_cache() {
		return delete_transient( self::ESTIMATION_CACHE_KEY );
	}

	/**
	 * Get the minimum and maximum latitudes and longitudes for all published events.
	 *
	 * @deprecated 4.4.19 `\Tribe__Events__Pro__Geo_Loc::get_min_max_coords()`
	 *
	 * @return array
	 */
	public function estimate_center_point() {
		return $this->get_min_max_coords();
	}

	/**
	 * Generates the array of markers to pin the events in the Google Map embed in the map view
	 *
	 * @param $events
	 *
	 * @return array
	 */
	public function generate_markers( $events ) {

		$markers = [];

		// let's track which recurrence venues have already been marked.
		$already_marked = [];

		foreach ( $events as $event ) {

			$venue_id = tribe_get_venue_id( $event->ID );
			$lat      = get_post_meta( $venue_id, self::LAT, true );
			$lng      = get_post_meta( $venue_id, self::LNG, true );
			$address  = tribe_get_address( $event->ID );
			$title    = $event->post_title;
			$link     = get_permalink( $event->ID );

			// let's keep track of the post ID/address combos that we've set markers for. If we get a
			// duplicate (a recurrence post with the same address), let's skip it.
			$location_id_hash = md5( $address . ( $event->post_parent ? $event->post_parent : $event->ID ) );
			if ( ! empty( $already_marked[ $location_id_hash ] ) ) {
				continue;
			}

			$already_marked[ $location_id_hash ] = true;

			// replace commas with decimals in case they were saved with the european number format.
			$lat = str_replace( ',', '.', $lat );
			$lng = str_replace( ',', '.', $lng );

			$markers[] = [
				'lat'      => $lat,
				'lng'      => $lng,
				'title'    => $title,
				'address'  => $address,
				'link'     => $link,
				'venue_id' => $venue_id,
				'event_id' => $event->ID,
			];
		}

		return $markers;
	}

	/**
	 * Attempts to perform the geocode resolution of an address.
	 *
	 * @since 4.7
	 *
	 * @param       string $address The formatted address.
	 * @param int    $venue_id The post ID of the Venue, if any, the address of which
	 *                         is currently being resolved.
	 * @param array  $pieces An array containing the address pieces for this Venue.
	 *
	 * @return array|false Either an array containing the Venue latitude and longitude
	 *                    coordinates in the shape [ 'lat' => <lat>, 'lng' => <lng> ] or
	 *                    `false` if the address could not be resolved to a set of coordinates.
	 */
	public function geocode_address( $address, $venue_id = 0, $pieces = [] ) {
		/**
		 * Allows customizing whether the Google Maps Geocode API will be used for geocoding addresses.
		 *
		 * @since 4.7
		 *
		 * @param boolean $geocode_addresses Whether the Geocode Address API is enabled for geocoding addresses.
		 * @param int     $venue_id          Venue post ID.
		 * @param string  $address           Address string that will be used for geocoding.
		 */
		$geocode_addresses = apply_filters( 'tribe_events_pro_geocode_addresses', true, $venue_id, $address );

		if ( false === $geocode_addresses ) {
			return false;
		}

		/**
		 * Allows filtering the geocode resolution completely to use a custom solution.
		 *
		 * Returning a non `null` value here will bail out of the geocode resolution solution
		 * implemented by the plugin completely.
		 *
		 * @since 4.7
		 *
		 * @param array  $result   An array specifying the latitude and longitude coordinates for
		 *                         the address or `false` to indicate a failure in resolving the
		 *                         address to a set of coordinates; returning a non `null` value
		 *                         here will make the function return the filtered value.
		 * @param string $address  The formatted address string.
		 * @param int    $venue_id The Venue post ID.
		 * @param array  $pieces   The Venue address pieces.
		 */
		$result = apply_filters( 'tribe_events_pro_geocode_resolved', null, $address, $venue_id, $pieces );
		if ( null !== $result ) {
			return $result;
		}

		$api_url = 'https://maps.googleapis.com/maps/api/geocode/json?address=' . urlencode( $address );
		$api_key = tribe_get_option( 'google_maps_js_api_key' );

		if ( ! empty( $api_key ) && is_string( $api_key ) ) {
			$api_url = add_query_arg( [ 'key' => $api_key ], $api_url );
		}

		/**
		 * Allows customizing the Google Maps Geocode API URL for a venue's address, which URL is
		 * used to validate the address as one that can be plotted on a Google Map.
		 *
		 * @param string $api_url The Google Maps Geocode API URL for this venue's address.
		 */
		$api_url = apply_filters( 'tribe_events_pro_geocode_request_url', $api_url );

		$data = wp_remote_get( $api_url );

		if ( is_wp_error( $data ) || ! isset( $data['body'] ) ) {
			tribe( 'logger' )->log_warning(
				sprintf(
				/* translators: 1: Error response text, 2: URL */
					_x( 'Geocode request failed (%1$s - %2$s)', 'debug geodata', 'tribe-events-calendar-pro' ),
					is_wp_error( $data ) ? $data->get_error_code() : _x( 'empty response', 'debug geodata' ),
					esc_url( $api_url )
				),
				__METHOD__
			);

			return false;
		}


		$data_arr = json_decode( $data['body'] );

		if ( isset( $data_arr->status ) && 'OVER_QUERY_LIMIT' === $data_arr->status ) {
			if ( $this->over_query_limit_displayed ) {
				return false;
			}

			set_transient( 'tribe-google-over-limit', 1, time() + MINUTE_IN_SECONDS );

			$this->over_query_limit_displayed = true;

			return false;
		}

		if ( isset( $data_arr->error_message ) ) {
			tribe_notice(
				'failed-geocode',
				sprintf(
					esc_html__(
						'There was an error while trying to fix the Venues geolocation information: %s',
						'tribe-events-calendar-pro'
					),
					$data_arr->error_message
				)
			);

			return false;
		}

		if ( empty( $data_arr->results[0] ) ) {
			return false;
		}

		$result     = [];
		$geo_result = $data_arr->results[0];

		if ( isset( $geo_result->geometry->location->lat ) ) {
			$result['lat'] = (string) $geo_result->geometry->location->lat;
		}

		if ( isset( $geo_result->geometry->location->lng ) ) {
			$result['lng'] = (string) $geo_result->geometry->location->lng;
		}

		if ( isset( $geo_result->address_components ) ) {
			foreach ( $geo_result->address_components as $component ) {
				if ( in_array( 'route', $component->types, true ) ) {
					$result['street'] = $component->long_name;
				}
				if ( in_array( 'street_number', $component->types, true ) ) {
					$result['number'] = $component->long_name;
				}
				if ( in_array( 'postal_code', $component->types, true ) ) {
					$result['Zip'] = $component->long_name;
				}
				if ( in_array( 'locality', $component->types, true ) ) {
					$result['City'] = $component->long_name;
				}
				if ( in_array( 'administrative_area_level_1', $component->types, true ) ) {
					$result['Province'] = $component->long_name;
				}
				if ( in_array( 'country', $component->types, true ) ) {
					$result['Country'] = $component->long_name;
				}
			}

			// If it's a US address, save administrative_area_level_1 in the State field instead.
			if ( isset( $result['Country'] ) && $result['Country'] === 'United States' ) {
				$result['State'] = $result['Province'];
				unset( $result['Province'] );
			}

			/**
			 * Allows changing the order of the street name and house number.
			 *
			 * @since 7.3.0
			 *
			 * @param string $address            The compiled address to be saved.
			 * @param array  $result             Array of the address components to be saved.
			 * @param object $address_components The address components received from the geocoding API.
			 */
			$result['Address'] = apply_filters(
				'tec_events_pro_geolocation_address_format',
				trim( ( $result['street'] ?? '' ) . ' ' . ( $result['number'] ?? '' ) ),
				$result,
				$geo_result->address_components
			);

			unset( $result['street'], $result['number'] );
		}

		/**
		 * Allows further processing of geodata for Venue.
		 *
		 * @since 4.4.31
		 *
		 * @param int    $venue_id    Venue ID.
		 * @param object $geo_result Geo result object.
		 * @param array  $pieces     User provided address pieces.
		 */
		do_action( 'tribe_geoloc_save_venue_geodata', $venue_id, $geo_result, $pieces );

		return $result;
	}

	/**
	 * Generates the button to add the geo data info to all venues that are missing it.
	 *
	 * @return string
	 */
	private function fix_geoloc_data_button() {
		$url = $this->get_fix_venues_url();

		return sprintf( '<a href="%s" class="button-secondary">%s</a>', esc_url( $url ), __( 'Fix venues data', 'tribe-events-calendar-pro' ) );
	}

	/**
	 * Check if there are venues without geo data and hook into admin_notices to show a message to the user.
	 */
	public function maybe_offer_generate_geopoints() {
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
			return;
		}

		// This is only available when a custom Geolocation data API key is being used.
		if ( tribe_is_using_basic_gmaps_api() ) {
			return;
		}

		$done = get_transient( '_tribe_geoloc_fixed' );

		if ( ! empty( $done ) ) {
			return;
		}

		$needs_fix = get_transient( '_tribe_geoloc_fix_needed' );

		if ( empty( $needs_fix ) ) {
			$venues = $this->get_venues_without_geoloc_info();

			if ( $venues->found_posts === 0 ) {
				// Let's run the Venue check once a day.
				set_transient( '_tribe_geoloc_fixed', 1, DAY_IN_SECONDS );

				return;
			}

			// cache this value for a day.
			set_transient( '_tribe_geoloc_fix_needed', 1, DAY_IN_SECONDS );
		}

		if ( ! Tribe__Admin__Notices::instance()->showing_notice( 'failed-venue-geocode-resolution' ) ) {
			// The notice about failed Venue fixing contains the same link, let's not show this one.
			add_action( 'admin_notices', [ $this, 'show_offer_to_fix_notice' ] );
		}
	}

	/**
	 * If there are venues without geo data, offer the user to fix them.
	 */
	public function show_offer_to_fix_notice() {
		$url = tribe( 'tec.main' )->settings()->get_url( [ 'tab' => 'display-maps-tab' ] );

		?>
		<div class="updated">
			<p>
			<?php
			printf(
				__(
					"You have venues for which we don't have geolocation information. <a href='%s'>Click here to generate it</a>.",
					'tribe-events-calendar-pro'
				),
				esc_url( $url ) . '#tec-settings-events-settings-display-maps'
			);
			?>
			</p>
		</div>
		<?php
	}

	/**
	 * If the user pressed the button to fix all the venues without geo data, it shows a message
	 * showing the amount of venues fixed.
	 */
	public function maybe_generate_geopoints_for_all_venues() {

		if ( empty( $_GET['geoloc_fix_venues'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'geoloc_fix_venues' ) ) {
			return;
		}

		$this->last_venues_fixed_count = $this->generate_geopoints_for_all_venues();

		if ( $this->last_venues_fixed_count > 0 ) {
			// Show a message if we've actually fixed something.
			add_action( 'admin_notices', [ $this, 'show_fixed_notice' ] );
		}
	}

	/**
	 * Shows a message with the amount of venues fixed.
	 */
	public function show_fixed_notice() {
		?>
		<div class="updated">
			<p><?php printf( esc_html__( 'Fixed geolocation data for %d venues', 'tribe-events-calendar-pro' ), $this->last_venues_fixed_count ); ?></p>
		</div>
		<?php
	}

	/**
	 * Grabs all the venues without geo data and uses the Google Maps API to get it.
	 *
	 * @static
	 * @return int
	 */
	public function generate_geopoints_for_all_venues() {

		tribe_set_time_limit( 5 * 60 );

		$venues = $this->get_venues_without_geoloc_info( true );

		$count = 0;
		foreach ( $venues->posts as $venue ) {
			$data             = [];
			$data['Address']  = get_post_meta( $venue->ID, '_VenueAddress', true );
			$data['City']     = get_post_meta( $venue->ID, '_VenueCity', true );
			$data['Province'] = get_post_meta( $venue->ID, '_VenueProvince', true );
			$data['State']    = get_post_meta( $venue->ID, '_VenueState', true );
			$data['Zip']      = get_post_meta( $venue->ID, '_VenueZip', true );
			$data['Country']  = get_post_meta( $venue->ID, '_VenueCountry', true );

			// If we're here we need to re-fetch the geolocation data, just force it.
			$updated = self::instance()->save_venue_geodata( $venue->ID, $data, true );

			if ( false === $updated ) {
				$this->show_venue_failure_notice( $venue );

				/**
				 * There is an issue that, most likely, will prevent any update to any Venue from happening.
				 * Let's stop; the inner methods should provide feedback to the user by means of a transient notice.
				 */
				return $count;
			}

			++$count;
		}

		// For back-compatibility purposes let's remove this.
		delete_option( '_tribe_geoloc_fixed' );

		// Let's remove the note that fixes are needed.
		delete_transient( '_tribe_geoloc_fix_needed' );

		// Let's run the Venue check once a day.
		set_transient( '_tribe_geoloc_fixed', 1, DAY_IN_SECONDS );

		return $count;
	}

	/**
	 * Static Singleton Factory Method.
	 *
	 * @return Tribe__Events__Pro__Geo_Loc
	 */
	public static function instance() {
		if ( ! isset( self::$instance ) ) {
			$class_name     = __CLASS__;
			self::$instance = new $class_name();
		}

		return self::$instance;
	}

	/**
	 * Update the Google Map Link to add the coordinates if present to increase accuracy on it.
	 *
	 * @since 4.4.26
	 *
	 * @param string $link The Google Map Link.
	 * @param int    $post_id The ID of the post being modified.
	 *
	 * @return string
	 */
	public function google_map_link( $link, $post_id ) {
		$venue_id = function_exists( 'tribe_get_venue_id' ) ? tribe_get_venue_id( $post_id ) : $post_id;
		$is_venue = function_exists( 'tribe_is_venue' ) ? tribe_is_venue( $venue_id ) : false;

		/**
		 * Disable the behavior to add the coordinates if present on the Google Map Link.
		 *
		 * @since 4.4.26
		 *
		 * @param bool $disable true to disable the behavior / false to keep doing it.
		 * @param int  $post_id The ID of the post being modified.
		 *
		 * @return boolean
		 */
		$disable_behavior = apply_filters( 'tribe_events_pro_google_map_link_disable_coordinates', false, $post_id );

		if ( ! $is_venue || $disable_behavior ) {
			return $link;
		}

		$coordinates = tribe_get_coordinates( $post_id );
		if ( empty( $coordinates['lat'] ) || empty( $coordinates['lng'] ) || ! function_exists( 'tribe_is_venue' ) ) {
			return $link;
		}

		return add_query_arg(
			[
				'api'   => 1,
				'query' => urlencode( $coordinates['lat'] . ',' . $coordinates['lng'] ),
			],
			'https://www.google.com/maps/search/'
		);
	}

	/**
	 * Returns the URL to fix the Venues information.
	 *
	 * @since 5.0.1
	 * @since 7.3.1 Corrected the settings page tab that is loaded after clicking the button.
	 *
	 * @return string The URL to fix the Venues.
	 */
	private function get_fix_venues_url() {
		$url = tribe( 'tec.main' )->settings()->get_url(
			[
				'geoloc_fix_venues' => '1',
				'tab'               => 'display-maps-tab',
			]
		);
		$url = wp_nonce_url( $url, 'geoloc_fix_venues' );

		return $url;
	}

	/**
	 * Shows a transient notice to the user to allow the manual fixing of the problematic Venue address details.
	 *
	 * @since 5.0.1
	 *
	 * @param \WP_Post $venue The problematic Venue post object.
	 */
	protected function show_venue_failure_notice( \WP_Post $venue ) {
		$venue_edit_link = sprintf(
			'<a href="%s" target="_blank">%s</a>',
			esc_attr( get_edit_post_link( $venue->ID ) ),
			esc_html_x(
				'update the venue address or use latitude & longitude',
				'The text of the link that will take the user to the Venue edit screen.',
				'tribe-events-calendar-pro'
			)
		);

		$fix_link = sprintf(
			'<a href="%s" target="_blank">%s</a>',
			esc_attr( $this->get_fix_venues_url() ),
			esc_html_x(
				'try again',
				'The text of the link that will trigger an attempt to fix the Venues geolocation information.',
				'tribe-events-calendar-pro'
			)
		);

		$html = sprintf(
			'<p>' .
			_x(
				'Geolocation could not be fixed for the Venue “%1$s” (ID: %2$d). Please %3$s and %4$s.',
				'tribe-events-calendar-pro'
			) . '</p>',
			$venue->post_title,
			$venue->ID,
			$venue_edit_link,
			$fix_link
		);
		$arguments = [
			'type'    => 'warning',
			'dismiss' => true,
		];

		$slug = 'failed-venue-geocode-resolution';

		// Show it immediately.
		tribe_notice( $slug, $html, $arguments );
	}
}
