<?php 
 
namespace Knp\Component\Pager\Event\Subscriber\Sortable; 
 
use Knp\Component\Pager\ArgumentAccess\ArgumentAccessInterface; 
use Knp\Component\Pager\Event\ItemsEvent; 
use Knp\Component\Pager\Exception\InvalidValueException; 
use Knp\Component\Pager\PaginatorInterface; 
use Symfony\Component\EventDispatcher\EventSubscriberInterface; 
use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; 
use Symfony\Component\PropertyAccess\PropertyAccess; 
use Symfony\Component\PropertyAccess\PropertyAccessorInterface; 
 
class ArraySubscriber implements EventSubscriberInterface 
{ 
    /** 
     * @var string the field used to sort current object array list 
     */ 
    private string $currentSortingField; 
 
    /** 
     * @var string the sorting direction 
     */ 
    private string $sortDirection; 
 
    private ?PropertyAccessorInterface $propertyAccessor; 
 
    private ArgumentAccessInterface $argumentAccess; 
 
    public function __construct(ArgumentAccessInterface $argumentAccess, PropertyAccessorInterface $accessor = null) 
    { 
        if (!$accessor && class_exists(PropertyAccess::class)) { 
            $accessor = PropertyAccess::createPropertyAccessorBuilder()->enableMagicCall()->getPropertyAccessor(); 
        } 
 
        $this->propertyAccessor = $accessor; 
        $this->argumentAccess = $argumentAccess; 
    } 
 
    public function items(ItemsEvent $event): void 
    { 
        // Check if the result has already been sorted by another sort subscriber 
        $customPaginationParameters = $event->getCustomPaginationParameters(); 
        if (!empty($customPaginationParameters['sorted']) ) { 
            return; 
        } 
        $sortField = $event->options[PaginatorInterface::SORT_FIELD_PARAMETER_NAME]; 
        if (!is_array($event->target) || null === $sortField || !$this->argumentAccess->has($sortField)) { 
            return; 
        } 
 
        $event->setCustomPaginationParameter('sorted', true); 
 
        if (isset($event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST]) && !in_array($this->argumentAccess->get($sortField), $event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST])) { 
            throw new InvalidValueException("Cannot sort by: [{$this->argumentAccess->get($sortField)}] this field is not in allow list."); 
        } 
 
        $sortFunction = $event->options['sortFunction'] ?? [$this, 'proxySortFunction']; 
        $sortField = $this->argumentAccess->get($sortField); 
 
        // compatibility layer 
        if ($sortField[0] === '.') { 
            $sortField = substr($sortField, 1); 
        } 
 
        call_user_func_array($sortFunction, [ 
            &$event->target, 
            $sortField, 
            $this->getSortDirection($event->options), 
        ]); 
    } 
 
    private function getSortDirection(array $options): string 
    { 
        if (!$this->argumentAccess->has($options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME])) { 
            return 'desc'; 
        } 
        $direction = $this->argumentAccess->get($options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME]); 
        if (strtolower($direction) === 'asc') { 
            return 'asc'; 
        } 
 
        return 'desc'; 
    } 
 
    private function proxySortFunction(&$target, $sortField, $sortDirection): bool 
    { 
        $this->currentSortingField = $sortField; 
        $this->sortDirection = $sortDirection; 
 
        return usort($target, [$this, 'sortFunction']); 
    } 
 
    private function sortFunction(object|array $object1, object|array $object2): int 
    { 
        if (null === $this->propertyAccessor) { 
            throw new \UnexpectedValueException('You need symfony/property-access component to use this sorting function'); 
        } 
 
        if (!$this->propertyAccessor->isReadable($object1, $this->currentSortingField) || !$this->propertyAccessor->isReadable($object2, $this->currentSortingField)) { 
            return 0; 
        } 
 
        try { 
            $fieldValue1 = $this->propertyAccessor->getValue($object1, $this->currentSortingField); 
        } catch (UnexpectedTypeException) { 
            return -1 * $this->getSortCoefficient(); 
        } 
 
        try { 
            $fieldValue2 = $this->propertyAccessor->getValue($object2, $this->currentSortingField); 
        } catch (UnexpectedTypeException) { 
            return $this->getSortCoefficient(); 
        } 
 
        if (is_string($fieldValue1)) { 
            $fieldValue1 = mb_strtolower($fieldValue1); 
        } 
 
        if (is_string($fieldValue2)) { 
            $fieldValue2 = mb_strtolower($fieldValue2); 
        } 
 
        if ($fieldValue1 === $fieldValue2) { 
            return 0; 
        } 
 
        return ($fieldValue1 > $fieldValue2 ? 1 : -1) * $this->getSortCoefficient(); 
    } 
 
    private function getSortCoefficient(): int 
    { 
        return $this->sortDirection === 'asc' ? 1 : -1; 
    } 
 
    public static function getSubscribedEvents(): array 
    { 
        return [ 
            'knp_pager.items' => ['items', 1], 
        ]; 
    } 
}