<?php
/**
 * WPJB Geolocation main search query class.
 *
 * The class modfies the search form ( filters ) and search results template files
 *
 * And add the geolocation features to them.
 *
 * The class also responsiable for modifying the search query and performs
 *
 * proximity search query.
 *
 * @package wpjm-jobs-geolocation.
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 *  GJM Form Query Class
 */
class GJM_Form_Query_Class {

	/**
	 * GJM settings
	 *
	 * @var [type]
	 */
	public $settings;

	/**
	 * Prefix
	 *
	 * @var string
	 */
	public $prefix = 'gjm';

	/**
	 * Form filters
	 *
	 * @var [type]
	 */
	public $filters;

	/**
	 * Locations data pulled from the locations table.
	 *
	 * @var array
	 */
	public $locations_data = array();

	/**
	 * Holder for the locations data that passe to the map.
	 *
	 * @var array
	 */
	public $map_locations = array();

	/**
	 * Post type to query.
	 *
	 * @var string
	 */
	public $post_type_query = 'job_listing';

	/**
	 * Query posts without location.
	 *
	 * When no address entered in the Location field of the search form.
	 *
	 * @var boolean
	 */
	public $query_posts_without_location = true;

	/**
	 * When order by featured and then distance.
	 *
	 * @var boolean
	 */
	public $orderby_featured_distance = false;

	/**
	 * When need to search for state or country in meta fields.
	 *
	 * @var boolean
	 */
	public $search_boundries_in_meta = false;

	/**
	 * Name of active theme.
	 *
	 * @var string
	 */
	public $active_theme = '';

	/**
	 * Boundries search?
	 *
	 * @var boolean
	 */
	public static $enable_boundaries_search = true;

	/**
	 * __construct
	 */
	public function __construct() {

		$this->settings     = get_option( 'gjm_options' );
		$this->active_theme = get_option( 'template' );

		// gjm shortcode attributes.
		add_filter( 'job_manager_output_jobs_defaults', array( $this, 'default_atts' ) );

		// modify the search form.
		add_action( 'job_manager_job_filters_search_jobs_end', array( $this, 'extend_search_form' ), 8 );

		// modify the search query based on location.
		add_filter( 'get_job_listings_query_args', array( $this, 'search_query' ), 99 );

		// add distance to search results of Listable theme.
		add_action( 'listable_job_listing_card_image_top', array( $this, 'listable_job_distance' ), 99 );

		// add distance to search results of Listable theme.
		add_action( 'listify_content_job_listing_before', array( $this, 'listify_job_distance' ), 99 );

		// add distance to search results of WorkScout theme.
		add_action( 'workscout_job_listing_meta_start', array( $this, 'the_distance' ), 99 );

		add_filter( 'gt3_skip_geolocation_formatted_address', array( $this, 'listing_easy_job_distance' ), 99 );

		// add distance to results for other themes.
		add_action( 'job_listing_meta_end', array( $this, 'the_distance' ), 99 );

		// append map to results.
		add_action( 'job_manager_job_filters_after', array( $this, 'results_map' ) );

		// create shortcode for map holder.
		// add_shortcode( 'gjm_results_map', array( $this, 'results_map_shortcode' ) );
		// add_shortcode( 'gjm_form_geolocation_features', array( $this, 'generate_geolocation_features' ) );

		// enable indeed features is extension activated.
		if ( class_exists( 'WP_Job_Manager_Indeed_Integration' ) ) {
			add_action( 'gjm_modify_search_form_element', array( $this, 'enable_indeed_features' ), 15, 2 );
			add_filter( 'job_manager_indeed_get_jobs_args', array( $this, 'modify_indeed_radius' ), 40 );
		}
	}

	/**
	 * GJM default shortcode attributes.
	 *
	 * @return modified shortcode attributes.
	 *
	 * @since 1.0
	 */
	public function shortcode_atts() {

		return array(
			$this->prefix . '_element_id'      => 'job_map_listing',
			$this->prefix . '_use'             => 0,
			$this->prefix . '_orderby'         => 'distance,featured,title,date',
			$this->prefix . '_radius'          => '5,10,15,25,50,100',
			$this->prefix . '_units'           => 'both',
			$this->prefix . '_distance'        => 1,
			$this->prefix . '_auto_locator'    => 1,
			$this->prefix . '_locator_button'  => 1,
			$this->prefix . '_map'             => 1,
			$this->prefix . '_map_width'       => '100%',
			$this->prefix . '_map_height'      => '250px',
			$this->prefix . '_map_type'        => 'ROADMAP',
			$this->prefix . '_scroll_wheel'    => 1,
			$this->prefix . '_group_markers'   => 'markers_clusterer',
			$this->prefix . '_user_marker'     => 'https://maps.google.com/mapfiles/ms/icons/blue-dot.png',
			$this->prefix . '_location_marker' => 'https://maps.google.com/mapfiles/ms/icons/red-dot.png',
			$this->prefix . '_clusters_path'   => 'https://raw.githubusercontent.com/googlemaps/js-marker-clusterer/gh-pages/images/m',
			$this->prefix . '_zoom_level'      => 'auto',
			$this->prefix . '_max_zoom_level'  => '',
		);
	}

	/**
	 * Check if current page is preset to enable geolocation features
	 *
	 * @return boolean
	 */
	public function check_preset_page() {

		if ( empty( $this->settings['search_form'][ $this->prefix . '_enabled_pages' ] ) || ! is_array( $this->settings['search_form'][ $this->prefix . '_enabled_pages' ] ) ) {
			return false;
		}

		global $wp_query;

		$preset_pages = $this->settings['search_form'][ $this->prefix . '_enabled_pages' ];

		// get current page ID.
		$page_id = $wp_query->get_queried_object_id();

		// Look for and enable the geolocation features on preset pages.
		if ( in_array( $page_id, $preset_pages ) || ( is_front_page() && in_array( 'front_page', $preset_pages, true ) ) ) {

			return true;
		}

		return false;
	}

	/**
	 * Merge GJM and WP Job Manager shortcode attributes
	 *
	 * @param array $args arry of arguments.
	 *
	 * @since 1.0
	 *
	 * @author Eyal Fitoussi
	 */
	public function default_atts( $args ) {
		return array_merge( $args, $this->shortcode_atts() );
	}

