<?php

namespace Botble\Table\Abstracts;

use Botble\ACL\Models\User;
use Botble\Base\Contracts\BaseModel as BaseModelContract;
use Botble\Base\Contracts\Builders\Extensible as ExtensibleContract;
use Botble\Base\Facades\Assets;
use Botble\Base\Facades\Form;
use Botble\Base\Facades\Html;
use Botble\Base\Models\BaseModel;
use Botble\Base\Supports\Builders\Extensible;
use Botble\Base\Supports\Builders\RenderingExtensible;
use Botble\Table\Abstracts\Concerns\DeprecatedFunctions;
use Botble\Table\Abstracts\Concerns\HasActions;
use Botble\Table\Abstracts\Concerns\HasBulkActions;
use Botble\Table\Abstracts\Concerns\HasFilters;
use Botble\Table\Abstracts\Concerns\HasHeaderActions;
use Botble\Table\Columns\CheckboxColumn;
use Botble\Table\Columns\Column;
use Botble\Table\Columns\RowActionsColumn;
use Botble\Table\Contracts\FormattedColumn;
use Botble\Table\HeaderActions\HeaderAction;
use Botble\Table\Supports\Builder as CustomTableBuilder;
use Botble\Table\Supports\TableExportHandler;
use Closure;
use Illuminate\Contracts\Routing\UrlGenerator;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation as EloquentRelation;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Conditionable;
use LogicException;
use Symfony\Component\HttpFoundation\Response;
use Yajra\DataTables\DataTableAbstract;
use Yajra\DataTables\DataTables;
use Yajra\DataTables\Services\DataTable;

abstract class TableAbstract extends DataTable implements ExtensibleContract
{
    use DeprecatedFunctions;
    use Extensible;
    use HasHeaderActions;
    use HasActions;
    use HasBulkActions;
    use HasFilters;
    use RenderingExtensible;
    use Conditionable;

    public const TABLE_TYPE_ADVANCED = 'advanced';

    public const TABLE_TYPE_SIMPLE = 'simple';

    protected bool $bStateSave = true;

    protected string $type = self::TABLE_TYPE_ADVANCED;

    protected string $ajaxUrl;

    protected int $pageLength = 10;

    protected $view = 'core/table::table';

    protected array $options = [];

    /**
     * @deprecated since v6.8.0
     */
    protected $repository;

    protected BaseModelContract|null $model = null;

    protected bool $useDefaultSorting = true;

    protected int $defaultSortColumn = 1;

    protected bool $hasResponsive = true;

    protected bool $hasColumnVisibility = true;

    protected string $exportClass = TableExportHandler::class;

    /**
     * @var \Closure(static $table): \Illuminate\Http\JsonResponse
     */
    protected Closure $onAjaxCallback;

    /**
     * @var \Botble\Table\Columns\Column[]
     */
    protected array $columns = [];

    /**
     * @var \Closure(\Illuminate\Contracts\Database\Eloquent\Builder $query): void
     */
    protected Closure $queryUsingCallback;

    /**
     * @var \Closure(\Illuminate\Contracts\Database\Eloquent\Builder $query): void
     */
    protected Closure $modifyQueryUsingCallback;

    protected string $dom = "fBrt<'card-footer d-flex flex-column flex-sm-row justify-content-between align-items-center gap-2'<'d-flex justify-content-between align-items-center gap-3'l<'m-0 text-muted'i>><'d-flex justify-content-center'p>>";

    public function __construct(protected DataTables $table, UrlGenerator $urlGenerator)
    {
        parent::__construct();

        $this->ajaxUrl = $urlGenerator->current();

        if (! $this->getOption('id')) {
            $this->setOption('id', strtolower(Str::slug(Str::snake($this::class))));
        }

        if (! $this->getOption('class')) {
            $this->setOption('class', 'table card-table table-vcenter table-striped table-hover');
        }

        $this->setup();

        $this->setupExtended();

        $this->booted();
    }

    public function setup(): void
    {
    }

    public function booted(): void
    {
    }

    public function getOption(string $key): string|null
    {
        return Arr::get($this->options, $key);
    }

    public function setOption(string $key, $value): static
    {
        $this->options[$key] = $value;

        return $this;
    }

