<?php

/**
 * @package     Joomla.Administrator
 * @subpackage  com_templates
 *
 * @copyright   (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\Component\Templates\Administrator\Model;

use Joomla\CMS\Application\ApplicationHelper;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Language\Multilanguage;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\MVC\Model\AdminModel;
use Joomla\CMS\Object\CMSObject;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Table\Table;
use Joomla\Database\ParameterType;
use Joomla\Filesystem\Path;
use Joomla\Registry\Registry;
use Joomla\String\StringHelper;
use Joomla\Utilities\ArrayHelper;

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

/**
 * Template style model.
 *
 * @since  1.6
 */
class StyleModel extends AdminModel
{
    /**
     * The help screen key for the module.
     *
     * @var     string
     * @since   1.6
     */
    protected $helpKey = 'Templates:_Edit_Style';

    /**
     * The help screen base URL for the module.
     *
     * @var     string
     * @since   1.6
     */
    protected $helpURL;

    /**
     * Item cache.
     *
     * @var    array
     * @since  1.6
     */
    private $_cache = [];

    /**
     * Constructor.
     *
     * @param   array                 $config   An optional associative array of configuration settings.
     * @param   ?MVCFactoryInterface  $factory  The factory.
     *
     * @see     \Joomla\CMS\MVC\Model\BaseDatabaseModel
     * @since   3.2
     */
    public function __construct($config = [], ?MVCFactoryInterface $factory = null)
    {
        $config = array_merge(
            [
                'event_before_delete' => 'onExtensionBeforeDelete',
                'event_after_delete'  => 'onExtensionAfterDelete',
                'event_before_save'   => 'onExtensionBeforeSave',
                'event_after_save'    => 'onExtensionAfterSave',
                'events_map'          => ['delete' => 'extension', 'save' => 'extension'],
            ],
            $config
        );

        parent::__construct($config, $factory);
    }

    /**
     * Method to auto-populate the model state.
     *
     * @note    Calling getState in this method will result in recursion.
     *
     * @return  void
     *
     * @since   1.6
     */
    protected function populateState()
    {
        $app = Factory::getApplication();

        // Load the User state.
        $pk = $app->getInput()->getInt('id');
        $this->setState('style.id', $pk);

        // Load the parameters.
        $params = ComponentHelper::getParams('com_templates');
        $this->setState('params', $params);
    }

