<?php
/**
 * Geo API
 *
 * @package Framework\Geo
 */

scb_register_table( 'app_geodata' );

add_action( 'appthemes_first_run', array( 'APP_Geo_Query', 'install' ), 9 );
add_action( 'deleted_post', 'appthemes_delete_coordinates', 10, 1 );

add_action( 'init', array( 'APP_Geo_Query', 'add_query_vars' ) );
add_action( 'parse_query', array( 'APP_Geo_Query', 'parse_query' ) );
add_filter( 'posts_clauses', array( 'APP_Geo_Query', 'posts_clauses' ), 10, 2 );


/**
 * Returns geo coordinates for post.
 *
 * @param int $post_id
 * @param bool $fallback_to_zero (optional)
 *
 * @return object
 */
function appthemes_get_coordinates( $post_id, $fallback_to_zero = true ) {
	global $wpdb;

	$coord = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->app_geodata WHERE post_id = %d", $post_id ) );

	if ( ! $coord && $fallback_to_zero ) {
		return (object) array( 'lat' => 0, 'lng' => 0 );
	}

	return $coord;
}

/**
 * Sets geo coordinates for post.
 *
 * @param int $post_id
 * @param float $lat
 * @param float $lng
 *
 * @return int|bool Boolean false on failure
 */
function appthemes_set_coordinates( $post_id, $lat, $lng ) {
	global $wpdb;

	$coord = appthemes_get_coordinates( $post_id, false );

	if ( ! $coord ) {
		return $wpdb->insert( $wpdb->app_geodata, compact( 'lat', 'lng', 'post_id' ) );
	} else {
		return $wpdb->update( $wpdb->app_geodata, compact( 'lat', 'lng' ), compact( 'post_id' ) );
	}
}

/**
 * Deletes geo coordinates for post.
 *
 * @param int $post_id
 *
 * @return int|bool Boolean false on failure
 */
function appthemes_delete_coordinates( $post_id ) {
	global $wpdb;

	return $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->app_geodata WHERE post_id = %d", $post_id ) );
}

/**
 * Enqueues scripts
 *
 * @param string $callback
 *
 * @return void
 */
function appthemes_enqueue_geo_scripts( $callback ) {
	extract( appthemes_geo_get_args() );

	$google_maps_url = ( is_ssl() ? 'https' : 'http' ) . '://maps.googleapis.com/maps/api/js';

	$params = array(
		'v' => 3,
		'sensor' => 'false',
		'region' => $region,
		'language' => $language,
		'callback' => $callback
	);

	if ( ! empty( $libraries ) ) {
		$params['libraries'] = implode( ',', (array) $libraries );
	}

	$google_maps_url = add_query_arg( $params, $google_maps_url );

	wp_enqueue_script( 'google-maps-api', $google_maps_url, array(), '3', true );
}

/**
 * Returns an array of component arguments
 *
 * @return array
 */
function appthemes_geo_get_args() {

	static $args = array();

	if ( ! current_theme_supports( 'app-geo' ) ) {
		return array();
	}

	if ( empty( $args ) ) {

		// numeric array, contains multiple sets of arguments
		// first item contains preferable set
		$args_sets = get_theme_support( 'app-geo' );

		if ( ! is_array( $args_sets ) ) {
			$args_sets = array();
		}

		foreach ( $args_sets as $args_set ) {
			foreach ( $args_set as $key => $arg ) {
				if ( ! isset( $args[ $key ] ) ) {
					$args[ $key ] = $arg;
				} elseif ( is_array( $arg ) ) {
					$args[ $key ] = array_merge_recursive( (array) $args[ $key ], $arg );
				}
			}
		}
	}

	return $args;
}

/**
 * Provides 'location' => 'San Francisco' and 'orderby' => 'distance' public query vars.
 */
class APP_Geo_Query {

