<?php

/**
 * @file classes/controllers/grid/GridHandler.php
 *
 * Copyright (c) 2014-2021 Simon Fraser University
 * Copyright (c) 2000-2021 John Willinsky
 * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
 *
 * @class GridHandler
 *
 * @ingroup classes_controllers_grid
 *
 * @brief This class defines basic operations for handling HTML grids. Grids
 *  are used to implement a standardized listing of elements, as would commonly
 *  be laid out in an HTML table, permitting rows, columns, row actions (such
 *  as "delete" and "edit" actions, which operate on a single row), and grid
 *  actions (such as "new element", which operates on the grid as a whole), and
 *  other functionality to be implemented consistently.
 *
 * An implemented grid consists of several classes, with a subclass of
 * GridHandler as the centerpiece. Each row is described by an instance of a
 * GridRow, which is generally extended for the row in question; each column
 * is described by an instance of GridColumn (for which several generic columns
 * are implemented). Often grids will make use of a specific subclass of
 * DataProvider in order to prepare data for display in the grid.
 *
 * Actions (be they row or grid actions) are implemented by LinkAction
 * instances.
 *
 * There are several subclasses of GridHandler that provide generalized grids
 * of particular forms, such as CategoryGridHandler and ListbuilderHandler.
 *
 * The JavaScript front-end is described at <https://pkp.sfu.ca/wiki/index.php?title=JavaScript_widget_controllers#Grids>.
 *
 * For a concrete example of a grid handler (and related classes), see
 * AnnouncementTypeGridHandler.
 */

namespace PKP\controllers\grid;

use APP\template\TemplateManager;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Enumerable;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use PKP\controllers\grid\files\FilesGridDataProvider;
use PKP\core\ItemIterator;
use PKP\core\JSONMessage;
use PKP\core\PKPRequest;
use PKP\db\DAOResultFactory;
use PKP\db\DBResultRange;
use PKP\form\Form;
use PKP\handler\PKPHandler;
use PKP\linkAction\LinkAction;
use PKP\linkAction\request\NullAction;
use PKP\plugins\Hook;
use PKP\template\PKPTemplateManager;

class GridHandler extends PKPHandler
{
    public const GRID_ACTION_POSITION_DEFAULT = 'default';
    public const GRID_ACTION_POSITION_ABOVE = 'above';
    public const GRID_ACTION_POSITION_LASTCOL = 'lastcol';
    public const GRID_ACTION_POSITION_BELOW = 'below';

    /** @var string grid title locale key */
    public $_title = '';

    /** @var string empty row locale key */
    public $_emptyRowText = 'grid.noItems';

    /** @var string Grid foot note locale key */
    public $_footNote = '';

    /** @var GridDataProvider */
    public $_dataProvider;

    /**
     * @var array Grid actions. The first key represents
     *  the position of the action in the grid, the second key
     *  represents the action id.
     */
    public $_actions = [self::GRID_ACTION_POSITION_DEFAULT => []];

    /** @var array The GridColumns of this grid. */
    public $_columns = [];

    /** @var array The grid's data source. */
    public $_data;

    /** @var ItemIterator The item iterator to be used for paging. */
    public $_itemIterator;

    /** @var string The grid template. */
    public $_template;

    /** @var array The urls that will be used in JS handler. */
    public $_urls;

    /** @var array The grid features. */
    public $_features;

    /** @var array Constants that should be passed to the template */
    public $_constants = [];


    /**
     * Constructor.
     *
     * @param GridDataProvider $dataProvider An optional data provider
     *  for the grid. If no data provider is given then the grid
     *  assumes that child classes will override default method
     *  implementations.
     */
    public function __construct($dataProvider = null)
    {
        $this->_dataProvider = $dataProvider;
        parent::__construct();
    }


    //
    // Getters and Setters
    //
    /**
     * Get the data provider.
     *
     * @return FilesGridDataProvider
     */
    public function getDataProvider()
    {
        return $this->_dataProvider;
    }

    /**
     * Get the grid request parameters. These
     * are the parameters that uniquely identify the
     * data within a grid.
     *
     * NB: You should make sure to authorize and/or
     * validate parameters before you publish them
     * through this interface. Callers will assume that
     * data accessed through this method will not have
     * to be sanitized.
     *
     * The default implementation tries to retrieve
     * request parameters from a data provider if there
     * is one.
     *
     * @return array
     */
    public function getRequestArgs()
    {
        $dataProvider = $this->getDataProvider();
        $requestArgs = [];
        if ($dataProvider instanceof GridDataProvider) {
            $requestArgs = $dataProvider->getRequestArgs();
        }

        $this->callFeaturesHook('getRequestArgs', ['grid' => &$this, 'requestArgs' => &$requestArgs]);

        return $requestArgs;
    }

