<?php
/**
 * @package     VikBooking
 * @subpackage  com_vikbooking
 * @author      Alessio Gaggii - e4j - Extensionsforjoomla.com
 * @copyright   Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE
 * @link        https://vikwp.com
 */

defined('ABSPATH') or die('No script kiddies please!');

/**
 * Helper class for the administrator widgets.
 * 
 * @since 	1.14 (J) - 1.4.0 (WP)
 */
class VikBookingHelperAdminWidgets
{
	/**
	 * The singleton instance of the class.
	 *
	 * @var VikBookingHelperAdminWidgets
	 */
	protected static $instance = null;

	/**
	 * An array to store some cached/static values.
	 *
	 * @var array
	 */
	protected static $helper = null;

	/**
	 * List of preloaded widgets.
	 *
	 * @var array
	 * 
	 * @since  1.18.3 (J) - 1.8.3 (WP)
	 */
	protected static $preloaded = [];

	/**
	 * The database handler instance.
	 *
	 * @var object
	 */
	protected $dbo;

	/**
	 * The list of widget instances loaded.
	 *
	 * @var array
	 */
	protected $widgets;

	/**
	 * The list of widget positions in the multitask panel.
	 *
	 * @var array
	 * 
	 * @since  1.16.10 (J) - 1.6.10 (WP)
	 */
	protected $widgets_pos_list;

	/**
	 * Class constructor is protected.
	 *
	 * @see 	getInstance()
	 */
	protected function __construct()
	{
		static::$helper = [];
		$this->dbo = JFactory::getDbo();
		$this->widgets = [];
		$this->widgets_pos_list = (array) VBOFactory::getConfig()->getArray('multitasking_wpos', []);
		$this->load();
	}

	/**
	 * Returns the global object, either
	 * a new instance or the existing instance
	 * if the class was already instantiated.
	 *
	 * @return 	self 	A new instance of the class.
	 */
	public static function getInstance()
	{
		if (is_null(static::$instance)) {
			static::$instance = new static();
		}

		return static::$instance;
	}

	/**
	 * Loads a list of all available admin widgets.
	 *
	 * @return 	self
	 */
	protected function load()
	{
		/** @var VBOPlatformDispatcherInterface */
		$dispatcher = VBOFactory::getPlatform()->getDispatcher();

		// require main/parent admin-widget class
		require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'admin_widget.php');

		$widgets_base   = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'widgets' . DIRECTORY_SEPARATOR;
		$widgets_files  = glob($widgets_base . '*.php');
		$widgets_banned = [];

		/**
		 * Trigger event to let other plugins register additional widgets.
		 *
		 * @return 	array 	A list of supported widgets.
		 */
		$list = $dispatcher->filter('onLoadAdminWidgets');

		foreach ($list as $chunk) {
			// merge default widget files with the returned ones
			$widgets_files = array_merge($widgets_files, (array)$chunk);
		}

		/**
		 * Trigger event to let other plugins unregister specific widgets.
		 *
		 * @return 	array 	A list of widget identifiers to unload.
		 * 
		 * @since 	1.16.0 (J) - 1.6.0 (WP)
		 */
		$unloaded = $dispatcher->filter('onUnloadAdminWidgets');

		foreach ($unloaded as $chunk) {
			// merge all the the returned ones
			$widgets_banned = array_merge($widgets_banned, (array)$chunk);
		}

		// parse all admin widgets
		foreach ($widgets_files as $wf) {
			try {
				/**
				 * Require widget class file only if available, to allow registering classes at
				 * runtime by third-party plugins without needing to have a dedicated PHP file.
				 * The widget class must obviously exist all the times, but the file must not.
				 */
				if (is_file($wf)) {
					require_once($wf);
				}

				// widget identifier
				$widget_identif = basename($wf, '.php');

				// check if the widget was unloaded
				if (in_array($widget_identif, $widgets_banned)) {
					continue;
				}

				// build widget class name
				$classname  = 'VikBookingAdminWidget' . str_replace(' ', '', ucwords(str_replace('_', ' ', $widget_identif)));

				if (class_exists($classname)) {
					// instantiate widget object
					$widget = new $classname();

					/**
					 * Call the widget's preflight method to see if it's compatible with the current environment.
					 * 
					 * @since 	1.16.10 (J) - 1.6.10 (WP)
					 */
					if (!$widget->preflight()) {
						continue;
					}

					// push the installed widget object
					$this->widgets[] = $widget;
				}
			} catch (Exception $e) {
				// do nothing
			}
		}

