<?php
/**
 * Class Geo_Utils
 *
 * @package immonex\WordPressFreePluginCore
 */

namespace immonex\WordPressFreePluginCore\DEV_7;

/**
 * Geocoding related utility methods.
 */
class Geo_Utils {

	const EARTH_RADIUS                       = 6378.388;
	const NOMINATIM_BASE_URL                 = 'https://nominatim.openstreetmap.org/search';
	const PHOTON_BASE_URL                    = 'https://photon.komoot.io/api/';
	const PHOTO_LOCATION_BIAS_LAT            = 51.163375; // Latitude of the geographical center of Germany.
	const PHOTO_LOCATION_BIAS_LNG            = 10.447683; // Longitude of the geographical center of Germany.
	const GOOGLE_MAPS_API_WITH_KEY_BASE_URL  = 'https://maps.googleapis.com/maps/api/geocode/';
	const BING_MAPS_API_BASE_URL             = 'https://dev.virtualearth.net/REST/v1/Locations';
	const USERAGENT                          = 'immonexWpPluginGeocoder/2.4';
	const COUNTRYCODES_GERMANY_AND_NEIGHBORS = 'de,dk,nl,be,lu,fr,ch,at,cz,pl';
	const COUNTRYCODES_EU                    = 'be,bg,cz,dk,de,ee,ie,el,es,fr,hr,it,cy,lv,lt,lu,hu,mt,nl,at,pl,pt,ro,si,sk,fi,se';
	const COUNTRYCODES_EUROPE                = 'ad,al,at,ba,be,bg,by,ch,cy,cz,de,dk,ee,es,fi,fo,fr,gg,gi,gr,hr,hu,ie,im,is,it,je,li,lt,lu,lv,mc,md,mk,mt,nl,no,pl,pt,ro,ru,se,si,sj,sk,sm,tr,ua,uk,va,yu';

	/**
	 * ISO 3166-1 country codes
	 * (alpha-2 => alpha-3 – to be filled on first use)
	 *
	 * @var mixed[]
	 */
	private $iso_country_codes;

	/**
	 * Supported geocoding providers
	 *
	 * @var mixed[]
	 */
	private static $providers = [
		'nominatim'   => [
			'name'         => 'Nominatim/OSM',
			'key_required' => false,
		],
		'photon'      => [
			'name'         => 'Photon/OSM',
			'key_required' => false,
		],
		'google_maps' => [
			'name'         => 'Google Maps',
			'key_required' => true,
		],
		'bing_maps'   => [
			'name'         => 'Bing Maps',
			'key_required' => true,
		],
	];

	/**
	 * Calculate the distance (beeline) in km between two coordinate pairs.
	 *
	 * @param float $lat1 Location 1 latitude.
	 * @param float $lng1 Location 1 longitude.
	 * @param float $lat2 Location 2 latitude.
	 * @param float $lng2 Location 2 longitude.
	 *
	 * @return float Distance in km.
	 */
	public static function calculate_distance_km( $lat1, $lng1, $lat2, $lng2 ) {
		$lat1_rad = deg2rad( $lat1 );
		$lng1_rad = deg2rad( $lng1 );
		$lat2_rad = deg2rad( $lat2 );
		$lng2_rad = deg2rad( $lng2 );

		return static::EARTH_RADIUS * acos(
			sin( $lat1_rad ) * sin( $lat2_rad )
			+ cos( $lat1_rad ) * cos( $lat2_rad )
			* cos( $lng2_rad - $lng1_rad )
		);
	} // calculate_distance_km