    /**
     * Get a single grid request parameter.
     *
     * @see getRequestArgs()
     *
     * @param string $key The name of the parameter to retrieve.
     */
    public function getRequestArg($key)
    {
        $requestArgs = $this->getRequestArgs();
        assert(isset($requestArgs[$key]));
        return $requestArgs[$key];
    }

    /**
     * Get the grid title.
     *
     * @return string locale key
     */
    public function getTitle()
    {
        return $this->_title;
    }

    /**
     * Set the grid title.
     *
     * @param string $title locale key
     */
    public function setTitle($title)
    {
        $this->_title = $title;
    }

    /**
     * Get the no items locale key
     *
     * @return string locale key
     */
    public function getEmptyRowText()
    {
        return $this->_emptyRowText;
    }

    /**
     * Set the no items locale key
     *
     * @param string $emptyRowText locale key
     */
    public function setEmptyRowText($emptyRowText)
    {
        $this->_emptyRowText = $emptyRowText;
    }

    /**
     * Get the grid foot note.
     *
     * @return string locale key
     */
    public function getFootNote()
    {
        return $this->_footNote;
    }

    /**
     * Set the grid foot note.
     *
     * @param string $footNote locale key
     */
    public function setFootNote($footNote)
    {
        $this->_footNote = $footNote;
    }

    /**
     * Get all actions for a given position within the grid.
     *
     * @param string $position The position of the actions.
     *
     * @return array The LinkActions for the given position.
     */
    public function getActions($position = self::GRID_ACTION_POSITION_ABOVE)
    {
        if (!isset($this->_actions[$position])) {
            return [];
        }
        return $this->_actions[$position];
    }

    /**
     * Add an action.
     *
     * @param mixed $action a single action.
     * @param string $position The position of the action.
     */
    public function addAction($action, $position = self::GRID_ACTION_POSITION_ABOVE)
    {
        if (!isset($this->_actions[$position])) {
            $this->_actions[$position] = [];
        }
        $this->_actions[$position][$action->getId()] = $action;
    }

    /**
     * Get all columns.
     *
     * @return array An array of GridColumn instances.
     */
    public function &getColumns()
    {
        return $this->_columns;
    }

    /**
     * Retrieve a single column by id.
     *
     * @param int $columnId
     *
     * @return GridColumn
     */
    public function getColumn($columnId)
    {
        assert(isset($this->_columns[$columnId]));
        return $this->_columns[$columnId];
    }

    /**
     * Get columns by flag.
     *
     * @param string $flag
     *
     * @return array
     */
    public function &getColumnsByFlag($flag)
    {
        $columns = [];
        foreach ($this->getColumns() as $column) {
            if ($column->hasFlag($flag)) {
                $columns[$column->getId()] = $column;
            }
        }

        return $columns;
    }

    /**
     * Get columns number. If a flag is passed, the columns
     * using it will not be counted.
     *
     * @param string $flag optional
     *
     * @return int
     */
    public function getColumnsCount($flag = null)
    {
        $count = 0;
        foreach ($this->getColumns() as $column) {
            if (!$column->hasFlag($flag)) {
                $count++;
            }
        }

        return $count;
    }

    /**
     * Checks whether a column exists.
     *
     * @param int $columnId
     *
     * @return bool
     */
    public function hasColumn($columnId)
    {
        return isset($this->_columns[$columnId]);
    }

    /**
     * Add a column.
     *
     * @param mixed $column A single GridColumn instance.
     */
    public function addColumn($column)
    {
        assert($column instanceof GridColumn);
        $this->_columns[$column->getId()] = $column;
    }

    /**
     * Get the grid data.
     *
     * @param PKPRequest $request
     *
     * @return array
     */
    public function &getGridDataElements($request)
    {
        $filter = $this->getFilterSelectionData($request);

        // Try to load data if it has not yet been loaded.
        if (is_null($this->_data)) {
            $data = $this->loadData($request, $filter);

            if (is_null($data)) {
                // Initialize data to an empty array.
                $data = [];
            }

            $this->setGridDataElements($data);
        }

        $this->callFeaturesHook('getGridDataElements', ['request' => &$request, 'grid' => &$this, 'gridData' => &$this->_data, 'filter' => &$filter]);

        return $this->_data;
    }