	/**
	 * Generate the geolocation field using a shortcode.
	 *
	 * This can be used to enable the geolocation features
	 *
	 * in a custom search form that does not use the [jobs] shortcode.
	 *
	 * Use the shortcode [gjm_form_geolocation_features] at the end of the <form> tag
	 *
	 * of the custom search form.
	 *
	 * @param  array $atts shortcode attributes.
	 *
	 * @return [type]       [description]
	 */
	public function generate_geolocation_features( $atts = array() ) {

		$atts = shortcode_atts( $this->shortcode_atts(), $atts );

		return $this->get_search_form_filters( $atts );
	}

	/**
	 * Results map.
	 *
	 * @param  array $atts shortcode attributes.
	 *
	 * @return [type]       [description]
	 */
	public function results_map( $atts ) {

		if ( empty( $this->filters[ $this->prefix . '_map' ] ) || 1 !== absint( $this->filters[ $this->prefix . '_map' ] ) ) {
			return;
		}
		
		// Pass the map arguments to generate the map element.
		$args = array(
			'map_id'         => $this->filters[ $this->prefix . '_element_id' ],
			'prefix'         => $this->prefix,
			'init_map_show'  => false,
			'map_width'      => $this->filters[ $this->prefix . '_map_width' ],
			'map_height'     => $this->filters[ $this->prefix . '_map_height' ],
			'user_marker'    => $this->filters[ $this->prefix . '_user_marker' ],
			'zoom_level'     => $this->filters[ $this->prefix . '_zoom_level' ],
			'max_zoom_level' => $this->filters[ $this->prefix . '_max_zoom_level' ],
			'map_type'       => $this->filters[ $this->prefix . '_map_type' ],
			'scrollwheel'    => $this->filters[ $this->prefix . '_scroll_wheel' ],
			'group_markers'  => $this->filters[ $this->prefix . '_group_markers' ],
			'clusters_path'  => $this->filters[ $this->prefix . '_clusters_path' ],
		);

		// get the map element.
		echo GJM_Maps::get_map_element( $args ); // WPCS: XSS ok.
	}

	/**
	 * Locator button element
	 *
	 * @since 1.2
	 *
	 * @author Eyal Fitoussi
	 */
	protected function locator_button() {

		if ( empty( $this->filters[ $this->prefix . '_locator_button' ] ) ) {
			return;
		}

		$locator = '<i class="' . $this->prefix . '-filter ' . $this->prefix . '-locator-btn gjm-icon-target-1" title="' . __( 'Get your current position', 'pointify' ) . '" style="display:none;"></i>';

		return apply_filters( $this->prefix . '_locator_button', $locator, $this->filters );
	}

	/**
	 * Radius filter to display in search form
	 *
	 * When multiple values pass, a dropdown select box will be displayed. Otherwise, when single value passes,
	 * it will be a default value in hidden field.
	 *
	 * @since 1.0
	 *
	 * @author Eyal Fitoussi
	 */
	protected function filters_radius() {

		if (isset($this->filters[ $this->prefix . '_radius' ])) {
			$radius = explode( ',', $this->filters[ $this->prefix . '_radius' ] );
		}	
		$output = '';

		// display dropdown.
		$radius ='';
		if ( count((is_countable($radius)?$radius:[]) ) > 1 ) {

			$default_value = apply_filters( 'gjm_default_radius_value', end( $radius ), $radius, $this->filters );

			// Displace dropdown.
			$output .= '<div class="' . $this->prefix . '-filter-wrapper ' . $this->prefix . '-radius-wrapper ' . $this->prefix . '-dropdown-filter ' . $this->prefix . '-filters-count-' . esc_attr( $this->filters['filters_count'] ) . '">';
			$output .= '<select name="radius" class="' . $this->prefix . '-filter" id="' . $this->prefix . '-radius">';
			$output .= '<option value="' . esc_attr( $default_value ) . '">';

			// set the first option of the dropdown.
			if ( 'imperial' === $this->filters[ $this->prefix . '_units' ] ) {

				$output .= $this->labels['miles'];

			} elseif ( 'metric' === $this->filters[ $this->prefix . '_units' ] ) {

				$output .= $this->labels['kilometers'];

			} else {

				$output .= $this->labels['within'];
			}

			$output .= '</option>';

			$default_value = ! empty( $_GET['radius'] ) ? wp_unslash( $_GET['radius'] ) : ''; // WPCS: CSRF ok, sanitization ok.

			foreach ( $radius as $value ) {

				$value    = esc_attr( $value );
				$selected = ( ! empty( $default_value ) && $default_value === $value ) ? 'selected="selected"' : '';

				$output .= '<option value="' . $value . '" ' . $selected . '>' . $value . '</option>';
			}

			$output .= '</select>';
			$output .= '</div>';

		} else {
			// display hidden default value.
			if (is_array($radius)) {
				$output .= '<input type="hidden" id="' . $this->prefix . '-radius" name="radius" value="' . esc_attr( end( $radius ) ) . '" />';
			}
		}

		return apply_filters( $this->prefix . '_form_radius_filter', $output, $radius, $this );
	}

	/**
	 * Units filter for search form
	 *
	 * Will display a dropdown menu when displaying both units. Otherwise, the default units value will be a hidden field.
	 *
	 * @since  1.0
	 *
	 * @author Eyal Fitoussi
	 */
	protected function filters_units() {

		$output = '';

		// display dropdown.
		if (isset($this->filters[ $this->prefix . '_units' ])) {
			if ( 'imperial' === $this->filters[ $this->prefix . '_units' ] || 'metric' === $this->filters[ $this->prefix . '_units' ] ) {

				// display hidden field.
				$output .= '<input type="hidden" id="' . $this->prefix . '-units" name="' . $this->prefix . '_units" value="' . esc_attr( $this->filters[ $this->prefix . '_units' ] ) . '" />';

			} else {

				$selected = ( ! empty( $_GET['units'] ) && 'metric' === $_GET['units'] ) ? 'selected="selected"' : ''; // WPCS: CSRF ok, sanitization ok.

				// display dropdown.
				$output .= '<div class="' . $this->prefix . '-filter-wrapper ' . $this->prefix . '-units-wrapper ' . $this->prefix . '-dropdown-filter ' . $this->prefix . '-filters-count-' . esc_attr( $this->filters['filters_count'] ) . '">';
				$output .= '<select name="' . $this->prefix . '_units" class="' . $this->prefix . '-filter" id="' . $this->prefix . '-units">';
				$output .= '<option selected="selected" value="imperial">' . esc_attr( $this->labels['miles'] ) . '</option>';
				$output .= '<option ' . $selected . ' value="metric">' . esc_attr( $this->labels['kilometers'] ) . '</option>';
				$output .= '</select>';
				$output .= '</div>';
			}
		}
		return apply_filters( $this->prefix . '_form_units_filter', $output, $this );
	}

