<?php
/**
 * GravityCalendar Add-On - AJAX handler
 *
 * @package   GravityCalendar
 * @license   GPL2+
 * @author    Katz Web Services, Inc.
 * @link      https://www.gravitykit.com
 * @copyright Copyright 2019, Katz Web Services, Inc.
 */

defined( 'ABSPATH' ) || exit; // Exit if accessed directly

use GV\GF_Entry;

class GravityView_Calendar_Ajax {
	/**
	 * @var string Unique nonce reference
	 */
	const NONCE_HANDLE = 'gv_calendar_nonce';

	/**
	 * GravityView_Calendar_Ajax constructor.
	 */
	public function __construct() {
		add_action( 'wp_ajax_gv_calendar_edit_event', array( $this, 'ajax_edit_event' ) );
		add_action( 'wp_ajax_gv_calendar_get_events', array( $this, 'ajax_get_events' ) );
		add_action( 'wp_ajax_nopriv_gv_calendar_get_events', array( $this, 'ajax_get_events' ) );
	}

	/**
	 * Return nonce handle used for AJAX requests
	 *
	 * @since  1.5
	 *
	 * @param null|string|int $calendar_id Calendar feed ID
	 *
	 * @return string
	 */
	public static function get_nonce_handle( $calendar_id = null ) {
		return $calendar_id ? sprintf( '%s_%s', self::NONCE_HANDLE, $calendar_id ) : self::NONCE_HANDLE;
	}

	/**
	 * Verify AJAX request nonce
	 *
	 * @since  1.5
	 *
	 * @return void Exit if nonce check fails
	 */
	public function verify_nonce( $request = array() ) {
		$nonce_action = self::get_nonce_handle( $request['calendar_id'] );

		if ( ! check_ajax_referer( $nonce_action, '_nonce', false ) ) {
			wp_die( false, false, array( 'response' => 403 ) );
		}
	}

