<?php

declare(strict_types=1);

/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

namespace TYPO3\CMS\Backend\Search\LiveSearch;

use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Backend\Search\Event\ModifyResultItemInLiveSearchEvent;
use TYPO3\CMS\Backend\Search\LiveSearch\SearchDemand\DemandPropertyName;
use TYPO3\CMS\Backend\Search\LiveSearch\SearchDemand\MutableSearchDemand;
use TYPO3\CMS\Backend\Search\LiveSearch\SearchDemand\SearchDemand;
use TYPO3\CMS\Core\Pagination\ArrayPaginator;

/**
 * Repository class to ease using the search API.
 *
 * @internal
 */
final readonly class SearchRepository
{
    public function __construct(
        private EventDispatcherInterface $eventDispatcher,
        private SearchProviderRegistry $searchProviderRegistry,
    ) {}

    /**
     * Returns a list of available search providers including a flag whether they are currently active.
     *
     * @param SearchDemand $searchDemand
     * @return array<class-string, array{instance: SearchProviderInterface, isActive: bool}>
     */
    public function getSearchProviderState(SearchDemand $searchDemand): array
    {
        $searchProviders = [];
        foreach ($this->searchProviderRegistry->getProviders() as $searchProviderClassName => $searchProvider) {
            $searchProviders[$searchProviderClassName] = [
                'instance' => $searchProvider,
                'isActive' => in_array($searchProviderClassName, $searchDemand->getSearchProviders(), true),
            ];
        }

        return $searchProviders;
    }

    /**
     * @return SearchProviderInterface[]
     */
    public function getViableSearchProviders(SearchDemand $searchDemand): array
    {
        return array_filter(
            $this->searchProviderRegistry->getProviders(),
            static fn(SearchProviderInterface $provider): bool => $searchDemand->getSearchProviders() === [] || in_array(get_class($provider), $searchDemand->getSearchProviders(), true)
        );
    }

    public function find(SearchDemand $searchDemand): ArrayPaginator
    {
        $searchResults = [];
        $totalCount = 0;
        $mutableSearchDemand = MutableSearchDemand::fromSearchDemand($searchDemand);
        $offset = $searchDemand->getOffset();
        $remainingItems = $searchDemand->getLimit();

        foreach ($this->getViableSearchProviders($searchDemand) as $provider) {
            // Initialize remaining-items and offset for current iteration
            $mutableSearchDemand
                ->setProperty(DemandPropertyName::limit, $remainingItems)
                ->setProperty(DemandPropertyName::offset, $offset);

            $count = $provider->count($mutableSearchDemand->freeze());
            // Total count is relevant outside of this loop:
            // Paginator calculates number of pages and fills up its result set with stub-entries.
            $totalCount += $count;

            if ($count < $offset) {
                // The number of potential results is smaller than the offset, do not query results
                $offset -= $count;
                continue;
            }

            if ($remainingItems < 1) {
                continue;
            }

            $providerResult = $provider->find($mutableSearchDemand->freeze());
            if ($providerResult !== []) {
                foreach ($providerResult as $key => $resultItem) {
                    $modifyRecordEvent = $this->eventDispatcher->dispatch(new ModifyResultItemInLiveSearchEvent($resultItem));
                    $providerResult[$key] = $modifyRecordEvent->getResultItem();
                }
                $remainingItems -= count($providerResult);
                // We have got a first usable result from the current SearchProvider here. The offset is thereby fulfilled.
                // All follow-up SearchProviders must run with offset=0 to start with their first item:
                // * to generate a correct item-count and
                // * to provide more items if there are still open remaining-items.
                $offset = 0;

                $searchResults[] = $providerResult;
            }
        }
        unset($mutableSearchDemand);

        $flattenedSearchResults = array_merge([], ...$searchResults);
        $resultCount = count($flattenedSearchResults);

        $currentPage = (int)floor(($searchDemand->getOffset() + $searchDemand->getLimit()) / $searchDemand->getLimit());
        if (ceil($totalCount / $searchDemand->getLimit()) < $currentPage) {
            // Requested page does not match with the overall amount of items, reset to first page
            $currentPage = 1;
        }

        if ($resultCount > 0) {
            // The paginator expects a full result set to be able to calculate its pagination. This will have negative
            // performance consequences, therefore we only consider the current result set and create stubs for the "gaps".
            $paginatorItems = array_merge(
                array_fill(0, $searchDemand->getOffset(), null),
                $flattenedSearchResults,
                array_fill(0, $totalCount - $searchDemand->getOffset() - $resultCount, null)
            );
        } else {
            $paginatorItems = [];
        }

        return new ArrayPaginator($paginatorItems, $currentPage, SearchDemand::DEFAULT_LIMIT);
    }
}