	/**
	 * Radius filter to display in search form
	 *
	 * When multiple values pass, a dropdown select box will be displayed. Otherwise, when single value passes,
	 * it will be a default value in hidden field.
	 *
	 * @since  1.0
	 *
	 * @author Eyal Fitoussi
	 */
	protected function filters_sort() {

		if (isset($this->filters[ $this->prefix . '_orderby' ])) {
			$orderby = explode( ',', str_replace( ' ', '', $this->filters[ $this->prefix . '_orderby' ] ) );
		}	
		$output  = '';

		// display dropdown.
		$orderby='';
		if ( count( (is_countable($orderby)?$orderby:[]) ) > 1 ) {

			// display dropdown.
			$output .= '<div class="' . $this->prefix . '-filter-wrapper ' . $this->prefix . '-orderby-wrapper ' . $this->prefix . '-dropdown-filter ' . $this->prefix . '-filters-count-' . esc_attr( $this->filters['filters_count'] ) . '">';
			$output .= '<select name="' . $this->prefix . '_orderby" class="' . $this->prefix . '-filter" id="' . $this->prefix . '-orderby">';
			$output .= '<option class="orderby-first-option" value="">' . esc_attr( $this->labels['orderby_filters']['label'] ) . '</option>';

			$val_count = 1;

			$allowed_values = array(
				'distance',
				'title',
				'featured',
				'date',
				'modified',
				'ID',
				'parent',
				'rand',
				'name',
			);

			$output .= '</option>';

			$default_value = ! empty( $_GET['gjm_orderby'] ) ? esc_attr( $_GET['gjm_orderby'] ) : ''; // WPCS: CSRF ok, sanitization ok.

			foreach ( $orderby as $value ) {

				if ( in_array( $value, $allowed_values ) ) {

					$selected = ( ! empty( $default_value ) && $default_value === $value ) ? 'selected="selected"' : '';

					$output .= '<option ' . $selected . ' value="' . esc_attr( $value ) . '" class="' . $this->prefix . '-orderby-value-' . $val_count . '">' . esc_attr( $this->labels['orderby_filters'][ $value ] ) . '</option>';
				}

				$val_count++;
			}

			$output .= '</select>';
			$output .= '</div>';

		} else {

			// display hidden default value.
			if (is_array($orderby)) {
				$output .= '<input type="hidden" id="' . $this->prefix . '-orderby" name="' . $this->prefix . '_orderby" value="' . esc_attr( reset( $orderby ) ) . '" />';
			}
		}

		return apply_filters( $this->prefix . '_form_orderby_filter', $output, $orderby, $this );
	}

	/**
	 * Generate geolocation filters elements
	 *
	 * @return [type] [description]
	 */
	public function output_form_filters() {

		// Field counter.
		$this->filters['filters_count'] = 0;

		$listing_easy_home = false;

		// We dont need the filters wrapper in the home page of the Listing Easy theme.
		if ( 'listingeasy' === $this->active_theme && is_front_page() ) {
			$listing_easy_home = true;
		}

		// look for radius dropdown.
		if (isset($this->filters[ $this->prefix . '_radius' ])) {
			if ( count( explode( ',', $this->filters[ $this->prefix . '_radius' ] ) ) > 1 ) {
				$this->filters['filters_count']++;
			}
		}
		// look for orderby dropdown.
		if ( isset( $this->filters[ $this->prefix . '_orderby' ] ) && count( explode( ',', $this->filters[ $this->prefix . '_orderby' ] ) ) > 1 ) {
			$this->filters['filters_count']++;
		}

		// look for units dropdown.
		if (isset($this->filters[ $this->prefix . '_units' ])) {
			if ( ! in_array( $this->filters[ $this->prefix . '_units' ], array( 'imperial', 'metric' ), true ) ) {
				$this->filters['filters_count']++;
			}
		}

		$output = array();

		// we need this wrapper only when there is at aleast one filter.
		if ( $this->filters['filters_count'] > 0 && ! $listing_easy_home ) {
			$output['start'] = '<div class="' . $this->prefix . '-filters-wrapper" style="display:none;">';
		}

		// add radius filter.
		$output['radius'] = self::filters_radius();

		// add units filter.
		$output['units'] = self::filters_units();

		// add sort by filter.
		$output['orderby'] = self::filters_sort();

		if ( $this->filters['filters_count'] > 0 && ! $listing_easy_home ) {
			$output['end'] = '</div>';
		}

		$output = apply_filters( $this->prefix . '_form_filters_output', $output, $this->filters );

		// display fields.
		return implode( ' ', $output );
	}

	/**
	 * Enable indeed features
	 *
	 * @param  string $prefix prefix.
	 *
	 * @param  array  $atts   arguments.
	 */
	public function enable_indeed_features( $prefix, $atts ) {

		// enable indeed integration.
		if ( class_exists( 'WP_Job_Manager_Indeed_Integration' ) && get_option( 'job_manager_indeed_enable_backfill', 1 ) ) {

			$indeed_map_icon = apply_filters( 'gjm_indeed_map_icon', $atts[ $prefix . '_location_marker' ], $prefix, $atts );

			echo '<div style="display:none" class="indeed-enabled" data-map_icon="' . esc_url( $indeed_map_icon ) . '" data-distance="' . esc_attr( $atts[ $prefix . '_distance' ] ) . '"></div>';
		}
	}

	/**
	 * Modify indeed radius
	 *
	 * @param  array $args arguments.
	 *
	 * @return [type]       [description]
	 */
	public function modify_indeed_radius( $args ) {

		$args['radius'] = $this->filters['radius'];

		return $args;
	}

	/**
	 * Generate the geolocation elements to add to the search form.
	 *
	 * @since  1.0
	 *
	 * @param  array $atts arguments.
	 *
	 * @author Eyal Fitoussi
	 */
	 
