<?php

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

namespace Joomla\Component\Categories\Administrator\Field;

use Joomla\CMS\Factory;
use Joomla\CMS\Form\Field\ListField;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\Database\ParameterType;
use Joomla\Utilities\ArrayHelper;

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

/**
 * Category Edit field..
 *
 * @since  1.6
 */
class CategoryeditField extends ListField
{
    /**
     * To allow creation of new categories.
     *
     * @var    integer
     * @since  3.6
     */
    protected $allowAdd;

    /**
     * Optional prefix for new categories.
     *
     * @var    string
     * @since  3.9.11
     */
    protected $customPrefix;

    /**
     * A flexible category list that respects access controls
     *
     * @var    string
     * @since  1.6
     */
    public $type = 'CategoryEdit';

    /**
     * Name of the layout being used to render the field
     *
     * @var    string
     * @since  4.0.0
     */
    protected $layout = 'joomla.form.field.categoryedit';

    /**
     * Method to attach a Form object to the field.
     *
     * @param   \SimpleXMLElement  $element  The SimpleXMLElement object representing the <field /> tag for the form field object.
     * @param   mixed              $value    The form field value to validate.
     * @param   string|null        $group    The field name group control value. This acts as an array container for the field.
     *                                       For example if the field has name="foo" and the group value is set to "bar" then the
     *                                       full field name would end up being "bar[foo]".
     *
     * @return  boolean  True on success.
     *
     * @see     FormField::setup()
     * @since   3.2
     */
    public function setup(\SimpleXMLElement $element, $value, $group = null)
    {
        $return = parent::setup($element, $value, $group);

        if ($return) {
            $this->allowAdd     = isset($this->element['allowAdd']) ? (bool) $this->element['allowAdd'] : false;
            $this->customPrefix = (string) $this->element['customPrefix'];
        }

        return $return;
    }

    /**
     * Method to get certain otherwise inaccessible properties from the form field object.
     *
     * @param   string  $name  The property name for which to get the value.
     *
     * @return  mixed  The property value or null.
     *
     * @since   3.6
     */
    public function __get($name)
    {
        switch ($name) {
            case 'allowAdd':
                return (bool) $this->$name;
            case 'customPrefix':
                return $this->$name;
        }

        return parent::__get($name);
    }

    /**
     * Method to set certain otherwise inaccessible properties of the form field object.
     *
     * @param   string  $name   The property name for which to set the value.
     * @param   mixed   $value  The value of the property.
     *
     * @return  void
     *
     * @since   3.6
     */
    public function __set($name, $value)
    {
        $value = (string) $value;

        switch ($name) {
            case 'allowAdd':
                $value       = (string) $value;
                $this->$name = ($value === 'true' || $value === $name || $value === '1');
                break;
            case 'customPrefix':
                $this->$name = (string) $value;
                break;
            default:
                parent::__set($name, $value);
        }
    }

    /**
     * Method to get a list of categories that respects access controls and can be used for
     * either category assignment or parent category assignment in edit screens.
     * Use the parent element to indicate that the field will be used for assigning parent categories.
     *
     * @return  array  The field option objects.
     *
     * @since   1.6
     */
    protected function getOptions()
    {
        $options   = [];
        $published = $this->element['published'] ? explode(',', (string) $this->element['published']) : [0, 1];
        $name      = (string) $this->element['name'];

        // Let's get the id for the current item, either category or content item.
        $jinput = Factory::getApplication()->getInput();

        // Is this field used to select parent category for a category ?
        $isParentCategoryField = isset($this->element['parent']) || $jinput->getCmd('option') === 'com_categories';

        // Load the category options for a given extension.

        // For categories the old category is the category id or 0 for new category.
        if ($isParentCategoryField) {
            $oldCat    = $jinput->get('id', 0);
            $oldParent = $this->form->getValue($name, 0);
            $extension = $this->element['extension'] ? (string) $this->element['extension'] : (string) $jinput->get('extension', 'com_content');
        } else { // For items the old category is the category they are in when opened or 0 if new.
            $oldCat    = $this->form->getValue($name, 0);
            $extension = $this->element['extension'] ? (string) $this->element['extension'] : (string) $jinput->get('option', 'com_content');
        }

        // Account for case that a submitted form has a multi-value category id field (e.g. a filtering form), just use the first category
        $oldCat = \is_array($oldCat)
            ? (int) reset($oldCat)
            : (int) $oldCat;

        $db   = $this->getDatabase();
        $user = $this->getCurrentUser();

        $query = $db->getQuery(true)
            ->select(
                [
                    $db->quoteName('a.id', 'value'),
                    $db->quoteName('a.title', 'text'),
                    $db->quoteName('a.level'),
                    $db->quoteName('a.published'),
                    $db->quoteName('a.lft'),
                    $db->quoteName('a.language'),
                ]
            )
            ->from($db->quoteName('#__categories', 'a'));

        // Filter by the extension type
        if ($isParentCategoryField) {
            $query->where('(' . $db->quoteName('a.extension') . ' = :extension OR ' . $db->quoteName('a.parent_id') . ' = 0)')
                ->bind(':extension', $extension);
        } else {
            $query->where($db->quoteName('a.extension') . ' = :extension')
                ->bind(':extension', $extension);
        }

        // Filter language
        if (isset($this->element['language'])) {
            if (str_contains($this->element['language'], ',')) {
                $language = explode(',', $this->element['language']);
            } else {
                $language = $this->element['language'];
            }

            $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING);
        }

        // Filter on the published state
        $state = ArrayHelper::toInteger($published);
        $query->whereIn($db->quoteName('a.published'), $state);

