<?php

namespace Velis\QuickReport;

use ArrayObject;
use Generator;
use PDO;
use ReflectionException;
use Report\Notification;
use User\User as SystemUser;
use Velis\Acl\Role;
use Velis\App;
use Velis\Arrays;
use Velis\Db\Exception as DbException;
use Velis\Db\Postgres;
use Velis\Exception;
use Velis\Filter;
use Velis\Lang;
use Velis\Model\DataObject;
use Velis\Model\Routable;
use Velis\Output;
use Velis\User;
use XLSXWriter;

/**
 * Quick report model
 *
 * @author Bartosz Izdebski <bartosz.izdebski@velis.pl>
 * @author Olek Procki <olo@velis.pl>
 */
class Report extends DataObject implements Routable
{
    const BATCH_DATA_COUNT = 400;

    /**
     * Filter list params by default
     * @var bool
     */
    protected static $_filterListParams = true;


    /**
     * Default order
     * @var string
     */
    protected static $_listDefaultOrder = 'name';


    /**
     * Allowed order statement regular expressions
     * @var array
     */
    protected static $_orderExpressions = ["/\(name\)\.[a-z]{2}/"];


    /**
     * @var User[]
     */
    private $_recipients = [];


    /**
     * @var User[]
     */
    private $_assignRecipients = [];

    /**
     * Returns related sql table
     * @return string
     */
    protected function _getTableName()
    {
        return 'app.quick_report_to_user';
    }


    /**
     * @param array|ArrayObject $params
     * @param int|null $userId
     * @return string
     */
    public function getQuery(&$params, int $userId = null): string
    {
        $sql = trim($this->sql_expression);
        if (preg_match('/^explain\s.*/mi', $sql) === 0) {
            $sql = "SELECT v.* FROM (" . trim($this->sql_expression, ';') . ") v";
        }

        $order = '';

        if ($params['order']) {
            $order .= ($params['order']);
            if ($params['sort']) {
                $order .= ' ' . ($params['sort']);
            }

            if (preg_match('/^(\d+\s(DESC|ASC))/', $order)) {
                $sql .= ' ORDER BY ' . $order;
            }
        }

        if (preg_match('#{userId!}#', $sql)) {
            $sql = str_replace('{userId!}', $userId ?: App::$user->id(), $sql);
        }

        if (preg_match('#{userId}#', $sql)) {
            $sql = str_replace('{userId}', ':user_id', $sql);
            $params['user_id'] = $userId ?: App::$user->id();
        }


        if (preg_match('#{baseUrl}#', $sql)) {
            $sql = str_replace('{baseUrl}', App::getBaseUrl(), $sql);
        }

        if (preg_match('#{lang}#', $sql)) {
            $sql = str_replace('{lang}', Lang::getLanguage(), $sql);
        }

        if (App::$config->anonymize->enabled) {
            $sql = preg_replace(
                [
                    '/u.last_name \|\| \' \' \|\| u.name/i',
                    '/u.name \|\| \' \' \|\| u.last_name/i',
                    '/acl_api.get_user_name\(([^)]+)\)/i',
                ],
                [
                    '\'' . Lang::get('USER_FULL_NAME') . '\'',
                    '\'' . Lang::get('USER_FULL_NAME') . '\'',
                    '\'' . Lang::get('USER_FULL_NAME') . '\' || \'-\' || $1::text',
                ],
                $sql
            );

            $anonymizedFields = [
                'first_name',
                'last_name',
                'email',
                'email_address',
                'user_name',
                'issuer_name',
                'identity_doc_no',
                'responsible_user_name',
                'owner_user_name',
            ];
            foreach ($anonymizedFields as $anonymizedField) {
                $sql = preg_replace(
                    '/([a-z1-9]+\.' . $anonymizedField . ') AS (\S+)/i',
                    '\'' . $anonymizedField . '\' AS $2',
                    $sql
                );
            }
        }

        if (preg_match_all("#(&([a-zA-Z0-9_*]+))!#", $sql, $matches)) {
            foreach ($matches[0] as $key => $directParam) {
                $sql = str_replace($directParam, $params[$matches[2][$key]], $sql);
            }
            foreach (array_unique($matches[2]) as $directParam) {
                unset($params[$directParam]);
            }
        }

        $sql = preg_replace("#(&[a-zA-Z0-9_*]+)(\*)#", "$1", $sql);
        return preg_replace('#(?:(?<!&))&(?!&)#', ':', $sql);
    }