    public function getType(): string
    {
        return $this->type;
    }

    public function setType(string $type): static
    {
        $this->type = $type;

        return $this;
    }

    public function getView(): string
    {
        return $this->view;
    }

    public function setView(string $view): static
    {
        $this->view = $view;

        return $this;
    }

    public function setDom(string $dom): static
    {
        $this->dom = $dom;

        return $this;
    }

    public function getOptions(): array
    {
        return $this->options;
    }

    public function setOptions(array $options): static
    {
        $this->options = array_merge($this->options, $options);

        return $this;
    }

    public function html()
    {
        if ($this->isFiltering()) {
            $this->bStateSave = false;
        }

        $parameters = [
            'dom' => $this->getDom(),
            'buttons' => $this->getBuilderParameters(),
            'initComplete' => $this->htmlInitComplete(),
            'drawCallback' => $this->htmlDrawCallback(),
            'paging' => true,
            'searching' => true,
            'info' => true,
            'searchDelay' => 350,
            'bStateSave' => $this->bStateSave,
            'lengthMenu' => [
                array_values(
                    array_unique(array_merge(Arr::sortRecursive([10, 30, 50, 100, 500, $this->pageLength]), [-1]))
                ),
                array_values(
                    array_unique(
                        array_merge(
                            Arr::sortRecursive([10, 30, 50, 100, 500, $this->pageLength]),
                            [trans('core/base::tables.all')]
                        )
                    )
                ),
            ],
            'pageLength' => $this->pageLength,
            'processing' => true,
            'serverSide' => true,
            'bServerSide' => true,
            'bDeferRender' => true,
            'bProcessing' => true,
            'language' => [
                'aria' => [
                    'sortAscending' => 'orderby asc',
                    'sortDescending' => 'orderby desc',
                    'paginate' => [
                        'next' => trans('pagination.next'),
                        'previous' => trans('pagination.previous'),
                    ],
                ],
                'emptyTable' => trans('core/base::tables.no_data'),
                'info' => view('core/table::table-info')->render(),
                'infoEmpty' => trans('core/base::tables.no_record'),
                'lengthMenu' => Html::tag('span', '_MENU_', ['class' => 'dt-length-style'])->toHtml(),
                'search' => '',
                'searchPlaceholder' => trans('core/table::table.search'),
                'zeroRecords' => trans('core/base::tables.no_record'),
                'processing' => Html::image('vendor/core/core/base/images/loading-spinner-blue.gif'),
                'paginate' => [
                    'next' => trans('pagination.next'),
                    'previous' => trans('pagination.previous'),
                ],
                'infoFiltered' => trans('core/table::table.filtered'),
            ],
            'aaSorting' => $this->useDefaultSorting ? [
                [
                    ($this->hasBulkActions() ? $this->defaultSortColumn : 0),
                    'desc',
                ],
            ] : [],
            'responsive' => $this->hasResponsive,
            'autoWidth' => false,
        ];

        if (setting('datatables_pagination_type') == 'dropdown') {
            $parameters['sPaginationType'] = 'listbox';
        }

        return $this->builder()
            ->columns($this->getColumns())
            ->ajax(['url' => $this->getAjaxUrl(), 'method' => 'POST'])
            ->parameters($parameters);
    }

    /**
     * @param \Closure(static $table): \Illuminate\Http\JsonResponse $onAjaxCallback
     */
    public function onAjax(Closure $onAjaxCallback): static
    {
        $this->onAjaxCallback = $onAjaxCallback;

        return $this;
    }

    public function ajax(): JsonResponse
    {
        if (isset($this->onAjaxCallback)) {
            return call_user_func($this->onAjaxCallback, $this);
        }

        return $this->toJson($this->table->eloquent($this->query()));
    }

