<?php
/** 
 * @package     VikWP - Libraries
 * @subpackage  adapter.sms
 * @author      E4J s.r.l.
 * @copyright   Copyright (C) 2023 E4J s.r.l. All Rights Reserved.
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
 * @link        https://vikwp.com
 */

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

JLoader::import('adapter.sms.status');

/**
 * This class describes the events that an abstract sms driver should handle.
 * 
 * @since 10.1.30
 */
abstract class JSmsDriver
{
	/**
	 * The name of the plugin that has requested the sms.
	 *
	 * @var string
	 */
	protected $caller;

	/**
	 * The driver identifier.
	 *
	 * @var string
	 */
	protected $driver;

	/**
	 * The order details.
	 *
	 * @var JObject
	 */
	protected $order;

	/**
	 * The driver configuration parameters.
	 *
	 * @var JObject
	 */
	protected $params;

	/**
	 * Class constructor.
	 *
	 * @param 	string 	$caller  The name of the plugin that requested the sms.
	 * @param 	mixed 	$order 	 The order details to start the transaction.
	 * @param 	mixed 	$params  The configuration of the driver.
	 */
	public function __construct($caller, $order, $params = array())
	{
		if (is_string($params))
		{
			$params = (array) json_decode($params);
		}

		$this->caller 	= $caller;
		$this->order 	= new JObject($order);
		$this->params 	= new JObject($params);
	}

	/**
	 * Returns the name of the plugin that dispatched the sms.
	 *
	 * @return 	string
	 */
	public function getCaller()
	{
		return $this->caller;
	}

	/**
	 * Checks if the given caller matches the current one.
	 *
	 * @param 	string 	 $caller  The caller to check.
	 *
	 * @return 	boolean  True if the callers are equal, otherwise false.
	 *
	 * @uses 	getCaller()
	 */
	public function isCaller($caller)
	{
		return !strcasecmp($this->getCaller(), $caller);
	}

	/**
	 * Returns the name of the driver.
	 *
	 * @return 	string
	 */
	public function getDriver()
	{
		if (!$this->driver)
		{
			$class = get_class($this);

			// extract driver name from classname
			$this->driver = preg_replace_callback("/{$this->caller}Sms(.*?)/i", function($match)
			{
				return $match[1];
			}, $class);

			$this->driver = strtolower($this->driver);
		}

		return $this->driver;
	}

	/**
	 * Checks if the given driver matches the current one.
	 *
	 * @param 	string 	 $driver  The driver to check.
	 *
	 * @return 	boolean  True if the drivers are equal, otherwise false.
	 *
	 * @uses 	getDriver()
	 */
	public function isDriver($driver)
	{
		return !strcasecmp($this->getDriver(), $driver);
	}

	/**
	 * Returns the order detail related to the specified key.
	 *
	 * @param 	string 	$key 	The registry key.
	 * @param 	mixed 	$def 	The default value if the key is not set.
	 *
	 * @return 	mixed 	The registry value, or the default one.
	 */
	public function get($key, $def = null)
	{
		return $this->order->get($key, $def);
	}

	/**
	 * Updates the specified key into the order details object.
	 *
	 * @param 	string 	$key 	The registry key.
	 * @param 	mixed 	$val 	The value to update.
	 *
	 * @param 	mixed 	The old value.
	 */
	public function set($key, $val)
	{
		return $this->order->set($key, $val);
	}

	/**
	 * Returns an associative array containing the order details.
	 *
	 * @return 	array
	 */
	public function getOrder()
	{
		return $this->order->getProperties();
	}

	/**
	 * Replaces the internal order details with the specified ones.
	 * 
	 * @param   array|object  $order
	 *
	 * @return 	void
	 * 
	 * @since   10.1.69
	 */
	public function setOrder($order)
	{
		$this->order = new JObject($order);
	}

	/**
	 * Returns the configuration parameter related to the specified key.
	 *
	 * @param 	string 	$key 	The registry key.
	 * @param 	mixed 	$def 	The default value if the key is not set.
	 *
	 * @return 	mixed 	The registry value, or the default one.
	 */
	public function getParam($key, $def = null)
	{
		return $this->params->get($key, $def);
	}

	/**
	 * Updates the specified key into the configuration object.
	 *
	 * @param 	string 	$key 	The registry key.
	 * @param 	mixed 	$val 	The value to update.
	 *
	 * @param 	mixed 	The old value.
	 */
	public function setParam($key, $val)
	{
		return $this->params->set($key, $val);
	}