    /**
     * Check whether the grid has rows.
     *
     * @return bool
     */
    public function hasGridDataElements($request)
    {
        $data = &$this->getGridDataElements($request);
        assert(is_array($data));
        return (bool) count($data);
    }

    /**
     * Set the grid data.
     *
     * @param mixed $data an array or ItemIterator with element data
     */
    public function setGridDataElements($data)
    {
        $this->callFeaturesHook('setGridDataElements', ['grid' => &$this, 'data' => &$data]);
    
        $this->_data = match (true) {
            $data instanceof LazyCollection => $this->toAssociativeArray($data),
    
            $data instanceof Enumerable => $this->toAssociativeArray(LazyCollection::make($data)),
    
            $data instanceof DAOResultFactory => $data->toAssociativeArray(),
            $data instanceof ItemIterator => $data->toArray(),
            is_iterable($data) => $data,
            default => [],
        };
    }

    /**
     * Get the grid template.
     *
     * @return string
     */
    public function getTemplate()
    {
        if (is_null($this->_template)) {
            $this->setTemplate('controllers/grid/grid.tpl');
        }

        return $this->_template;
    }

    /**
     * Set the grid template.
     *
     * @param string $template
     */
    public function setTemplate($template)
    {
        $this->_template = $template;
    }

    /**
     * Return all grid urls that will be used
     * in JS handler.
     *
     * @return array
     */
    public function getUrls()
    {
        return $this->_urls;
    }

    /**
     * Define the urls that will be used
     * in JS handler.
     *
     * @param PKPRequest $request
     * @param array $extraUrls Optional extra urls.
     */
    public function setUrls($request, $extraUrls = [])
    {
        $router = $request->getRouter();
        $urls = [
            'fetchGridUrl' => $router->url($request, null, null, 'fetchGrid', null, $this->getRequestArgs()),
            'fetchRowsUrl' => $router->url($request, null, null, 'fetchRows', null, $this->getRequestArgs()),
            'fetchRowUrl' => $router->url($request, null, null, 'fetchRow', null, $this->getRequestArgs())
        ];
        $this->_urls = array_merge($urls, $extraUrls);
    }

    /**
     * Override this method to return true if you want
     * to use the grid within another component (e.g. to
     * remove the title or change the layout accordingly).
     *
     * @return bool
     */
    public function getIsSubcomponent()
    {
        return false;
    }

    /**
     * Get all grid attached features.
     *
     * @return array
     */
    public function getFeatures()
    {
        return $this->_features;
    }

    /**
     * Get the item iterator that represents this grid data.
     * Should only be used for retrieving paging data.
     * See #6498.
     *
     * @return ItemIterator
     */
    public function getItemIterator()
    {
        return $this->_itemIterator;
    }

    /**
     * Get "publish data changed" event list.
     *
     * @return array
     */
    public function getPublishChangeEvents()
    {
        return [];
    }

    // FIXME: Since we've moved to PHP5, maybe those methods
    // should be moved into interfaces like OrderableItems
    // and SelectableItems. Then each grid can implement
    // them in a clear way. It will also simplify this base
    // class hiding optional interfaces.

    //
    // Orderable items.
    //
    /**
     * Override to return the data element sequence value.
     *
     *
     * @return int
     */
    public function getDataElementSequence($gridDataElement)
    {
        return 0; // Ordering is ambiguous or irrelevant.
    }

    /**
     * Override to set the data element new sequence.
     *
     * @param PKPRequest $request
     * @param int $rowId
     * @param int $newSequence
     */
    public function setDataElementSequence($request, $rowId, $gridDataElement, $newSequence)
    {
        assert(false);
    }

    //
    // Selectable items.
    //
    /**
     * Returns the current selection state
     * of the grid data element.
     *
     *
     * @return bool
     */
    public function isDataElementSelected($gridDataElement)
    {
        assert(false);
    }

    /**
     * Get the select parameter name to store
     * the selected files.
     *
     * @return string
     */
    public function getSelectName()
    {
        assert(false);
    }

