<?php

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

namespace Joomla\CMS\Captcha;

use Joomla\CMS\Factory;
use Joomla\CMS\Filter\InputFilter;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\Event\DispatcherAwareInterface;
use Joomla\Event\DispatcherAwareTrait;
use Joomla\Event\DispatcherInterface;
use Joomla\Registry\Registry;

// phpcs:disable PSR1.Files.SideEffects
\defined('JPATH_PLATFORM') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Joomla! Captcha base object
 *
 * @abstract
 * @since  2.5
 */
class Captcha implements DispatcherAwareInterface
{
    use DispatcherAwareTrait;

    /**
     * Captcha Plugin object
     *
     * @var    CMSPlugin
     * @since  2.5
     */
    private $captcha;

    /**
     * Editor Plugin name
     *
     * @var    string
     * @since  2.5
     */
    private $name;

    /**
     * Array of instances of this class.
     *
     * @var    Captcha[]
     * @since  2.5
     */
    private static $instances = array();

    /**
     * Class constructor.
     *
     * @param   string  $captcha  The plugin to use.
     * @param   array   $options  Associative array of options.
     *
     * @since   2.5
     * @throws  \RuntimeException
     */
    public function __construct($captcha, $options)
    {
        $this->name = $captcha;

        if (!empty($options['dispatcher']) && $options['dispatcher'] instanceof DispatcherInterface) {
            $this->setDispatcher($options['dispatcher']);
        } else {
            $this->setDispatcher(Factory::getApplication()->getDispatcher());
        }

        $this->_load($options);
    }

    /**
     * Returns the global Captcha object, only creating it
     * if it doesn't already exist.
     *
     * @param   string  $captcha  The plugin to use.
     * @param   array   $options  Associative array of options.
     *
     * @return  Captcha|null  Instance of this class.
     *
     * @since   2.5
     * @throws  \RuntimeException
     */
    public static function getInstance($captcha, array $options = array())
    {
        $signature = md5(serialize(array($captcha, $options)));

        if (empty(self::$instances[$signature])) {
            self::$instances[$signature] = new Captcha($captcha, $options);
        }

        return self::$instances[$signature];
    }

    /**
     * Fire the onInit event to initialise the captcha plugin.
     *
     * @param   string  $id  The id of the field.
     *
     * @return  boolean  True on success
     *
     * @since   2.5
     * @throws  \RuntimeException
     */
    public function initialise($id)
    {
        $arg = ['id' => $id];

        $this->update('onInit', $arg);

        return true;
    }

    /**
     * Get the HTML for the captcha.
     *
     * @param   string  $name   The control name.
     * @param   string  $id     The id for the control.
     * @param   string  $class  Value for the HTML class attribute
     *
     * @return  string  The return value of the function "onDisplay" of the selected Plugin.
     *
     * @since   2.5
     * @throws  \RuntimeException
     */
    public function display($name, $id, $class = '')
    {
        // Check if captcha is already loaded.
        if ($this->captcha === null) {
            return '';
        }

        // Initialise the Captcha.
        if (!$this->initialise($id)) {
            return '';
        }

        $arg = [
            'name'  => $name,
            'id'    => $id ?: $name,
            'class' => $class,
        ];

        $result = $this->update('onDisplay', $arg);

        return $result;
    }

    /**
     * Checks if the answer is correct.
     *
     * @param   string  $code  The answer.
     *
     * @return  bool    Whether the provided answer was correct
     *
     * @since   2.5
     * @throws  \RuntimeException
     */
    public function checkAnswer($code)
    {
        // Check if captcha is already loaded
        if ($this->captcha === null) {
            return false;
        }

        $arg = ['code'  => $code];

        $result = $this->update('onCheckAnswer', $arg);

        return $result;
    }

    /**
     * Method to react on the setup of a captcha field. Gives the possibility
     * to change the field and/or the XML element for the field.
     *
     * @param   \Joomla\CMS\Form\Field\CaptchaField  $field    Captcha field instance
     * @param   \SimpleXMLElement                    $element  XML form definition
     *
     * @return void
     */
    public function setupField(\Joomla\CMS\Form\Field\CaptchaField $field, \SimpleXMLElement $element)
    {
        if ($this->captcha === null) {
            return;
        }

        $arg = [
            'field' => $field,
            'element' => $element,
        ];

        $result = $this->update('onSetupField', $arg);

        return $result;
    }

    /**
     * Method to call the captcha callback if it exist.
     *
     * @param   string  $name   Callback name
     * @param   array   &$args  Arguments
     *
     * @return  mixed
     *
     * @since   4.0.0
     */
    private function update($name, &$args)
    {
        if (method_exists($this->captcha, $name)) {
            return call_user_func_array(array($this->captcha, $name), array_values($args));
        }

        return null;
    }

    /**
     * Load the Captcha plugin.
     *
     * @param   array  $options  Associative array of options.
     *
     * @return  void
     *
     * @since   2.5
     * @throws  \RuntimeException
     */
    private function _load(array $options = array())
    {
        // Build the path to the needed captcha plugin
        $name = InputFilter::getInstance()->clean($this->name, 'cmd');
        $path = JPATH_PLUGINS . '/captcha/' . $name . '/' . $name . '.php';

        if (!is_file($path)) {
            throw new \RuntimeException(Text::sprintf('JLIB_CAPTCHA_ERROR_PLUGIN_NOT_FOUND', $name));
        }

        // Require plugin file
        require_once $path;

        // Get the plugin
        $plugin = PluginHelper::getPlugin('captcha', $this->name);

        if (!$plugin) {
            throw new \RuntimeException(Text::sprintf('JLIB_CAPTCHA_ERROR_PLUGIN_NOT_FOUND', $name));
        }

        // Check for already loaded params
        if (!($plugin->params instanceof Registry)) {
            $params = new Registry($plugin->params);
            $plugin->params = $params;
        }

        // Build captcha plugin classname
        $name = 'PlgCaptcha' . $this->name;
        $dispatcher     = $this->getDispatcher();
        $this->captcha = new $name($dispatcher, (array) $plugin, $options);
    }
}