	/**
	 * Returns an associative array containing the driver parameters.
	 *
	 * @return 	array
	 */
	public function getParams()
	{
		return $this->params->getProperties();
	}

	/**
	 * Returns an associative array containing the form
	 * fields used to construct the configuration of the driver.
	 *
	 * This method triggers two ACTIONS to manipulate the 
	 * configuration array before it is returned:
	 * - sms_driver_before_admin_params
	 * - sms_driver_after_admin_params
	 *
	 * @return 	array 	The sms driver configuration.
	 *
	 * @uses 	buildAdminParameters()
	 */
	public function getAdminParameters()
	{
		/**
		 * Plugins can manipulate the properties of this object.
		 * Fires before the configuration array is generated.
		 *
		 * @param 	self 	A reference to this object.
		 *
		 * @since 	10.1.30
		 */
		do_action('sms_driver_before_admin_params_' . $this->caller, array(&$this));

		// children drivers will create the configuration form (as array)
		$config = $this->buildAdminParameters();

		/**
		 * Plugins can manipulate the configuration form of the driver.
		 * Fires after generating the config form.
		 *
		 * @param 	self 	A reference to this object.
		 * @param 	array 	A reference to the configuration array.
		 *
		 * @since 	10.1.30
		 */
		do_action_ref_array('sms_driver_after_admin_params_' . $this->caller, array(&$this, &$config));

		return $config;
	}

	/**
	 * Abstract method used to build the associative array 
	 * to allow the plugins to construct a configuration form.
	 *
	 * In case the driver needs an API Key, the array should
	 * be built as follows:
	 *
	 * {"apikey": {"type": "text", "label": "API Key"}}
	 *
	 * @return 	array 	The associative array.
	 */
	protected function buildAdminParameters()
	{
		// return an empty array because a sms driver
		// may not need a configuration
		return array();
	}

	/**
	 * Dispatches the SMS to the specified phone number.
	 *
	 * This method triggers the ACTIONS below:
	 * - sms_driver_before_send
	 * - sms_driver_after_send
	 *
	 * @param 	string  $phone  The sms receiver.
	 * @param 	string  $text   The message to send.
	 *
	 * @return 	mixed 	An object describing the status of the sms.
	 *
	 * @uses 	dispatch()
	 */
	public function sendMessage($phone, $text)
	{
		/**
		 * Plugins can manipulate the properties of this object.
		 * Fires before sending the SMS.
		 *
		 * @param 	self 	A reference to this object.
		 * @param 	string  The sms receiver.
		 * @param 	string  The message to send.
		 *
		 * @since 	10.1.30
		 */
		do_action('sms_driver_before_send_' . $this->caller, array(&$this, &$phone, &$text));

		$status = new JSmsStatus();

		// sanitize phone number
		$phone = $this->sanitizePhone($phone);

		// children drivers will dispatch the message
		$this->dispatch($phone, $text, $status);

		// inject receiver and SMS within the status object
		$status->setData('phone', $phone);
		$status->setData('sms', $text);

		$response = null;

		/**
		 * Plugins can manipulate the response object to return.
		 * By filling the &$response variable, this method will return
		 * it instead of the default &$status one.
		 * Fires after dispatching the sms.
		 *
		 * @param 	self 		A reference to this object.
		 * @param 	JSmsStatus 	A reference to the status object.
		 * @param 	mixed 		A reference to the final response (null by default).
		 *
		 * @since 	10.1.30
		 */
		do_action_ref_array('sms_driver_after_send_' . $this->caller, array(&$this, &$status, &$response));

		if (is_null($response))
		{
			// no hook fired, the response will be equals to the status object
			$response = $status;
		}

		return $response;
	}

	/**
	 * Abstract method used to dispatch a SMS to the specified phone.
	 *
	 * @param 	string      $phone    The sms receiver.
	 * @param 	string      $text     The message to send.
	 * @param 	JSmsStatus  &$status  The status of the notification.
	 *
	 * @return 	void
	 */
	abstract protected function dispatch($phone, $text, JSmsStatus &$status);

	/**
	 * Checks whether the current driver is able to estimate the credit.
	 *
	 * @return 	boolean
	 */
	public function canEstimate()
	{
		// inherit in chidlren classes
		return false;
	}