	/**
	 * Get geo information for a given address.
	 *
	 * @param string        $address Address for geocoding.
	 * @param string        $return_type "full" or "compact" (the latter one only returns coordinates).
	 * @param string[]|bool $use_providers Geocoding API to use in given order until a valid result is
	 *     returned (false = use default provider list).
	 * @param array         $keys Array of API keys (if required).
	 * @param string        $language ISO-2 Language code for results (if supported, optional, default "de").
	 * @param string|bool   $countrycodes Comma separated list of ISO-2 country codes for limiting the geo query
	 *       ("germany_and_neighbors", "eu" or "europe" for respective default lists), false for no country limiting.
	 *
	 * @return object|array|bool Geo information/coordinates, false if geocoding failed.
	 */
	public static function geocode( $address, $return_type = 'compact', $use_providers = false, $keys = [], $language = 'de', $countrycodes = false ) {
		if ( $address ) {
			$providers = self::validate_providers( $use_providers );

			if ( is_array( $providers ) && count( $providers ) > 0 ) {
				if ( $countrycodes ) {
					$countrycodes = self::parse_countrycodes( $countrycodes );
				}

				foreach ( $providers as $provider => $api_attribs ) {
					if (
						$api_attribs['key_required'] &&
						(
							! isset( $keys[ $provider ] ) ||
							! $keys[ $provider ]
						)
					) {
						// Skip provider if no required key has been provided.
						continue;
					}

					$provider_geocode_method = "geocode_$provider";
					$provider_result_method  = "get_result_$provider";

					$geocode_response = self::$provider_geocode_method( $address, isset( $keys[ $provider ] ) ? $keys[ $provider ] : false, $language, $countrycodes );
					if ( $geocode_response ) {
						$geocode_response = json_decode( $geocode_response );
					}

					if ( $geocode_response ) {
						$geocode_result = self::$provider_result_method( $geocode_response, $return_type );
						if ( $geocode_result ) {
							if ( 'compact' === $return_type ) {
								$geocode_result['provider'] = isset( $api_attribs['name'] ) ? $api_attribs['name'] : '';
							}

							return $geocode_result;
						}
					}
				}
			}
		}

		// Geocoding failed.
		return false;
	} // function getGeoPosition

	/**
	 * Get the geocoding status for a given address.
	 *
	 * @since 0.5.1
	 *
	 * @param string        $address Address for geocoding.
	 * @param string[]|bool $use_providers Geocoding API to use in given order until a valid result is
	 *     returned (false = use default provider list).
	 * @param array         $keys Array of API keys (if required).
	 * @param string        $language ISO-2 Language code for results (if supported, optional, default "de").
	 * @param string|bool   $countrycodes Comma separated list of ISO-2 country codes for limiting the geo query
	 *     ("germany_and_neighbors", "eu" or "europe" for respective default lists), false for no country limiting.
	 *
	 * @return string|bool Status information or false on retrieval error.
	 */
	public static function get_geocoding_status( $address = 'Platz der Republik 1, Berlin, Germany', $use_providers = false, $keys = [], $language = 'de', $countrycodes = false ) {
		$providers = self::validate_providers( $use_providers );

		if ( $countrycodes ) {
			$countrycodes = self::parse_countrycodes( $countrycodes );
		}

		$provider_states = [];

		foreach ( $providers as $provider => $api_attribs ) {
			if (
				$api_attribs['key_required'] &&
				(
					! isset( $keys[ $provider ] )
					|| ! $keys[ $provider ]
				)
			) {
				// Skip provider if no required key has been provided.
				continue;
			}

			$provider_geocode_method = "geocode_{$provider}";
			$provider_status_method  = "get_status_{$provider}";

			$geocode_response = self::$provider_geocode_method( $address, isset( $keys[ $provider ] ) ? $keys[ $provider ] : false, $language, $countrycodes );
			if ( $geocode_response ) {
				$geocode_decoded = json_decode( $geocode_response );
				if ( null !== $geocode_decoded ) {
					$geocode_response = $geocode_decoded;
				}
			}

			if ( $geocode_response ) {
				$provider_status = self::$provider_status_method( $geocode_response );
			}

			$provider_states[] = wp_sprintf(
				'[%s] %s',
				$api_attribs['name'],
				! empty( $provider_status ) && is_string( $provider_status ) ? $provider_status : __( 'unknown', 'immonex-wp-free-plugin-core' )
			);
		}

		return ! empty( $provider_states ) ? implode( ', ', $provider_states ) : false;
	} // get_geocoding_status

