<?php
/**
 * Joomla! Content Management System
 *
 * @copyright  (C) 2016 Open Source Matters, Inc. <https://www.joomla.org>
 * @license    GNU General Public License version 2 or later; see LICENSE
 */

namespace Joomla\CMS\Event;

\defined('JPATH_PLATFORM') or die;

use BadMethodCallException;
use Joomla\Event\Event as BaseEvent;
use Joomla\String\Normalise;

/**
 * This class implements the base Event object used system-wide to offer orthogonality. Core objects such as Models,
 * Controllers, etc create such events on-the-fly and dispatch them through the application's Dispatcher (colloquially
 * known as the "Joomla! plugin system"). This way a suitable plugin, typically a "system" plugin, can modify the
 * behaviour of any internal class, providing system-wide services such as tags, content versioning, comments or even
 * low-level services such as the implementation of created/modified/locked behaviours, record hit counter etc.
 *
 * You can create a new Event with something like this:
 *
 * $event = AbstractEvent::create('onModelBeforeSomething', $myModel, $arguments);
 *
 * You can access the subject object from your event Listener using $event['subject']. It is up to your listener to
 * determine whether it should apply its functionality against the subject.
 *
 * This AbstractEvent class implements a mutable event which is allowed to change its arguments at runtime. This is
 * generally unadvisable. It's best to use AbstractImmutableEvent instead and constrict all your interaction to the
 * subject class.
 *
 * @since  4.0.0
 */
abstract class AbstractEvent extends BaseEvent
{
	/**
	 * Creates a new CMS event object for a given event name and subject. The following arguments must be given:
	 * subject		object	The subject of the event. This is the core object you are going to manipulate.
	 * eventClass	string	The Event class name. If you do not provide it Joomla\CMS\Events\<eventNameWithoutOnPrefix>
	 *                      will be used.
	 *
	 * @param   string  $eventName  The name of the event, e.g. onTableBeforeLoad
	 * @param   array   $arguments  Additional arguments to pass to the event
	 *
	 * @return  static
	 *
	 * @since   4.0.0
	 * @throws  BadMethodCallException  If you do not provide a subject argument
	 */
	public static function create(string $eventName, array $arguments = [])
	{
		// Get the class name from the arguments, if specified
		$eventClassName = '';

		if (isset($arguments['eventClass']))
		{
			$eventClassName = $arguments['eventClass'];

			unset($arguments['eventClass']);
		}

		/**
		 * If the class name isn't set/found determine it from the event name, e.g. TableBeforeLoadEvent from
		 * the onTableBeforeLoad event name.
		 */
		if (empty($eventClassName) || !class_exists($eventClassName, true))
		{
			$bareName = strpos($eventName, 'on') === 0 ? substr($eventName, 2) : $eventName;
			$parts = Normalise::fromCamelCase($bareName, true);
			$eventClassName = __NAMESPACE__ . '\\' . ucfirst(array_shift($parts)) . '\\';
			$eventClassName .= implode('', $parts);
			$eventClassName .= 'Event';
		}

		// Make sure a non-empty subject argument exists and that it is an object
		if (!isset($arguments['subject']) || empty($arguments['subject']) || !\is_object($arguments['subject']))
		{
			throw new BadMethodCallException("No subject given for the $eventName event");
		}

		// Create and return the event object
		if (class_exists($eventClassName, true))
		{
			return new $eventClassName($eventName, $arguments);
		}

		return new GenericEvent($eventName, $arguments);
	}

	/**
	 * Constructor. Overridden to go through the argument setters.
	 *
	 * @param   string  $name       The event name.
	 * @param   array   $arguments  The event arguments.
	 *
	 * @since   4.0.0
	 */
	public function __construct(string $name, array $arguments = [])
	{
		parent::__construct($name, $arguments);

		$this->arguments = [];

		foreach ($arguments as $argumentName => $value)
		{
			$this->setArgument($argumentName, $value);
		}
	}

	/**
	 * Get an event argument value. It will use a getter method if one exists. The getters have the signature:
	 *
	 * get<ArgumentName>($value): mixed
	 *
	 * where:
	 *
	 * $value  is the value currently stored in the $arguments array of the event
	 * It returns the value to return to the caller.
	 *
	 * @param   string  $name     The argument name.
	 * @param   mixed   $default  The default value if not found.
	 *
	 * @return  mixed  The argument value or the default value.
	 *
	 * @since   4.0.0
	 */
	public function getArgument($name, $default = null)
	{
		$methodName = 'get' . ucfirst($name);

		$value = parent::getArgument($name, $default);

		if (method_exists($this, $methodName))
		{
			return $this->{$methodName}($value);
		}

		return $value;
	}

	/**
	 * Add argument to event. It will use a setter method if one exists. The setters have the signature:
	 *
	 * set<ArgumentName>($value): mixed
	 *
	 * where:
	 *
	 * $value  is the value being set by the user
	 * It returns the value to return to set in the $arguments array of the event.
	 *
	 * @param   string  $name   Argument name.
	 * @param   mixed   $value  Value.
	 *
	 * @return  $this
	 *
	 * @since   4.0.0
	 */
	public function setArgument($name, $value)
	{
		$methodName = 'set' . ucfirst($name);

		if (method_exists($this, $methodName))
		{
			$value = $this->{$methodName}($value);
		}

		return parent::setArgument($name, $value);
	}
}