    /**
     * Tries to identify the data element in the grids
     * data source that corresponds to the requested row id.
     * Raises a fatal error if such an element cannot be
     * found.
     *
     * @param PKPRequest $request
     * @param array $args
     *
     * @return ?\PKP\controllers\grid\GridRow the requested grid row, already
     *  configured with id and data or null if the row
     *  could not been found.
     */
    public function getRequestedRow($request, $args)
    {
        $isModified = isset($args['modify']);
        if (isset($args['rowId']) && !$isModified) {
            // A row ID was specified. Fetch it
            $elementId = $args['rowId'];

            // Retrieve row data for the requested row id
            $dataElement = $this->getRowDataElement($request, $elementId);
            if (is_null($dataElement)) {
                // If the row doesn't exist then
                // return null. It may be that the
                // row has been deleted in the meantime
                // and the client does not yet know about this.
                $nullVar = null;
                return $nullVar;
            }
        } elseif ($isModified) {
            $elementId = null;
            // The row is modified. The client may be asking
            // for a formatted new entry, to be saved later, or
            // for a representation of a modified row.
            $dataElement = $this->getRowDataElement($request, $elementId);
            if (isset($args['rowId'])) {
                // the rowId holds the elementId being modified
                $elementId = $args['rowId'];
            }
        }

        // Instantiate a new row
        return $this->_getInitializedRowInstance($request, $elementId, $dataElement, $isModified);
    }

    /**
     * Render the passed row and return its markup.
     *
     * @param PKPRequest $request
     * @param \PKP\controllers\grid\GridRow $row
     *
     * @return string
     */
    public function renderRow($request, $row)
    {
        $this->setFirstDataColumn();
        return $this->renderRowInternally($request, $row);
    }

    /**
     * Get grid range info.
     *
     * @param PKPRequest $request
     * @param string $rangeName The grid id.
     * @param null|mixed $contextData
     *
     * @return DBResultRange
     */
    public function getGridRangeInfo($request, $rangeName, $contextData = null)
    {
        $rangeInfo = parent::getRangeInfo($request, $rangeName, $contextData);

        $this->callFeaturesHook('getGridRangeInfo', ['request' => &$request, 'grid' => &$this, 'rangeInfo' => $rangeInfo]);

        return $rangeInfo;
    }


    //
    // Overridden methods from PKPHandler
    //
    /**
     * @copydoc PKPHandler::authorize()
     */
    public function authorize($request, &$args, $roleAssignments)
    {
        $dataProvider = $this->getDataProvider();
        $hasDataProvider = $dataProvider instanceof \PKP\controllers\grid\GridDataProvider;
        if ($hasDataProvider) {
            $this->addPolicy($dataProvider->getAuthorizationPolicy($request, $args, $roleAssignments));
        }

        $success = parent::authorize($request, $args, $roleAssignments);

        if ($hasDataProvider && $success === true) {
            $dataProvider->setAuthorizedContext($this->getAuthorizedContext());
        }

        return $success;
    }

    /**
     * @see PKPHandler::initialize()
     *
     * @param PKPRequest $request
     * @param array $args optional
     */
    public function initialize($request, $args = null)
    {
        parent::initialize($request);

        if ($this->getFilterForm() && $this->isFilterFormCollapsible()) {
            $this->addAction(
                new LinkAction(
                    'search',
                    new NullAction(),
                    __('common.search'),
                    'search_extras_expand'
                )
            );
        }

        // Give a chance to grid add features before calling hooks.
        // Because we must control when features are added to a grid,
        // this is the only place that should use the _addFeature() method.
        $this->_addFeatures($this->initFeatures($request, $args));
        $this->callFeaturesHook('gridInitialize', ['grid' => &$this]);
    }


    //
    // Public handler methods
    //
    /**
     * Render the entire grid controller and send
     * it to the client.
     *
     * @param array $args
     * @param PKPRequest $request
     *
     * @return JSONMessage JSON object
     */
    public function fetchGrid($args, $request)
    {
        $this->checkIfResetActionsNeeded($request);

        $this->setUrls($request);

        // Prepare the template to render the grid.
        $templateMgr = TemplateManager::getManager($request);
        $templateMgr->registerClass(GridColumn::class, GridColumn::class);
        $templateMgr->assign('grid', $this);
        $templateMgr->assign('request', $request);

        // Add rendered filter
        $renderedFilter = $this->renderFilter($request);
        $templateMgr->assign('gridFilterForm', $renderedFilter);

        // Add columns.
        $this->setFirstDataColumn();
        $columns = $this->getColumns();
        $templateMgr->assign('columns', $columns);

        $this->_fixColumnWidths();

        // Do specific actions to fetch this grid.
        $this->doSpecificFetchGridActions($args, $request, $templateMgr);

        // Assign additional params for the fetchRow and fetchGrid URLs to use.
        $templateMgr->assign('gridRequestArgs', $this->getRequestArgs());

        $this->callFeaturesHook('fetchGrid', ['grid' => &$this, 'request' => &$request]);

        // Assign features.
        $templateMgr->assign('features', $this->getFeatures());

        // Assign constants.
        $templateMgr->assign('gridConstants', $this->_constants);

        // Let the view render the grid.
        return new JSONMessage(true, $templateMgr->fetch($this->getTemplate()));
    }