    public function getColumns(): array
    {
        $columns = array_merge($this->columns(), $this->columns);

        if (! $this->isSimpleTable()) {
            foreach ($columns as $key => &$column) {
                $className = implode(
                    ' ',
                    array_filter(
                        [Arr::get($column, 'className'), Arr::get($column, 'class'), ' column-key-' . $key]
                    )
                );

                $column['class'] = $className;
                $column['className'] = $className;
            }

            if ($this->hasBulkActions()) {
                $columns = array_merge($this->getCheckboxColumnHeading(), $columns);
            }
        }

        $columns = apply_filters(BASE_FILTER_TABLE_HEADINGS, $columns, $this->getModel());

        // TODO: Will be removed after operations removed.
        if ($this->hasOperations()) {
            $columns = array_merge($columns, $this->getOperationsHeading());
        }

        if (! empty($this->getRowActions()) && ! $this->isSimpleTable()) {
            $columns = array_merge($columns, $this->getRowActionsHeading());

            foreach ($columns as $index => $item) {
                if ($item instanceof Column && $item->name === 'operations') {
                    unset($columns[$index]);

                    break;
                }
            }
        }

        return $columns;
    }

    /**
     * @param BaseModel|class-string<BaseModel> $model
     */
    public function model(BaseModelContract|string $model): static
    {
        if (is_string($model)) {
            throw_unless(
                class_exists($model),
                new LogicException(sprintf('Class [%s] does not exists.', $model))
            );

            throw_unless(
                ($model = app($model)) instanceof BaseModelContract,
                new LogicException(
                    sprintf('Class [%s] must be an instance of %s.', $model::class, BaseModelContract::class)
                )
            );

            $this->model = $model;

            return $this;
        }

        $this->model = $model;

        return $this;
    }

    protected function getModel(): BaseModelContract|Model
    {
        return $this->model ?: ($this->repository ? $this->repository->getModel() : new BaseModel());
    }

    public function columns()
    {
        return [];
    }

    public function addColumn(Column $column): static
    {
        $this->columns[] = $column;

        return $this;
    }

    /**
     * @param \Botble\Table\Columns\Column[] $columns
     */
    public function addColumns(array $columns): static
    {
        foreach ($columns as $column) {
            $this->addColumn($column);
        }

        return $this;
    }

    public function removeColumn(string $name): static
    {
        foreach ($this->columns as $index => $column) {
            if ($column->get('data') === $name || $column->get('name') === $name) {
                unset($this->columns[$index]);

                break;
            }
        }

        $this->columns = array_values($this->columns);

        return $this;
    }

    public function removeColumns(array $columns): static
    {
        foreach ($columns as $column) {
            $this->removeColumn($column);
        }

        return $this;
    }

    public function getCheckboxColumnHeading(): array
    {
        return [
            CheckboxColumn::make(),
        ];
    }

    public function getAjaxUrl(): string
    {
        return $this->ajaxUrl;
    }

    public function setAjaxUrl(string $ajaxUrl): static
    {
        $this->ajaxUrl = $ajaxUrl;

        return $this;
    }

    protected function getDom(): string|null
    {
        if ($this->isSimpleTable()) {
            $this->dom = $this->simpleDom();
        }

        return $this->dom;
    }

    public function getBuilderParameters(): array
    {
        $params = [
            'stateSave' => true,
        ];

        if ($this->isSimpleTable()) {
            return $params;
        }

        $buttons = array_merge($this->getButtons(), $this->getActionsButton());

        $buttons = array_merge($buttons, array_unique($this->getDefaultButtons(), SORT_REGULAR));

        if (! $buttons) {
            return $params;
        }

        return $params + compact('buttons');
    }

    public function getButtons(): array
    {
        $buttons = [
            ...$this->getHeaderActions(),
            ...$this->buttons(),
        ];

        $buttons = apply_filters(BASE_FILTER_TABLE_BUTTONS, $buttons, $this->getModel()::class);

        if (! $buttons) {
            return [];
        }

        $data = [];

        foreach ($buttons as $key => $button) {
            if ($button instanceof HeaderAction) {
                if ($button->currentUserHasAnyPermissions()) {
                    $data[] = $button->toArray();
                }

                continue;
            }

            $buttonClass = 'action-item' . (isset($button['class']) ? ' ' . $button['class'] : null);

            if (Arr::get($button, 'extend') == 'collection') {
                $button['className'] = ($button['className'] ?? null) . $buttonClass;

                $data[] = $button;
            } else {
                $data[] = [
                    'className' => $buttonClass,
                    'text' => Html::tag('span', $button['text'], [
                        'data-action' => $key,
                        'data-href' => Arr::get($button, 'link'),
                    ])->toHtml(),
                ];
            }
        }

        return $data;
    }