		return $this;
	}

	/**
	 * Gets the default map of admin widgets.
	 *
	 * @return 	object 	the associative map of sections,
	 * 					containers and widgets.
	 */
	protected function getDefaultWidgetsMap()
	{
		// sections container
		$sections = [];

		// build default sections

		// new section
		$section = new stdClass;
		$section->name = 'Top';
		$section->containers = [];
		// start container
		$container = new stdClass;
		$container->size = 'medium';
		$container->widgets = [
			'sticky_notes',
			'arriving_today',
			'departing_today',
			'check_availability',
			'latest_events',
		];
		// push container
		array_push($section->containers, $container);
		// start container
		$container = new stdClass;
		$container->size = 'small';
		$container->widgets = [
			'guest_messages',
			'latest_from_guests',
			'booking_details',
			'visitors_counter',
		];
		// push container
		array_push($section->containers, $container);
		// start container
		$container = new stdClass;
		$container->size = 'small';
		$container->widgets = [
			'aitools',
			'forecast',
			'bookings_calendar',
			'reminders',
		];
		// push container
		array_push($section->containers, $container);
		// push section
		array_push($sections, $section);

		// new section
		$section = new stdClass;
		$section->name = 'Middle';
		$section->containers = [];
		// start container
		$container = new stdClass;
		$container->size = 'full';
		$container->widgets = [
			'today_rooms_occupancy',
			'weekly_bookings',
		];
		// push container
		array_push($section->containers, $container);
		// push section
		array_push($sections, $section);

		// new section
		$section = new stdClass;
		$section->name = 'Middle 2';
		$section->containers = [];
		// start container
		$container = new stdClass;
		$container->size = 'small';
		$container->widgets = [
			'latest_events',
		];
		// push container
		array_push($section->containers, $container);
		// start container
		$container = new stdClass;
		$container->size = 'small';
		$container->widgets = [
			'rates_flow',
		];
		// push container
		array_push($section->containers, $container);
		// start container
		$container = new stdClass;
		$container->size = 'medium';
		$container->widgets = [
			'finance',
			'currency_converter',
		];
		// push container
		array_push($section->containers, $container);
		// push section
		array_push($sections, $section);

		// new section
		$section = new stdClass;
		$section->name = 'Bottom';
		$section->containers = [];
		// start container
		$container = new stdClass;
		$container->size = 'medium';
		$container->widgets = [
			'last_reservations',
		];
		// push container
		array_push($section->containers, $container);
		// start container
		$container = new stdClass;
		$container->size = 'medium';
		$container->widgets = [
			'next_bookings',
		];
		// push container
		array_push($section->containers, $container);
		// push section
		array_push($sections, $section);

		// new section
		$section = new stdClass;
		$section->name = 'Bottom 2';
		$section->containers = [];
		// start container
		$container = new stdClass;
		$container->size = 'medium';
		$container->widgets = [
			'orphan_dates',
			'rooms_locked',
		];
		// push container
		array_push($section->containers, $container);
		// start container
		$container = new stdClass;
		$container->size = 'medium';
		$container->widgets = [
			'sticky_notes',
		];
		// push container
		array_push($section->containers, $container);
		// push section
		array_push($sections, $section);

		// compose the final map object
		$map = new stdClass;
		$map->sections = $sections;
		
		return $map;
	}

	/**
	 * Gets the list of admin widgets instantiated.
	 *
	 * @return 	array 	list of admin widget objects.
	 */
	public function getWidgets()
	{
		return $this->widgets;
	}

	/**
	 * Gets a single admin widget instantiated.
	 * 
	 * @param 	string 	$id 	the widget identifier.
	 *
	 * @return 	mixed 	the admin widget object, false otherwise.
	 */
	public function getWidget($id)
	{
		$id = $this->simplifyId($id);

		foreach ($this->widgets as $widget) {
			if ($widget->getIdentifier() == $id) {
				return $widget;
			}
		}

		return false;
	}

	/**
	 * Gets a list of sorted widget names, ids, descriptions, icons and styles.
	 * If called with pre-loading, widgets will be able to load their CSS and JS
	 * assets. Moreover, pre-loading a widget can return a watch-data object to
	 * implement the notifications scheduling of a specific widget.
	 * 
	 * @param 	bool 	$preload 	whether to call the widget's preload method.
	 *
	 * @return 	array 	associative and sorted widgets list.
	 * 
	 * @since 	1.15.0 (J) - 1.5.0 (WP)  added support for icons and style names.
	 * 									 implemented widgets preloading for assets.
	 */
	public function getWidgetNames($preload = false)
	{
		// containers
		$names = [];
		$pool  = [];
		$watch = [];

		foreach ($this->widgets as $widget) {
			// get widget details
			$id 	= $widget->getIdentifier();
			$name 	= $widget->getName();
			$descr 	= $widget->getDescription();

			if ($preload) {
				// preload widget's CSS/JS assets (if any)
				$watch_data = array_key_exists($id, static::$preloaded) ? static::$preloaded[$id] : $widget->preload();

				if ($watch_data) {
					// assign watching data to this widget
					$watch[$id] = $watch_data;
				}

				// overwrite cache for the preloaded widget result
				static::$preloaded[$id] = $watch_data;
			}

			// build widget info object
			$wtdata = new stdClass;
			$wtdata->id 	  = $id;
			$wtdata->name 	  = $name;
			$wtdata->descr 	  = $descr;
			$wtdata->icon 	  = $widget->getIcon();
			$wtdata->style 	  = str_replace(array(' ', '_'), '-', $widget->getStyleName());
			$wtdata->priority = $widget->getPriority();

			// set object for sorting
			$names[$name] = $wtdata;
		}

		// apply sorting by name
		ksort($names);

		// apply sorting by priority
		usort($names, function($a, $b) {
			// the higher the priority is, the higher the position will be
			return $b->priority - $a->priority;
		});

		if ($this->widgets_pos_list) {
			// get the custom positions list
			$widgets_pos_list = $this->widgets_pos_list;
			// apply custom sorting
			usort($names, function($a, $b) use ($widgets_pos_list) {
				$wa_id = $a->id;
				$wb_id = $b->id;

				if (($widgets_pos_list[$wa_id] ?? null) && ($widgets_pos_list[$wb_id] ?? null)) {
					// the higher the custom position is, the lower the sorting position will be
					return $widgets_pos_list[$wa_id] > $widgets_pos_list[$wb_id] ? 1 : -1;
				}

				return 0;
			});
		}

		// push sorted widgets to pool
		foreach ($names as $wtdata) {
			$pool[] = $wtdata;
		}

		/**
		 * In case some widgets were pre-loaded for watching certain
		 * events, enqueue the watch-data list to the document.
		 */
		if ($watch) {
			// schedule the browser notifications watching data
			VBONotificationScheduler::getInstance()->registerWatchData($watch);
		}

		// return the list of admin widgets
		return $pool;
	}

	/**
	 * Gets the current or default map of admin widgets.
	 * If no map currently set, stores the default map.
	 *
	 * @return 	array 	the associative map of sections,
	 * 					containers and widgets.
	 */
	public function getWidgetsMap()
	{
		$q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='admin_widgets_map';";
		$this->dbo->setQuery($q);
		$map = $this->dbo->loadResult();
		if ($map) {
			$map = json_decode($map);
			return is_object($map) && !empty($map->sections) && is_array($map->sections) ? $map : $this->getDefaultWidgetsMap();
		}

		$default_map = $this->getDefaultWidgetsMap();
		$q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('admin_widgets_map', " . $this->dbo->quote(json_encode($default_map)) . ");";
		$this->dbo->setQuery($q);
		$this->dbo->execute();

		return $default_map;
	}

	/**
	 * Updates the map of admin widgets.
	 * 
	 * @param 	array 	$sections 	the list of sections for the map.
	 *
	 * @return 	bool 	True on success, false otherwise.
	 */
	public function updateWidgetsMap($sections)
	{
		if (!is_array($sections) || !$sections) {
			return false;
		}

		// prepare new map object
		$map = new stdClass;
		$map->sections = $sections;

		$q = "UPDATE `#__vikbooking_config` SET `setting`=" . $this->dbo->quote(json_encode($map)) . " WHERE `param`='admin_widgets_map';";
		$this->dbo->setQuery($q);
		$this->dbo->execute();

		return true;
	}

	/**
	 * Restores the default admin widgets map.
	 * First it resets the settings of each widget.
	 *
	 * @return 	bool 	True on success, false otherwise.
	 */
	public function restoreDefaultWidgetsMap()
	{
		foreach ($this->widgets as $widget) {
			$widget->resetSettings();
		}

		$default_map = $this->getDefaultWidgetsMap();

		return $this->updateWidgetsMap($default_map->sections);
	}

	/**
	 * Forces the rendering of a specific widget identifier.
	 * 
	 * @param 	string 				$id 	the widget identifier.
	 * @param 	?VBOMultitaskData 	$data 	optional multitask data for the widget.
	 *
	 * @return 	void
	 */
	public function renderWidget($id, ?VBOMultitaskData $data = null)
	{
		$id = $this->simplifyId($id);

		foreach ($this->widgets as $widget) {
			if ($widget->getIdentifier() == $id) {
				// render the requested widget
				$widget->render($data);

				// return void
				return;
			}
		}
	}

	/**
	 * Maps the size identifier to a CSS class.
	 * 
	 * @param 	string 	$size 	the container size identifier.
	 *
	 * @return 	string 	the full CSS class for the container.
	 */
	public function getContainerCssClass($size)
	{
		$css_size_map = [
			'small'  => 'vbo-admin-widgets-container-small',
			'medium' => 'vbo-admin-widgets-container-medium',
			'large'  => 'vbo-admin-widgets-container-large',
			'full'   => 'vbo-admin-widgets-container-fullwidth',
		];

		return isset($css_size_map[$size]) ? $css_size_map[$size] : $css_size_map['full'];
	}

	/**
	 * Returns an associative array with the class names for the containers.
	 *
	 * @return 	array 	a text representation list of all sizes.
	 */
	public function getContainerClassNames()
	{
		return array(
			'full' => array(
				'name' => JText::translate('VBO_WIDGETS_CONTFULL'),
				'css' => $this->getContainerCssClass('full'),
			),
			'large' => array(
				'name' => JText::translate('VBO_WIDGETS_CONTLARGE'),
				'css' => $this->getContainerCssClass('large'),
			),
			'medium' => array(
				'name' => JText::translate('VBO_WIDGETS_CONTMEDIUM'),
				'css' => $this->getContainerCssClass('medium'),
			),
			'small' => array(
				'name' => JText::translate('VBO_WIDGETS_CONTSMALL'),
				'css' => $this->getContainerCssClass('small'),
			),
		);
	}

	/**
	 * Maps the size identifier to the corresponding name.
	 * 
	 * @param 	string 	$size 	the container size identifier.
	 *
	 * @return 	string 	the size name for the container.
	 */
	public function getContainerName($size)
	{
		$names = $this->getContainerClassNames();

		return isset($names[$size]) ? $names[$size]['name'] : $names['full']['name'];
	}

	/**
	 * Many widgets may need to know some values about the rooms.
	 * This method uses the static instance of the class to cache data.
	 * 
	 * @return 	void
	 */
	protected function loadRoomsData()
	{
		if (isset(static::$helper['all_rooms_ids'])) {
			// do not execute the same queries again
			return;
		}

		$all_rooms_ids = [];
		$all_rooms_units = [];
		$all_rooms_features = [];
		$unpublished_rooms = [];
		$tot_rooms_units = 0;

		$q = "SELECT `id`,`name`,`units`,`params`,`avail` FROM `#__vikbooking_rooms`;";
		$this->dbo->setQuery($q);
		$all_rooms = $this->dbo->loadAssocList();
		if ($all_rooms) {
			foreach ($all_rooms as $k => $r) {
				if ($r['avail'] < 1) {
					$unpublished_rooms[] = $r['id'];
				} else {
					$tot_rooms_units += $r['units'];
				}
				$all_rooms_ids[$r['id']] = $r['name'];
				$all_rooms_units[$r['id']] = $r['units'];
				$rparams = json_decode((string) $r['params'], true);
				$all_rooms_features[$r['id']] = is_array($rparams) && array_key_exists('features', $rparams) && is_array($rparams['features']) ? $rparams['features'] : [];
			}
		}

		// update static values
		static::$helper['all_rooms_ids'] = $all_rooms_ids;
		static::$helper['all_rooms_units'] = $all_rooms_units;
		static::$helper['all_rooms_features'] = $all_rooms_features;
		static::$helper['unpublished_rooms'] = $unpublished_rooms;
		static::$helper['tot_rooms_units'] = $tot_rooms_units;
	}

	/**
	 * Many widgets could use this method to access cached information.
	 * 
	 * @param 	string 	$key 	the data key identifier.
	 * 
	 * @return 	mixed 	array/int on success, false otherwise.
	 */
	public function getRoomsData($key)
	{
		if (empty($key)) {
			return false;
		}

		if (!static::$helper) {
			$this->loadRoomsData();
		}

		if (isset(static::$helper[$key])) {
			return static::$helper[$key];
		}

		return false;
	}

	/**
	 * Helper method to load all busy real records for all rooms for dates near today.
	 * This is useful to avoid double queries in the various widgets and to get data
	 * about yesterday, today and the next week.
	 * 
	 * @return 	array
	 * 
	 * @since 	1.15.2 (J) - 1.5.5 (WP) changed the way limit timestamps are calculated.
	 */
	public function loadBusyRecordsUnclosed()
	{
		if (!isset(static::$helper['all_rooms_ids'])) {
			$this->loadRoomsData();
		}

		if (isset(static::$helper['busy_records_unclosed'])) {
			return static::$helper['busy_records_unclosed'];
		}

		// cache value and return it
		$base_from_ts = mktime(0, 0, 0, date("n"), (date("j") - 1), date("Y"));
		$base_end_ts  = mktime(23, 59, 59, date("n"), (date("j") + 7), date("Y"));
		static::$helper['busy_records_unclosed'] = VikBooking::loadBusyRecordsUnclosed(array_keys(static::$helper['all_rooms_ids']), $base_from_ts, $base_end_ts);

		return static::$helper['busy_records_unclosed'];
	}

	/**
	 * The first time the widget's customizer is open, the welcome is displayed.
	 * Congig value >= 1 means hide the welcome text, 0 or lower means show it.
	 * 
	 * @return 	bool
	 */
	public function showWelcome()
	{
		$q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='admin_widgets_welcome';";
		$this->dbo->setQuery($q);
		$show_welcome = $this->dbo->loadResult();
		if ($show_welcome !== null) {
			return ((int)$show_welcome < 1);
		}

		$q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('admin_widgets_welcome', '0');";
		$this->dbo->setQuery($q);
		$this->dbo->execute();

		return true;
	}

	/**
	 * Updates the status of the welcome message for the widget's customizer.
	 * Congig value >= 1 means hide the welcome text, 0 or lower means show it.
	 * 
	 * @param 	int 	$val 	the new value to set in the configuration.
	 * 
	 * @return 	void
	 */
	public function updateWelcome($val)
	{
		$q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='admin_widgets_welcome';";
		$this->dbo->setQuery($q);
		$this->dbo->execute();
		if ($this->dbo->getNumRows()) {
			$q = "UPDATE `#__vikbooking_config` SET `setting`=" . $this->dbo->quote((int)$val) . " WHERE `param`='admin_widgets_welcome';";
			$this->dbo->setQuery($q);
			$this->dbo->execute();
			return;
		}

		$q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('admin_widgets_welcome', " . $this->dbo->quote((int)$val) . ");";
		$this->dbo->setQuery($q);
		$this->dbo->execute();
		return;
	}

	/**
	 * Gets the default map for multitasking pages.
	 *
	 * @return 	object 	the associative map of widgets.
	 */
	protected function getDefaultMultitaskingMap()
	{
		$map = new stdClass;

		// booking details page
		$map->editorder = [
			'reminders',
			'check_availability',
			'bookings_calendar',
		];

		// booking modifying page
		$map->editbusy = [
			'bookings_calendar',
			'check_availability',
			'reminders',
		];

		// availability overview
		$map->overv = [
			'reminders',
			'rates_flow',
			'latest_events',
		];

		return $map;
	}

	/**
	 * Gets the list of widgets assigned to the current page.
	 * 
	 * @param 	string 	$page 	the name of the current View/task.
	 * @param 	bool 	$whole 	whether to get the entire map object.
	 * 
	 * @return 	array|object 	the list of widgets for the given page,
	 * 							or entire map object if $whole is true.
	 */
	public function getMultitaskingMap($page = null, $whole = false)
	{
		// get saved map
		$saved_map = VBOFactory::getConfig()->get('multitasking_map');
		if (!empty($saved_map)) {
			// try decoding the map data
			$saved_map = json_decode($saved_map);
		}

		if (!is_object($saved_map)) {
			// revert to the default map
			$saved_map = $this->getDefaultMultitaskingMap();
		}

		if ($whole) {
			// do not proceed and return the entire map object
			return $saved_map;
		}

		if (empty($page) || !is_string($page)) {
			return [];
		}

		return isset($saved_map->$page) && is_array($saved_map->$page) ? $saved_map->$page : [];
	}

	/**
	 * Updates the map of multitasking widgets.
	 * 
	 * @param 	string 			$page 		the name of the current View/task.
	 * @param 	array|string 	$widgets 	the list of or the widget(s) to set.
	 * @param 	int 			$action 	0 = replace, -1 = prepend, 1 = append.
	 *
	 * @return 	bool 	true on success, false otherwise.
	 */
	public function updateMultitaskingMap($page, $widgets, $action = 0)
	{
		if (empty($page) || !is_string($page)) {
			return false;
		}

		// check if we have one widget or a list
		$widgets = is_string($widgets) ? [$widgets] : $widgets;
		if (!is_array($widgets)) {
			return false;
		}

		// get the whole current map
		$map = $this->getMultitaskingMap($page, true);

		if (!isset($map->$page)) {
			$map->$page = [];
		}

		if ($action > 0) {
			// append widgets
			foreach ($widgets as $widget_id) {
				if (empty($widget_id)) {
					continue;
				}
				array_push($map->$page, $widget_id);
			}
		} elseif ($action < 0) {
			// prepend widgets
			foreach ($widgets as $widget_id) {
				if (empty($widget_id)) {
					continue;
				}
				array_unshift($map->$page, $widget_id);
			}
		} else {
			// replace current list when $action === 0
			$map->$page = $widgets;
		}

		// update configuration record
		VBOFactory::getConfig()->set('multitasking_map', json_encode($map));

		return true;
	}

	/**
	 * Sets the position for an admin widget in the multitask panel.
	 * 
	 * @param 	array 	$pos_list 	Associative list of widget positions.
	 *
	 * @return 	array 	The updated widgets position list.
	 * 
	 * @since 	1.16.10 (J) - 1.6.10 (WP)
	 */
	public function setMultitaskingWidgetPos($pos_list)
	{
		if (!is_array($pos_list)) {
			return false;
		}

		// turn all position values into integers
		$pos_list = array_map(function($pos) {
			return (int) $pos;
		}, $pos_list);

		// update configuration record
		VBOFactory::getConfig()->set('multitasking_wpos', $pos_list);

		return $pos_list;
	}

	/**
	 * Admin widgets used to be named as the entire file name,
	 * inclusive of the ".php" extension. For BC we make sure
	 * to evaluate the admin widget identifiers correclty.
	 * 
	 * @param 	string 	$id 	the widget identifier to simplify.
	 * 
	 * @return 	string 			the simplified widget identifier.
	 * 
	 * @since 	1.15.0 (J) - 1.5.0 (WP)
	 */
	protected function simplifyId($id)
	{
		return str_replace('.php', '', $id);
	}
}
