<?php

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

namespace Joomla\CMS\MVC\View;

use Joomla\CMS\Document\Document;
use Joomla\CMS\Document\DocumentAwareInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\LanguageAwareInterface;
use Joomla\CMS\Language\LanguageAwareTrait;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\Object\LegacyErrorHandlingTrait;
use Joomla\CMS\Object\LegacyPropertyManagementTrait;
use Joomla\Event\DispatcherAwareInterface;
use Joomla\Event\DispatcherAwareTrait;
use Joomla\Event\DispatcherInterface;
use Joomla\Event\EventInterface;

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

/**
 * Base class for a Joomla View
 *
 * Class holding methods for displaying presentation data.
 *
 * @since  2.5.5
 */
#[\AllowDynamicProperties]
abstract class AbstractView implements ViewInterface, DispatcherAwareInterface, DocumentAwareInterface, LanguageAwareInterface
{
    use DispatcherAwareTrait;
    use LanguageAwareTrait;
    use LegacyErrorHandlingTrait;
    use LegacyPropertyManagementTrait {
        get as private legacyGet;
    }


    /**
     * The active document object
     *
     * @var    Document
     * @since  3.0
     *
     * @deprecated 4.4.0 will be removed in 6.0
     *             Use $this->getDocument() instead
     */
    public $document;

    /**
     * The URL option for the component. It is usually passed by controller while it creates the view
     *
     * @var    string
     * @since  3.0
     */
    protected $option = null;

    /**
     * The name of the view
     *
     * @var    string
     * @since  3.0
     */
    protected $_name = null;

    /**
     * Registered models
     *
     * @var    array
     * @since  3.0
     */
    protected $_models = [];

    /**
     * The default model
     *
     * @var    string
     * @since  3.0
     */
    protected $_defaultModel = null;

    /**
     * Constructor
     *
     * @param   array  $config  A named configuration array for object construction.
     *                          name: the name (optional) of the view (defaults to the view class name suffix).
     *                          charset: the character set to use for display
     *                          escape: the name (optional) of the function to use for escaping strings
     *                          base_path: the parent path (optional) of the views directory (defaults to the component folder)
     *                          template_plath: the path (optional) of the layout directory (defaults to base_path + /views/ + view name
     *                          helper_path: the path (optional) of the helper files (defaults to base_path + /helpers/)
     *                          layout: the layout (optional) to use to display the view
     *
     * @since   3.0
     */
    public function __construct($config = [])
    {
        // Set the view name
        if (empty($this->_name)) {
            if (\array_key_exists('name', $config)) {
                $this->_name = $config['name'];
            } else {
                $this->_name = $this->getName();
            }
        }

        // Set the component name if passed
        if (!empty($config['option'])) {
            $this->option = $config['option'];
        }
    }

    /**
     * Execute and display a template script.
     *
     * @param   string  $tpl  The name of the template file to parse; automatically searches through the template paths.
     *
     * @return  void
     *
     * @since   3.0
     */
    abstract public function display($tpl = null);

    /**
     * Method to get data from a registered model or a property of the view
     *
     * @param   string  $property  The name of the method to call on the model or the property to get
     * @param   string  $default   The name of the model to reference or the default value [optional]
     *
     * @return  mixed  The return value of the method
     *
     * @since   3.0
     *
     * @deprecated 5.3.0 will be removed in 7.0.
     *              Retrieve the model with $model = $this->getModel(); and call the methods
     *              to the model directly, e.g. $model->getItems() instead of $this->get('Items').
     */
    public function get($property, $default = null)
    {
        trigger_deprecation(
            'joomla/mvc/view',
            '5.3',
            'The %s() method is deprecated and will be removed in 7.0. use $model = $this->getModel();
            $this->items = $model->getItems(); instead',
            __METHOD__
        );

        // If $model is null we use the default model
        if ($default === null) {
            $model = $this->_defaultModel;
        } else {
            $model = strtolower($default);
        }

        // First check to make sure the model requested exists
        if (isset($this->_models[$model])) {
            // Model exists, let's build the method name
            $method = 'get' . ucfirst($property);

            // Does the method exist?
            if (method_exists($this->_models[$model], $method)) {
                // The method exists, let's call it and return what we get
                return $this->_models[$model]->$method();
            }
        }

        if (isset($this->$property)) {
            return $this->$property;
        }

        return $default;
    }