	public $labels;
	public function get_search_form_filters( $atts = array() ) {

		$this->labels = gjm_labels();
		$this->prefix = esc_attr( $this->prefix );

		if ( empty( $atts ) ) {
			$atts = $this->shortcode_atts();
		}

		// look for preset pages and dynamically enable the geo feature if needed.
		if ( $this->check_preset_page() ) {

			$atts[ $this->prefix . '_use' ] = 2;

		} elseif ( empty( $atts[ $this->prefix . '_use' ] ) ) {

			$atts[ $this->prefix . '_use' ] = 2;
		}

		$output = '';

		// When using shotcode attributes.
		if ( 1 === absint( $atts[ $this->prefix . '_use' ] ) ) {

			// get settings from shortcode attrs.
			$this->filters = $atts;

			$output .= '<input type="hidden" class="gjm_map url-disabled" name="' . $this->prefix . '_map" value="' . esc_attr( $this->filters[ $this->prefix . '_map' ] ) . '" />';

			// when using the options set in the admin.
		} elseif ( 2 === absint( $atts[ $this->prefix . '_use' ] ) ) {

			if (isset($this->settings['search_form'])) {
				$this->filters = wp_parse_args( $this->settings['search_form'], $atts );
			}
			// generate random element ID.
			$this->filters[ $this->prefix . '_element_id' ] = 'job_map_listing';

		} else {

			return;
		}

		// load stylsheet.
		wp_enqueue_style( 'gjm-frontend-style' );

		// add hidden locator button that will dynamically append into the address field.
		$latitude  = ! empty( $_GET['latitude'] ) ? esc_attr( urldecode( $_GET['latitude'] ) ) : ''; // WPCS: CSRF ok, sanitization ok.
		$longitude = ! empty( $_GET['longitude'] ) ? esc_attr( urldecode( $_GET['longitude'] ) ) : ''; // WPCS: CSRF ok, sanitization ok.
		$state     = ! empty( $_GET['state'] ) ? esc_attr( urldecode( $_GET['state'] ) ) : ''; // WPCS: CSRF ok, sanitization ok.
		$country   = ! empty( $_GET['country'] ) ? esc_attr( urldecode( $_GET['country'] ) ) : ''; // WPCS: CSRF ok, sanitization ok.

		if ( ! defined( 'DOING_AJAX' ) && ! empty( $_GET['search_location'] ) && ( empty( $latitude ) || empty( $longitude ) ) ) { // WPCS: CSRF ok, sanitization ok.

			$location = WP_Job_Manager_Geocode::get_location_data( $_GET['search_location'] ); // WPCS: CSRF ok, sanitization ok.

			if ( is_array( $location ) && ! is_wp_error( $location ) && ! empty( $location['lat'] ) && ! empty( $location['long'] ) ) {

				$latitude  = $location['lat'];
				$longitude = $location['long'];

				if ( empty( $location['street'] ) && empty( $location['city'] ) && empty( $location['postcode'] ) ) {

					if ( empty( $state ) ) {
						$state = $location['state_short'];
					}

					if ( empty( $country ) ) {
						$country = $location['country_short'];
					}
				}
			}
		}

		// hidden fields to hold some values.
		$output .= '<div class="gjm-hidden-fields" style="display:none">';
		$output .= '<input type="hidden" class="gjm_use url-disabled" name="' . $this->prefix . '_use" value="' . esc_attr( $atts[ $this->prefix . '_use' ] ) . '" />';
		$output .= '<input type="hidden" class="gjm_element_id url-disabled" id="' . $this->prefix . '_element_id" name="' . $this->prefix . '_element_id" value="' . $this->filters[ $this->prefix . '_element_id' ] . '" />';
		$output .= '<input type="hidden" class="gjm-lat" name="latitude" value="' . $latitude . '" />';
		$output .= '<input type="hidden" class="gjm-lng" name="longitude" value="' . $longitude . '" />';
		$output .= '<input type="hidden" class="gjm-state" name="state" value="' . $state . '" />';
		$output .= '<input type="hidden" class="gjm-country" name="country" value="' . $country . '" />';
		$output .= '<input type="hidden" class="gjm-prev-address url-disabled" name="prev_address" value="" />';

		if (isset($this->filters[ $this->prefix . '_location_marker' ])) {
			$output .= '<input type="hidden" class="gjm-location-marker url-disabled" name="' . $this->prefix . '_location_marker" value="' . esc_attr( $this->filters[ $this->prefix . '_location_marker' ] ) . '" />';
		}
		$output .= '</div>';

		$autolocator  = 'disabled';
		$locator_btn  = 'disabled';
		$autocomplete = 'disabled';
		$results_type = 'geocode';
		$country      = '';

		// enable auto-locator feature.
		if ( ! empty( $this->filters[ $this->prefix . '_auto_locator' ] ) ) {
			$autolocator = 'disabled';
		}

		// enable locator button feature.
		if ( ! empty( $this->filters[ $this->prefix . '_locator_button' ] ) ) {
			$locator_btn = 'enabled';
		}

		$api_language     = ! empty( $this->settings['general_settings']['gjm_language'] ) ? $this->settings['general_settings']['gjm_language'] : 'en';
		$api_region       = ! empty( $this->settings['general_settings']['gjm_region'] ) ? $this->settings['general_settings']['gjm_region'] : 'us';
		$save_autolocator = apply_filters( 'gjm_save_autolocator', true, $this->filters, $this ) ? '1' : '0';

		$output .= '<div style="display:none" class="gjm-enabled" data-id="' . $this->filters[ $this->prefix . '_element_id' ] . '" data-prefix="' . $this->prefix . '" data-autocomplete="' . $autocomplete . '" data-results_type="' . $results_type . '" data-country="' . $country . '" data-autolocator="' . $autolocator . '" data-locator_button="' . $locator_btn . '" data-save_autolocator="' . $save_autolocator . '" data-api_region="' . esc_attr( $api_region ) . '" data-api_language="' . esc_attr( $api_language ) . '" data-locator_btn_title="' . __( 'Get your current position', 'pointify' ) . '"></div>';

		// output filters in the form.
		$output .= $this->output_form_filters();

		// append data to the form.
		do_action( 'gjm_modify_search_form_element', $this->prefix, $this->filters );

		GJM_Init::enqueue_google_maps_api();

		if ( ! wp_script_is( 'gjm', 'enqueued' ) ) {
			wp_enqueue_script( 'gjm' );
		}
		return $output;
	}