    public function buttons()
    {
        return [];
    }

    public function getActionsButton(): array
    {
        if (! $this->getActions()) {
            return [];
        }

        return [
            [
                'extend' => 'collection',
                'text' => '<span>' . trans('core/base::forms.actions') . ' <span class="caret"></span></span>',
                'buttons' => $this->getActions(),
            ],
        ];
    }

    public function getActions(): array
    {
        if ($this->isSimpleTable() || ! $this->actions()) {
            return [];
        }

        $actions = [];

        foreach ($this->actions() as $key => $action) {
            $actions[] = [
                'className' => 'action-item',
                'text' => '<span data-action="' . $key . '" data-href="' . $action['link'] . '"> ' . $action['text'] . '</span>',
            ];
        }

        return $actions;
    }

    public function actions(): array
    {
        return [];
    }

    public function getDefaultButtons(): array
    {
        $buttons = ['reload'];

        if (setting('datatables_default_show_export_button')) {
            $buttons[] = 'export';
        }

        $this->hasColumnVisibility = (bool) setting('datatables_default_show_column_visibility');

        if ($this->hasColumnVisibility) {
            $buttons[] = 'visibility';
        }

        return $buttons;
    }

    public function htmlInitComplete(): string|null
    {
        return 'function () {' . $this->htmlInitCompleteFunction() . '}';
    }

    public function htmlInitCompleteFunction(): string|null
    {
        return '
            Botble.initResources();

            document.dispatchEvent(new CustomEvent("core-table-init-completed", {
                detail: {
                    table: this
                }
            }));
        ';
    }

    public function htmlDrawCallback(): string|null
    {
        if ($this->isSimpleTable()) {
            return null;
        }

        return 'function () {' . $this->htmlDrawCallbackFunction() . '}';
    }

    public function htmlDrawCallbackFunction(): string|null
    {
        return <<<JS
            var tableWrapper = $(this).closest(".dataTables_wrapper");
            var dtDataCount = this.api().data().count();

            if (dtDataCount === 0) {
                tableWrapper.find(".card-footer").prop('style', 'display: none !important;');
            } else {
                tableWrapper.find(".card-footer").prop('style', null);
            }

            tableWrapper.find(".dataTables_paginate").toggle(this.api().page.info().pages > 1);

            tableWrapper.find(".dataTables_length").toggle(dtDataCount >= 10);
            tableWrapper.find(".dataTables_info").toggle(dtDataCount > 0);

            setTimeout(function () {
                var searchInputWrapper = $(".table-wrapper .table-search-input input");
                if (! searchInputWrapper.val()) {
                    searchInputWrapper.val(tableWrapper.find(".dataTables_filter input").val());
                }

                if (searchInputWrapper.val()) {
                    searchInputWrapper.addClass('border-primary bg-info-subtle')
                } else {
                    searchInputWrapper.removeClass('border-primary bg-info-subtle')
                }
            }, 200);
        JS . $this->htmlInitCompleteFunction();
    }

    public function renderTable(array $data = [], array $mergeData = []): View|Factory|Response
    {
        return $this->render($this->view, $data, $mergeData);
    }

    public function render(string $view = null, array $data = [], array $mergeData = [])
    {
        Assets::addScripts(['datatables', 'moment', 'datepicker'])
            ->addStyles(['datatables', 'datepicker'])
            ->addStylesDirectly('vendor/core/core/table/css/table.css')
            ->addScriptsDirectly([
                'vendor/core/core/base/libraries/bootstrap3-typeahead.min.js',
                'vendor/core/core/table/js/table.js',
                'vendor/core/core/table/js/filter.js',
            ]);

        if (setting('datatables_pagination_type') == 'dropdown') {
            Assets::addScriptsDirectly(['vendor/core/core/base/libraries/datatables/extensions/Pagination/js/dataTables.pagination.min.js'])
                ->addStylesDirectly(['vendor/core/core/base/libraries/datatables/extensions/Pagination/css/dataTables.pagination.min.css']);
        }

        $data['id'] = Arr::get($data, 'id', $this->getOption('id'));
        $data['class'] = Arr::get($data, 'class', $this->getOption('class'));

        $this->setAjaxUrl($this->ajaxUrl . '?' . http_build_query(request()->input()));

        $this->setOptions($data);

        $data['actions'] = $this->getBulkActions();

        $data['table'] = $this;

        return parent::render($view, $data, $mergeData);
    }