    /**
     * Fetch all grid rows from loaded data.
     *
     * @param array $args
     * @param PKPRequest $request
     *
     * @return JSONMessage JSON object.
     */
    public function fetchRows($args, $request)
    {
        // Render the rows.
        $this->setFirstDataColumn();
        $elements = $this->getGridDataElements($request);
        $renderedRows = $this->renderRowsInternally($request, $elements);

        $json = new JSONMessage();
        $json->setStatus(false);

        if ($renderedRows) {
            $renderedRowsString = null;
            foreach ($renderedRows as $rowString) {
                $renderedRowsString .= $rowString;
            }
            $json->setStatus(true);
            $json->setContent($renderedRowsString);
        }

        $this->callFeaturesHook('fetchRows', ['request' => &$request, 'grid' => &$this, 'jsonMessage' => &$json]);

        return $json;
    }

    /**
     * Render a row and send it to the client. If the row no
     * longer exists then inform the client.
     *
     * @param array $args
     * @param PKPRequest $request
     *
     * @return JSONMessage JSON object.
     */
    public function fetchRow($args, $request)
    {
        // Instantiate the requested row (includes a
        // validity check on the row id).
        $row = $this->getRequestedRow($request, $args);

        $json = new JSONMessage(true);
        if (is_null($row)) {
            // Inform the client that the row does no longer exist.
            $json->setAdditionalAttributes(['elementNotFound' => $args['rowId']]);
        } else {
            // Render the requested row
            $renderedRow = $this->renderRow($request, $row);
            $json->setContent($renderedRow);

            // Add the sequence map so grid can place the row at the correct position.
            $sequenceMap = $this->getRowsSequence($request);
            $json->setAdditionalAttributes(['sequenceMap' => $sequenceMap]);
        }

        $this->callFeaturesHook('fetchRow', ['request' => &$request, 'grid' => &$this, 'row' => &$row, 'jsonMessage' => &$json]);

        // Render and return the JSON message.
        return $json;
    }

    /**
     * Render a cell and send it to the client
     *
     * @param array $args
     * @param PKPRequest $request
     *
     * @return JSONMessage JSON object
     */
    public function fetchCell(&$args, $request)
    {
        // Check the requested column
        if (!isset($args['columnId'])) {
            throw new \Exception('Missing column id!');
        }
        if (!$this->hasColumn($args['columnId'])) {
            throw new \Exception('Invalid column id!');
        }
        $this->setFirstDataColumn();
        $column = $this->getColumn($args['columnId']);

        // Instantiate the requested row
        $row = $this->getRequestedRow($request, $args);
        if (is_null($row)) {
            throw new \Exception('Row not found!');
        }

        // Render the cell
        return new JSONMessage(true, $this->_renderCellInternally($request, $row, $column));
    }

    /**
     * Hook opportunity for grid features to request a save items sequence
     * operation. If no grid feature that implements the saveSequence
     * hook is attached to this grid, this operation will only return
     * the data changed event json message.
     *
     * @param array $args
     * @param PKPRequest $request
     *
     * @return JSONMessage JSON object
     */
    public function saveSequence($args, $request)
    {
        if (!$request->checkCSRF()) {
            throw new \Exception('CSRF mismatch!');
        }
        $this->callFeaturesHook('saveSequence', ['request' => &$request, 'grid' => &$this]);

        return \PKP\db\DAO::getDataChangedEvent();
    }

    /**
     * Get the js handler for this component.
     *
     * @return string
     */
    public function getJSHandler()
    {
        return '$.pkp.controllers.grid.GridHandler';
    }

    //
    // Protected methods to be overridden/used by subclasses
    //
    /**
     * Return the sequence map of the current loaded grid items.
     * This is not the sequence value of the data represented by the
     * row, it's just the mapping of the rows sequence, in the order
     * that they are loaded. To handle grid items ordering, see
     * OrderItemsFeature class.
     *
     * @param PKPRequest $request
     *
     * @return array
     */
    protected function getRowsSequence($request)
    {
        return array_keys($this->getGridDataElements($request));
    }


    /**
     * Get a new instance of a grid row. May be
     * overridden by subclasses if they want to
     * provide a custom row definition.
     *
     * @return \PKP\controllers\grid\GridRow
     */
    protected function getRowInstance()
    {
        //provide a sensible default row definition
        return new GridRow();
    }