    /**
     * Method to get the model object
     *
     * @param   string  $name  The name of the model (optional)
     *
     * @return  BaseDatabaseModel  The model object
     *
     * @since   3.0
     */
    public function getModel($name = null)
    {
        if ($name === null) {
            $name = $this->_defaultModel;
        }

        return $this->_models[strtolower($name)];
    }

    /**
     * Method to add a model to the view.  We support a multiple model single
     * view system by which models are referenced by classname.  A caveat to the
     * classname referencing is that any classname prepended by \JModel will be
     * referenced by the name without \JModel, eg. \JModelCategory is just
     * Category.
     *
     * @param   BaseDatabaseModel  $model    The model to add to the view.
     * @param   boolean            $default  Is this the default model?
     *
     * @return  BaseDatabaseModel  The added model.
     *
     * @since   3.0
     */
    public function setModel($model, $default = false)
    {
        $name                 = strtolower($model->getName());
        $this->_models[$name] = $model;

        if ($default) {
            $this->_defaultModel = $name;
        }

        return $model;
    }

    /**
     * Method to get the view name
     *
     * The model name by default parsed using the classname, or it can be set
     * by passing a $config['name'] in the class constructor
     *
     * @return  string  The name of the model
     *
     * @since   3.0
     * @throws  \Exception
     */
    public function getName()
    {
        if (empty($this->_name)) {
            $reflection = new \ReflectionClass($this);

            if ($viewNamespace = $reflection->getNamespaceName()) {
                $pos = strrpos($viewNamespace, '\\');

                if ($pos !== false) {
                    $this->_name = strtolower(substr($viewNamespace, $pos + 1));
                }
            } else {
                $className = \get_class($this);
                $viewPos   = strpos($className, 'View');

                if ($viewPos !== false) {
                    $this->_name = strtolower(substr($className, $viewPos + 4));
                }
            }

            if (empty($this->_name)) {
                throw new \Exception(\sprintf($this->getLanguage()->_('JLIB_APPLICATION_ERROR_GET_NAME'), __METHOD__), 500);
            }
        }

        return $this->_name;
    }

    /**
     * Get the Document.
     *
     * @return  Document
     *
     * @since   4.4.0
     * @throws  \UnexpectedValueException May be thrown if the document has not been set.
     */
    protected function getDocument(): Document
    {
        if ($this->document) {
            return $this->document;
        }

        throw new \UnexpectedValueException('Document not set in ' . __CLASS__);
    }

    /**
     * Set the document to use.
     *
     * @param   Document  $document  The document to use
     *
     * @return  void
     *
     * @since   4.4.0
     */
    public function setDocument(Document $document): void
    {
        $this->document = $document;
    }

    /**
     * Get the event dispatcher.
     *
     * The override was made to keep a backward compatibility for legacy component.
     * TODO: Remove the override in 6.0
     *
     * @return  DispatcherInterface
     *
     * @since   4.4.0
     * @throws  \UnexpectedValueException May be thrown if the dispatcher has not been set.
     */
    public function getDispatcher()
    {
        if (!$this->dispatcher) {
            @trigger_error(
                \sprintf('Dispatcher for %s should be set through MVC factory. It will throw an exception in 6.0', __CLASS__),
                E_USER_DEPRECATED
            );

            return Factory::getContainer()->get(DispatcherInterface::class);
        }

        return $this->dispatcher;
    }

    /**
     * Dispatches the given event on the internal dispatcher, does a fallback to the global one.
     *
     * @param   EventInterface  $event  The event
     *
     * @return  void
     *
     * @since   4.1.0
     *
     * @deprecated 4.4 will be removed in 6.0. Use $this->getDispatcher() directly.
     */
    protected function dispatchEvent(EventInterface $event)
    {
        $this->getDispatcher()->dispatch($event->getName(), $event);

        @trigger_error(
            \sprintf(
                'Method %s is deprecated and will be removed in 6.0. Use getDispatcher()->dispatch() directly.',
                __METHOD__
            ),
            E_USER_DEPRECATED
        );
    }
}
