<?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!');

/**
 * Admin Widget parent Class of all sub-classes.
 * 
 * @since 	1.14 (J) - 1.4.0 (WP)
 */
abstract class VikBookingAdminWidget
{
	/**
	 * The name of the widget.
	 *
	 * @var 	string
	 */
	protected $widgetName = null;

	/**
	 * The description of the widget.
	 *
	 * @var 	string
	 */
	protected $widgetDescr = '';

	/**
	 * The icon of the widget in HTML.
	 *
	 * @var 	string
	 * 
	 * @since 	1.15.0 (J) - 1.5.0 (WP)
	 */
	protected $widgetIcon = '';

	/**
	 * The style name of the widget.
	 *
	 * @var 	string
	 * 
	 * @since 	1.15.0 (J) - 1.5.0 (WP)
	 */
	protected $widgetStyleName = 'regular';

	/**
	 * The widget settings.
	 *
	 * @var 	mixed
	 */
	protected $widgetSettings = null;

	/**
	 * The VBO application object.
	 *
	 * @var 	object
	 */
	protected $vbo_app = null;

	/**
	 * The date format.
	 *
	 * @var 	string
	 */
	protected $df = '';

	/**
	 * The date separator.
	 *
	 * @var 	string
	 */
	protected $datesep = '';

	/**
	 * Whether the multitask panel
	 * is invoking the widget.
	 *
	 * @var 	bool
	 */
	protected $is_multitask = false;

	/**
	 * The widget identifier.
	 *
	 * @var 	string
	 */
	protected $widgetId = null;

	/**
	 * The widget multitask options registry.
	 *
	 * @var 	VBOMultitaskOptions
	 * 
	 * @since 	1.16.5 (J) - 1.6.5 (WP)
	 */
	protected $options;

	/**
	 * Class constructors should define some vars for the widget in use.
	 */
	public function __construct()
	{
		$this->vbo_app = VikBooking::getVboApplication();
		$this->datesep = VikBooking::getDateSeparator(true);

		$nowdf = VikBooking::getDateFormat(true);
		if ($nowdf == "%d/%m/%Y") {
			$this->df = 'd/m/Y';
		} elseif ($nowdf == "%m/%d/%Y") {
			$this->df = 'm/d/Y';
		} else {
			$this->df = 'Y/m/d';
		}

		$this->options = VBOMultitaskOptions::getInstance();
	}

	/**
	 * Tells if the admin-widget can be installed on the current environment.
	 * 
	 * @return 	bool
	 * 
	 * @since 	1.16.10 (J) - 1.6.10 (WP)
	 */
	public function preflight()
	{
		/**
		 * Trigger event to let external plugins to choose whether this widget should be supported or not.
		 * 
		 * @param   string  $widgetId  The widget identifier.
		 * 
		 * @return  bool    False to drop support to this widget.
		 * 
		 * @since   1.18 (J) - 1.8 (WP)
		 */
		$results = VBOFactory::getPlatform()->getDispatcher()->filter('onPreflightAdminWidget', [$this->widgetId]);

		// supported only in case none of the plugins attached to this event returned FALSE
		return !in_array(false, $results, true);
	}

	/**
	 * Returns the admin-widget priority for a custom ordering.
	 * 
	 * @return 	int
	 * 
	 * @since 	1.16.10 (J) - 1.6.10 (WP)
	 */
	public function getPriority()
	{
		return 10;
	}

	/**
	 * Gets the name of the current widget.
	 * 
	 * @return 	string 	the widget name.
	 */
	public function getName()
	{
		return $this->widgetName;
	}

	/**
	 * Gets the description for the current widget.
	 * 
	 * @return 	string 	the widget description.
	 */
	public function getDescription()
	{
		return $this->widgetDescr;
	}

	/**
	 * Gets the icon for the current widget.
	 * 
	 * @return 	string 	the widget HTML string icon.
	 */
	public function getIcon()
	{
		return !empty($this->widgetIcon) ? $this->widgetIcon : $this->getDefaultIcon();
	}

	/**
	 * Gets the style name for the current widget.
	 * 
	 * @return 	string 	the widget style name.
	 */
	public function getStyleName()
	{
		return $this->widgetStyleName;
	}