    /**
     * Create a data element from a request. This is used to format
     * new rows prior to their insertion or existing rows that have
     * been edited but not saved.
     *
     * @param PKPRequest $request
     * @param int $elementId Reference to be filled with element
     *  ID (if one is to be used)
     *
     * @return object
     */
    protected function &getDataElementFromRequest($request, &$elementId)
    {
        throw new \Exception('Grid does not support data element creation!');
    }

    /**
     * Retrieve a single data element from the grid's data
     * source corresponding to the given row id. If none is
     * found then return null.
     *
     * @param PKPRequest $request
     * @param string $rowId The row ID; reference permits modification.
     */
    protected function getRowDataElement($request, &$rowId)
    {
        $elements = &$this->getGridDataElements($request);

        assert(is_array($elements));
        if (!isset($elements[$rowId])) {
            return null;
        }

        return $elements[$rowId];
    }

    /**
     * Implement this method to load data into the grid.
     *
     * @param PKPRequest $request
     * @param ?array $filter An associative array with filter data as returned by
     *  getFilterSelectionData(). If no filter has been selected by the user
     *  then the array will be empty.
     *
     * @return array grid data
     */
    protected function loadData($request, $filter)
    {
        $gridData = null;
        $dataProvider = $this->getDataProvider();
        if ($dataProvider instanceof \PKP\controllers\grid\GridDataProvider) {
            // Populate the grid with data from the
            // data provider.
            $gridData = $dataProvider->loadData($filter);
        }

        $this->callFeaturesHook('loadData', ['request' => &$request, 'grid' => &$this, 'gridData' => &$gridData]);

        return $gridData;
    }

    /**
     * Returns a Form object or the path name of a filter template.
     *
     * @return Form|string|null
     */
    protected function getFilterForm()
    {
        return null;
    }

    /**
     * Determine whether a filter form should be collapsible.
     *
     * @return bool
     */
    protected function isFilterFormCollapsible()
    {
        return true;
    }

    /**
     * Method that extracts the user's filter selection from the request either
     * by instantiating the filter's Form object or by reading the request directly
     * (if using a simple filter template only).
     *
     * @param PKPRequest $request
     *
     * @return ?array
     */
    protected function getFilterSelectionData($request)
    {
        return null;
    }

    /**
     * Render the filter (a template).
     *
     * @param PKPRequest $request
     * @param array $filterData Data to be used by the filter template.
     *
     * @return string
     */
    protected function renderFilter($request, $filterData = [])
    {
        $form = $this->getFilterForm();
        switch (true) {
            case $form === null: // No filter form.
                return '';
            case is_string($form): // HTML mark-up
                $templateMgr = TemplateManager::getManager($request);

                // Assign data to the filter.
                $templateMgr->assign('filterData', $filterData);

                // Assign current selected filter data.
                $filterSelectionData = $this->getFilterSelectionData($request);
                $templateMgr->assign('filterSelectionData', $filterSelectionData);

                return $templateMgr->fetch($form);
        }
        assert(false);
    }

    /**
     * Returns a common 'no matches' result when subclasses find no results for
     * AJAX autocomplete requests.
     *
     * @return JSONMessage JSON object
     */
    protected function noAutocompleteResults()
    {
        $returner = [];
        $returner[] = ['label' => __('common.noMatches'), 'value' => ''];

        return new JSONMessage(true, $returner);
    }

    /**
     * Override this method if your subclass needs to perform
     * different actions than the ones implemented here.
     * This method is called by GridHandler::fetchGrid()
     *
     * @param array $args
     * @param PKPRequest $request
     * @param PKPTemplateManager $templateMgr
     */
    protected function doSpecificFetchGridActions($args, $request, $templateMgr)
    {
        // Render the body elements.
        $gridBodyParts = $this->renderGridBodyPartsInternally($request);
        $templateMgr->assign('gridBodyParts', $gridBodyParts);
    }

    /**
     * Define the first column that will contain
     * grid data.
     *
     * Override this method to define a different column
     * than the first one.
     */
    protected function setFirstDataColumn()
    {
        $columns = &$this->getColumns();
        $firstColumn = reset($columns);
        $firstColumn->addFlag('firstColumn', true);
    }

    /**
     * Override to init grid features.
     * This method is called by GridHandler::initialize()
     * method that use the returned array with the initialized
     * features to add them to grid.
     *
     * @param PKPRequest $request
     * @param array $args
     *
     * @return array Array with initialized grid features objects.
     */
    protected function initFeatures($request, $args)
    {
        $returner = [];
        $classNameParts = explode('\\', get_class($this)); // Separate namespace info from class name
        Hook::call(strtolower(end($classNameParts) . '::initFeatures'), [$this, $request, $args, &$returner]);
        return $returner;
    }