	/**
	 * Validate/Sanitize/Convert a given latitude, longitude or combined (comma-separated)
	 * coordinate string or float value. Examples:
	 *
	 *  - (string) 12.345                       -> (float) 12.345
	 *  - (float) -123.456                      -> (float) -123.456
	 *  - (string) foo 51.163375, 10.447683 bar -> (string) 51.163375,10.447683
	 *  - (float) 185.123                       -> false
	 *
	 * @since 2.3.0
	 *
	 * @param string|float $source Latitude/Longitude or Coordinate pair to check.
	 * @param string       $type   Optional validation type (lat, lng or default: coords).
	 *
	 * @return float|string|bool Validated/Sanitized/Converted value or false if invalid.
	 */
	public static function validate_coords( $source, $type = 'coords' ) {
		if ( ! in_array( $type, [ 'lat', 'lng', 'coords' ], true ) ) {
			$type = 'coords';
		}

		switch ( $type ) {
			case 'coords':
				if ( ! is_string( $source ) || false === strpos( $source, ',' ) ) {
					return false;
				}

				$split     = [];
				$split_raw = explode( ',', $source );

				if ( count( $split_raw ) < 2 ) {
					return false;
				}

				foreach ( $split_raw as $element ) {
					if ( preg_match( '/[-0-9]+\.[0-9]+/', $element, $matches ) ) {
						$split[] = $matches[0];
					}
				}

				$lat = $split[0];
				$lng = $split[1];

				if (
					count( $split ) < 2
					|| ! is_numeric( $lat )
					|| ! is_numeric( $lng )
					|| ( (float) $lat < -90 || (float) $lat > 90 )
					|| ( (float) $lng < -180 || (float) $lng > 180 )
				) {
					return false;
				}

				return "{$lat},{$lng}";
			default:
				$minmax_deg = 'lat' === $type ? 90 : 180;

				if ( is_string( $source ) ) {
					$found = preg_match( '/[-0-9]+\.[0-9]+/', $source, $matches );
					if ( ! $found ) {
						return false;
					}

					$source = (float) $matches[0];
				}

				if ( ! is_numeric( $source ) ) {
					false;
				}

				$source = (float) $source;

				return $source >= $minmax_deg * -1 && $source <= $minmax_deg ? $source : false;
		}
	} // validate_coords