	/**
	 * Gets the identifier of the current widget.
	 * 
	 * @return 	string 	the widget identifier.
	 */
	public function getIdentifier()
	{
		if (!$this->widgetId) {
			// fetch widget ID from class name
			$this->widgetId = preg_replace("/^VikBookingAdminWidget/i", '', get_class($this));
			// place an underscore between each camelCase
			$this->widgetId = strtolower(preg_replace("/([a-z])([A-Z])/", '$1_$2', $this->widgetId));
			$this->widgetId = strtolower($this->widgetId);
		}

		return $this->widgetId;
	}

	/**
	 * Turns on/off the flag to detect multitask rendering.
	 * 
	 * @param 	bool 	$in 	true if in multitask panel, or false.
	 * 
	 * @return 	self
	 * 
	 * @since 	1.15.0 (J) - 1.5.0 (WP)
	 */
	public function setInMultitask($in = false)
	{
		$this->is_multitask = (bool)$in;

		return $this;
	}

	/**
	 * Default method to preload the necessary assets of the widget.
	 * Extending classes needing certain assets to be available should
	 * override this method and let it load the CSS/JS assets.
	 * Useful to avoid JS errors during the AJAX generation of a widget.
	 * 
	 * The method can also be helpful for a widget to register watch data.
	 * For example, the last ID retrieved of a certain record can be used
	 * to receive it back during the periodic data-watching and be able
	 * to detect if a new event has occurred to fire a browser notification.
	 * 
	 * @return 	mixed 	watch-data object or null.
	 * 
	 * @since 	1.15.0 (J) - 1.5.0 (WP)
	 */
	public function preload()
	{
		return null;
	}

	/**
	 * Default method to return a list of watch-data and new browser notifications.
	 * Extending classes needing to periodically watch for new events and capable
	 * of emitting browser notifications, can override this method. The last data
	 * to watch should be returned during the preloading. To be used with list().
	 * 
	 * @param 	?VBONotificationWatchdata 	$watch_data 	the preloaded watch-data object.
	 * 
	 * @return 	array 						data object to watch next and notifications array.
	 * 
	 * @see 	preload()
	 * 
	 * @since 	1.15.0 (J) - 1.5.0 (WP)
	 */
	public function getNotifications(?VBONotificationWatchdata $watch_data = null)
	{
		$watch_next = null;
		$notifications = null;

		return [$watch_next, $notifications];
	}

	/**
	 * Default method to return a list of browser events to dispatch.
	 * Extending classes needing to periodically watch for new events
	 * and capable of emitting browser events, can override this method.
	 * 
	 * @param 	?VBONotificationWatchdata 	$watch_data 	the preloaded watch-data object.
	 * 
	 * @return 	array 						list of browser events to emit.
	 * 
	 * @see 	preload()
	 * 
	 * @since 	1.16.8 (J) - 1.6.8 (WP)
	 */
	public function getNotificationEvents(?VBONotificationWatchdata $watch_data = null)
	{
		return [];
	}

	/**
	 * Binds the widget options at runtime when multitask data is available.
	 * Options live on a separate layer than regular multitask data.
	 * 
	 * @param 	VBOMultitaskData|VBOMultitaskOptions|array|object 	$options 	options to bind.
	 * 
	 * @return 	self
	 * 
	 * @since 	1.16.5 (J) - 1.6.5 (WP)
	 */
	public function bindOptions($options)
	{
		if ($options instanceof VBOMultitaskData) {
			// set options by extracting them from multitask data
			$this->options->setProperties($options->getDataOptions());
		} elseif ($options instanceof VBOMultitaskOptions) {
			// replace options with given instance
			$this->options = $options;
		} elseif (is_array($options) || is_object($options)) {
			// bind new options
			$this->options = VBOMultitaskOptions::getInstance($options);
		}

		return $this;
	}

	/**
	 * Gets all widget options previously set through Multitask data.
	 * Options live on a separate layer than regular Multitask data.
	 * 
	 * @param 	bool 	$public 	true to get only the public options.
	 * 
	 * @return 	array 				associative list of options, if any.
	 * 
	 * @since 	1.16.5 (J) - 1.6.5 (WP)
	 */
	public function getOptions($public = false)
	{
		return $this->options->getProperties($public);
	}

	/**
	 * Proxy to access the multitask options object.
	 * 
	 * @return 	VBOMultitaskOptions
	 * 
	 * @since 	1.16.5 (J) - 1.6.5 (WP)
	 */
	public function options()
	{
		return $this->options;
	}