        // Filter categories on User Access Level
        // Filter by access level on categories.
        if (!$user->authorise('core.admin')) {
            $groups = $user->getAuthorisedViewLevels();
            $query->whereIn($db->quoteName('a.access'), $groups);
        }

        $query->order($db->quoteName('a.lft') . ' ASC');

        // If parent isn't explicitly stated but we are in com_categories assume we want parents
        if ($oldCat != 0 && $isParentCategoryField) {
            // Prevent parenting to children of this item.
            // To rearrange parents and children move the children up, not the parents down.
            $query->join(
                'LEFT',
                $db->quoteName('#__categories', 'p'),
                $db->quoteName('p.id') . ' = :oldcat'
            )
                ->bind(':oldcat', $oldCat, ParameterType::INTEGER)
                ->where('NOT(' . $db->quoteName('a.lft') . ' >= ' . $db->quoteName('p.lft')
                    . ' AND ' . $db->quoteName('a.rgt') . ' <= ' . $db->quoteName('p.rgt') . ')');
        }

        // Get the options.
        $db->setQuery($query);

        try {
            $options = $db->loadObjectList();
        } catch (\RuntimeException $e) {
            Factory::getApplication()->enqueueMessage($e->getMessage(), 'error');
        }

        // Pad the option text with spaces using depth level as a multiplier.
        foreach ($options as $option) {
            // Translate ROOT
            if ($isParentCategoryField && $option->level == 0) {
                $option->text = Text::_('JGLOBAL_ROOT_PARENT');
            }

            if ($option->published == 1) {
                $option->text = str_repeat('- ', !$option->level ? 0 : $option->level - 1) . $option->text;
            } else {
                $option->text = str_repeat('- ', !$option->level ? 0 : $option->level - 1) . '[' . $option->text . ']';
            }

            // Displays language code if not set to All
            if ($option->language !== '*') {
                $option->text .= ' (' . $option->language . ')';
            }
        }

        // For new items we want a list of categories you are allowed to create in.
        if ($oldCat == 0) {
            foreach ($options as $i => $option) {
                /*
                 * To take save or create in a category you need to have create rights for that category unless the item is already in that category.
                 * Unset the option if the user isn't authorised for it. In this field assets are always categories.
                 */
                if ($option->level != 0 && !$user->authorise('core.create', $extension . '.category.' . $option->value)) {
                    unset($options[$i]);
                }
            }
        } else {
            // If you have an existing category id things are more complex.
            /*
             * If you are only allowed to edit in this category but not edit.state, you should not get any
             * option to change the category parent for a category or the category for a content item,
             * but you should be able to save in that category.
             */
            foreach ($options as $i => $option) {
                $assetKey = $extension . '.category.' . $oldCat;

                if ($option->level != 0 && !isset($oldParent) && $option->value != $oldCat && !$user->authorise('core.edit.state', $assetKey)) {
                    unset($options[$i]);
                    continue;
                }

                if ($option->level != 0 && isset($oldParent) && $option->value != $oldParent && !$user->authorise('core.edit.state', $assetKey)) {
                    unset($options[$i]);
                    continue;
                }

                /*
                 * However, if you can edit.state you can also move this to another category for which you have
                 * create permission and you should also still be able to save in the current category.
                 */
                $assetKey = $extension . '.category.' . $option->value;

                if ($option->level != 0 && !isset($oldParent) && $option->value != $oldCat && !$user->authorise('core.create', $assetKey)) {
                    unset($options[$i]);
                    continue;
                }

                if ($option->level != 0 && isset($oldParent) && $option->value != $oldParent && !$user->authorise('core.create', $assetKey)) {
                    unset($options[$i]);
                }
            }
        }

        if (
            $oldCat != 0 && $isParentCategoryField
            && !isset($options[0])
            && isset($this->element['show_root'])
        ) {
            $rowQuery = $db->getQuery(true)
                ->select(
                    [
                        $db->quoteName('a.id', 'value'),
                        $db->quoteName('a.title', 'text'),
                        $db->quoteName('a.level'),
                        $db->quoteName('a.parent_id'),
                    ]
                )
                ->from($db->quoteName('#__categories', 'a'))
                ->where($db->quoteName('a.id') . ' = :aid')
                ->bind(':aid', $oldCat, ParameterType::INTEGER);
            $db->setQuery($rowQuery);
            $row = $db->loadObject();

            if ($row->parent_id == '1') {
                $parent       = new \stdClass();
                $parent->text = Text::_('JGLOBAL_ROOT_PARENT');
                array_unshift($options, $parent);
            }

            array_unshift($options, HTMLHelper::_('select.option', '0', Text::_('JGLOBAL_ROOT')));
        }

        // Merge any additional options in the XML definition.
        return array_merge(parent::getOptions(), $options);
    }

    /**
     * Method to get the field input markup for a generic list.
     * Use the multiple attribute to enable multiselect.
     *
     * @return  string  The field input markup.
     *
     * @since   3.6
     */
    protected function getInput()
    {
        $data = $this->getLayoutData();

        $data['options']        = $this->getOptions();
        $data['allowCustom']    = $this->allowAdd;
        $data['customPrefix']   = $this->customPrefix;
        $data['refreshPage']    = (bool) $this->element['refresh-enabled'];
        $data['refreshCatId']   = (string) $this->element['refresh-cat-id'];
        $data['refreshSection'] = (string) $this->element['refresh-section'];

        $renderer = $this->getRenderer($this->layout);
        $renderer->setComponent('com_categories');
        $renderer->setClient(1);

        return $renderer->render($data);
    }
}