    /**
     * Call the passed hook in all attached features.
     *
     * @param string $hookName
     * @param array $args Arguments provided by this handler.
     */
    protected function callFeaturesHook($hookName, $args)
    {
        $features = $this->getFeatures();
        if (is_array($features)) {
            foreach ($features as &$feature) {
                if (is_callable([$feature, $hookName])) {
                    $feature->$hookName($args);
                } else {
                    assert(false);
                }
            }
        }
    }

    /**
     * Cycle through the data and get generate the row HTML.
     *
     * @param PKPRequest $request
     * @param array $elements The grid data elements to be rendered.
     *
     * @return array of HTML Strings for Grid Rows.
     */
    protected function renderRowsInternally($request, $elements)
    {
        // Iterate through the rows and render them according
        // to the row definition.
        $renderedRows = [];
        foreach ($elements as $elementId => $element) {
            // Instantiate a new row.
            $row = $this->_getInitializedRowInstance($request, $elementId, $element);

            // Render the row
            $renderedRows[] = $this->renderRowInternally($request, $row);
        }

        return $renderedRows;
    }

    /**
     * Method that renders a single row.
     *
     * NB: You must have initialized the row
     * before you call this method.
     *
     * @param PKPRequest $request
     * @param \PKP\controllers\grid\GridRow $row
     *
     * @return string the row HTML
     */
    protected function renderRowInternally($request, $row)
    {
        // Iterate through the columns and render the
        // cells for the given row.
        $renderedCells = [];
        $columns = $this->getColumns();
        foreach ($columns as $column) {
            assert($column instanceof GridColumn);
            $renderedCells[] = $this->_renderCellInternally($request, $row, $column);
        }

        // Pass control to the view to render the row
        $templateMgr = TemplateManager::getManager($request);
        $templateMgr->assign([
            'grid' => $this,
            'columns' => $columns,
            'cells' => $renderedCells,
            'row' => $row,
        ]);
        return $templateMgr->fetch($row->getTemplate());
    }

    /**
     * Method that renders tbodys to go in the grid main body.
     *
     * @param PKPRequest $request
     *
     * @return array
     */
    protected function renderGridBodyPartsInternally($request)
    {
        // Render the rows.
        $elements = $this->getGridDataElements($request);
        $renderedRows = $this->renderRowsInternally($request, $elements);

        // Render the body part.
        $templateMgr = TemplateManager::getManager($request);
        $gridBodyParts = [];
        if (count($renderedRows) > 0) {
            $templateMgr->assign('grid', $this);
            $templateMgr->assign('rows', $renderedRows);
            $gridBodyParts[] = $templateMgr->fetch('controllers/grid/gridBodyPart.tpl');
        }
        return $gridBodyParts;
    }


    //
    // Private helper methods
    //
    /**
     * Instantiate a new row.
     *
     * @param PKPRequest $request
     * @param string $elementId
     * @param bool $isModified optional
     *
     * @return \PKP\controllers\grid\GridRow
     */
    private function _getInitializedRowInstance($request, $elementId, &$element, $isModified = false)
    {
        // Instantiate a new row
        $row = $this->getRowInstance();
        $row->setGridId($this->getId());
        $row->setId($elementId);
        $row->setData($element);
        $row->setRequestArgs($this->getRequestArgs());
        $row->setIsModified($isModified);

        // Initialize the row before we render it
        $row->initialize($request);
        $this->callFeaturesHook('getInitializedRowInstance', ['grid' => &$this, 'row' => &$row]);
        return $row;
    }

    /**
     * Method that renders a cell.
     *
     * NB: You must have initialized the row
     * before you call this method.
     *
     * @param PKPRequest $request
     * @param \PKP\controllers\grid\GridRow $row
     * @param GridColumn $column
     *
     * @return string the cell HTML
     */
    private function _renderCellInternally($request, $row, $column)
    {
        // If there is no object, then we want to return an empty row.
        // override the assigned GridCellProvider and provide the default.
        $element = &$row->getData();
        if (is_null($element) && $row->getIsModified()) {
            $cellProvider = new GridCellProvider();
            return $cellProvider->render($request, $row, $column);
        }

        // Otherwise, get the cell content.
        // If row defines a cell provider, use it.
        $cellProvider = $row->getCellProvider();
        if (!$cellProvider instanceof \PKP\controllers\grid\GridCellProvider) {
            // Remove reference to the row variable.
            unset($cellProvider);
            // Get cell provider from column.
            $cellProvider = $column->getCellProvider();
        }

        return $cellProvider->render($request, $row, $column);
    }