	/**
	 * Sets a widget option value.
	 * 
	 * @param 	string 	$key 	the option key identifier.
	 * @param 	mixed 	$value 	the option value to set.
	 * 
	 * @return 	self
	 * 
	 * @since 	1.16.5 (J) - 1.6.5 (WP)
	 */
	public function setOption($key, $value)
	{
		$this->options->set($key, $value);

		return $this;
	}

	/**
	 * Gets the value for the given widget option key.
	 * 
	 * @param 	string 	$key 		the option key identifier.
	 * @param 	mixed 	$default 	the default value to get.
	 * 
	 * @return 	mixed
	 * 
	 * @since 	1.16.5 (J) - 1.6.5 (WP)
	 */
	public function getOption($key, $default = null)
	{
		return $this->options->get($key, $default);
	}

	/**
	 * Returns an associative array with the current widget details.
	 * Method's visibility should never change from public, because
	 * the controller may call it, and widgets may override it.
	 * 
	 * @return 	array
	 * 
	 * @since 	1.16.7 (J) - 1.6.7 (WP)
	 */
	public function getWidgetDetails()
	{
		return [
			'id'    => $this->getIdentifier(),
			'name'  => $this->getName(),
			'descr' => $this->getDescription(),
			'icon'  => $this->getIcon(),
			'style' => $this->getStyleName(),
		];
	}

	/**
	 * Extending Classes should define this method to render the actual
	 * output of the admin widget. Multitask data can be passed along.
	 * 
	 * @param 	?VBOMultitaskData 	$data 	optional multitask data object.
	 * 
	 * @since 	1.15.0 (J) - 1.5.0 (WP) type hint added for $data argument.
	 */
	abstract public function render(?VBOMultitaskData $data = null);

	/**
	 * Returns the default icon for a widget.
	 * 
	 * @return 	string 	the default widget icon HTML string.
	 */
	protected function getDefaultIcon()
	{
		return '<i class="' . VikBookingIcons::i('cube') . '"></i>';
	}

	/**
	 * Tells the widget if its being rendered via AJAX.
	 * 
	 * @return 	bool 	true if rendering is being made via AJAX.
	 */
	protected function isAjaxRendering()
	{
		$widget_id = VikRequest::getString('widget_id', '', 'request');
		$call 	   = VikRequest::getString('call', '', 'request');

		return ($widget_id == $this->getIdentifier() && $call == 'render');
	}

	/**
	 * Tells the widget if its being rendered in the multitask panel.
	 * This kind of rendering could be in an AJAX context or in a regular
	 * loading flow. Use the apposite method to see if it's an AJAX rendering.
	 * 
	 * @return 	bool 	true if rendering is handled by the multitask panel.
	 * 
	 * @since 	1.15.0 (J) - 1.5.0 (WP)
	 */
	protected function isMultitaskRendering()
	{
		$multitask = VikRequest::getInt('multitask', 0, 'request');

		return ($multitask > 0 || $this->is_multitask === true);
	}

	/**
	 * Tells the widget if VCM is available.
	 * 
	 * @return 	bool 	true if Vik Channel Manager is available.
	 * 
	 * @since 	1.16.0 (J) - 1.6.0 (WP)
	 */
	protected function hasChannelManager()
	{
		return is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
	}

	/**
	 * Gets the configuration parameter name for the widget's settings.
	 * 
	 * @return 	string 	the param name of the settings record.
	 */
	protected function getSettingsParamName()
	{
		return 'admin_widget_' . $this->getIdentifier();
	}