	/**
	 * Tries to recover the remaining balance.
	 *
	 * This method triggers the ACTIONS below:
	 * - sms_driver_before_estimate
	 * - sms_driver_after_estimate
	 *
	 * @param 	string  $phone  An optional receiver.
	 * @param 	string  $text   An optional message.
	 *
	 * @return 	mixed 	The remaining balance on success, false otherwise.
	 *
	 * @uses 	estimate()
	 */
	public function getCredit($phone = null, $text = null)
	{
		/**
		 * Plugins can manipulate the properties of this object.
		 * Fires before estimating the remaining credit.
		 *
		 * @param 	self 	A reference to this object.
		 * @param 	string  The sms receiver.
		 * @param 	string  The message to send.
		 *
		 * @since 	10.1.30
		 */
		do_action('sms_driver_before_estimate_' . $this->caller, array(&$this, &$phone, &$text));

		// sanitize phone number
		$phone = $this->sanitizePhone($phone);

		try
		{
			// children drivers will estimate the credit
			$credit = $this->estimateCredit($phone, $text);
		}
		catch (Exception $e)
		{
			$credit = false;
		}

		/**
		 * Plugins can manipulate the resulting credit.
		 * Fires after estimating the user credit.
		 *
		 * @param 	self 	A reference to this object.
		 * @param 	mixed 	A reference to the user credit (null by default).
		 *
		 * @since 	10.1.30
		 */
		do_action_ref_array('sms_driver_after_estimate_' . $this->caller, array(&$this, &$credit));

		return $credit;
	}

	/**
	 * Tries to estimate the remaining user credit.
	 *
	 * @param 	string  $phone  An optional receiver.
	 * @param 	string  $text   An optional message.
	 *
	 * @return 	mixed 	The remaining balance on success, false otherwise.
	 *
	 * @throws 	Exception
	 */
	protected function estimateCredit($phone, $text)
	{
		// inherit in children classes to estimate credit
		throw new Exception('Not implemented', 501);
	}

	/**
	 * Validates the response.
	 * Implement for platform BC.
	 * 
	 * @param 	mixed 	$response
	 * 
	 * @return 	boolean
	 */
	public function validateResponse($response)
	{
		// reset log
		$this->log = '';

		if (!$response instanceof JSmsStatus)
		{
			// invalid argument
			return false;
		}

		// register log
		$this->log = $response->log;

		// check sms status
		return $response->isVerified();
	}

	/**
	 * Returns the latest logs.
	 *
	 * @return 	string
	 */
	public function getLog()
	{
		return isset($this->log) ? $this->log : '';
	}

	/**
	 * Helper method used to sanitize phone numbers.
	 *
	 * @param 	string 	$phone 	The phone number to sanitize.
	 *
	 * @return 	string 	The cleansed number.
	 */
	public function sanitizePhone($phone)
	{
		// trim unexpected characters
		$phone = preg_replace("/[^0-9+]/", '', $phone);

		// replace 00 dial code with +
		if (substr($phone, 0, 2) == '00')
		{
			$phone = '+' . substr($phone, 2);
		}

		return $phone;
	}

	/**
	 * Adds the dial code to the phone number.
	 *
	 * @param 	string  $prefix  The dial code to prepend.
	 * @param  	string 	$phone   The phone number.
	 *
	 * @return 	string  The resulting phone number.
	 */
	public function addDialCode($prefix, $phone)
	{
		// check if the phone number already owns a dial code
		if ($prefix && !preg_match("/^\+/", $phone))
		{
			// sanitize dial code
			$prefix = $this->sanitizePhone($prefix);

			// normalize dial code
			$prefix = '+' . ltrim($prefix, '+');

			// prepend dial code
			$phone = $prefix . $phone;
		}

		return $phone;
	}

	/**
	 * Check if the specified message contains UTF-8 characters.
	 *
	 * @param 	string 	 $message 	The string to check.
	 *
	 * @return 	boolean  True if unicode, otherwise false.
	 */
	public function isUnicode($message)
	{
		if (function_exists('iconv'))
		{
			$latin = @iconv('UTF-8', 'ISO-8859-1', $message);

			if (strcmp($latin, $message))
			{
				$arr = unpack('H*hex', @iconv('UTF-8', 'UCS-2BE', $message));
				//return strtoupper($arr['hex']);
				return true;
			}
		}

		return false;
	}
}
