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

// Only load GV widget when GravityView is loaded
if ( ! class_exists( '\GV\Widget' ) ) {
	return;
}

use GravityKit\GravityCalendar\QueryFilters\QueryFilters;
use GV\Entry_Collection;
use GV\GF_Entry;
use GV\View;
use GV\Widget;
use GV\Template_Context;

/**
 * Widget to display calendar view
 *
 * @extends GravityView_Widget
 *
 * @since   1.1.0
 */
class GravityView_Widget_Calendar extends Widget {
	/**
	 * @inheritDoc
	 */
	public $icon = 'dashicons-calendar';

	/**
	 * @since 2.4
	 *
	 * @var array Translations for the UI script.
	 */
	private $_translations;

	/**
	 * @var string Unique reference name for UI script
	 */
	const ASSETS_HANDLE = 'gv-calendar-gravityview-widget';

	function __construct() {
		// DataTables support.
		add_filter( 'gravityview/datatables/output', [ $this, 'modify_datatables_output' ], 10, 3 );

		$this->widget_description = __( 'Display View entries in a calendar.', 'gk-gravitycalendar' );
		$this->_translations      = GVCalendarWidgetBlockHelpers::get_translations();

		$default_values = [
			'header' => 1,
			'footer' => 1,
		];

		$settings = [
			'feed'            => [
				'type'    => 'select',
				'label'   => sprintf( '%s:', esc_html__( 'Calendar', 'gk-gravitycalendar' ) ),
				'value'   => '',
				'choices' => $this->get_feed_select_options(),
			],
			'display_entries' => [
				'type'       => 'radio',
				'full_width' => true,
				'label'      => esc_html__( 'Display events for:', 'gk-gravitycalendar' ),
				'options'    => [
					'all'     => esc_html__( 'All View entries', 'gk-gravitycalendar' ),
					'visible' => esc_html__( 'Entries shown on the page', 'gk-gravitycalendar' ),
				],
				'value'      => 'all',
			],
		];

		add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_ui_assets' ] );
		add_filter( 'gravityview_noconflict_scripts', [ $this, 'register_no_conflicts' ] );

		parent::__construct( esc_html__( 'Calendar', 'gk-gravitycalendar' ), 'gravityview-calendar', $default_values, $settings );
	}

	/**
	 * Enqueues UI assets.
	 *
	 * @since 1.1.0
	 *
	 * @return void
	 */
	function admin_enqueue_ui_assets() {
		$min = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) || isset( $_GET['gform_debug'] ) ? '' : '.min';

		$forms_and_feeds = GVCalendarWidgetBlockHelpers::get_forms_and_feeds();

		if ( empty( $forms_and_feeds['feeds'] ) ) {
			return;
		}

		if ( ! wp_script_is( 'gravityview_views_scripts', 'registered' ) ) {
			return;
		}

		$options = [
			'feeds'        => $forms_and_feeds['feeds'],
			'translations' => $this->_translations,
		];

		wp_enqueue_script( self::ASSETS_HANDLE, GV_CALENDAR_URL . 'assets/js/admin-gravityview-view' . $min . '.js', [ 'gravityview_views_scripts' ], GV_CALENDAR_VERSION, true );