    /**
     * Method to delete rows.
     *
     * @param   array  &$pks  An array of item ids.
     *
     * @return  boolean  Returns true on success, false on failure.
     *
     * @since   1.6
     * @throws  \Exception
     */
    public function delete(&$pks)
    {
        $pks        = (array) $pks;
        $user       = $this->getCurrentUser();
        $table      = $this->getTable();
        $context    = $this->option . '.' . $this->name;

        PluginHelper::importPlugin($this->events_map['delete']);

        // Iterate the items to delete each one.
        foreach ($pks as $pk) {
            if ($table->load($pk)) {
                // Access checks.
                if (!$user->authorise('core.delete', 'com_templates')) {
                    throw new \Exception(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'));
                }

                // You should not delete a default style
                if ($table->home != '0') {
                    Factory::getApplication()->enqueueMessage(Text::_('COM_TEMPLATES_STYLE_CANNOT_DELETE_DEFAULT_STYLE'), 'error');

                    return false;
                }

                // Trigger the before delete event.
                $result = Factory::getApplication()->triggerEvent($this->event_before_delete, [$context, $table]);

                if (\in_array(false, $result, true) || !$table->delete($pk)) {
                    $this->setError($table->getError());

                    return false;
                }

                // Trigger the after delete event.
                Factory::getApplication()->triggerEvent($this->event_after_delete, [$context, $table]);
            } else {
                $this->setError($table->getError());

                return false;
            }
        }

        // Clean cache
        $this->cleanCache();

        return true;
    }

    /**
     * Method to duplicate styles.
     *
     * @param   array  &$pks  An array of primary key IDs.
     *
     * @return  boolean  True if successful.
     *
     * @throws  \Exception
     */
    public function duplicate(&$pks)
    {
        $user = $this->getCurrentUser();

        // Access checks.
        if (!$user->authorise('core.create', 'com_templates')) {
            throw new \Exception(Text::_('JERROR_CORE_CREATE_NOT_PERMITTED'));
        }

        $context    = $this->option . '.' . $this->name;

        // Include the plugins for the save events.
        PluginHelper::importPlugin($this->events_map['save']);

        $table = $this->getTable();

        foreach ($pks as $pk) {
            if ($table->load($pk, true)) {
                // Reset the id to create a new record.
                $table->id = 0;

                // Reset the home (don't want dupes of that field).
                $table->home = 0;

                // Alter the title.
                $m            = null;
                $table->title = $this->generateNewTitle(null, null, $table->title);

                if (!$table->check()) {
                    throw new \Exception($table->getError());
                }

                // Trigger the before save event.
                $result = Factory::getApplication()->triggerEvent($this->event_before_save, [$context, &$table, true]);

                if (\in_array(false, $result, true) || !$table->store()) {
                    throw new \Exception($table->getError());
                }

                // Trigger the after save event.
                Factory::getApplication()->triggerEvent($this->event_after_save, [$context, &$table, true]);
            } else {
                throw new \Exception($table->getError());
            }
        }

        // Clean cache
        $this->cleanCache();

        return true;
    }

    /**
     * Method to change the title.
     *
     * @param   integer  $categoryId  The id of the category.
     * @param   string   $alias       The alias.
     * @param   string   $title       The title.
     *
     * @return  string  New title.
     *
     * @since   1.7.1
     */
    protected function generateNewTitle($categoryId, $alias, $title)
    {
        // Alter the title
        $table = $this->getTable();

        while ($table->load(['title' => $title])) {
            $title = StringHelper::increment($title);
        }

        return $title;
    }

    /**
     * Method to get the record form.
     *
     * @param   array    $data      An optional array of data for the form to interrogate.
     * @param   boolean  $loadData  True if the form is to load its own data (default case), false if not.
     *
     * @return  Form  A Form object on success, false on failure
     *
     * @since   1.6
     */
    public function getForm($data = [], $loadData = true)
    {
        // The folder and element vars are passed when saving the form.
        if (empty($data)) {
            $item      = $this->getItem();
            $clientId  = $item->client_id;
            $template  = $item->template;
        } else {
            $clientId  = ArrayHelper::getValue($data, 'client_id');
            $template  = ArrayHelper::getValue($data, 'template');
        }

        // Add the default fields directory
        $baseFolder = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE;
        Form::addFieldPath($baseFolder . '/templates/' . $template . '/field');

        // These variables are used to add data from the plugin XML files.
        $this->setState('item.client_id', $clientId);
        $this->setState('item.template', $template);

        // Get the form.
        $form = $this->loadForm('com_templates.style', 'style', ['control' => 'jform', 'load_data' => $loadData]);

        if (empty($form)) {
            return false;
        }

        // Modify the form based on access controls.
        if (!$this->canEditState((object) $data)) {
            // Disable fields for display.
            $form->setFieldAttribute('home', 'disabled', 'true');

            // Disable fields while saving.
            // The controller has already verified this is a record you can edit.
            $form->setFieldAttribute('home', 'filter', 'unset');
        }

        return $form;
    }

    /**
     * Method to get the data that should be injected in the form.
     *
     * @return  mixed  The data for the form.
     *
     * @since   1.6
     */
    protected function loadFormData()
    {
        // Check the session for previously entered form data.
        $data = Factory::getApplication()->getUserState('com_templates.edit.style.data', []);

        if (empty($data)) {
            $data = $this->getItem();
        }

        $this->preprocessData('com_templates.style', $data);

        return $data;
    }

    /**
     * Method to get a single record.
     *
     * @param   integer  $pk  The id of the primary key.
     *
     * @return  mixed  Object on success, false on failure.
     */
    public function getItem($pk = null)
    {
        $pk = (!empty($pk)) ? $pk : (int) $this->getState('style.id');

        if (!isset($this->_cache[$pk])) {
            // Get a row instance.
            $table = $this->getTable();

            // Attempt to load the row.
            $return = $table->load($pk);

            // Check for a table object error.
            if ($return === false && $table->getError()) {
                $this->setError($table->getError());

                return false;
            }

            // Convert to the \Joomla\CMS\Object\CMSObject before adding other data.
            $properties        = $table->getProperties(1);
            $this->_cache[$pk] = ArrayHelper::toObject($properties, CMSObject::class);

            // Convert the params field to an array.
            $registry                  = new Registry($table->params);
            $this->_cache[$pk]->params = $registry->toArray();

            // Get the template XML.
            $client = ApplicationHelper::getClientInfo($table->client_id);
            $path   = Path::clean($client->path . '/templates/' . $table->template . '/templateDetails.xml');

            if (file_exists($path)) {
                $this->_cache[$pk]->xml = simplexml_load_file($path);
            } else {
                $this->_cache[$pk]->xml = null;
            }
        }

        return $this->_cache[$pk];
    }

    /**
     * Method to allow derived classes to preprocess the form.
     *
     * @param   Form   $form   A Form object.
     * @param   mixed   $data   The data expected for the form.
     * @param   string  $group  The name of the plugin group to import (defaults to "content").
     *
     * @return  void
     *
     * @since   1.6
     * @throws  \Exception if there is an error in the form event.
     */
    protected function preprocessForm(Form $form, $data, $group = 'content')
    {
        $clientId = $this->getState('item.client_id');
        $template = $this->getState('item.template');
        $lang     = Factory::getLanguage();
        $client   = ApplicationHelper::getClientInfo($clientId);

        if (!$form->loadFile('style_' . $client->name, true)) {
            throw new \Exception(Text::_('JERROR_LOADFILE_FAILED'));
        }

        $formFile = Path::clean($client->path . '/templates/' . $template . '/templateDetails.xml');

        // Load the core and/or local language file(s).
        // Default to using parent template language constants
        $lang->load('tpl_' . $data->parent, $client->path)
            || $lang->load('tpl_' . $data->parent, $client->path . '/templates/' . $data->parent);

        // Apply any, optional, overrides for child template language constants
        $lang->load('tpl_' . $template, $client->path)
            || $lang->load('tpl_' . $template, $client->path . '/templates/' . $template);

        if (file_exists($formFile)) {
            // Get the template form.
            if (!$form->loadFile($formFile, false, '//config')) {
                throw new \Exception(Text::_('JERROR_LOADFILE_FAILED'));
            }
        }

        // Disable home field if it is default style

        if (
            (\is_array($data) && \array_key_exists('home', $data) && $data['home'] == '1')
            || (\is_object($data) && isset($data->home) && $data->home == '1')
        ) {
            $form->setFieldAttribute('home', 'readonly', 'true');
        }

        if ($client->name === 'site' && !Multilanguage::isEnabled()) {
            $form->setFieldAttribute('home', 'type', 'radio');
            $form->setFieldAttribute('home', 'layout', 'joomla.form.field.radio.switcher');
        }

        // Attempt to load the xml file.
        if (!$xml = simplexml_load_file($formFile)) {
            throw new \Exception(Text::_('JERROR_LOADFILE_FAILED'));
        }

        // Get the help data from the XML file if present.
        $help = $xml->xpath('/extension/help');

        if (!empty($help)) {
            $helpKey = trim((string) $help[0]['key']);
            $helpURL = trim((string) $help[0]['url']);

            $this->helpKey = $helpKey ?: $this->helpKey;
            $this->helpURL = $helpURL ?: $this->helpURL;
        }

        // Trigger the default form events.
        parent::preprocessForm($form, $data, $group);
    }

    /**
     * Method to save the form data.
     *
     * @param   array  $data  The form data.
     *
     * @return  boolean  True on success.
     */
    public function save($data)
    {
        // Detect disabled extension
        $extension = Table::getInstance('Extension', 'Joomla\\CMS\\Table\\');

        if ($extension->load(['enabled' => 0, 'type' => 'template', 'element' => $data['template'], 'client_id' => $data['client_id']])) {
            $this->setError(Text::_('COM_TEMPLATES_ERROR_SAVE_DISABLED_TEMPLATE'));

            return false;
        }

        $app        = Factory::getApplication();
        $table      = $this->getTable();
        $pk         = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('style.id');
        $isNew      = true;

        // Include the extension plugins for the save events.
        PluginHelper::importPlugin($this->events_map['save']);

        // Load the row if saving an existing record.
        if ($pk > 0) {
            $table->load($pk);
            $isNew = false;
        }

        if ($app->getInput()->get('task') == 'save2copy') {
            $data['title']    = $this->generateNewTitle(null, null, $data['title']);
            $data['home']     = 0;
            $data['assigned'] = '';
        }

        // Bind the data.
        if (!$table->bind($data)) {
            $this->setError($table->getError());

            return false;
        }

        // Prepare the row for saving
        $this->prepareTable($table);

        // Check the data.
        if (!$table->check()) {
            $this->setError($table->getError());

            return false;
        }

        // Trigger the before save event.
        $result = Factory::getApplication()->triggerEvent($this->event_before_save, ['com_templates.style', &$table, $isNew]);

        // Store the data.
        if (\in_array(false, $result, true) || !$table->store()) {
            $this->setError($table->getError());

            return false;
        }

        $user = $this->getCurrentUser();

        if ($user->authorise('core.edit', 'com_menus') && $table->client_id == 0) {
            $n       = 0;
            $db      = $this->getDatabase();
            $user    = $this->getCurrentUser();
            $tableId = (int) $table->id;
            $userId  = (int) $user->id;

            if (!empty($data['assigned']) && \is_array($data['assigned'])) {
                $data['assigned'] = ArrayHelper::toInteger($data['assigned']);

                // Update the mapping for menu items that this style IS assigned to.
                $query = $db->getQuery(true)
                    ->update($db->quoteName('#__menu'))
                    ->set($db->quoteName('template_style_id') . ' = :newtsid')
                    ->whereIn($db->quoteName('id'), $data['assigned'])
                    ->where($db->quoteName('template_style_id') . ' != :tsid')
                    ->where('(' . $db->quoteName('checked_out') . ' IS NULL OR ' . $db->quoteName('checked_out') . ' = :userid)')
                    ->bind(':userid', $userId, ParameterType::INTEGER)
                    ->bind(':newtsid', $tableId, ParameterType::INTEGER)
                    ->bind(':tsid', $tableId, ParameterType::INTEGER);
                $db->setQuery($query);
                $db->execute();
                $n += $db->getAffectedRows();
            }

            // Remove style mappings for menu items this style is NOT assigned to.
            // If unassigned then all existing maps will be removed.
            $query = $db->getQuery(true)
                ->update($db->quoteName('#__menu'))
                ->set($db->quoteName('template_style_id') . ' = 0');

            if (!empty($data['assigned'])) {
                $query->whereNotIn($db->quoteName('id'), $data['assigned']);
            }

            $query->where($db->quoteName('template_style_id') . ' = :templatestyleid')
                ->where('(' . $db->quoteName('checked_out') . ' IS NULL OR ' . $db->quoteName('checked_out') . ' = :userid)')
                ->bind(':userid', $userId, ParameterType::INTEGER)
                ->bind(':templatestyleid', $tableId, ParameterType::INTEGER);
            $db->setQuery($query);
            $db->execute();

            $n += $db->getAffectedRows();

            if ($n > 0) {
                $app->enqueueMessage(Text::plural('COM_TEMPLATES_MENU_CHANGED', $n));
            }
        }

        // Clean the cache.
        $this->cleanCache();

        // Trigger the after save event.
        Factory::getApplication()->triggerEvent($this->event_after_save, ['com_templates.style', &$table, $isNew]);

        $this->setState('style.id', $table->id);

        return true;
    }

    /**
     * Method to set a template style as home.
     *
     * @param   integer  $id  The primary key ID for the style.
     *
     * @return  boolean  True if successful.
     *
     * @throws  \Exception
     */
    public function setHome($id = 0)
    {
        $user = $this->getCurrentUser();
        $db   = $this->getDatabase();

        // Access checks.
        if (!$user->authorise('core.edit.state', 'com_templates')) {
            throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'));
        }

        $style = $this->getTable();

        if (!$style->load((int) $id)) {
            throw new \Exception(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND'));
        }

        // Detect disabled extension
        $extension = Table::getInstance('Extension', 'Joomla\\CMS\\Table\\');

        if ($extension->load(['enabled' => 0, 'type' => 'template', 'element' => $style->template, 'client_id' => $style->client_id])) {
            throw new \Exception(Text::_('COM_TEMPLATES_ERROR_SAVE_DISABLED_TEMPLATE'));
        }

        $clientId = (int) $style->client_id;
        $id       = (int) $id;

        // Reset the home fields for the client_id.
        $query = $db->getQuery(true)
            ->update($db->quoteName('#__template_styles'))
            ->set($db->quoteName('home') . ' = ' . $db->quote('0'))
            ->where($db->quoteName('client_id') . ' = :clientid')
            ->where($db->quoteName('home') . ' = ' . $db->quote('1'))
            ->bind(':clientid', $clientId, ParameterType::INTEGER);
        $db->setQuery($query);
        $db->execute();

        // Set the new home style.
        $query = $db->getQuery(true)
            ->update($db->quoteName('#__template_styles'))
            ->set($db->quoteName('home') . ' = ' . $db->quote('1'))
            ->where($db->quoteName('id') . ' = :id')
            ->bind(':id', $id, ParameterType::INTEGER);
        $db->setQuery($query);
        $db->execute();

        // Clean the cache.
        $this->cleanCache();

        return true;
    }

    /**
     * Method to unset a template style as default for a language.
     *
     * @param   integer  $id  The primary key ID for the style.
     *
     * @return  boolean  True if successful.
     *
     * @throws  \Exception
     */
    public function unsetHome($id = 0)
    {
        $user = $this->getCurrentUser();
        $db   = $this->getDatabase();

        // Access checks.
        if (!$user->authorise('core.edit.state', 'com_templates')) {
            throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'));
        }

        $id = (int) $id;

        // Lookup the client_id.
        $query = $db->getQuery(true)
            ->select($db->quoteName(['client_id', 'home']))
            ->from($db->quoteName('#__template_styles'))
            ->where($db->quoteName('id') . ' = :id')
            ->bind(':id', $id, ParameterType::INTEGER);
        $db->setQuery($query);
        $style = $db->loadObject();

        if (!is_numeric($style->client_id)) {
            throw new \Exception(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND'));
        }

        if ($style->home == '1') {
            throw new \Exception(Text::_('COM_TEMPLATES_ERROR_CANNOT_UNSET_DEFAULT_STYLE'));
        }

        // Set the new home style.
        $query = $db->getQuery(true)
            ->update($db->quoteName('#__template_styles'))
            ->set($db->quoteName('home') . ' = ' . $db->quote('0'))
            ->where($db->quoteName('id') . ' = :id')
            ->bind(':id', $id, ParameterType::INTEGER);
        $db->setQuery($query);
        $db->execute();

        // Clean the cache.
        $this->cleanCache();

        return true;
    }

    /**
     * Get the necessary data to load an item help screen.
     *
     * @return  object  An object with key, url, and local properties for loading the item help screen.
     *
     * @since   1.6
     */
    public function getHelp()
    {
        return (object) ['key' => $this->helpKey, 'url' => $this->helpURL];
    }

    /**
     * Returns the back end template for the given style.
     *
     * @param   int  $styleId  The style id
     *
     * @return  \stdClass
     *
     * @since   4.2.0
     */
    public function getAdminTemplate(int $styleId): \stdClass
    {
        $db    = $this->getDatabase();
        $query = $db->getQuery(true)
            ->select($db->quoteName(['s.template', 's.params', 's.inheritable', 's.parent']))
            ->from($db->quoteName('#__template_styles', 's'))
            ->join(
                'LEFT',
                $db->quoteName('#__extensions', 'e'),
                $db->quoteName('e.type') . ' = ' . $db->quote('template')
                    . ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quoteName('s.template')
                    . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('s.client_id')
            )
            ->where(
                [
                    $db->quoteName('s.client_id') . ' = 1',
                    $db->quoteName('s.home') . ' = ' . $db->quote('1'),
                ]
            );

        if ($styleId) {
            $query->extendWhere(
                'OR',
                [
                    $db->quoteName('s.client_id') . ' = 1',
                    $db->quoteName('s.id') . ' = :style',
                    $db->quoteName('e.enabled') . ' = 1',
                ]
            )
                ->bind(':style', $styleId, ParameterType::INTEGER);
        }

        $query->order($db->quoteName('s.home'));
        $db->setQuery($query);

        return $db->loadObject();
    }

    /**
     * Returns the front end templates.
     *
     * @return  array
     *
     * @since   4.2.0
     */
    public function getSiteTemplates(): array
    {
        $db    = $this->getDatabase();
        $query = $db->getQuery(true)
            ->select($db->quoteName(['id', 'home', 'template', 's.params', 'inheritable', 'parent']))
            ->from($db->quoteName('#__template_styles', 's'))
            ->where(
                [
                    $db->quoteName('s.client_id') . ' = 0',
                    $db->quoteName('e.enabled') . ' = 1',
                ]
            )
            ->join(
                'LEFT',
                $db->quoteName('#__extensions', 'e'),
                $db->quoteName('e.element') . ' = ' . $db->quoteName('s.template')
                    . ' AND ' . $db->quoteName('e.type') . ' = ' . $db->quote('template')
                    . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('s.client_id')
            );

        $db->setQuery($query);

        return $db->loadObjectList('id');
    }

    /**
     * Custom clean cache method
     *
     * @param   string   $group     The cache group
     * @param   integer  $clientId  No longer used, will be removed without replacement
     *                              @deprecated   4.3 will be removed in 6.0
     *
     * @return  void
     *
     * @since   1.6
     */
    protected function cleanCache($group = null, $clientId = 0)
    {
        parent::cleanCache('com_templates');
        parent::cleanCache('_system');
    }
}