	/**
	 * Convert an ISO 3166-1 country code (alpha-2 <-> alpha-3 – depending on the
	 * source country code length).
	 *
	 * @since 2.4.7
	 *
	 * @param string $source_code Source country code (2 or 3 characters).
	 *
	 * @return string|bool Converted code or false if indeterminable.
	 */
	public function convert_iso_country_code( $source_code ) {
		if ( ! $this->iso_country_codes ) {
			$this->iso_country_codes = json_decode( '{"AFG":"AF","ALA":"AX","ALB":"AL","DZA":"DZ","ASM":"AS","AND":"AD","AGO":"AO","AIA":"AI","ATA":"AQ","ATG":"AG","ARG":"AR","ARM":"AM","ABW":"AW","AUS":"AU","AUT":"AT","AZE":"AZ","BHS":"BS","BHR":"BH","BGD":"BD","BRB":"BB","BLR":"BY","BEL":"BE","BLZ":"BZ","BEN":"BJ","BMU":"BM","BTN":"BT","BOL":"BO","BIH":"BA","BWA":"BW","BVT":"BV","BRA":"BR","VGB":"VG","IOT":"IO","BRN":"BN","BGR":"BG","BFA":"BF","BDI":"BI","KHM":"KH","CMR":"CM","CAN":"CA","CPV":"CV","CYM":"KY","CAF":"CF","TCD":"TD","CHL":"CL","CHN":"CN","HKG":"HK","MAC":"MO","CXR":"CX","CCK":"CC","COL":"CO","COM":"KM","COG":"CG","COD":"CD","COK":"CK","CRI":"CR","CIV":"CI","HRV":"HR","CUB":"CU","CYP":"CY","CZE":"CZ","DNK":"DK","DKK":"DK","DJI":"DJ","DMA":"DM","DOM":"DO","ECU":"EC","Sal":"El","GNQ":"GQ","ERI":"ER","EST":"EE","ETH":"ET","FLK":"FK","FRO":"FO","FJI":"FJ","FIN":"FI","FRA":"FR","GUF":"GF","PYF":"PF","ATF":"TF","GAB":"GA","GMB":"GM","GEO":"GE","DEU":"DE","GHA":"GH","GIB":"GI","GRC":"GR","GRL":"GL","GRD":"GD","GLP":"GP","GUM":"GU","GTM":"GT","GGY":"GG","GIN":"GN","GNB":"GW","GUY":"GY","HTI":"HT","HMD":"HM","VAT":"VA","HND":"HN","HUN":"HU","ISL":"IS","IND":"IN","IDN":"ID","IRN":"IR","IRQ":"IQ","IRL":"IE","IMN":"IM","ISR":"IL","ITA":"IT","JAM":"JM","JPN":"JP","JEY":"JE","JOR":"JO","KAZ":"KZ","KEN":"KE","KIR":"KI","PRK":"KP","KOR":"KR","KWT":"KW","KGZ":"KG","LAO":"LA","LVA":"LV","LBN":"LB","LSO":"LS","LBR":"LR","LBY":"LY","LIE":"LI","LTU":"LT","LUX":"LU","MKD":"MK","MDG":"MG","MWI":"MW","MYS":"MY","MDV":"MV","MLI":"ML","MLT":"MT","MHL":"MH","MTQ":"MQ","MRT":"MR","MUS":"MU","MYT":"YT","MEX":"MX","FSM":"FM","MDA":"MD","MCO":"MC","MNG":"MN","MNE":"ME","MSR":"MS","MAR":"MA","MOZ":"MZ","MMR":"MM","NAM":"NA","NRU":"NR","NPL":"NP","NLD":"NL","ANT":"AN","NCL":"NC","NZL":"NZ","NIC":"NI","NER":"NE","NGA":"NG","NIU":"NU","NFK":"NF","MNP":"MP","NOR":"NO","OMN":"OM","PAK":"PK","PLW":"PW","PSE":"PS","PAN":"PA","PNG":"PG","PRY":"PY","PER":"PE","PHL":"PH","PCN":"PN","POL":"PL","PRT":"PT","PRI":"PR","QAT":"QA","REU":"RE","ROU":"RO","RUS":"RU","RWA":"RW","BLM":"BL","SHN":"SH","KNA":"KN","LCA":"LC","MAF":"MF","SPM":"PM","VCT":"VC","WSM":"WS","SMR":"SM","STP":"ST","SAU":"SA","SEN":"SN","SRB":"RS","SYC":"SC","SLE":"SL","SGP":"SG","SVK":"SK","SVN":"SI","SLB":"SB","SOM":"SO","ZAF":"ZA","SGS":"GS","SSD":"SS","ESP":"ES","LKA":"LK","SDN":"SD","SUR":"SR","SJM":"SJ","SWZ":"SZ","SWE":"SE","CHE":"CH","SYR":"SY","TWN":"TW","TJK":"TJ","TZA":"TZ","THA":"TH","TLS":"TL","TGO":"TG","TKL":"TK","TON":"TO","TTO":"TT","TUN":"TN","TUR":"TR","TKM":"TM","TCA":"TC","TUV":"TV","UGA":"UG","UKR":"UA","ARE":"AE","GBR":"GB","USA":"US","UMI":"UM","URY":"UY","UZB":"UZ","VUT":"VU","VEN":"VE","VNM":"VN","VIR":"VI","WLF":"WF","ESH":"EH","YEM":"YE","ZMB":"ZM","ZWE":"ZW","GBP":"GB","RUB":"RU","NOK":"NO"}', true );
		}

		$source_code = strtoupper( trim( $source_code ) );
		$iso_codes   = 2 === strlen( $source_code ) ?
			array_flip( $this->iso_country_codes ) :
			$this->iso_country_codes;

		return isset( $iso_codes[ $source_code ] ) ? $iso_codes[ $source_code ] : false;
	} // convert_iso_country_code