	/**
	 * Creates database table for geo coordinates.
	 *
	 * @return void
	 */
	static function install() {
		scb_install_table( 'app_geodata', "
			post_id bigint(20) unsigned NOT NULL,
			lat decimal(10,6) NOT NULL,
			lng decimal(10,6) NOT NULL,
			PRIMARY KEY  (post_id)
		" );
	}

	/**
	 * Deletes database table for geo coordinates.
	 *
	 * @return void
	 */
	static function uninstall() {
		scb_uninstall_table( 'app_geodata' );
	}

	/**
	 * Calculates the distance between two points on the surface of an Earth-sized sphere
	 *
	 * @param float $lat_1
	 * @param float $lng_1
	 * @param float $lat_2
	 * @param float $lng_2
	 * @param string $unit
	 *
	 * @return float
	 */
	protected static function distance( $lat_1, $lng_1, $lat_2, $lng_2, $unit ) {
		$earth_radius = ('mi' == $unit) ? 3959 : 6371;

		$alpha    = ($lat_2 - $lat_1)/2;
		$beta     = ($lng_2 - $lng_1)/2;

		$a        = sin(deg2rad($alpha)) * sin(deg2rad($alpha)) +
		            cos(deg2rad($lat_1)) * cos(deg2rad($lat_2)) *
		            sin(deg2rad($beta)) * sin(deg2rad($beta));

		$distance = 2 * $earth_radius * asin(min(1, sqrt($a)));

		$distance = round( $distance, 4 );

		return $distance;
	}

	static function add_query_vars() {
		global $wp;

		$wp->add_query_var( 'location' );
		$wp->add_query_var( 'radius' );
	}


	static function parse_query( $wp_query ) {
		extract( appthemes_geo_get_args() );

		$location = trim( $wp_query->get( 'location' ) );
		if ( ! $location ) {
			return;
		}

		$wp_query->is_search = true;

		$smart_radius = false;

		$radius = is_numeric( $wp_query->get( 'radius' ) ) ? $wp_query->get( 'radius' ) : false;
		if ( ! $radius ) {
			$radius = ! empty( $default_radius ) && is_numeric( $default_radius ) ? $default_radius : false;
		}

		$args = array(
			'address' => urlencode( $location ),
			'sensor' => 'false',
			'region' => $region,
			'language' => $language,
		);

		$url = add_query_arg( $args, 'http://maps.googleapis.com/maps/api/geocode/json' );

		$transient_key = 'app_geo_' . md5( $url );

		if ( defined( 'WP_DEBUG' ) ) {
			$geo_coord = false;
		} else {
			$geo_coord = get_transient( $transient_key );
		}

		if ( ! $geo_coord ) {
			$response = wp_remote_get( $url );

			$geocode = json_decode( wp_remote_retrieve_body( $response ) );

			if ( ! $radius ) {
				if ( isset( $geocode->results[0]->geometry ) ) {
					$geometry = $geocode->results[0]->geometry;

					// bounds are not always returned, so fall back to viewport
					$bounds_type = isset( $geometry->bounds ) ? 'bounds' : 'viewport';

					$distance_a = self::distance(
						$geometry->{$bounds_type}->northeast->lat,
						$geometry->{$bounds_type}->southwest->lng,
						$geometry->{$bounds_type}->southwest->lat,
						$geometry->{$bounds_type}->southwest->lng,
						$unit
					);

					$distance_b = self::distance(
						$geometry->{$bounds_type}->northeast->lat,
						$geometry->{$bounds_type}->northeast->lng,
						$geometry->{$bounds_type}->northeast->lat,
						$geometry->{$bounds_type}->southwest->lng,
						$unit
					);

					// Find the longest distance, so we can make a square that covers the full area.
					$longer_distance = $distance_a > $distance_b ? $distance_a : $distance_b;

					// Make a square out of the non-square bounds.
					$distance_c = sqrt( pow( $longer_distance, 2 ) * 2 );

					/*
					 * Since distance is a diameter, and since the bounds are a square,
					 * use half the "diameter" of the square to make a radius (circle)
					 * so that it covers the area of the square bounds.
					 */
					$radius = $distance_c / 2;

					$smart_radius = true;
				}
			}

			if ( $geocode && 'OK' == $geocode->status ) {
				$geo_coord = array(
					'lat' => $geocode->results[0]->geometry->location->lat,
					'lng' => $geocode->results[0]->geometry->location->lng,
				);
				set_transient( $transient_key, $geo_coord, 60*60*24*7 ); // Cache for a week
			}
		}

		// Final fallback just in case $wp_query->get( 'radius' ) and default_radius are not set and smart_radius fails due to API not returning a bounds/viewport.
		if ( ! $radius ) {
			$radius = 50;
		}

		if ( $geo_coord ) {
			$wp_query->set( 'app_geo_query', apply_filters( 'appthemes_geo_query', array(
				'lat' => $geo_coord['lat'],
				'lng' => $geo_coord['lng'],
				'rad' => $radius,
				'smart_radius' => $smart_radius,
			) ) );
		} else {
			// Fall back to basic string matching
			$wp_query->set( 'meta_query', array(
				array(
					'key' => 'address',
					'value' => $location,
					'compare' => 'LIKE'
				)
			) );
		}
	}

	static function posts_clauses( $clauses, $wp_query ) {
		extract( appthemes_geo_get_args() );

		global $wpdb;

		$geo_query = $wp_query->get( 'app_geo_query' );

		if ( ! $geo_query ) {
			return $clauses;
		}

		extract( $geo_query, EXTR_SKIP );

		$R = 'mi' == $unit ? 3959 : 6371;

		$clauses['join'] .= $wpdb->prepare( " INNER JOIN (
			SELECT post_id, ( %d * acos( cos( radians(%f) ) * cos( radians(lat) ) * cos( radians(lng) - radians(%f) ) + sin( radians(%f) ) * sin( radians(lat) ) ) ) AS distance FROM $wpdb->app_geodata
		) as distances ON ($wpdb->posts.ID = distances.post_id)
		", $R, $lat, $lng, $lat );

		$clauses['where'] .= $wpdb->prepare( " AND distance < %f", (float) $rad );

		if ( 'distance' == $wp_query->get( 'orderby' ) ) {
			$clauses['orderby'] = 'distance ' . ( 'DESC' == strtoupper( $wp_query->get( 'order' ) ) ? 'DESC' : 'ASC' );
		}

		return $clauses;
	}
}