    protected function applyScopes(
        EloquentBuilder|QueryBuilder|EloquentRelation|Collection|AnonymousResourceCollection $query
    ): EloquentBuilder|QueryBuilder|EloquentRelation|Collection|AnonymousResourceCollection {
        if (isset($this->modifyQueryUsingCallback)) {
            call_user_func($this->modifyQueryUsingCallback, $query);
        }

        $request = $this->request();

        $requestFilters = [];

        if ($this->isFiltering()) {
            foreach ($this->getFilterColumns() as $key => $item) {
                $operator = $request->input('filter_operators.' . $key);

                $value = $request->input('filter_values.' . $key);

                if (is_array($operator) || is_array($value) || is_array($item)) {
                    continue;
                }

                $requestFilters[] = [
                    'column' => $item,
                    'operator' => $operator,
                    'value' => $value,
                ];
            }
        }

        foreach ($requestFilters as $requestFilter) {
            if (! empty($requestFilter['column'])) {
                $query = $this->applyFilterCondition(
                    $query,
                    $requestFilter['column'],
                    $requestFilter['operator'],
                    $requestFilter['value']
                );
            }
        }

        return parent::applyScopes(apply_filters(BASE_FILTER_TABLE_QUERY, $query));
    }

    public function getValueInput(string|null $title, string|null $value, string|null $type, array $data = []): array
    {
        $inputName = 'value';

        if (empty($title)) {
            $inputName = 'filter_values[]';
        }

        $attributes = [
            'class' => 'form-control input-value filter-column-value',
            'placeholder' => trans('core/table::table.value'),
            'autocomplete' => 'off',
        ];

        switch ($type) {
            case 'select':
            case 'customSelect':
                $attributes['class'] = str_replace('form-control ', '', $attributes['class']);
                $attributes['placeholder'] = trans('core/table::table.select_option');
                $html = Form::customSelect($inputName, $data, $value, $attributes)->toHtml();

                break;

            case 'select-search':
                $attributes['class'] = str_replace('form-control ', '', $attributes['class']);
                $attributes['class'] = $attributes['class'] . ' select-search-full';
                $attributes['placeholder'] = trans('core/table::table.select_option');
                $html = Form::customSelect($inputName, $data, $value, $attributes)->toHtml();

                break;

            case 'select-ajax':
                $attributes['class'] = str_replace('form-control ', '', $attributes['class']);
                $attributes = [
                    'class' => $attributes['class'] . ' select-autocomplete',
                    'data-url' => Arr::get($data, 'url'),
                    'data-minimum-input' => Arr::get($data, 'minimum-input', 2),
                    'multiple' => Arr::get($data, 'multiple', false),
                    'data-placeholder' => Arr::get($data, 'placeholder', $attributes['placeholder']),
                ];

                $html = Form::customSelect($inputName, Arr::get($data, 'selected', []), $value, $attributes)->toHtml();

                break;

            case 'number':
                $html = Form::number($inputName, $value, $attributes)->toHtml();

                break;

            case 'date':
                $html = Form::date($inputName, $value, $attributes)->toHtml();

                break;

            case 'datePicker':
                $html = Form::datePicker($inputName, $value, $attributes)->toHtml();

                break;

            default:
                $html = Form::text($inputName, $value, $attributes)->toHtml();

                break;
        }

        return compact('html', 'data');
    }

    public function getFilters(): array
    {
        return apply_filters('base_filter_table_filters', $this->getAllBulkChanges(), $this);
    }

    protected function addCreateButton(string $url, string|null $permission = null, array $buttons = []): array
    {
        if (! $permission || $this->hasPermission($permission)) {
            $queryString = http_build_query(Request::query());

            if ($queryString) {
                $url .= '?' . $queryString;
            }

            $buttons['create'] = [
                'link' => $url,
                'text' => view('core/table::partials.create')->render(),
                'class' => 'btn-primary',
            ];
        }

        return $buttons;
    }