	/**
	 * Extend search form
	 *
	 * @param  array $atts arguments.
	 */
	public function extend_search_form( $atts = array() ) {
		echo $this->get_search_form_filters( $atts ); // WPCS: XSS ok.
	}

	/**
	 * Get locations from the geolocation database based on address and radius.
	 *
	 * This can be used to pull locations data and pass them into the WP_Query
	 *
	 * using the "includes" argument.
	 *
	 * @param  array $query_args origina lquery args.
	 *
	 * @return [type]             [description]
	 */
	public static function get_locations_data( $query_args = array() ) {

		// default values.
		$query_args = wp_parse_args(
			$query_args,
			array(
				'post_type' => 'job_listing',
				'lat'       => false,
				'lng'       => false,
				'radius'    => false,
				'units'     => 'imperial',
				'state'     => false,
				'country'   => false,
			)
		);

		$locations_data = false;

		// if WP Job Manager internal cache is enabled, we can use that to save the location queries.
		$cache_enabled = apply_filters( 'get_job_listings_cache_results', true );

		if ( $cache_enabled ) {

			// generate query hash for cache.
			$hash            = md5( wp_json_encode( $query_args ) );
			$query_args_hash = 'jm_' . $hash . WP_Job_Manager_Cache_Helper::get_transient_version( 'get_job_listings' );

			// look for saved query in transient.
			$locations_data = get_transient( $query_args_hash );
		}

		// if no query found in cache, we generate one.
		if ( false === $locations_data ) {

			// Get earth radius based on units.
			if ( 'imperial' === $query_args['units'] ) {
				$earth_radius = 3959;
				$units        = 'mi';
			} else {
				$earth_radius = 6371;
				$units        = 'km';
			}

			global $wpdb;

			$clauses['select']   = 'SELECT';
			$clauses['fields']   = "gjmLocations.post_id, gjmLocations.formatted_address, gjmLocations.address, gjmLocations.lat, gjmLocations.`long`, gjmLocations.`long` as lng, '{$units}' AS units";
			$clauses['distance'] = '';
			$clauses['from']     = "FROM {$wpdb->prefix}places_locator gjmLocations";
			$clauses['where']    = $wpdb->prepare( 'WHERE post_type = %s', $query_args['post_type'] ); // WPCS: unprepared SQL ok.
			$clauses['having']   = '';
			$clauses['orderby']  = '';

			// When searchin boundaries of a state.
			self::$enable_boundaries_search = apply_filters( 'gjm_search_within_boundaries', self::$enable_boundaries_search );

			if ( self::$enable_boundaries_search && ! empty( $query_args['state'] ) ) {

				$clauses['where'] .= $wpdb->prepare( ' AND gjmLocations.state = %s', $query_args['state'] );

				// When searchin boundaries of a country.
			} elseif ( self::$enable_boundaries_search && ! empty( $query_args['country'] ) ) {

				$clauses['where'] .= $wpdb->prepare( ' AND gjmLocations.country = %s ', $query_args['country'] );

				// otherwise, if coords exist we do proximity search.
			} elseif ( false !== $query_args['lat'] && false !== $query_args['lng'] ) {

				$clauses['distance'] = $wpdb->prepare(
					', 
	        		ROUND( %d * acos( cos( radians( %s ) ) * cos( radians( gjmLocations.lat ) ) * cos( radians( gjmLocations.long ) - radians( %s ) ) + sin( radians( %s ) ) * sin( radians( gjmLocations.lat ) ) ),1 ) AS distance',
					array(
						$earth_radius,
						$query_args['lat'],
						$query_args['lng'],
						$query_args['lat'],
					)
				);

				// make sure we pass only numeric or decimal as radius.
				if ( ! empty( $query_args['radius'] ) && is_numeric( $query_args['radius'] ) ) {
					$clauses['having'] = $wpdb->prepare( 'HAVING distance <= %s OR distance IS NULL', $query_args['radius'] );
				}

				$clauses['orderby'] = 'ORDER BY distance';
			}

			// query the locations.
			$locations = $wpdb->get_results(
				implode( ' ', apply_filters( 'gjm_get_locations_query_clauses', $clauses, $query_args['post_type'] ) )
			); // WPCS: db call ok, cache ok, unprepared SQL ok.

			$locations_data = array(
				'posts_id' => array(),
				'data'     => array(),
			);

			// locations found ?
			if ( ! empty( $locations ) ) {

				// modify the locations query.
				foreach ( $locations as $value ) {

					// collect posts id into an array to pass into the WP_Query.
					$locations_data['posts_id'][]              = $value->post_id;
					$locations_data['data'][ $value->post_id ] = $value;
				}
			}

			// set new query in transient only if cache enabled.
			if ( $cache_enabled ) {
				set_transient( $query_args_hash, $locations_data, DAY_IN_SECONDS );
			}
		}

		return $locations_data;
	}