	/**
	 * Geocode a given address using Nominatim (OpenStreetMap).
	 *
	 * @since 0.9
	 *
	 * @param string      $address Address for geocoding.
	 * @param string      $key API key not required for this service.
	 * @param string      $language ISO-2 Language code for results (if supported, optional, default "de").
	 * @param string|bool $countrycodes Comma separated list of ISO-2 country codes for limiting the geo query
	 *     or false (default) for no limiting.
	 *
	 * @return string|bool Geo data or false on error.
	 */
	private static function geocode_nominatim( $address, $key = false, $language = 'de', $countrycodes = false ) {
		$url = self::NOMINATIM_BASE_URL . '?q=' . rawurlencode( $address ) . '&format=json&accept-language=' . $language;
		if ( $countrycodes ) {
			$url .= '&countrycodes=' . $countrycodes;
		}

		return General_Utils::get_url_contents( $url, self::USERAGENT );
	} // geocode_nominatim

	/**
	 * Check/return Nominatim geocoding results.
	 *
	 * @since 0.9
	 *
	 * @param object $response Raw response object.
	 * @param string $return_type "full" or "compact" (the latter one only returns coordinates).
	 *
	 * @return string|bool Geo data or false on error.
	 */
	private static function get_result_nominatim( $response, $return_type ) {
		if ( is_array( $response ) && count( $response ) > 0 ) {
			foreach ( $response as $geo_object ) {
				if (
					isset( $geo_object->lat ) &&
					isset( $geo_object->lon )
				) {
					if ( 'compact' === $return_type ) {
						return [
							'lat'     => $geo_object->lat,
							'lng'     => $geo_object->lon,
							'lat_rad' => $geo_object->lat * ( pi() / 180 ),
							'lng_rad' => $geo_object->lon * ( pi() / 180 ),
						];
					} else {
						return $geo_object;
					}
				}
			}
		} else {
			return false;
		}
	} // get_result_nominatim

	/**
	 * Check/return status information for a Nominatim geocoding request.
	 *
	 * @since 0.9
	 *
	 * @param object|string $response Raw response object or string.
	 *
	 * @return string|bool Status or false on error.
	 */
	private static function get_status_nominatim( $response ) {
		if ( ! is_string( $response ) ) {
			return false;
		}

		$title = preg_match( '/\<title\>(.*)\<\/title\>/', $response, $matches );
		return $title ? '[Nominatim] ' . $matches[1] : false;
	} // get_status_nominatim

	/**
	 * Geocode a given address using Photon (OpenStreetMap).
	 *
	 * @since 0.9
	 *
	 * @param string      $address Address for geocoding.
	 * @param string      $key API key not required for this service.
	 * @param string      $language ISO-2 Language code for results (if supported, optional, default "de").
	 * @param string|bool $countrycodes Comma separated list of ISO-2 country codes for limiting the geo query
	 *     or false (default) for no limiting (NOT SUPPORTED HERE YET).
	 *
	 * @return string|bool Geo data or false on error.
	 */
	private static function geocode_photon( $address, $key = false, $language = 'de', $countrycodes = false ) {
		$url = self::PHOTON_BASE_URL . '?q=' . rawurlencode( $address ) . '&lang=' . $language .
			'&lat=' . self::PHOTO_LOCATION_BIAS_LAT . '&lon=' . self::PHOTO_LOCATION_BIAS_LNG . '&limit=100';

		return General_Utils::get_url_contents( $url, self::USERAGENT );
	} // geocode_photon

	/**
	 * Check/return Photon geocoding results.
	 *
	 * @since 0.9
	 *
	 * @param object $response Raw response object.
	 * @param string $return_type "full" or "compact" (the latter one only returns coordinates).
	 *
	 * @return string|bool Geo data or false on error.
	 */
	private static function get_result_photon( $response, $return_type ) {
		if (
			isset( $response->features )
			&& is_array( $response->features )
			&& count( $response->features ) > 0
		) {
			foreach ( $response->features as $geo_object ) {
				if ( isset( $geo_object->geometry->coordinates ) ) {
					if ( 'compact' === $return_type ) {
						return [
							'lat'     => $geo_object->geometry->coordinates[1],
							'lng'     => $geo_object->geometry->coordinates[0],
							'lat_rad' => $geo_object->geometry->coordinates[1] * ( pi() / 180 ),
							'lng_rad' => $geo_object->geometry->coordinates[0] * ( pi() / 180 ),
						];
					} else {
						return $geo_object;
					}
				}
			}
		} else {
			return false;
		}
	} // get_result_photon

