<?php

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

namespace Joomla\Component\Tags\Site\Service;

use Joomla\CMS\Application\SiteApplication;
use Joomla\CMS\Categories\CategoryFactoryInterface;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Component\Router\RouterBase;
use Joomla\CMS\Language\Multilanguage;
use Joomla\CMS\Menu\AbstractMenu;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\Database\DatabaseInterface;
use Joomla\Database\ParameterType;
use Joomla\Registry\Registry;
use Joomla\Utilities\ArrayHelper;

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

/**
 * Routing class from com_tags
 *
 * @since  3.3
 */
class Router extends RouterBase
{
    /**
     * The db
     *
     * @var DatabaseInterface
     *
     * @since  4.0.0
     */
    private $db;

    /**
     * Lookup array of the menu items
     *
     * @var   array
     * @since 4.3.0
     */
    protected $lookup = [];

    /**
     * System - SEF Plugin parameters
     *
     * @var   Registry
     * @since 5.2.0
     * @deprecated  5.2.0 will be removed in 6.0
     *              without replacement
     */
    private $sefparams;

    /**
     * Tags Component router constructor
     *
     * @param   SiteApplication            $app              The application object
     * @param   AbstractMenu               $menu             The menu object to work with
     * @param   ?CategoryFactoryInterface  $categoryFactory  The category object
     * @param   DatabaseInterface          $db               The database object
     *
     * @since  4.0.0
     */
    public function __construct(SiteApplication $app, AbstractMenu $menu, ?CategoryFactoryInterface $categoryFactory, DatabaseInterface $db)
    {
        $this->db = $db;

        parent::__construct($app, $menu);

        $sefPlugin       = PluginHelper::getPlugin('system', 'sef');

        if ($sefPlugin) {
            $this->sefparams = new Registry($sefPlugin->params);
        } else {
            $this->sefparams = new Registry();
        }

        $this->buildLookup();
    }


    /**
     * Preprocess com_tags URLs
     *
     * @param   array  $query  An associative array of URL arguments
     *
     * @return  array  The URL arguments to use to assemble the subsequent URL.
     *
     * @since   4.3.0
     */
    public function preprocess($query)
    {
        // Make sure the alias for the tags is correct
        if (isset($query['id'])) {
            if (!\is_array($query['id'])) {
                $query['id'] = [$query['id']];
            }

            foreach ($query['id'] as &$item) {
                if (!strpos($item, ':')) {
                    $dbquery = $this->db->getQuery(true);
                    $id      = (int) $item;

                    $dbquery->select($dbquery->quoteName('alias'))
                        ->from('#__tags')
                        ->where($dbquery->quoteName('id') . ' = :key')
                        ->bind(':key', $id, ParameterType::INTEGER);

                    $obj = $this->db->setQuery($dbquery)->loadObject();

                    if ($obj) {
                        $item .= ':' . $obj->alias;
                    }
                }
            }
        }

        $active = $this->menu->getActive();

        /**
         * If the active item id is not the same as the supplied item id or we have a supplied item id and no active
         * menu item then we just use the supplied menu item and continue
         */
        if (isset($query['Itemid']) && ($active === null || $query['Itemid'] != $active->id)) {
            return $query;
        }

        // Get query language
        $lang = $query['lang'] ?? '*';

        // Set the language to the current one when multilang is enabled and item is tagged to ALL
        if (Multilanguage::isEnabled() && $lang === '*') {
            $lang = $this->app->get('language');
        }

        foreach (array_unique([$lang, '*']) as $language) {
            if (isset($query['view']) && $query['view'] === 'tags') {
                if (isset($query['parent_id'], $this->lookup[$language]['tags'][$query['parent_id']])) {
                    $query['Itemid'] = $this->lookup[$language]['tags'][$query['parent_id']];
                    break;
                }

                if (isset($this->lookup[$language]['tags'][0])) {
                    $query['Itemid'] = $this->lookup[$language]['tags'][0];
                    break;
                }
            } elseif (isset($query['view']) && $query['view'] === 'tag') {
                if (isset($query['id'])) {
                    if (!\is_array($query['id'])) {
                        $query['id'] = [$query['id']];
                    }

                    $id = ArrayHelper::toInteger($query['id']);
                    sort($id);

                    if (isset($this->lookup[$language]['tag'][implode(',', $id)])) {
                        $query['Itemid'] = $this->lookup[$language]['tag'][implode(',', $id)];
                        break;
                    }

                    foreach ($id as $i) {
                        if (isset($this->lookup[$language]['tag'][$i])) {
                            $query['Itemid'] = $this->lookup[$language]['tag'][$i];
                            break 2;
                        }
                    }

                    if (isset($this->lookup[$language]['tags'][implode(',', $id)])) {
                        $query['Itemid'] = $this->lookup[$language]['tags'][implode(',', $id)];
                        break;
                    }

                    if (isset($this->lookup[$language]['tags'][0])) {
                        $query['Itemid'] = $this->lookup[$language]['tags'][0];
                        break;
                    }
                }
            }
        }

        // TODO: Remove this whole block in 6.0 as it is a bug
        if (!$this->sefparams->get('strictrouting', 0)) {
            // If not found, return language specific home link
            if (!isset($query['Itemid'])) {
                $default = $this->menu->getDefault($lang);

                if (!empty($default->id)) {
                    $query['Itemid'] = $default->id;
                }
            }
        }

        return $query;
    }