	/**
	 * Modify wp_query clauses by proxmity.
	 *
	 * @param array $clauses query clauses.
	 *
	 * @param array $query   search query args.
	 *
	 * @return modified clauses
	 */
	public function query_clauses( $clauses, $query ) {

		global $wpdb;

		// add the location db fields to the query.
		$clauses['fields'] .= ', gjmLocations.lat, gjmLocations.long, gjmLocations.street, gjmLocations.city, gjmLocations.state, gjmLocations.state_long, gjmLocations.country, gjmLocations.country_long, gjmLocations.zipcode, gjmLocations.address, gjmLocations.formatted_address';

		$address_filters  = '';
		$boundries_search = false;

		// Check if only state entered.
		if ( self::$enable_boundaries_search && $this->filters['doing_boundries_search'] ) {

			$boundries_search = true;

			if ( ! $this->search_boundries_in_meta ) {

				if ( ! empty( $this->filters['state'] ) ) {
					$address_filters .= $wpdb->prepare( ' AND gjmLocations.state = %s', $this->filters['state'] );
				}

				if ( ! empty( $this->filters['country'] ) ) {
					$address_filters .= $wpdb->prepare( ' AND gjmLocations.country = %s ', $this->filters['country'] );
				}
			}
		}

		// when address provided, and not filtering based on boundries, we will do proximity search.
		if ( ! $boundries_search && ! empty( $this->filters['user_location']['lat'] ) && ! empty( $this->filters['user_location']['lng'] ) ) {

			// generate some radius/units data.
			if ( in_array( $this->filters[ $this->prefix . '_units' ], array( 'imperial', 3959, 'miles', '3959s' ), true ) ) {
				$earth_radius = 3959;
				$units        = 'mi';
				$degree       = 69.0;
			} else {
				$earth_radius = 6371;
				$units        = 'km';
				$degree       = 111.045;
			}

			// add units to locations data.
			$clauses['fields'] .= ", '{$units}' AS units";

			// since these values are repeatable, we escape them previous
			// the query instead of running multiple prepares.
			$lat      = esc_sql( $this->filters['user_location']['lat'] );
			$lng      = esc_sql( $this->filters['user_location']['lng'] );
			$distance = esc_sql( $this->filters['radius'] );

			$clauses['fields'] .= ", ROUND( {$earth_radius} * acos( cos( radians( {$lat} ) ) * cos( radians( gjmLocations.lat ) ) * cos( radians( gjmLocations.long ) - radians( {$lng} ) ) + sin( radians( {$lat} ) ) * sin( radians( gjmLocations.lat ) ) ),1 ) AS distance";

			$clauses['join'] .= " INNER JOIN {$wpdb->prefix}places_locator gjmLocations ON $wpdb->posts.ID = gjmLocations.post_id ";

			// calculate the between point.
			$bet_lat1 = $lat - ( $distance / $degree );
			$bet_lat2 = $lat + ( $distance / $degree );
			$bet_lng1 = $lng - ( $distance / ( $degree * cos( deg2rad( $lat ) ) ) );
			$bet_lng2 = $lng + ( $distance / ( $degree * cos( deg2rad( $lat ) ) ) );

			$clauses['where'] .= " AND gjmLocations.lat BETWEEN {$bet_lat1} AND {$bet_lat2}";
			$clauses['where'] .= " AND gjmLocations.long BETWEEN {$bet_lng1} AND {$bet_lng2} ";
			$clauses['where'] .= ' AND ( gjmLocations.lat != 0.000000 && gjmLocations.long != 0.000000 ) ';

			// filter locations based on the distance.
			$clauses['having'] = "HAVING distance <= {$distance} OR distance IS NULL";

			// if we order by the distance.
			if ( 'distance' === $this->filters[ $this->prefix . '_orderby' ] ) {

				if ( $this->orderby_featured_distance ) {

					$clauses['orderby'] = "{$wpdb->posts}.menu_order ASC, distance";

				} else {

					$clauses['orderby'] = 'distance';
				}
			}
		} else {

			// if showing posts without location.
			if ( $this->query_posts_without_location ) {

				// left join the location table into the query to display posts with no location as well.
				$clauses['join']  .= " LEFT JOIN {$wpdb->prefix}places_locator gjmLocations ON $wpdb->posts.ID = gjmLocations.post_id ";
				$clauses['where'] .= " {$address_filters} ";

			} else {

				$clauses['join']  .= " INNER JOIN {$wpdb->prefix}places_locator gjmLocations ON $wpdb->posts.ID = gjmLocations.post_id ";
				$clauses['where'] .= " {$address_filters} AND ( gjmLocations.lat != 0.000000 && gjmLocations.long != 0.000000 ) ";
			}
		}

		// modify the clauses.
		$clauses = apply_filters( 'gjm_location_query_clauses', $clauses, $this->filters, $this );

		if ( ! empty( $clauses['having'] ) ) {

			if ( empty( $clauses['groupby'] ) ) {
				$clauses['groupby'] = $wpdb->prefix . 'posts.ID';
			}

			$clauses['groupby'] .= ' ' . $clauses['having'];

			unset( $clauses['having'] );

		} else {

			if ( empty( $clauses['groupby'] ) ) {
				$clauses['groupby'] = $wpdb->prefix . 'posts.ID';
			}
		}

		return $clauses;
	}

