<?php

/**
 * Matomo - free/libre analytics platform
 *
 * @link    https://matomo.org
 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 */

namespace Piwik\Plugin;

use Piwik\Common;
use Piwik\Date;
use Piwik\Development;
use Piwik\Menu\MenuAdmin;
use Piwik\Menu\MenuTop;
use Piwik\Period;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\Plugins\UsersManager\UserPreferences;
use Piwik\Site;
use Piwik\Url;

/**
 * Base class of all plugin menu providers. Plugins that define their own menu items can extend this class to easily
 * add new items, to remove or to rename existing items.
 *
 * Descendants of this class can overwrite any of these methods. Each method will be executed only once per request
 * and cached for any further menu requests.
 *
 * For an example, see the {@link https://github.com/piwik/piwik/blob/master/plugins/ExampleUI/Menu.php} plugin.
 *
 * @api
 * @since 2.4.0
 */
class Menu
{
    public function __construct()
    {
        // Constructor kept for BC (because called in implementations)
    }

    private function getModule()
    {
        $className = get_class($this);
        $className = explode('\\', $className);

        return $className[2];
    }

    /**
     * Generates a URL for the default action of the plugin controller.
     *
     * Example:
     * ```
     * $menu->addItem('MyPlugin_MyPlugin', '', $this->urlForDefaultAction(), $orderId = 30);
     * // will add a menu item that leads to the default action of the plugin controller when a user clicks on it.
     * // The default action is usually the `index` action - meaning the `index()` method the controller -
     * // but the default action can be customized within a controller
     * ```
     *
     * @param  array $additionalParams  Optional URL parameters that will be appended to the URL
     * @return array
     *
     * @since 2.7.0
     * @api
     */
    protected function urlForDefaultAction($additionalParams = array())
    {
        $params = (array) $additionalParams;
        $params['action'] = '';
        $params['module'] = $this->getModule();

        return $params;
    }

    /**
     * Generates a URL for the given action. In your plugin controller you have to create a method with the same name
     * as this method will be executed when a user clicks on the menu item. If you want to generate a URL for the
     * action of another module, meaning not your plugin, you should use the method {@link urlForModuleAction()}.
     *
     * @param  string $controllerAction  The name of the action that should be executed within your controller
     * @param  array  $additionalParams  Optional URL parameters that will be appended to the URL
     * @return array
     *
     * @since 2.7.0
     * @api
     */
    protected function urlForAction($controllerAction, $additionalParams = array())
    {
        $module = $this->getModule();
        $this->checkisValidCallable($module, $controllerAction);

        $params = (array) $additionalParams;
        $params['action'] = $controllerAction;
        $params['module'] = $module;

        return $params;
    }

    /**
     * Generates a URL for the given action of the given module. We usually do not recommend to use this method as you
     * should make sure the method of that module actually exists. If the plugin owner of that module changes the method
     * in a future version your link might no longer work. If you want to link to an action of your controller use the
     * method {@link urlForAction()}. Note: We will generate a link only if the given module is installed and activated.
     *
     * @param  string $module            The name of the module/plugin the action belongs to. The module name is case sensitive.
     * @param  string $controllerAction  The name of the action that should be executed within your controller
     * @param  array  $additionalParams  Optional URL parameters that will be appended to the URL
     * @return array|null   Returns null if the given module is either not installed or not activated. Returns the array
     *                      of query parameter names and values to the given module action otherwise.
     *
     * @since 2.7.0
     * // not API for now
     */
    protected function urlForModuleAction($module, $controllerAction, $additionalParams = array())
    {
        $this->checkisValidCallable($module, $controllerAction);

        $pluginManager = PluginManager::getInstance();

        if (
            !$pluginManager->isPluginLoaded($module) ||
            !$pluginManager->isPluginActivated($module)
        ) {
            return null;
        }

        $params = (array) $additionalParams;
        $params['action'] = $controllerAction;
        $params['module'] = $module;

        return $params;
    }

    /**
     * Generates a URL to the given action of the current module, and it will also append some URL query parameters from the
     * User preferences: idSite, period, date. If you do not need the parameters idSite, period and date to be generated
     * use {@link urlForAction()} instead.
     *
     * @param  string $controllerAction  The name of the action that should be executed within your controller
     * @param  array  $additionalParams  Optional URL parameters that will be appended to the URL
     * @return array   Returns the array of query parameter names and values to the given module action and idSite date and period.
     */
    protected function urlForActionWithDefaultUserParams($controllerAction, $additionalParams = array())
    {
        $module = $this->getModule();

        return $this->urlForModuleActionWithDefaultUserParams($module, $controllerAction, $additionalParams);
    }