	/**
	 * AJAX function to edit events
	 *
	 * @since  1.0.0
	 *
	 * @return void
	 */
	public function ajax_edit_event() {
		$defaults = array(
			'_nonce'      => null,
			'calendar_id' => null,
			'event_id'    => 0,
			'all_day'     => 'false',
			'start'       => 0,
			'end'         => 0,
		);

		$request = wp_parse_args( $_REQUEST, $defaults );

		$this->verify_nonce( $request );

		$request['calendar_id'] = (int) rgar( $request, 'calendar_id', 0 );

		if ( empty( $request['calendar_id'] ) ) {
			wp_send_json_error( new WP_Error( 'invalid_feed', esc_html__( 'Invalid feed ID.', 'gk-gravitycalendar' ) ) );
		}

		if ( empty( $request['event_id'] ) ) {
			wp_send_json_error( new WP_Error( 'missing_event_id', esc_html__( 'The request is missing the `event_id` parameter.', 'gk-gravitycalendar' ) ) );
		}

		$calendar_feed = GV_Extension_Calendar_Feed::get_instance();

		// Get Feed and Form objects
		$feed     = $calendar_feed->get_feed( $request['calendar_id'] );
		$settings = $feed['meta'];

		if ( empty( $settings['enable-editing'] ) ) {
			wp_send_json_error( new WP_Error( 'editing_not_enabled', esc_html__( 'Editing this calendar is not enabled in the feed settings.', 'gk-gravitycalendar' ) ) );
		}

		$edit_roles = rgar( $settings, 'editroles', false );

		if ( empty( $edit_roles ) ) {
			wp_send_json_error( new WP_Error( 'edit_roles_not_defined', esc_html__( 'The user role required to edit events is not defined in the feed settings.', 'gk-gravitycalendar' ) ) );
		}

		// User has the wrong edit access level
		if ( ! $calendar_feed->user_has_edit_roles( $edit_roles ) ) {
			wp_send_json_error( new WP_Error( 'edit_role_not_met', esc_html__( 'The current user does not have the required role to edit entries.', 'gk-gravitycalendar' ), $edit_roles ) );
		}

		$entry_id          = ! empty( $request['event_id'] ) ? absint( $request['event_id'] ) : 0;
		$all_day           = ! empty( $request['all_day'] ) && ( 'true' === $request['all_day'] );
		$start_date_string = ! empty( $request['start'] ) ? $request['start'] : 0;
		$end_date_string   = ! empty( $request['end'] ) ? $request['end'] : $start_date_string;

		if ( ! GFAPI::entry_exists( $entry_id ) ) {
			wp_send_json_error( new WP_Error( 'entry_does_not_exist', esc_html__( 'The entry associated with the edited event does not exist.', 'gk-gravitycalendar' ), $entry_id ) );
		}

		// Map fields from settings to field IDs
		$field_map = $calendar_feed->map_fields( $feed );

		// Create a DateTime object for start and end
		$start_timestamp = DateTime::createFromFormat( 'D M d Y H:i:s e+', $start_date_string );
		$end_timestamp   = DateTime::createFromFormat( 'D M d Y H:i:s e+', $end_date_string );

		// Full calendar sets the end date to the next day for all day events.
		// In this case, we need to subtract 1 day to get the correct end date
		// as stored in the database.
		if ( $all_day && ( $end_date_string !== $start_date_string ) ) {
			$end_timestamp->modify( '-1 day' );
		}

		$updates               = array();
		$updates['start_date'] = $start_timestamp->format( 'Y-m-d' );
		$updates['end_date']   = $end_timestamp->format( 'Y-m-d' );

		// Add times if the event isn't an all day event
		if ( ! $all_day ) {

			$updates['start_time'] = $start_timestamp->format( 'h:i a' );
			$updates['end_time']   = $end_timestamp->format( 'h:i a' );

		} else {

			$updates['start_time'] = null;
			$updates['end_time']   = null;

		}

		$did_not_update = array();

		foreach ( $updates as $key => $value ) {

			// Field mapping isn't set up for this key; don't try updating the value.
			if ( false === rgar( $field_map, $key, false ) ) {
				continue;
			}

			$updated = GFAPI::update_entry_field( $entry_id, $field_map[ $key ], $value );

			if ( ! $updated ) {
				$did_not_update[ $key ] = $value;
			}

		}

		if ( empty( $did_not_update ) ) {

			wp_send_json_success();

		} else {

			wp_send_json_error( new WP_Error( 'failed_to_update_all', esc_html__( 'The following fields were not updated:', 'gk-gravitycalendar' ), $did_not_update ) );

		}
	}