    private function getTotalNumberOfRows(string $sqlExpression, array $params = []): int
    {
        $row = static::$_db->getRow("SELECT COUNT(*) AS list_items_found FROM ({$sqlExpression}) qr", $params);
        return $row['list_items_found'] ?? 0;
    }

    /**
     * Returns primary key field name for object
     * @return string
     */
    protected function _getPrimaryKeyField()
    {
        return 'quick_report_id';
    }


    /**
     * Returns primary key sequence name
     * @return string
     */
    protected function _getPrimaryKeySeq()
    {
        return 'app.quick_report_tab_quick_report_id_seq';
    }


    /**
     * Returns rewrite route name
     * @return string
     */
    public function getRouteName()
    {
        return 'report';
    }


    /**
     * Return standard url (without rewrite)
     * @return string
     */
    public function getStandardUrl()
    {
        return '/report/index/show?quick_report_id=' . $this->id();
    }


    /**
     * Returns report name
     * @return string
     */
    public function getName()
    {
        return $this->getTranslatedName('name', Lang::get('GENERAL_DEFAULT_TITLE'));
    }


    /**
     * Returns translated comment
     * @return string
     */
    public function getComment()
    {
        return $this->getTranslatedName('comment', false);
    }


    /**
     * Returns translated notification text
     * @return string
     */
    public function getNotificationText()
    {
        return $this->getTranslatedName('notification_text', false);
    }


    /**
     * Universal factory method
     *
     * @param mixed $objectId
     * @param bool  $useListDatasource Whether to select from listDatasource instead of table
     * @param array $fields
     * @return Report[]|Report
     * @throws ReflectionException
     */
    public static function instance($objectId, $useListDatasource = false, $fields = null)
    {
        $params = [
            'quick_report_id' => $objectId,
            'user_id' => App::$user->id(),
        ];

        return reset(static::listAll($params));
    }


    /**
     * Returns report's category
     * @return Category
     */
    public function getCategory()
    {
        return Category::get($this->quick_report_category_id);
    }

    /**
     * Returns db timeout during quick report execution
     * @return float|int
     */
    private function getQuickReportDbTimeout()
    {
        if (App::$config->TimeoutQuickReportAfter) {
            return (int)App::$config->TimeoutQuickReportAfter * 1000;
        }

        return Postgres::POSTGRES_QUICK_REPORT_TIMEOUT;
    }

    public function getDataGenerator(Filter $params, ?int $userId = null): Generator
    {
        $commit = self::$_db->startTrans();

        try {
            $params = $this->prepareParams($params);

            self::$_db->extractMetadata = true;
            $this->addCursorDeclaration(
                $this->getQuery($params, $userId),
                $params->getArrayCopy()
            );
            $stmt = self::$_db->prepare("FETCH FORWARD " . self::BATCH_DATA_COUNT . " FROM report_cursor_" . $this->id());
            while ($stmt->execute() && $rows = self::$_db->fetchCursor($stmt)) {
                $tmpData = $this->getHistoryTranslation($rows, true);
                foreach ($tmpData as $row) {
                    yield $row;
                }
            }

            if ($commit) {
                self::$_db->commit();
            }
        } catch (Exception $ex) {
            throw $ex;
        }
    }