    /**
     * Generates a URL to the given action of the given module, and it will also append some URL query parameters from the
     * User preferences: idSite, period, date. If you do not need the parameters idSite, period and date to be generated
     * use {@link urlForModuleAction()} instead.
     *
     * @param  string $module            The name of the module/plugin the action belongs to. The module name is case sensitive.
     * @param  string $controllerAction  The name of the action that should be executed within your controller
     * @param  array  $additionalParams  Optional URL parameters that will be appended to the URL
     * @return array|null   Returns the array of query parameter names and values to the given module action and idSite date and period.
     *                      Returns null if the module or action is invalid.
     */
    protected function urlForModuleActionWithDefaultUserParams($module, $controllerAction, $additionalParams = array())
    {
        $urlModuleAction = $this->urlForModuleAction($module, $controllerAction);

        $date = Common::getRequestVar('date', false);
        if ($date) {
            $urlModuleAction['date'] = $date;
        }
        $period = Common::getRequestVar('period', false);
        if ($period) {
            $urlModuleAction['period'] = $period;
        }

        // We want the current query parameters to override the user's defaults
        return array_merge(
            $this->urlForDefaultUserParams(),
            $urlModuleAction,
            $additionalParams
        );
    }

    /**
     * Builds a top-menu link that opens a reporting section (group) in the reporting single-page-app.
     * The section is placed in the URL hash rather than the query string, so it does not leak into the
     * other top-menu links (which are built from the current query string).
     *
     * @param array  $params query parameters for the link (e.g. from urlForModuleActionWithDefaultUserParams)
     * @param string $group  reporting section id (empty string for the default section)
     */
    protected function urlForReportingSection(array $params, string $group): string
    {
        $hashParams = array_merge(
            array_intersect_key($params, array_flip(['idSite', 'period', 'date'])),
            ['group' => $group]
        );

        return 'index.php?' . Url::getQueryStringFromParameters($params)
            . '#?' . Url::getQueryStringFromParameters($hashParams);
    }

    /**
     * Returns the &idSite=X&period=Y&date=Z query string fragment,
     * fetched from current logged-in user's preferences.
     *
     * @param int|string|false $websiteId
     * @param string|false $defaultPeriod
     * @param string|false $defaultDate
     * @return array{idSite: int|string, period: string, date: string}
     * @throws \Exception in case a website was not specified and a default website id could not be found
     */
    public function urlForDefaultUserParams($websiteId = false, $defaultPeriod = false, $defaultDate = false)
    {
        $userPreferences = new UserPreferences();
        if (empty($websiteId)) {
            $websiteId = $userPreferences->getDefaultWebsiteId();
        }
        if (empty($websiteId)) {
            throw new \Exception("A website ID was not specified and a website to default to could not be found.");
        }
        if (empty($defaultPeriod)) {
            $defaultPeriod = $userPreferences->getDefaultPeriod(false);
        }
        if (empty($defaultDate)) {
            $defaultDate = $userPreferences->getDefaultDate();
        }

        if ($defaultPeriod !== 'range' && !empty($defaultDate) && $defaultDate !== 'today') {
            // not easy to make it work for range... is rarely the default anyway especially when just setting up
            // Matomo as this logic is basically only applied on the first day a site is created
            // no need to run logic when today is selected. It basically runs currently only when "yesterday" is selected
            // as a default date but would also support future new default dates like past month etc.
            try {
                $siteCreationDate = Site::getCreationDateFor($websiteId);
                $siteTimezone = Site::getTimezoneFor($websiteId);

                if (!empty($siteCreationDate)) {
                    if (is_numeric($defaultDate)) {
                        $defaultDate = (int) $defaultDate; //prevent possible exception should defaultDate be a string timestamp
                    }
                    $siteCreationDate = Date::factory($siteCreationDate, $siteTimezone);
                    $defaultDateObj = Date::factory($defaultDate, $siteTimezone);

                    $period = Period\Factory::build($defaultPeriod, $defaultDateObj);
                    $endDate = $period->getDateEnd();

                    if ($endDate->isEarlier($siteCreationDate)) {
                        // when selected date is before site creation date or it is the site creation day
                        $defaultDate = $siteCreationDate->toString();
                    }
                }
            } catch (\Exception $e) {
                //ignore any error in case site was just deleted or the given date is not valid etc.
            }
        }
        return array(
            'idSite' => $websiteId,
            'period' => $defaultPeriod,
            'date'   => $defaultDate,
        );
    }

    /**
     * Configures the top menu which is supposed to contain analytics related items such as the
     * "All Websites Dashboard".
     */
    public function configureTopMenu(MenuTop $menu)
    {
    }

    /**
     * Configures the admin menu which is supposed to contain only administration related items such as
     * "Websites", "Users" or "Settings".
     */
    public function configureAdminMenu(MenuAdmin $menu)
    {
    }

    private function checkisValidCallable($module, $action)
    {
        if (!Development::isEnabled()) {
            return;
        }

        $prefix = 'Menu item added in ' . get_class($this) . ' will fail when being selected. ';

        if (!is_string($action)) {
            Development::error($prefix . 'No valid action is specified. Make sure the defined action that should be executed is a string.');
        }

        $reportAction = lcfirst(substr($action, 4));
        if (ReportsProvider::factory($module, $reportAction)) {
            return;
        }

        $controllerClass = '\\Piwik\\Plugins\\' . $module . '\\Controller';

        if (!Development::methodExists($controllerClass, $action)) {
            Development::error($prefix . 'The defined action "' . $action . '" does not exist in ' . $controllerClass . '". Make sure to define such a method.');
        }

        if (!Development::isCallableMethod($controllerClass, $action)) {
            Development::error($prefix . 'The defined action "' . $action . '" is not callable on "' . $controllerClass . '". Make sure the method is public.');
        }
    }
}