	/**
	 * Check/return status information for a Photon geocoding request.
	 *
	 * @since 0.9
	 *
	 * @param object $response Raw response object.
	 *
	 * @return string|bool Status or false on error.
	 */
	private static function get_status_photon( $response ) {
		if ( isset( $response->message ) ) {
			return '[Photon] ' . $response->message;
		} else {
			return false;
		}
	} // get_status_photon

	/**
	 * Geocode a given address using the Google Maps API.
	 *
	 * @since 0.5.1
	 *
	 * @param string      $address Address for geocoding.
	 * @param string      $key Google Maps API key.
	 * @param string      $language ISO-2 Language code for results (if supported, optional, default "de").
	 * @param string|bool $countrycodes Comma separated list of ISO-2 country codes for limiting the geo query
	 *     or false (default) for no limiting (NOT SUPPORTED HERE YET).
	 *
	 * @return string|bool Geo data or false on error.
	 */
	private static function geocode_google_maps( $address, $key = false, $language = 'de', $countrycodes = false ) {
		if ( ! $key ) {
			return false;
		}

		$url  = self::GOOGLE_MAPS_API_WITH_KEY_BASE_URL;
		$url .= 'json?address=' . rawurlencode( $address ) . '&sensor=false&language=' . $language . ( $key ? '&key=' . trim( $key ) : '' );

		return General_Utils::get_url_contents( $url, self::USERAGENT );
	} // geocode_google_maps

	/**
	 * Check/return Google Maps geocoding results.
	 *
	 * @since 0.5.1
	 *
	 * @param object $response Raw response object.
	 * @param string $return_type "full" or "compact" (the latter one only returns coordinates).
	 *
	 * @return string|bool Geo data or false on error.
	 */
	private static function get_result_google_maps( $response, $return_type ) {
		if (
			isset( $response->status ) &&
			'OK' === $response->status &&
			isset( $response->results[0]->geometry->location )
		) {
			if ( 'compact' === $return_type ) {
				return [
					'lat'     => $response->results[0]->geometry->location->lat,
					'lng'     => $response->results[0]->geometry->location->lng,
					'lat_rad' => $response->results[0]->geometry->location->lat * ( pi() / 180 ),
					'lng_rad' => $response->results[0]->geometry->location->lng * ( pi() / 180 ),
				];
			} else {
				return $response;
			}
		} else {
			return false;
		}
	} // get_result_google_maps

	/**
	 * Check/return status information for a Google Maps geocoding request.
	 *
	 * @since 0.5.1
	 *
	 * @param object $response Raw response object.
	 *
	 * @return string|bool Status or false on error.
	 */
	private static function get_status_google_maps( $response ) {
		if ( isset( $response->status ) ) {
			return '[Google Maps] ' . $response->status;
		} else {
			return false;
		}
	} // get_status_google_maps

	/**
	 * Geocode a given address using the Bing Maps API.
	 *
	 * @since 0.5.1
	 *
	 * @param string      $address Address for geocoding.
	 * @param string      $key Bing Maps API key (required).
	 * @param string      $language ISO-2 Language code for results (if supported, optional, default "de").
	 * @param string|bool $countrycodes Comma separated list of ISO-2 country codes for limiting the geo query
	 *     or false (default) for no limiting (NOT SUPPORTED HERE YET).
	 *
	 * @return string|bool Geo data or false on error.
	 */
	private static function geocode_bing_maps( $address, $key, $language = 'de', $countrycodes = false ) {
		$url = self::BING_MAPS_API_BASE_URL . '?q=' . rawurlencode( $address ) . '&maxResults=1&includeNeighborhood=0&key=' . trim( $key );

		return General_Utils::get_url_contents( $url, self::USERAGENT );
	} // geocode_bing_maps