    /**
     * Return rows from sql expresion
     *
     * @param array $params
     * @param int|null $userId
     * @return array
     * @throws Exception
     */
    public function query($params, $userId = null)
    {
        $pageNum = $params['page'] ?? 1;
        $exportToFile = $params['xls'] == 1 || $params['csv'] == 1;

        if ($exportToFile) {
            $commit = self::$_db->startTrans();
        }
        try {
            $sql = $this->getQuery($params, $userId);

            if (!isset($this->_toXls)) {
                $this->_toXls = $exportToFile;
            }

            if (!$params instanceof Filter) {
                $params = new Filter($params);
            }

            $params = $this->prepareParams($params);

            self::$_db->extractMetadata = true;

            if (!$exportToFile && $this['page_size']) {
                self::$listItemsFound = $this->getTotalNumberOfRows($sql, (array) $params);

                $offset = $pageNum > 1 ? $this['page_size'] * ($pageNum - 1) : 0;
                $sql = "SELECT * FROM ($sql) qr LIMIT {$this['page_size']} OFFSET {$offset}";
            }


            if (!$exportToFile) {
                // Set specific value for timeout during quick report execution
                self::$_db->execDML("SET statement_timeout TO " . $this->getQuickReportDbTimeout());

                $rows = self::$_db->getAll($sql, $params);
                self::$_db->extractMetadata = false;

                // Set specific value for timeout during quick report execution
                self::$_db->execDML("SET statement_timeout TO " . Postgres::DEFAULT_POSTGRES_TIMEOUT);

                $result = $this->getHistoryTranslation($rows);
            } else {
                $this->addCursorDeclaration(
                    $this->getQuery($params),
                    $params->getArrayCopy()
                );

                $stmt = self::$_db->prepare("FETCH FORWARD " . self::BATCH_DATA_COUNT . " FROM report_cursor_" . $this->id());
                $result = [];
                while ($stmt->execute() && $rows = $stmt->fetchAll(PDO::FETCH_ASSOC)) {
                    $tmpData = $this->getHistoryTranslation($rows, true);
                    foreach ($tmpData as $row) {
                        $result[] = $row;
                    }
                }
            }
            if ($commit) {
                self::$_db->commit();
            }
            return $result;
        } catch (Exception $ex) {
            // Set specific value for timeout during quick report execution
            self::$_db->execDML("SET statement_timeout TO " . Postgres::DEFAULT_POSTGRES_TIMEOUT);
            throw $ex;
        }
    }

    private function addCursorDeclaration(string $sql, array $params = null)
    {
        $cursorSql = "DECLARE report_cursor_" . $this->id() . " SCROLL CURSOR FOR SELECT v.* FROM ($sql) v";
        $stmt = self::$_db->prepare($cursorSql);
        $stmt->execute($params);
    }

    public function getHistoryTranslation(array $data, bool $isExport = false): array
    {
        return array_map(
            function ($row) use ($isExport) {
                foreach ($row as $key => $value) {
                    $translatedKey = Lang::getHistoryTranslation($key);
                    $dataType = self::$_db->metadata[$key]['native_type'] ?: self::$_db->metadata[$translatedKey]['native_type'];

                    if (
                        !(
                            ($isExport || App::isConsole())
                            && in_array($dataType, ['timestamptz', 'timestamp', 'date'])
                        )
                    ) {
                        $value = Lang::getHistoryTranslation($value);
                    }

                    unset($row[$key]);
                    $row[$translatedKey] = $value;
                    if ($translatedKey != $key) {
                        if (array_key_exists($key, self::$_db->metadata)) {
                            self::$_db->metadata[$translatedKey] = self::$_db->metadata[$key];
                            unset(self::$_db->metadata[$key]);
                        }
                    }
                }

                return $row;
            },
            $data
        );
    }

    /**
     * Return parameters from sql expresion
     * @return array
     */
    public function parameters()
    {
        $sql = $this->sql_expression;
        $parameters = array();

        preg_match_all("#(&[a-zA-Z0-9_*]+)#", $sql, $matches);

        if ($matches[1]) {
            $parameters = $matches[1];
        }

        return array_unique($parameters);
    }


    /**
     * Check if report require userId
     * @return bool
     */
    public function requireUser()
    {
        return (bool) preg_match('#{userId}#', $this->sql_expression);
    }


    /**
     * Export to xls
     *
     * @param array|Generator $rows

     * @param bool  $return
     *
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
     *
     * @return false|string
     */
    public static function toXLS(&$rows, $headers = [], $return = true)
    {

        /**
         * If library mk-j/PHP_XLSXWriter exists in the project use XlsWriterLite to generate XLS reports.
         */
        $writer = class_exists(XLSXWriter::class) ? new XlsWriterLite($rows, $headers) : new XlsWriter($rows, $headers);

        return $writer->export($return);
    }

    /**
     * Export to CSV
     *
     * @param Generator $rows
     * @param array $headers
     */
    public static function toCSV(Generator &$rows, array $headers = []): string
    {
        $tmpFile = tempnam(DATA_PATH . 'temp/', 'report_');
        $basename = basename($tmpFile);
        $output = fopen($tmpFile, 'w');

        if (empty($headers)) {
            $headers = array_keys($rows->current());
        }

        fputcsv($output, $headers);

        foreach ($rows as $row) {
            fputcsv($output, (array) $row);
        }

        fclose($output);

        return $basename;
    }

