<?php
declare(strict_types=1);

namespace App\Filter\Utils;

use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\QueryBuilder;
use Dunglas\ApiBundle\Api\ResourceInterface;
use JetBrains\PhpStorm\ArrayShape;

/**
 * Add a distance filter to en entity
 *
 * To use it, add the following query :
 *
 *     withinDistance={latitude},{longitude},{distance}
 *
 * Where latitude and longitude are the coordinates of the origine point, and distance the maximum
 * distance in Km.
 *
 * /!\ The subject entity shall have a longitude and a latitude properties
 */
final class DistanceFilter extends AbstractFilter
{
    /**
     * API docs
     * @param string $resourceClass
     * @return array<string, mixed[]>
     */
    #[ArrayShape(['search' => "array"])]
    public function getDescription(string $resourceClass): array
    {
        if (!property_exists($resourceClass, 'latitude') || !property_exists($resourceClass, 'longitude')) {
            throw new \RuntimeException('DistanceFilter can only used with resources having both latitude and longitude properties');
        }
        return [
            'search' => [
                'property' => 'withinDistance',
                'type' => 'string',
                'required' => false,
                'swagger' => [
                    'description' => "Filtre une entity selon sa distance (km) à un point (latitude, longitude). " .
                                     "L'entité doit-elle aussi posséder des propriétés 'latitude' et 'longitude'." .
                                     "Pass the following query to use it : `withinDistance=({latitude}, {longitude}, {distance})`, " .
                                     "where {latitude} and {longitude} are the coordinates of the origine point, and {distance} the maximum distance in Km.",
                    'name' => 'Distance Filter',
                    'type' => 'Utils Filter',
                ],
            ]
        ];
    }

    /**
     * @param string $property
     * @param mixed $value
     * @param QueryBuilder $queryBuilder
     * @param QueryNameGeneratorInterface $queryNameGenerator
     * @param string $resourceClass
     * @param Operation|null $operation
     * @param mixed[] $context
     * @return void
     */
    protected function filterProperty(string $property,
                                      mixed $value,
                                      QueryBuilder $queryBuilder,
                                      QueryNameGeneratorInterface $queryNameGenerator,
                                      string $resourceClass,
                                      Operation $operation = null,
                                      array $context = []): void
    {
        if ($property !== 'withinDistance') {
            return;
        }
        if (!preg_match('/^(-?\d+(\.\d+)?,){2}\d+(\.\d+)?$/', $value)) {
            throw new \RuntimeException('DistanceFilter : Invalid argument, please pass latitude, longitude and distance to the parameter as comma separated floating numbers.');
        }

        [$latitude, $longitude, $distance] = explode(',', $value);

        $alias = $queryBuilder->getRootAliases()[0];

        // Generate unique parameters names to avoid collisions with other filters
        $latitudeParameterName = $queryNameGenerator->generateParameterName('latitude');
        $longitudeParameterName = $queryNameGenerator->generateParameterName('longitude');
        $distanceParameterName = $queryNameGenerator->generateParameterName('distance');

        $queryBuilder
            ->andWhere(
                sprintf(
                    'SPHERICAL_DISTANCE(%1$s.latitude, %1$s.longitude, :%2$s, :%3$s) <= :%4$s',
                    $alias, $latitudeParameterName, $longitudeParameterName, $distanceParameterName
                )
            )
            ->setParameter($latitudeParameterName, $latitude)
            ->setParameter($longitudeParameterName, $longitude)
            ->setParameter($distanceParameterName, $distance);
    }
}