    /**
     * Build the route for the com_tags component
     *
     * @param   array  &$query  An array of URL arguments
     *
     * @return  array  The URL arguments to use to assemble the subsequent URL.
     *
     * @since   3.3
     */
    public function build(&$query)
    {
        $segments = [];

        $menuItem = !empty($query['Itemid']) ? $this->menu->getItem($query['Itemid']) : false;

        if ($menuItem && $menuItem->query['option'] == 'com_tags') {
            if ($menuItem->query['view'] == 'tags') {
                if (isset($query['id'])) {
                    $ids = $query['id'];

                    foreach ($ids as $id) {
                        $segments[] = $id;
                    }

                    unset($query['id']);
                } elseif (isset($query['parent_id'], $menuItem->query['parent_id'])) {
                    if ($query['parent_id'] == $menuItem->query['parent_id']) {
                        unset($query['parent_id']);
                    }
                }
            } elseif ($menuItem->query['view'] == 'tag') {
                $ids     = $query['id'];
                $int_ids = ArrayHelper::toInteger($ids);
                $mIds    = (array) $menuItem->query['id'];

                /**
                 * We check if there is a difference between the tags of the menu item and the query.
                 * If they are identical, we exactly match the menu item. Otherwise we append all tags to the URL
                 */
                if (\count(array_diff($int_ids, $mIds)) > 0 || \count(array_diff($mIds, $int_ids)) > 0) {
                    foreach ($ids as $id) {
                        $segments[] = $id;
                    }
                }

                unset($query['id']);
            }

            unset($query['view']);
        } else {
            if (isset($query['view'])) {
                $segments[] = $query['view'];
                unset($query['view'], $query['Itemid']);
            }

            if (isset($query['id']) && \is_array($query['id'])) {
                foreach ($query['id'] as $id) {
                    $segments[] = $id;
                }

                unset($query['id']);
            }
        }

        unset($query['layout']);

        foreach ($segments as &$segment) {
            if (strpos($segment, ':')) {
                [, $segment] = explode(':', $segment, 2);
            }
        }

        return $segments;
    }