	/**
	 * AJAX function to fetch events
	 *
	 * @since  1.5
	 *
	 * @return void
	 */
	public function ajax_get_events() {
		$defaults = array(
			'_nonce'      => null,
			'calendar_id' => 0,
			'view_id'     => 0,
			'display'     => 'all',
			'url_query'   => null,
			'start'       => 0,
			'end'         => 0,
		);

		$request = wp_parse_args( $_REQUEST, $defaults );

		$this->verify_nonce( $request );

		$calendar_id = (int) rgar( $request, 'calendar_id', 0 );
		$view_id     = (int) rgar( $request, 'view_id', 0 );

		if ( empty( $request['start'] ) ) {
			wp_send_json_error( new WP_Error( 'missing_start_parameter', esc_html__( 'The request is missing the `start` parameter.', 'gk-gravitycalendar' ) ) );
		}

		if ( empty( $request['end'] ) ) {
			wp_send_json_error( new WP_Error( 'missing_end_parameter', esc_html__( 'The request is missing the `end` parameter.', 'gk-gravitycalendar' ) ) );
		}

		$from_date = date( 'Y-m-d', strtotime( $request['start'] ) );
		$to_date   = date( 'Y-m-d', strtotime( $request['end'] ) );

		$calendar_feed = GV_Extension_Calendar_Feed::get_instance();
		$feed          = $calendar_feed->get_feed( $calendar_id );
		$field_map     = $calendar_feed->map_fields( $feed );

		$view = null;

		if ( $view_id ) {
			if ( ! class_exists( '\GV\View' ) ) {
				wp_send_json_error( new WP_Error( 'gv_required', esc_html__( 'GravityView plugin must be installed and activated.', 'gk-gravitycalendar' ) ) );
			}

			$view = \GV\View::by_id( $view_id );

			if ( ! $view ) {
				$error_message = sprintf( esc_html_x( 'View ID #%s does not exist.', '%s is replaced with View ID', 'gk-gravitycalendar' ), $view_id );
				wp_send_json_error( new WP_Error( 'view_does_not_exist', $error_message ) );
			}
		}

		if ( $view ) {
			if ( 'visible' !== $request['display'] ) {
				// When displaying all View entries, we need to disable pagination to get a complete list of events.
				add_filter( 'gravityview/view/query', function ( &$query ) {
					$query->limit( 0 );
					$query->offset( 0 );
				} );
			}

			// When entries are fetched using the feed settings in GV_Extension_Calendar_Feed::calendar_events(),
			// we need to filter those that are not displayed by the View (i.e., "include" only those that are displayed).
			add_filter( 'gravityview/calendar/events/include', function ( $include ) use ( $view ) {
				$view_entries = $view->get_entries()->all();

				return array_column( $view_entries, 'ID' );
			} );
		}

		if ( $request['url_query'] ) {
			parse_str( $request['url_query'], $url_query );

			foreach ( $url_query as $parameter => $value ) {
				$_GET[ $parameter ] = ! is_array( $value ) ? $value : http_build_query( $value );
			}
		}

		try {
			$events = $calendar_feed->calendar_events( $calendar_id, $from_date, $to_date );
		} catch ( Exception $exception ) {
			$events = [];
		}

		// When no events are found and Calendar widget display is set to "all entries", check to see if there is a single past or future event.
		// When past/current events are found, UI will re-adjust the displayed date range according to the selected view and re-fetch all associated events.
		$navigate_to_events = rgars( $feed, 'meta/navigateToEvents' );

		if ( empty( $events ) && 'visible' !== $request['display'] && $navigate_to_events && 'current' !== $navigate_to_events ) {
			$sorting = array(
				'key' => $field_map['start_date'],
			);

			if ( 'past' === $navigate_to_events ) {
				$from_date            = null;
				$sorting['direction'] = \GV\Entry_Sort::DESC;
			} else {
				$to_date              = null;
				$sorting['direction'] = \GV\Entry_Sort::ASC;
			}

			add_filter( 'gravityview_get_entries', function ( $params ) use ( $field_map, $from_date, $to_date, $sorting ) {
				$field_filters = array();

				if ( $from_date ) {
					$field_filters[] = array(
						'key'      => $field_map['start_date'],
						'operator' => '>=',
						'value'    => $from_date,
					);
				}
				if ( $to_date ) {
					$field_filters[] = array(
						'key'      => $field_map['start_date'],
						'operator' => '<=',
						'value'    => $to_date,
					);
				}

				if ( isset( $params['search_criteria'] ) ) {
					if ( ! isset( $params['search_criteria']['field_filters'] ) ) {
						$params['search_criteria']['field_filters'] = array_merge( $params['search_criteria']['field_filters'], $field_filters );
					} else {
						$params['search_criteria']['field_filters'] = $field_filters;
					}
				} else {
					$params['search_criteria'] = array( 'field_filters' => $field_filters );
				}

				$params['paging'] = [
					'offset'    => 0,
					'page_size' => 1,
				];

				$params['sorting'] = $sorting;

				return $params;
			} );

			try {
				$events = $calendar_feed->calendar_events( $calendar_id, $from_date, $to_date );
			} catch ( Exception $exception ) {
				$events = array();
			}
		}

		wp_send_json( $events );
	}
}

new GravityView_Calendar_Ajax();