    /**
     * Reports list
     *
     * @param int $page
     * @param array|ArrayObject $params
     * @param string $order
     * @param int $limit
     * @param string|array $fields
     *
     * @return Report[]
     *
     * @throws Exception
     */
    public static function getList($page = 1, $params = null, $order = null, $limit = self::ITEMS_PER_PAGE, $fields = null)
    {
        if (!$params) {
            $params = [];
        }

        if (App::$user->hasPriv('QuickReport', 'EditSql') && !empty($params['sql_search'])) {
            self::$_listConditions[] = "(sql_expression ILIKE :sql_search)";
            self::$_listParams['sql_search'] = '%' . trim($params['sql_search'], '%') . '%';
        }

        if ($params['search']) {
            $instance = new self();
            $columns = self::$_db->getColumns($instance->_getTableName());

            if (Arrays::byValue($columns, 'data_type', 'USER-DEFINED')) {
                $searchCondition = [];

                foreach (array_keys(Lang::getLanguages()) as $langId) {
                    $searchCondition[] = "
                        (name)." . $langId . " ILIKE :search
                        OR (comment)." . $langId . " ILIKE :search
                    ";
                }
                self::$_listConditions[] = "(" . implode(" OR ", $searchCondition) . ")";
            } else {
                self::$_listConditions[] = "(name ILIKE :search OR comment ILIKE :search)";
            }
            self::$_listParams['search'] = '%' . trim($params['search'], '%') . '%';
        }

        if ($params['report_date']) {
            $order = 'quick_report_id';

            self::$_listDatasource = 'app.quick_report qr';
            self::$_listConditions[] = "(
                is_cycle = 1
                AND ticket_schedule_interval_id IS NOT NULL
                AND ((date_trunc(ticket_schedule_interval_id, now()) + (cycle_start_date - date_trunc(ticket_schedule_interval_id, cycle_start_date)) = :report_date AND cycle_start_date < CURRENT_DATE)
                OR (date_trunc(ticket_schedule_interval_id, now()) + (date(qr.date_added) - date_trunc(ticket_schedule_interval_id, date(qr.date_added))) = :report_date AND cycle_start_date IS NULL))
            )";

            self::$_listParams['report_date'] = $params['report_date'];
        }

        $rv = parent::getList($page, $params, $order, $limit, $fields);
        self::$_listDatasource = null;