	/**
	 * Modify search query based on location
	 *
	 * @param array $query_args query args.
	 *
	 * @since  1.0
	 *
	 * @author Eyal Fitoussi
	 */
	public function search_query( $query_args ) {

		if ( ! empty( $_REQUEST['form_data'] ) ) { // WPCS: CSRF ok.

			wp_parse_str( $_REQUEST['form_data'], $form_data ); // WPCS: CSRF ok, sanitization ok.

			// It seems that the form_date key has changed in WP Job Manager v1.33.0.
		} elseif ( ! empty( $_REQUEST['$form_data'] ) ) { // WPCS: CSRF ok.

			wp_parse_str( $_REQUEST['$form_data'], $form_data ); // WPCS: CSRF ok, sanitization ok.

		} else {

			return $query_args;
		}

		if ( empty( $form_data[ $this->prefix . '_use' ] ) ) {

			return $query_args;

			// check if we are using the shortcode attributes settings.
		} elseif ( 1 === absint( $form_data[ $this->prefix . '_use' ] ) ) {

			$this->filters = wp_parse_args( $_REQUEST['form_data'], $this->shortcode_atts() ); // WPCS: CSRF ok, sanitization ok.

			// otherwise, are we using the admin settings.
		} elseif ( 2 === absint( $form_data[ $this->prefix . '_use' ] ) ) {

			if (isset($this->settings['search_form'])) {
				$this->filters = wp_parse_args( $form_data, $this->settings['search_form'] );
			}

			// abort if geolocation disable for this shortcode.
		} else {

			return $query_args;
		}

		$this->labels                       = gjm_labels();
		$this->query_posts_without_location = apply_filters( 'gjm_query_posts_without_location', $this->query_posts_without_location, $this->filters );
		$this->orderby_featured_distance    = apply_filters( 'gjm_orderby_featured_distance', $this->orderby_featured_distance, $this->filters );
		self::$enable_boundaries_search     = apply_filters( 'gjm_search_within_boundaries', self::$enable_boundaries_search, $this->filters );
		$this->search_boundries_in_meta     = apply_filters( 'gjm_search_boundries_in_meta', $this->search_boundries_in_meta, $this->filters );

		// add values to query args cache.
		$query_args[ $this->prefix ]['query_posts_without_location'] = $this->query_posts_without_location;
		$query_args[ $this->prefix ]['orderby_featured_distance']    = $this->orderby_featured_distance;
		$query_args[ $this->prefix ]['search_boundries_in_meta']     = $this->search_boundries_in_meta;
		$query_args[ $this->prefix ]['enable_boundaries_search']     = self::$enable_boundaries_search;

		// set default user coords.
		$this->filters['user_location']['lat'] = false;
		$this->filters['user_location']['lng'] = false;

		if ( empty( $this->filters['radius'] ) ) {
			$this->filters['radius'] = ! empty( $this->filters[ $this->prefix . '_radius' ] ) ? $this->filters[ $this->prefix . '_radius' ] : '200';
		}

		// add geo values to query args so it can be saved in WP Job Manager cache.
		if (isset($this->filters['search_location'])) {
			$query_args[ $this->prefix ]['location'] = $this->filters['search_location'];
		}
		if (isset($this->filters[ $this->prefix . '_units' ])) {
			$query_args[ $this->prefix ]['units']    = $this->filters[ $this->prefix . '_units' ];
		}
		if (isset($this->filters['radius'])) {
			$query_args[ $this->prefix ]['radius']   = $this->filters['radius'];
		}

		$this->filters['doing_boundries_search'] = false;

		if ( ! empty( $this->filters['state'] ) || ! empty( $this->filters['country'] ) ) {
			$this->filters['doing_boundries_search'] = true;
		}

		// disable the location query made by WP Job Manager
		// We are doing our own proximity search query.
		if ( ! empty( $query_args[ $this->prefix ]['location'] ) ) {

			if ( ! $this->filters['doing_boundries_search'] || ( ! $this->search_boundries_in_meta && $this->filters['doing_boundries_search'] ) ) {

				unset( $query_args['meta_query'][0] );
			}
		}

		// if we are using gjm orderby we will need to override the original setting created by Wp Jobs Manager plugin.
		// Unless when using orderby "featured" which is when we will leave it as is.
		if ( ! empty( $this->filters[ $this->prefix . '_orderby' ] ) ) {

			if ( 'featured' !== $this->filters[ $this->prefix . '_orderby' ] ) {

				// force gjm orderby value from dropdown or default value.
				$query_args['orderby'] = $this->filters[ $this->prefix . '_orderby' ];

				// adjust the order of posts when choosing to order by title.
				if ( 'title' === $this->filters[ $this->prefix . '_orderby' ] ) {
					$query_args['order'] = 'ASC';
				}
			} else {

				$query_args['orderby'] = array(
					'menu_order' => 'ASC',
					'date'       => 'DESC',
					'ID'         => 'DESC',
				);
			}

			// set the original orderby by Wp Job Manager plugin.
		} elseif ( empty( $this->filters[ $this->prefix . '_orderby' ] ) && ! empty( $this->filters['search_location'] ) ) {

			$query_args['orderby']                       = apply_filters( 'gjm_default_orderby', $query_args['orderby'], $this );
			$this->filters[ $this->prefix . '_orderby' ] = $query_args['orderby'];
		}

		// when searching by address.
		if ( ! empty( $this->filters['search_location'] ) && 'Any Location' !== $this->filters['search_location'] && 'Location' !== $this->filters['search_location'] ) {

			// look for coords in filters.
			if ( ! empty( $this->filters['latitude'] ) && ! empty( $this->filters['longitude'] ) ) {

				$this->filters['user_location']['lat'] = $this->filters['latitude'];
				$this->filters['user_location']['lng'] = $this->filters['longitude'];

				// look for coords in URL.
			} elseif ( ! empty( $_GET['latitude'] ) && ! empty( $_GET['longitude'] ) ) { // WPCS: CSRF ok.

				$this->filters['user_location']['lat'] = $_GET['latitude']; // WPCS: CSRF ok, sanitization ok.
				$this->filters['user_location']['lng'] = $_GET['longitude']; // WPCS: CSRF ok, sanitization ok.

				// in case that an address was entered and was not geocoded via client site
				// try again via serverside.
			} elseif ( class_exists( 'WP_Job_Manager_Geocode' ) ) {

				$this->geocoded = WP_Job_Manager_Geocode::get_location_data( $this->filters['search_location'] );

				if ( is_array( $this->geocoded ) && ! is_wp_error( $this->geocoded ) && ! empty( $this->geocoded['lat'] ) && ! empty( $this->geocoded['long'] ) ) {

					$this->filters['user_location']['lat'] = $this->geocoded['lat'];
					$this->filters['user_location']['lng'] = $this->geocoded['long'];
				}
			}
		}

		// Query locations by joining the locations table.
		if ( ! apply_filters( 'gjm_stand_alone_locations_query_enabled', false, $this->filters ) ) {

			add_filter( 'posts_clauses', array( $this, 'query_clauses' ), 50, 2 );

			// Or do a separate location query then pass the posts ID to the WP_Query.
		} else {

			// default locations query args.
			$locations_query_args = array(
				'post_type' => $this->post_type_query,
				'lat'       => $this->filters['user_location']['lat'],
				'lng'       => $this->filters['user_location']['lng'],
				'radius'    => $this->filters['radius'],
				'units'     => $this->filters[ $this->prefix . '_units' ],
				'state'     => $this->filters['state'],
				'country'   => $this->filters['country'],
				'orderby'   => $this->filters['orderby'],
			);

			// do locations query.
			$locations_data = self::get_locations_data( $locations_query_args );

			// get locations data.
			$this->locations_data = $locations_data['data'];

			// When searching based on location, we need to include the locatios post ID in the WP_Query.
			// This is to restricts the query to only show posts with location and exclude the "Anywhere" jobs.
			// Otherwise, we skip this and the query will include all jobs, with or without location.
			if ( ! empty( $this->filters['user_location']['lat'] ) && ! empty( $this->filters['user_location']['lng'] ) ) {

				// pass the locations post ID into the WP_Query.
				$query_args['post__in'] = ! empty( $locations_data['posts_id'] ) ? $locations_data['posts_id'] : array( '0' );

				// when sorting by distance we pass "post__in" to the orderby arg.
				// This way the query will sort the results based on the locations post ID, which are ordered
				// by the distance.
				if ( 'distance' === $this->filters[ $this->prefix . '_orderby' ] ) {
					$query_args['orderby'] = 'post__in';
				}
			}
		}

		// when map enabled we use the_post action to generate some data for the map
		// and pass it to the JS file.
		
		add_action( 'the_post', array( $this, 'the_post' ) );

		if ( 'grm' === $this->prefix ) {
			add_filter( 'resume_manager_get_listings_result', array( $this, 'map_element' ), 90, 2 );
		} else {
			add_filter( 'job_manager_get_listings_result', array( $this, 'map_element' ), 90, 2 );
		}

		return $query_args;
	}