    /**
     * Parse the segments of a URL.
     *
     * @param   array  &$segments  The segments of the URL to parse.
     *
     * @return  array  The URL attributes to be used by the application.
     *
     * @since   3.3
     */
    public function parse(&$segments)
    {
        $vars = [];

        // Get the active menu item.
        $item = $this->menu->getActive();

        // We don't have a menu item
        if (!$item || $item->query['option'] != 'com_tags') {
            if (!isset($segments[0])) {
                return $vars;
            }

            $vars['view'] = array_shift($segments);
        }

        $ids          = [];
        $matchedAlias = false;

        if ($item && $item->query['view'] == 'tag') {
            $ids = $item->query['id'];
        }

        // Iterate through all URL segments and try to parse tag IDs from them
        while (\count($segments)) {
            $id    = array_shift($segments);

            // We have a numeric ID
            if (!$matchedAlias && is_numeric($id)) {
                $ids[] = $id;

                // We allow more than one numeric segment in the URL
                continue;
            }

            // We have a comma-separated list of IDs
            if (!$matchedAlias && str_contains($id, ',')) {
                $ids[] = $id;

                // We don't allow more than one list of IDs in a URL
                break;
            }

            $slug  = $this->fixSegment($id);

            // We did not find the segment as a tag in the DB
            if ($slug === $id) {
                array_unshift($segments, $id);
                break;
            }

            // We don't want to match numeric or comma-separated segments after we matched an alias
            $matchedAlias = true;
            $ids[]        = $slug;
        }

        if (\count($ids)) {
            $vars['id']   = $ids;
            $vars['view'] = 'tag';
        }

        return $vars;
    }

    /**
     * Method to build the lookup array
     *
     * @param   string  $language  The language that the lookup should be built up for
     *
     * @return  void
     *
     * @since   4.3.0
     */
    protected function buildLookup()
    {
        $component = ComponentHelper::getComponent('com_tags');
        $items     = $this->app->getMenu()->getItems(['component_id'], [$component->id]);

        foreach ($items as $item) {
            $itemParams = $item->getParams();

            if (!isset($this->lookup[$item->language])) {
                $this->lookup[$item->language] = ['tags' => [], 'tag' => []];
            }

            if ($item->query['view'] == 'tag') {
                $id = $item->query['id'];
                sort($id);
                $this->lookup[$item->language]['tag'][implode(',', $id)] = $item->id;

                // Only apply to menu items with match type any
                if ($itemParams->get('return_any_or_all') == 1) {
                    foreach ($id as $i) {
                        $this->lookup[$item->language]['tag'][$i] = $item->id;
                    }
                }
            }

            if ($item->query['view'] == 'tags') {
                $id                                         = (int) ($item->query['parent_id'] ?? 0);
                $this->lookup[$item->language]['tags'][$id] = $item->id;
            }
        }

        foreach ($this->lookup as $language => $items) {
            // We have tags views with parent_id set and need to load child tags to be assigned to this menu item
            if (
                \count($this->lookup[$language]['tags']) > 1
                || (\count($this->lookup[$language]['tags']) == 1 && !isset($this->lookup[$language]['tags'][0]))
            ) {
                foreach ($this->lookup[$language]['tags'] as $id => $menu) {
                    if ($id === 0) {
                        continue;
                    }

                    $query = $this->db->getQuery(true);
                    $query->select($this->db->quoteName('a.id'))
                        ->from($this->db->quoteName('#__tags', 'a'))
                        ->leftJoin(
                            $this->db->quoteName('#__tags', 'b')
                            . ' ON ' . $this->db->quoteName('b.lft') . ' < ' . $this->db->quoteName('a.lft')
                            . ' AND ' . $this->db->quoteName('a.rgt') . ' < ' . $this->db->quoteName('b.rgt')
                        )
                        ->where($this->db->quoteName('b.id') . ' = :id')
                        ->bind(':id', $id);
                    $this->db->setQuery($query);
                    $ids = (array) $this->db->loadColumn();

                    foreach ($ids as $i) {
                        $this->lookup[$language]['tags'][$i] = $menu;
                    }
                }
            }
        }
    }

    /**
     * Try to add missing id to segment
     *
     * @param   string  $segment  One piece of segment of the URL to parse
     *
     * @return  string  The segment with founded id
     *
     * @since   3.7
     */
    protected function fixSegment($segment)
    {
        // Try to find tag id
        $alias = str_replace(':', '-', $segment);

        $query = $this->db->getQuery(true)
            ->select($this->db->quoteName('id'))
            ->from($this->db->quoteName('#__tags'))
            ->where($this->db->quoteName('alias') . ' = :alias')
            ->bind(':alias', $alias);

        $id = $this->db->setQuery($query)->loadResult();

        if ($id) {
            $segment = $id . ':' . $alias;
        }

        return $segment;
    }
}
