<?php

namespace Knp\Component\Pager\Event\Subscriber\Sortable;

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 readonly ?PropertyAccessorInterface $propertyAccessor;

    public function __construct(?PropertyAccessorInterface $accessor = null)
    {
        if (!$accessor && class_exists(PropertyAccess::class)) {
            $accessor = PropertyAccess::createPropertyAccessorBuilder()->enableMagicCall()->getPropertyAccessor();
        }

        $this->propertyAccessor = $accessor;
    }

    public function items(ItemsEvent $event): void
    {
        $argumentAccess = $event->getArgumentAccess();

        // 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 || !$argumentAccess->has($sortField)) {
            return;
        }

        $event->setCustomPaginationParameter('sorted', true);

        if (isset($event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST]) && !in_array($argumentAccess->get($sortField), $event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST])) {
            throw new InvalidValueException("Cannot sort by: [{$argumentAccess->get($sortField)}] this field is not in allow list.");
        }

        $sortFunction = $event->options['sortFunction'] ?? [$this, 'proxySortFunction'];
        $sortField = $argumentAccess->get($sortField);

        // compatibility layer
        if ($sortField[0] === '.') {
            $sortField = substr($sortField, 1);
        }

        call_user_func_array($sortFunction, [
            &$event->target,
            $sortField,
            $this->getSortDirection($event),
        ]);
    }

    private function getSortDirection(ItemsEvent $event): string
    {
        $argumentAccess = $event->getArgumentAccess();
        $options = $event->options;

        if (!$argumentAccess->has($options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME])) {
            return 'desc';
        }
        $direction = $argumentAccess->get($options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME]);
        if (strtolower($direction) === 'asc') {
            return 'asc';
        }

        return 'desc';
    }

    private function proxySortFunction(mixed &$target, string $sortField, string $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],
        ];
    }
}
