<?php
/**
 * Geo Class
 *
 * @package Page_Generator_Pro
 * @author WP Zinc
 */

/**
 * Stores latitude and longitude to Post ID relations
 * in its own database table for performance and Related Links
 * radius searches.
 *
 * @package Page_Generator_Pro
 * @author  WP Zinc
 * @version 2.3.6
 */
class Page_Generator_Pro_Geo {

	/**
	 * Holds the base object.
	 *
	 * @since   2.3.6
	 *
	 * @var     object
	 */
	public $base;

	/**
	 * Primary SQL Table
	 *
	 * @since   2.3.6
	 *
	 * @var     string
	 */
	public $table = 'page_generator_area_geo';

	/**
	 * Primary SQL Table Primary Key
	 *
	 * @since   2.3.6
	 *
	 * @var     string
	 */
	public $key = 'id';

	/**
	 * Constructor.
	 *
	 * @since   2.3.6
	 *
	 * @param   object $base    Base Plugin Class.
	 */
	public function __construct( $base ) {

		// Store base class.
		$this->base = $base;

	}

	/**
	 * Create database table on activation
	 *
	 * @since   2.3.6
	 *
	 * @global  $wpdb   WordPress DB Object.
	 */
	public function activate() {

		global $wpdb;

		// Enable error output if WP_DEBUG is enabled.
		$wpdb->show_errors = true;

		// Create database tables.
		$wpdb->query(
			"CREATE TABLE IF NOT EXISTS {$wpdb->prefix}page_generator_area_geo (
            `id` int(10) NOT NULL AUTO_INCREMENT,
            `post_id` int(10) NOT NULL,
            `group_id` int(10) NOT NULL DEFAULT 0,
            `latitude` decimal(9,6) NOT NULL,
            `longitude` decimal(9,6) NOT NULL,
            PRIMARY KEY `id` (`id`),
            KEY `post_id` (`post_id`),
            KEY `group_id` (`group_id`)
        ) {$wpdb->get_charset_collate()} AUTO_INCREMENT=1"
		);

	}

	/**
	 * Returns an array of all Post IDs that were generated by the optional Group ID
	 * within the given radius.
	 *
	 * @since   2.3.6
	 *
	 * @param   int         $post_id    Post ID.
	 * @param   float       $latitude   Starting Latitude.
	 * @param   float       $longitude  Starting Longitude.
	 * @param   int         $radius     Radius (Miles).
	 * @param   bool|string $order      Order (false|asc|desc).
	 * @param   bool|array  $group_ids  Group IDs (false | array).
	 * @return  bool|array              false | array (Post ID Keys => Distance from original Post ID)
	 */
	public function get_post_ids( $post_id, $latitude, $longitude, $radius = 5, $order = false, $group_ids = false ) {

		global $wpdb;

		// Check that the radius is a number.
		if ( ! is_numeric( $radius ) ) {
			$radius = 5;
		}

		// Fetch bounds.
		$bounds = $this->get_min_max_latitude_longitude_from_point(
			$latitude,
			$longitude,
			$radius
		);

		// Build query.
		$query = sprintf(
			'SELECT post_id, ( 3959 * acos( 
                cos( radians( %s ) ) * 
                cos( radians( latitude ) ) * 
                cos( radians( longitude ) - radians( %s ) ) +  sin( radians( %s ) ) * 
                sin( radians( latitude ) ) 
            ) ) AS distance
            FROM ' . $wpdb->prefix . $this->table . '
            WHERE latitude >= %s
            AND latitude <= %s
            AND longitude >= %s
            AND longitude <= %s
            AND post_id != %s',
			$latitude,
			$longitude,
			$latitude,
			$bounds['min']['lat'],
			$bounds['max']['lat'],
			$bounds['min']['lng'],
			$bounds['max']['lng'],
			$post_id
		);

		// Add Group ID constraint to query, if required.
		if ( $group_ids ) {
			$query .= sprintf(
				' AND group_id IN (%s)',
				implode( ',', $group_ids )
			);
		}

		// Add Distance Constraint.
		$query .= sprintf(
			' HAVING distance <= %s',
			$radius
		);

		// Add ORDER BY if required.
		if ( $order ) {
			$query .= ' ORDER BY distance ' . strtoupper( $order );
		}

		// Run query.
		$results = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		// Bail if no results.
		if ( ! is_array( $results ) || ! count( $results ) ) {
			return false;
		}

		// Build array of Post ID => Distance key/value pairs.
		$post_ids = array();
		foreach ( $results as $result ) {
			$post_ids[ $result->post_id ] = $result->distance;
		}

		// Return.
		return $post_ids;

	}

	/**
	 * Returns the latitude and longitude for the given Post ID
	 *
	 * @since   2.3.6
	 *
	 * @param   int $post_id    Post ID.
	 * @return  bool|array
	 */
	public function get( $post_id ) {

		global $wpdb;

		// Return result.
		return $wpdb->get_row(
			$wpdb->prepare(
				"SELECT latitude, longitude FROM {$wpdb->prefix}page_generator_area_geo WHERE post_id = %d LIMIT 1",
				$post_id
			),
			ARRAY_A
		);

	}

	/**
	 * Creates or updates a record to store the latitude and longitude
	 * for the given Post ID and Group ID.
	 *
	 * @since   2.3.6
	 *
	 * @param   int   $post_id    Post ID.
	 * @param   int   $group_id   Group ID.
	 * @param   float $latitude   Latitude.
	 * @param   float $longitude  Longitude.
	 * @return  WP_Error|bool
	 */
	public function update( $post_id, $group_id, $latitude, $longitude ) {

		global $wpdb;

		// Fetch latitude and longitude for the given Post ID.
		$latitude_longitude = $this->get( $post_id );

		// Update or Insert, depending on whether data was found.
		if ( ! $latitude_longitude ) {
			// Insert.
			$result = $wpdb->insert(
				$wpdb->prefix . $this->table,
				array(
					'post_id'   => $post_id,
					'group_id'  => $group_id,
					'latitude'  => $latitude,
					'longitude' => $longitude,
				),
				array(
					'%d',
					'%d',
					'%f',
					'%f',
				)
			);
		} else {
			// Update.
			$result = $wpdb->update(
				$wpdb->prefix . $this->table,
				array(
					'latitude'  => $latitude,
					'longitude' => $longitude,
				),
				array(
					'post_id'  => $post_id,
					'group_id' => $group_id,
				),
				array(
					'%f',
					'%f',
				),
				array(
					'%d',
					'%d',
				)
			);
		}

		// Return WP_Error if insert/update failed.
		if ( $result === false ) {
			return new WP_Error(
				'page_generator_pro_geo_update',
				sprintf(
					/* translators: Post ID */
					__( 'Could not update Latitude and Longitude for Post ID %s', 'page-generator-pro' ),
					$post_id
				)
			);
		}

		return true;

	}

	/**
	 * Deletes a record for the given Post ID.
	 *
	 * @since   2.3.6
	 *
	 * @param   int $post_id    Post ID.
	 * @return  WP_Error|bool
	 */
	public function delete( $post_id ) {

		global $wpdb;

		$result = $wpdb->delete(
			$wpdb->prefix . $this->table,
			array(
				'post_id' => $post_id,
			),
			array(
				'%d',
			)
		);

		// Return WP_Error if insert/update failed.
		if ( $result === false ) {
			return new WP_Error(
				'page_generator_pro_geo_delete',
				sprintf(
					/* translators: Post ID */
					__( 'Could not delete Latitude and Longitude for Post ID %s', 'page-generator-pro' ),
					$post_id
				)
			);
		}

		return true;

	}

	/**
	 * Deletes records for the given Post IDs.
	 *
	 * @since   2.3.6
	 *
	 * @param   array $post_ids   Post IDs.
	 * @return  WP_Error|bool
	 */
	public function delete_multiple( $post_ids ) {

		global $wpdb;

		$post_ids = implode( ',', $post_ids );

		$result = $wpdb->query( "DELETE FROM {$wpdb->prefix}page_generator_area_geo WHERE post_id IN {$post_ids}" ); // phpcs:ignore WordPress.DB.PreparedSQL

		// Return WP_Error if insert/update failed.
		if ( $result === false ) {
			return new WP_Error(
				'page_generator_pro_geo_delete_multiple',
				__( 'Could not delete Latitude and Longitude for Post IDs', 'page-generator-pro' )
			);
		}

		return true;

	}

	/**
	 * Helper method to determine if the given value is a valid latitude
	 *
	 * @since   2.3.6
	 *
	 * @param   string|int|float $value     Value.
	 * @return  bool                        Is Latitude
	 */
	public function is_latitude( $value ) {

		if ( empty( $value ) ) {
			return false;
		}

		if ( ! is_numeric( $value ) ) {
			return false;
		}

		if ( $value < -90 || $value > 90 ) {
			return false;
		}

		return true;

	}

	/**
	 * Helper method to determine if the given value is a valid longitude
	 *
	 * @since   2.3.6
	 *
	 * @param   string|int|float $value     Value.
	 * @return  bool                        Is Latitude
	 */
	public function is_longitude( $value ) {

		if ( empty( $value ) ) {
			return false;
		}

		if ( ! is_numeric( $value ) ) {
			return false;
		}

		if ( $value < -180 || $value > 180 ) {
			return false;
		}

		return true;

	}

	/**
	 * Returns the minimum and maximum latitude and longitudes based on our inputs and distance
	 *
	 * @since   2.3.6
	 *
	 * @param   float $latitude   Latitude of center point.
	 * @param   float $longitude  Longitude of center point.
	 * @param   int   $radius     Radius from center point.
	 * @return  array                   Min/Max Latitude and Longitudes
	 */
	private function get_min_max_latitude_longitude_from_point( $latitude, $longitude, $radius ) {

		$earth_radius_km = 6371.009;

		$distance = ( $radius * 1.60934 );

		$max_latitude = $latitude + rad2deg( $distance / $earth_radius_km );
		$min_latitude = $latitude - rad2deg( $distance / $earth_radius_km );

		$max_longitude = $longitude + rad2deg( $distance / $earth_radius_km / cos( deg2rad( $latitude ) ) );
		$min_longitude = $longitude - rad2deg( $distance / $earth_radius_km / cos( deg2rad( $latitude ) ) );

		// Return.
		return array(
			'min' => array(
				'lat' => $min_latitude,
				'lng' => $min_longitude,
			),
			'max' => array(
				'lat' => $max_latitude,
				'lng' => $max_longitude,
			),
		);

	}

}