	/**
	 * Loads the widget's settings from the configuration table, if any.
	 * If no record found for this widget, an empty record will be inserted.
	 * 
	 * @return 	mixed 	the widget settings.
	 */
	protected function loadSettings()
	{
		$dbo = JFactory::getDbo();

		$param_name = $this->getSettingsParamName();

		$q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`=" . $dbo->quote($param_name);
		$dbo->setQuery($q, 0, 1);
		$settings = $dbo->loadResult();		

		if (!$settings) {
			return null;
		}

		if (in_array(substr($settings, 0, 1), array('{', '['))) {
			// we have detected a JSON string, try to decoded it
			$decoded = json_decode($settings);
			if (function_exists('json_last_error') && json_last_error()) {
				// json is broken, reset settings and return null
				$this->resetSettings();
				return null;
			}
			// return the decoded settings
			return $decoded;
		}

		// return the plain db value otherwise
		return $settings;
	}

	/**
	 * Updates the widget's settings in the configuration table.
	 * 
	 * @param 	mixed 	$data 	the settings to store, must be a scalar.
	 * 
	 * @return 	bool 	true on success, false otherwise.
	 */
	protected function updateSettings($data)
	{
		if ($data === null || !is_scalar($data)) {
			return false;
		}

		$param_name = $this->getSettingsParamName();

		$config = VBOFactory::getConfig();
		$config->set($param_name, $data);

		return true;
	}

	/**
	 * Resets the settings of the widget.
	 * 
	 * @return 	bool
	 */
	public function resetSettings()
	{
		$dbo = JFactory::getDbo();

		$param_name = $this->getSettingsParamName();

		$q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`=" . $dbo->quote($param_name) . ";";
		$dbo->setQuery($q);
		$dbo->execute();
		if (!$dbo->getNumRows()) {
			// settings never used
			return false;
		}

		$q = "DELETE FROM `#__vikbooking_config` WHERE `param`=" . $dbo->quote($param_name) . ";";
		$dbo->setQuery($q);
		$dbo->execute();

		return true;
	}

	/**
	 * Method invoked during AJAX requests for removing an instance of this widget.
	 * By default we try to unset the passed instance index from the settings array.
	 * If the widget does not use settings, nothing is done. Widgets could override this method.
	 * 
	 * @return 	bool 	true if settings needed to be updated, false otherwise.
	 */
	public function removeInstance()
	{
		$settings = $this->loadSettings();
		if ($settings === null || !is_array($settings)) {
			return false;
		}

		$widget_instance = VikRequest::getInt('widget_instance', -1, 'request');

		if (!isset($settings[$widget_instance])) {
			// settings index not found
			return false;
		}

		// splice the array to remove the requested settings instance
		array_splice($settings, $widget_instance, 1);

		// update widget's settings
		$this->updateSettings(json_encode($settings));

		return true;
	}

	/**
	 * Method invoked during AJAX requests for moving an instance of this widget.
	 * This occurs when dragging and dropping the same type of widget to a different position.
	 * If the widget does not use settings, nothing is done. Widgets could override this method.
	 * 
	 * @return 	bool 	true if settings needed to be updated, false otherwise.
	 */
	public function sortInstance()
	{
		$settings = $this->loadSettings();
		if ($settings === null || !is_array($settings)) {
			return false;
		}

		$widget_index_old = VikRequest::getInt('widget_index_old', -1, 'request');
		$widget_index_new = VikRequest::getInt('widget_index_new', -1, 'request');

		if (!isset($settings[$widget_index_old]) || !isset($settings[$widget_index_new])) {
			// settings index not found
			return false;
		}

		// move the settings requested from the old index to the new index
		$extracted = array_splice($settings, $widget_index_old, 1);
		array_splice($settings, $widget_index_new, 0, $extracted);

		// update widget's settings
		$this->updateSettings(json_encode($settings));

		return true;
	}

	/**
	 * Returns the name of the user currently logged in.
	 * 
	 * @return 	string 	the name of the current user.
	 */
	protected function getLoggedUserName()
	{
		$user = JFactory::getUser();
		$name = $user->name;

		return $name;
	}

	/**
	 * Rewrites a given URI to perform an AJAX request.
	 * 
	 * @param 	string 	$uri 	the optional URI plus query to force.
	 * 
	 * @return 	string 	the proper AJAX uri for the current platform.
	 * 
	 * @since 	1.15.0 (J) - 1.5.0 (WP)
	 */
	protected function getExecWidgetAjaxUri($uri = '')
	{
		if (empty($uri)) {
			$uri = 'index.php?option=com_vikbooking&task=exec_admin_widget';
		}

		return VikBooking::ajaxUrl($uri);
	}

	/**
	 * Returns the date format in VBO of a particular type.
	 *
	 * @param 	string 	$type
	 *
	 * @return 	string
	 * 
	 * @since 	1.15.0 (J) - 1.5.0 (WP)
	 */
	protected function getDateFormat($type = 'date')
	{
		if ($this->df == 'd/m/Y') {
			$juidf = 'dd/mm/yy';
		} elseif ($this->df == 'm/d/Y') {
			$juidf = 'mm/dd/yy';
		} else {
			$juidf = 'yy/mm/dd';
		}

		switch ($type) {
			case 'jui':
				return $juidf;
			case 'joomla':
			case 'wordpress':
				return VikBooking::getDateFormat(true);
			default:
				return $this->df;
		}
	}
}