		wp_localize_script( self::ASSETS_HANDLE, 'gvCalendarWidget', $options );

	}

	/**
	 * Adds GravityView scripts and styles to Gravity Forms and GravityView No-Conflict modes.
	 *
	 * @since 1.1.0
	 *
	 * @param array $registered Existing scripts or styles that have been registered (array of the handles).
	 *
	 * @return array $registered
	 */
	function register_no_conflicts( $registered ) {
		$registered[] = self::ASSETS_HANDLE;

		return $registered;
	}

	/**
	 * Gets a list of calendar feeds formatted for use as select element options.
	 *
	 * @since 1.1.0
	 *
	 * @return array $feeds_data
	 */
	function get_feed_select_options() {
		$feeds_data = [
			'' => __( 'Select a calendar', 'gk-gravitycalendar' ),
		];

		if ( ! is_admin() && ! ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) {
			return $feeds_data;
		}

		$forms_and_feeds = GVCalendarWidgetBlockHelpers::get_forms_and_feeds();

		if ( empty( $forms_and_feeds ) ) {
			return $feeds_data;
		}

		foreach ( $forms_and_feeds['feeds'] as $feeds ) {
			foreach ( $feeds as $feed ) {
				$feeds_data[ $feed['id'] ] = $feed['title'];
			}
		}

		return $feeds_data;
	}

	/**
	 * Modifies output returned from the DataTables Ajax request.
	 *
	 * @since 1.4
	 *
	 * @param array                 $output       Ajax output.
	 * @param View                  $view         GravityView View.
	 * @param Entry_Collection|null $view_entries View entries.
	 *
	 * @return array Modified output
	 */
	function modify_datatables_output( $output, $view, $view_entries = null ) {
		if ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) {
			return $output;
		}

		if ( ! $view_entries instanceof Entry_Collection ) {
			return $output;
		}

		static $datatables_processed;

		if ( $datatables_processed ) {
			return $output;
		}

		$gv_calendar = GV_Extension_Calendar_Feed::get_instance();

		// 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 );
		} );

		$all_view_entries = $view->get_entries()->all();

		$visible_view_entries = $view_entries->all();

		foreach ( $view->widgets->all() as $widget ) {
			if ( ! $widget instanceof GravityView_Widget_Calendar ) {
				continue;
			}

			$feed_id = $widget->configuration->get( 'feed' );

			// Skip widgets without a feed.
			if ( empty( $feed_id ) ) {
				continue;
			}

			try {
				// 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).
				$calendar_filter = function () use ( $visible_view_entries, $all_view_entries, $widget ) {
					return array_column(
						'all' === $widget->configuration->get( 'display_entries' ) ? $all_view_entries : $visible_view_entries,
						'ID'
					);
				};

				add_filter( 'gravityview/calendar/events/include', $calendar_filter );

				$events = $gv_calendar->calendar_events( $feed_id );

				remove_filter( 'gravityview/calendar/events/include', $calendar_filter );

				$output['calendar_widgets_data'][ $feed_id ] = array_values( $events );
			} catch ( Exception $exception ) {
				$output['calendar_widgets_data'][ $feed_id ] = [];
			}
		}

		$datatables_processed = true;

		return $output;
	}

	/**
	 * Renders the widget.
	 *
	 * @since 1.1.0
	 *
	 * @param array            $widget_args The Widget shortcode args.
	 * @param string           $content     The content.
	 * @param Template_Context $context     The context.
	 *
	 * @return string|void Rendered calendar widget or return nothing if feed is not defined
	 */
	function render_frontend( $widget_args, $content = '', $context = '' ) {
		$view = View::by_id( gravityview_get_view_id() );

		if ( empty( $widget_args['feed'] ) || ! $view ) {
			return;
		}

		$gv_calendar = GV_Extension_Calendar_Feed::get_instance();

		$feed = $gv_calendar->get_feed( $widget_args['feed'] );

		if ( ! $feed ) {
			return;
		}

		$is_datatables_layout = 'datatables_table' === $view->settings->get( 'template' );

		// When displaying events for visible View entries, it makes sense to disable dynamic events loading and initialize Calendar only with events being displayed.
		if ( 'visible' === rgar( $widget_args, 'display_entries' ) ) {
			add_filter( 'gravityview/calendar/extra_options', function ( $options ) use ( $is_datatables_layout ) {
				$options['dynamic_events_loading'] = false;
				$options['loader']                 = $is_datatables_layout; // Calendar should always display a loader with the DataTables layout

				return $options;
			} );

			// While we disabled dynamic events loading in the options that are passed to FullCalendar, it is still deemed enabled by GV_Extension_Calendar_Feed::get_calendar_instance_options()
			// because it relies on the feed's meta/dynamicEventsLoading value and thus skips the events fetching logic. We need to manually fetch them and pass to FullCalendar.
			add_filter( 'gravityview/calendar/options', function ( $options ) use ( $gv_calendar, $widget_args ) {
				try {
					$options['events'] = $gv_calendar->calendar_events( $widget_args['feed'] );
				} catch ( Exception $exception ) {
					$options['events'] = [];
				}

				return $options;
			} );

			$view_entries = ( $context instanceof Template_Context ) ? $context->entries->all() : GravityView_View::getInstance()->entries;
		} else {
			// 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 );
			} );

			$view_entries = $view->get_entries()->all();

			add_filter( 'gravityview/calendar/extra_options', function ( $extra_options ) use ( $view, $widget_args ) {
				if ( empty( $extra_options['ajax_params'] ) ) {
					$extra_options['ajax_params'] = [];
				}

				$extra_options['ajax_params'] = array_merge(
					$extra_options['ajax_params'],
					[
						'view_id' => $view->ID,
						'display' => rgar( $widget_args, 'display_entries' ),
					] );

				return $extra_options;
			} );
		}

		if ( ! empty( $view_entries ) ) {
			// 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_entries ) {
				return array_column( $view_entries, $view_entries[0] instanceof GF_Entry || $view_entries[0] instanceof GV\GF_Entry ? 'ID' : 'id' );
			} );
		}

		if ( 'datatables_table' === $view->settings->get( 'template' ) ) {
			add_filter( 'gk/gravitycalendar/events/custom-response', '__return_empty_array' );
		}

		echo gv_calendar_render(
			$widget_args['feed'],
			gravityview_get_view_id(),
			$gv_calendar->is_secure( $feed ) ? $gv_calendar->get_validation_secret( $feed ) : ''
		);
	}

	/**
	 * Gets entries (events) based on View configuration.
	 *
	 * @deprecated 2.4
	 *
	 * @param array                      $widget_args
	 * @param GV\View                    $view
	 * @param string|GV\Template_Context $context
	 *
	 * @return array[GV\GF_Entry]
	 */
	function get_entries( $widget_args, $view, $context = '' ) {
		$feed              = GFAPI::get_feed( rgar( $widget_args, 'feed' ) );
		$form              = GFAPI::get_form( rgar( $feed, 'form_id' ) );
		$conditional_logic = rgars( $feed, 'meta/conditional_logic', 'null' );

		if ( $form && $feed && 'null' !== $conditional_logic ) {
			$query_filters_callback = function ( &$query ) use ( $feed, $form, $conditional_logic ) {
				$query_filters = new QueryFilters();
				$query_filters->set_form( $form );
				$query_filters->set_filters( $conditional_logic );

				$conditions = $query_filters->get_query_conditions();

				$query_parts = $query->_introspect();

				$query->where( GF_Query_Condition::_and( $query_parts['where'], $conditions ) );
			};

			add_filter( 'gravityview/view/query', $query_filters_callback );
		}

		if ( 'all' !== rgar( $widget_args, 'display_entries' ) ) {
			$entries = ( $context instanceof Template_Context ) ? $context->entries->all() : GravityView_View::getInstance()->entries;
		} else {
			$query_callback = function ( &$query ) use ( $widget_args ) {
				$query->limit( 0 );
				$query->offset( 0 );
			};

			add_filter( 'gravityview/view/query', $query_callback );

			$entries = $view->get_entries()->all();
		}

		return $entries;
	}
}

new GravityView_Widget_Calendar;