    protected function setupFormattedColumns(DataTableAbstract $table): void
    {
        foreach ($this->getColumnsFromBuilder() as $column) {
            switch (true) {
                case $column instanceof RowActionsColumn:
                    $table->addColumn($column->name, function ($item) use ($column) {
                        return $column
                            ->setRowActions($this->getRowActions())
                            ->renderCell($item, $this);
                    });

                    break;

                case $column instanceof Column && $column instanceof FormattedColumn:
                    $table->editColumn($column->name, function (BaseModelContract|array $item) use ($column) {
                        return $column->renderCell($item, $this);
                    });

                    break;
            }
        }
    }

    public function toJson($data, array $escapeColumn = [], bool $mDataSupport = true)
    {
        if ($data instanceof DataTableAbstract) {
            $this->setupFormattedColumns($data);
        }

        $this->dispatchBeforeRendering();

        $data = apply_filters(BASE_FILTER_GET_LIST_DATA, $data, $this->getModel());

        return tap(
            $data
                ->escapeColumns($escapeColumn)
                ->make($mDataSupport),
            fn ($response) => $this->dispatchAfterRendering($response)
        );
    }

    public function htmlBuilder(): CustomTableBuilder
    {
        return app(CustomTableBuilder::class);
    }

    protected function simpleDom(): string
    {
        return "rt<'card-footer d-flex flex-column flex-sm-row justify-content-between align-items-center gap-2'<'d-flex justify-content-between align-items-center gap-3'l<'m-0 text-muted'i>><'d-flex justify-content-center'p>>";
    }

    protected function isEmpty(): bool
    {
        return ! $this->request()->wantsJson() &&
            ! $this->request()->ajax() &&
            ! $this->isFiltering() &&
            ! (method_exists($this, 'query') && $this->query()->exists());
    }

    public function hasPermission(string $permission): bool
    {
        $user = Auth::guard()->user();

        if (! $user instanceof User) {
            return true;
        }

        return $user->hasPermission($permission);
    }

    public function hasAnyPermissions(array $permissions): bool
    {
        foreach ($permissions as $permission) {
            if ($this->hasPermission($permission)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param \Closure|callable(\Illuminate\Contracts\Database\Eloquent\Builder $query): void $queryUsingCallback
     */
    public function queryUsing(Closure|callable $queryUsingCallback): static
    {
        $this->queryUsingCallback = $queryUsingCallback;

        return $this;
    }

    /**
     * @param \Closure|callable(\Illuminate\Contracts\Database\Eloquent\Builder $query): void $modifyQueryCallback
     */
    public function modifyQueryUsing(Closure|callable $modifyQueryCallback): static
    {
        $this->modifyQueryUsingCallback = $modifyQueryCallback;

        return $this;
    }

    public function query()
    {
        $query = $this->getModel()->query();

        if (isset($this->queryUsingCallback)) {
            call_user_func($this->queryUsingCallback, $query);

            $query = $this->applyScopes($query);
        }

        return $query;
    }

    protected function isSimpleTable(): bool
    {
        return $this->view === $this->simpleTableView() || $this->type === self::TABLE_TYPE_SIMPLE;
    }

    protected function simpleTableView(): string
    {
        return 'core/table::simple-table';
    }

    public function isExportingToExcel(): bool
    {
        return $this->request()->input('action') === 'excel';
    }

    public function isExportingToCSV(): bool
    {
        return $this->request()->input('action') === 'csv';
    }

    public static function getFilterPrefix(): string
    {
        return sprintf('base_table_%s', Str::of(static::class)->snake()->lower()->replace('\\', '')->toString());
    }

    public static function getGlobalClassName(): string
    {
        return TableAbstract::class;
    }

    public static function hasGlobalExtend(): bool
    {
        return true;
    }

    public static function globalExtendFilterName(): string
    {
        return TableAbstract::getFilterPrefix() . '_extended';
    }

    public static function hasGlobalRendering(): bool
    {
        return true;
    }

    public static function globalBeforeRenderingFilterName(): string
    {
        return TableAbstract::getFilterPrefix() . '_before_rendering';
    }

    public static function globalAfterRenderingFilterName(): string
    {
        return TableAbstract::getFilterPrefix() . '_after_rendering';
    }
}