	/**
	 * Check/return Bing Maps geocoding results.
	 *
	 * @since 0.5.1
	 *
	 * @param object $response Raw response object.
	 * @param string $return_type "full" or "compact" (the latter one only returns coordinates).
	 *
	 * @return string|bool Geo data or false on error.
	 */
	private static function get_result_bing_maps( $response, $return_type ) {
		// phpcs:ignore
		if ( isset( $response->resourceSets[0]->resources[0]->geocodePoints[0]->coordinates ) ) {
			if ( 'compact' === $return_type ) {
				// phpcs:ignore
				$lat = $response->resourceSets[0]->resources[0]->geocodePoints[0]->coordinates[0];
				// phpcs:ignore
				$lng = $response->resourceSets[0]->resources[0]->geocodePoints[0]->coordinates[1];

				return [
					'lat'     => $lat,
					'lng'     => $lng,
					'lat_rad' => $lat * ( pi() / 180 ),
					'lng_rad' => $lng * ( pi() / 180 ),
				];
			} else {
				return $response;
			}
		} else {
			return false;
		}
	} // get_result_bing_maps

	/**
	 * Check/return status information for a Bing Maps geocoding request.
	 *
	 * @since 0.5.1
	 *
	 * @param object $response Raw response object.
	 *
	 * @return string|bool Status or false on error.
	 */
	private static function get_status_bing_maps( $response ) {
		$status = '';
		// phpcs:disable
		if ( isset( $response->authenticationResultCode ) ) {
			$status .= 'Authentication: ' . $response->authenticationResultCode;
		}
		if ( isset( $response->errorDetails ) ) {
			if ( $status ) {
				$status .= ', ';
			}
			$status .= 'Error(s): ' . implode( $response->errorDetails );
		}
		// phpcs:enable

		return trim( $status ) ? '[Bing Maps] ' . $status : false;
	} // get_status_google_maps

	/**
	 * Parse and check a given country code list or alias string.
	 *
	 * @since 0.9
	 *
	 * @param string $countrycodes Comma separated code list or alias for a default code set.
	 *
	 * @return string|bool Parsed/Checked list or false on invalid strings.
	 */
	private static function parse_countrycodes( $countrycodes ) {
		if ( ! $countrycodes ) {
			return false;
		}

		// Replace default code sets if the related name is stated.
		switch ( $countrycodes ) {
			case 'germany_and_neighbors':
				$countrycodes = self::COUNTRYCODES_GERMANY_AND_NEIGHBORS;
				break;
			case 'eu':
				$countrycodes = self::COUNTRYCODES_EU;
				break;
			case 'europe':
				$countrycodes = self::COUNTRYCODES_EUROPE;
		}

		// Extract and recompile only valid string elements.
		$check_codes  = preg_match_all( '/[ ]?([a-z]{2})[ ]?,/', strtolower( $countrycodes ), $matches );
		$countrycodes = $check_codes > 0 ? implode( ',', $matches[1] ) : false;

		return $countrycodes;
	} // parse_countrycodes

	/**
	 * Validate a given key or array of keys of geocoding providers to use for
	 * a specific address.
	 *
	 * @since 1.8.22
	 *
	 * @param string|string[]|bool $use_providers Single provider key, array of keys
	 *     or false to use all available providers one by one.
	 *
	 * @return string[] Array of valid geocoding provider keys.
	 */
	private static function validate_providers( $use_providers ) {
		if ( empty( $use_providers ) ) {
			return self::$providers;
		}

		if ( is_string( $use_providers ) ) {
			$use_providers = [ $use_providers ];
		}

		$providers = [];
		foreach ( $use_providers as $provider ) {
			if ( isset( self::$providers[ $provider ] ) ) {
				$providers[ $provider ] = self::$providers[ $provider ];
			}
		}

		return ! empty( $providers ) ? $providers : self::$providers;
	} // validate_providers

} // Geo_Utils