	/**
	 * The post action hook
	 *
	 * We use this hook to collect the location data of each post and pass it as the map arument.
	 */
	public function the_post() {

		global $post;

		// Collect location data to pass to the map.
		if ( ! empty( $post->lat ) && ! empty( $post->long ) ) {

			$location = (object) array();

			$location->post_id             = $post->ID;
			$location->address             = $post->address;
			$location->formatted_address   = $post->formatted_address;
			$location->lat                 = $post->lat;
			$location->long                = $post->long;
			$location->lng                 = $post->long;
			$location->distance            = $post->distance;
			$location->units               = $post->units;
			$location->info_window_content = $this->info_window_content( $post );

			$map_icon = '';
			$location->map_icon = apply_filters( $this->prefix . '_map_icon', $map_icon, $post, $this->filters );

			$this->map_locations[] = $location;
		}
	}

	/**
	 * Generate the map.
	 *
	 * Pass locations and other data to javasctip to display on the map.
	 *
	 * @param array $query search query.
	 *
	 * @since  1.0
	 *
	 * @author Eyal Fitoussi
	 */
	public function map_element( $result, $jobs ) {

		// create the map object.
		if (isset($map_args)) {
		$map_args = array(
				'map_id' => $this->filters[ $this->prefix . '_element_id' ],
				'prefix' => $this->prefix,
			);
		}
		$map_options = array();

		if (isset($user_location)) {
			$user_location = array(
				'lat'        => $this->filters['user_location']['lat'],
				'lng'        => $this->filters['user_location']['lng'],
				'address'    => $this->filters['search_location'],
				'map_icon'   => $this->filters[ $this->prefix . '_user_marker' ],
				'iw_content' => __( 'You are here', 'pointify' ),
				'iw_open'    => false,
			);
		}
		$map_args ='';
		$user_location = '';
		$result['gjm_map'] = GJM_Maps::get_map_object( $map_args, $map_options, $this->map_locations, $user_location );

		// remove actions.
		remove_action( 'the_post', array( $this, 'the_post' ) );
		remove_filter( 'posts_clauses', array( $this, 'query_clauses' ), 50, 2 );

		return $result;
	}

	/**
	 * Info window function callback
	 *
	 * @param  object $post post object.
	 *
	 * @return [type]       [description]
	 */
	public function info_window_content( $post ) {
		return GJM_Maps::get_info_window_content( $post );
	}

	/**
	 * Get the job distance in the results.
	 *
	 * @param  object $post post object.
	 *
	 * @return [type]       [description]
	 */
	public function get_the_distance( $post ) {

		if ( ! isset( $this->filters ) ) {
			return false;
		}

		if (isset($this->filters[ $this->prefix . '_distance' ])) {
			if ( ! isset( $this->filters ) || 0 === absint( $this->filters[ $this->prefix . '_use' ] ) || 1 !== absint( $this->filters[ $this->prefix . '_distance' ] ) ) {
				return false;
			}
		}

		if ( empty( $post->distance ) ) {
			return false;
		}

		return '<span class="' . $this->prefix . '-distance-wrapper">' . esc_attr( $post->distance ) . ' ' . esc_attr( $post->units ) . '</span>';
	}

	/**
	 * Append distance value to results in Listable theme
	 *
	 * @param  object $post post object.
	 *
	 * @since  1.0
	 *
	 * @author Eyal Fitoussi
	 */
	public function listable_job_distance( $post ) {

		$distance = $this->get_the_distance( $post );

		if ( ! $distance ) {
			return;
		}

		$location_data = isset( $this->locations_data[ $post->ID ] ) ? $this->locations_data[ $post->ID ] : false;

		echo apply_filters( $this->prefix . '_results_distance', $distance, $post, $location_data, $this->filters ); // WPCS: XSS ok.
	}

	/**
	 * Append distance value to results in Listify theme
	 *
	 * @since  1.0
	 *
	 * @author Eyal Fitoussi
	 */
	public function listify_job_distance() {

		global $post;

		if ( empty( $post ) ) {
			return;
		}

		$distance = $this->get_the_distance( $post );

		if ( ! $distance ) {
			return;
		}

		$location_data = isset( $this->locations_data[ $post->ID ] ) ? $this->locations_data[ $post->ID ] : false;

		echo apply_filters( $this->prefix . '_results_distance', $distance, $post, $location_data, $this->filters ); // WPCS: XSS ok.
	}

	/**
	 * Append distance value to results in Listify theme
	 *
	 * @since  1.0
	 *
	 * @author Eyal Fitoussi
	 */
	public function listing_easy_job_distance() {

		global $post;

		if ( empty( $post ) ) {
			return;
		}

		$distance = $this->get_the_distance( $post );

		if ( ! $distance ) {
			return;
		}

		$location_data = isset( $this->locations_data[ $post->ID ] ) ? $this->locations_data[ $post->ID ] : false;

		echo apply_filters( $this->prefix . '_results_distance', $distance, $post, $location_data, $this->filters ); // WPCS: XSS ok.

		return $post;
	}

	/**
	 * Append distance value to results
	 *
	 * @since  1.0
	 *
	 * @author Eyal Fitoussi
	 */
	public function the_distance() {

		global $post;

		$distance = $this->get_the_distance( $post );

		if ( ! $distance ) {
			return;
		}

		$location_data = isset( $this->locations_data[ $post->ID ] ) ? $this->locations_data[ $post->ID ] : false;

		echo apply_filters( $this->prefix . '_results_distance', $distance, $post, $location_data, $this->filters ); // WPCS: XSS ok.
	}

	/**
	 * Map holder for map when using map results shortcode.
	 *
	 * @param  array $atts element ID.
	 *
	 * @return HTML.
	 */
	public function results_map_shortcode( $atts ) {

		$element_id = ! empty( $atts['element_id'] ) ? '-' . $atts['element_id'] . '"' : '';

		return '<div id="' . esc_attr( $this->prefix ) . '-results-map-holder' . $element_id . '" class="results-map-holder"></div>';
	}
}
new GJM_Form_Query_Class();