    /**
     * Method that grabs all the existing columns and makes sure the column widths add to exactly 100
     * N.B. We do some extra column fetching because PHP makes copies of arrays with foreach.
     */
    private function _fixColumnWidths()
    {
        $columns = &$this->getColumns();
        $width = 0;
        $noSpecifiedWidthCount = 0;
        // Find the total width and how many columns do not specify their width.
        foreach ($columns as $column) {
            if ($column->hasFlag('width')) {
                $width += $column->getFlag('width');
            } else {
                $noSpecifiedWidthCount++;
            }
        }

        // Four cases: we have to add or remove some width, and either we have wiggle room or not.
        // First case, width less than 100 and some unspecified columns to add it to.
        if ($width < 100) {
            if ($noSpecifiedWidthCount > 0) {
                // We need to add width to columns that did not specify it.
                foreach ($columns as $column) {
                    if (!$column->hasFlag('width')) {
                        $modifyColumn = $this->getColumn($column->getId());
                        $modifyColumn->addFlag('width', round((100 - $width) / $noSpecifiedWidthCount));
                        unset($modifyColumn);
                    }
                }
            }
        }

        // Second case, width higher than 100 and all columns width specified.
        if ($width > 100) {
            if ($noSpecifiedWidthCount == 0) {
                // We need to remove width from all columns equally.
                $columnsToModify = $columns;
                foreach ($columns as $key => $column) {
                    // We don't want to change the indent column widht, so avoid it.
                    if ($column->getId() == 'indent') {
                        unset($columnsToModify[$key]);
                    }
                }

                // Calculate the value to remove from all columns.
                $difference = $width - 100;
                $columnsCount = count($columnsToModify);
                $removeValue = round($difference / $columnsCount);
                foreach ($columnsToModify as $column) {
                    $modifyColumn = $this->getColumn($column->getId());
                    if (end($columnsToModify) === $column) {
                        // Handle rounding problems.
                        $totalWidth = $width - ($removeValue * $columnsCount);
                        if ($totalWidth < 100) {
                            $removeValue -= 100 - $totalWidth;
                        }
                    }

                    $modifyColumn->addFlag('width', $modifyColumn->getFlag('width') - $removeValue);
                }
            }
        }
    }

    /**
     * Add grid features.
     *
     * @param array $features
     */
    private function _addFeatures($features)
    {
        assert(is_array($features));
        foreach ($features as &$feature) {
            assert($feature instanceof \PKP\controllers\grid\feature\GridFeature);
            $this->_features[$feature->getId()] = $feature;
        }
    }

    private function checkIfResetActionsNeeded($request)
    {
        // #8696: This is added in order to reset the page of a grid to 1 if the "search" button is clicked, effectively executing a
        //        new search.

        // Check if the grid has any PagingFeature features
        if ($this->getFeatures() != null) {
            $pagingFeatureArray = array_filter($this->getFeatures(), function ($value) {
                return $value instanceof \PKP\controllers\grid\feature\PagingFeature;
            });

            if (!empty($pagingFeatureArray)) {
                if (array_key_exists('search', $request->getUserVars()) && array_key_exists('submitFormButton', $request->getUserVars())) {
                    $filteredKeys = array_filter(array_keys($request->getUserVars()), function ($key) {
                        return Str::endsWith($key, 'gridPage');
                    });

                    if (!empty($filteredKeys)) {
                        $request->_requestVars[$filteredKeys[0]] = 1;
                    }
                }
            }
        }
    }

    /**
     * Legacy function for grid handlers.
     *Prepares LazyCollections in associative array GridHandler expects, e.g. [item_id => item]
     *
     * @see PKP\db\DAOResultFactory::toAssociativeArray()
     *
     */
    public static function toAssociativeArray(LazyCollection $lazyCollection, string $idField = 'id'): array
    {
        $returner = [];
        foreach ($lazyCollection as $item) {
            if ($item instanceof Model) {
                $returner[$item->$idField] = $item;
            } else {
                $returner[$item->getData($idField)] = $item;
            }
        }
        return $returner;
    }
}

if (!PKP_STRICT_MODE) {
    class_alias('\PKP\controllers\grid\GridHandler', '\GridHandler');
    foreach ([
        'GRID_ACTION_POSITION_DEFAULT',
        'GRID_ACTION_POSITION_ABOVE',
        'GRID_ACTION_POSITION_LASTCOL',
        'GRID_ACTION_POSITION_BELOW',
    ] as $constantName) {
        define($constantName, constant('\GridHandler::' . $constantName));
    }
}