        return $rv;
    }


    /**
     * Return type of column
     *
     * @param string $columnName
     * @return string
     */
    public static function columnType($columnName)
    {
        return self::$_db->metadata[$columnName]['native_type'];
    }


    /**
     * Return column types
     *
     * @return array
     */
    public function getColumnTypes()
    {
        $callback = function ($item) {
            return $item['native_type'];
        };

        return array_map($callback, self::$_db->metadata);
    }


    /**
     * Removes report
     */
    public function remove()
    {
        $params = array('quick_report_id' => $this->id());
        self::$_db->execDML("DELETE FROM app.quick_report_tab WHERE quick_report_id = :quick_report_id", $params);
    }


    /**
     * Returns all recipient users
     * @param bool includeFromRoles
     * @return User[]
     * @throws Exception
     */
    public function getRecipients()
    {
        if (empty($this->_recipients)) {
            $this->_recipients = $this->getAssignRecipients();
        }
        if (self::$_db->tableExists('app.quick_report_role_recipient_tab')) {
            $this->_recipients += $this->getRecipientsFromRoles();
        }

        return $this->_recipients;
    }


    /**
     * Returns recipients selected separately than recipients in roles
     * @param bool includeFromRoles
     * @return User[]
     * @throws Exception
     */
    public function getAssignRecipients()
    {
        if (empty($this->_assignRecipients)) {
            $result = self::$_db->getAll(
                "SELECT * FROM app.quick_report_recipient_tab WHERE quick_report_id = :quick_report_id",
                [
                    'quick_report_id' => $this->quick_report_id,
                ]
            );
            if ($result) {
                $this->_assignRecipients = SystemUser::get(Arrays::getColumn($result, 'user_id'));
            }
        }

        return $this->_assignRecipients;
    }


    /**
     * Returns roles which are notification receivers
     * @return Role[]
     * @throws Exception
     */
    public function getRoles()
    {
        if (!isset($this->_roles)) {
            $this->_roles = [];

            $result = self::$_db->getAll(
                "SELECT * FROM app.quick_report_role_recipient_tab WHERE quick_report_id=:quick_report_id",
                array(
                    'quick_report_id' => $this->quick_report_id,
                )
            );
            if ($result) {
                $this->_roles = Role::get(Arrays::getColumn($result, 'role_id'));
            }
        }

        return $this->_roles;
    }


    /**
     * Returns recipients only from roles
     * @return User[]
     * @throws Exception
     */
    public function getRecipientsFromRoles()
    {
        $recipients = [];

        foreach ($this->getRoles() as $role) {
            foreach ($role->getUsers(true) as $user) {
                $recipients[$user->id()] = $user;
            }
        }

        return $recipients;
    }


    /**
     * Adds quick report notification recipient
     *
     * @param User|int $recipient
     * @return Report
     * @throws Exception
     */
    public function addRecipient($recipient)
    {
        try {
            self::$_db->insert(
                'app.quick_report_recipient_tab',
                [
                    'quick_report_id' => $this->quick_report_id,
                    'user_id' => $recipient instanceof User ? $recipient->id() : $recipient,
                ]
            );
        } catch (DbException $e) {
            if ($e->getCode() == 23505) {
                throw new Exception(Lang::get('REPORT_PERSON_ALREADY_ADDED'), $e->getCode(), $e);
            }
            throw $e;
        }

        return $this;
    }


    /**
     * Adds quick report notification role
     *
     * @param Role|int $role
     * @return Report
     * @throws Exception
     */
    public function addRole($role)
    {
        try {
            self::$_db->insert(
                'app.quick_report_role_recipient_tab',
                array(
                    'quick_report_id' => $this->quick_report_id,
                    'role_id' => $role instanceof Role ? $role->id() : $role
                )
            );
        } catch (DbException $e) {
            if ($e->getCode() == 23505) {
                throw new Exception(Lang::get('REPORT_ROLE_ALREADY_ADDED'), $e->getCode(), $e);
            }
            throw $e;
        }

        return $this;
    }


    /**
     * Removes quick report notification role
     *
     * @param Role|int $role
     * @return Report
     */
    public function removeRole($role)
    {

        self::$_db->execDML(
            "DELETE FROM app.quick_report_role_recipient_tab WHERE
                quick_report_id=:quick_report_id
                AND role_id=:role_id",
            array(
                'quick_report_id' => $this->quick_report_id,
                'role_id' => $role instanceof Role ? $role->id() : $role
            )
        );

        return $this;
    }


    /**
     * Removes quick report notification recipient
     *
     * @param User|int $recipient
     * @return Report
     */
    public function removeRecipient($recipient)
    {
        self::$_db->execDML(
            "DELETE FROM app.quick_report_recipient_tab WHERE
                quick_report_id=:quick_report_id
                AND user_id=:user_id",
            [
                'quick_report_id' => $this->quick_report_id,
                'user_id' => $recipient instanceof User ? $recipient->id() : $recipient,
            ]
        );

        return $this;
    }


    /**
     * Sends cycle notification
     *
     * @throws Exception
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
     */
    public function sendCycleNotification()
    {
        $parameters = $this->parameters();

        $reportUrl = App::$config->settings->httpMethod . "://" . App::$config->settings->domain
                . $this->getStandardUrl()
                . "&execute=1";

        $params = [];

        foreach ($parameters as $param) {
            $param = preg_replace("#(&[a-zA-Z0-9_*]+)(\*)#", "$1", $param);
            $param = substr($param, 1);

            if (substr(strtolower($param), 0, 3) == 'dat') {
                if (in_array(strtolower($param), array('date_from', 'data_od'))) {
                    $params[$param] = $this['current_date_from'];
                    $reportUrl .=  '&' . $param . "=" . $this['current_date_from'];
                }
                if (in_array((strtolower($param)), array('date_to', 'data_do'))) {
                    $params[$param] = $this['current_date_to'];
                    $reportUrl .= '&' . $param . "=" . $this['current_date_to'];
                }
            } else {
                $params[$param] = '';
                $reportUrl .= '&' . $param . "=";
            }
        }

        //If app has module customer portal, then all tenants receive notification and xls files.
        //The rest of the users receive xls files if notification_xls setting is enabled
        if (App::hasModule('CustomerPortal')) {
            //split recipients to tenants and the rest of the users
            $recipients = $this->getFilteredRecipients();

            //send to only tenants
            if (count($recipients->tenants) > 0) {
                $this->sendXlsNotification($recipients->tenants, $params);
            }

            if (count($recipients->others) > 0) {
                $this->sendNotification($recipients->others, $params, $reportUrl);
            }
        } else {
            $recipients = $this->getRecipients();
            $this->sendNotification($recipients, $params, $reportUrl);
        }
    }


    /**
     * Prepares report file to send
     *
     * @param array    $params
     * @param int|null $forUser
     *
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
     *
     * @return array|bool
     */
    private function prepareReportFile($params, $forUser = null)
    {
        $result = $this->query($params, $forUser);
        $files = array();

        if (!$result) {
            return false;
        }

        $listHeader = [];

        if (is_array($result[0])) {
            $listHeader = array_keys($result[0]);
            $headerTypes = [];
            foreach ($listHeader as $header) {
                $headerTypes[$header] = Report::columnType($header);
            }
        }

        ob_start();

        $fileName = Filter::filterAcronym($this->getName()) . date('-Y-m-d') . '.xlsx';
        $tmpFile = Report::toXLS($result, $listHeader);
        $files[$fileName] = file_get_contents(DATA_PATH . 'temp/' . $tmpFile);
        unset($tmpFile);

        ob_end_clean();
        return $files;
    }

    /**
     * Filters list of the recipients and splits it to tenants and others array.
     *
     * @return object
     * @throws Exception
     */
    private function getFilteredRecipients()
    {
        $recipients = $this->getRecipients();
        $filtered   = [
            'tenants' => [],
            'others'  => []
        ];

        foreach ($recipients as $recipient) {
            $dist = $recipient->isTenant() ? 'tenants' : 'others';
            array_push($filtered[$dist], $recipient);
        }

        return (object) $filtered;
    }

    /**
     * @param $recipients
     * @param $params
     * @param string $reportUrl
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
     */
    private function sendXlsNotification($recipients, $params, $reportUrl = '')
    {
        $files = [];
        $this->_toXls = 1;

        $notification = new Notification(
            Notification::CYCLE_QUICK_REPORT_NOTIFICATION,
            [
                'report' => $this,
                'reportUrl' => $reportUrl,
            ]
        );

        if ($this->requireUser()) {
            foreach ($recipients as $recipient) {
                if (!$files = $this->prepareReportFile($params, $recipient->id())) {
                    continue;
                }

                $notification->send($recipient, $files);
            }
        } else {
            if ($files = $this->prepareReportFile($params)) {
                $notification->send($recipients, $files);
            }
        }
    }

    /**
     * @param $recipients
     * @param $params
     * @param $reportUrl
     */
    private function sendNotification($recipients, $params, $reportUrl)
    {
        $this->_toXls = 0;
        $notification = new Notification(
            Notification::CYCLE_QUICK_REPORT_NOTIFICATION,
            [
                'report' => $this,
                'reportUrl' => $reportUrl,
            ]
        );

        if ($this['notification_xls']) {
            $this->sendXlsNotification($recipients, $params, $reportUrl);
        } else {
            $notification->send(
                $recipients,
                null,
                []
            );
        }
    }

    private function prepareParams(Filter $params): Filter
    {
        unset(
            $params['quick_report_id'],
            $params['execute'],
            $params['results_only'],
            $params['order'],
            $params['sort'],
            $params['page'],
            $params['xls'],
            $params['csv'],
        );

        foreach ($this->parameters() as $requiredParam) {
            $strippedRequiredParam = preg_replace('/^&([a-zA-Z0-9_]+)\*?$/', '$1', $requiredParam);
            if (substr($strippedRequiredParam, 0, 5) == 'multi' && !isset($params[$strippedRequiredParam])) {
                $params[$strippedRequiredParam] = '';
            }
        }

        return $params;
    }
}
