<?php

namespace App\Doctrine\ORM\AST;

use Doctrine\ORM\Query\AST\ASTException;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\AST\Node;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Query\SqlWalker;

/**
 * SphericalDistanceFunction ::= "SPHERICAL_DISTANCE" "(" ArithmeticPrimary "," ArithmeticPrimary "," ArithmeticPrimary "," ArithmeticPrimary ")"
 *
 * Calcule la distance en km à vol d'oiseau entre les coordonnées géographiques données (latitude, longitude) de deux points.
 *
 * Implémentation de la formule de Haversine, dont la précision est de l'ordre de la dizaine de mètres dans les cas les plus courants.
 *
 * Pour utiliser la fonction :
 *
 *     SPHERICAL_DISTANCE(latitude1, longitude1, latitude2, longitude2)
 *
 * WARNING: passing latitude2 and longitude2 as parameter, even named, is not properly interpreted, pass them directly.
 *
 * @see https://fr.wikipedia.org/wiki/Coordonn%C3%A9es_sph%C3%A9riques
 * @see https://fr.wikipedia.org/wiki/Formule_de_haversine
 */
class SphericalDistance extends FunctionNode
{
    private Node | string $latitude1;
    private Node | string $longitude1;
    private Node | string $latitude2;
    private Node | string $longitude2;

    /**
     * Parse DQL Function
     *
     * @param Parser $parser
     * @throws QueryException
     */
    public function parse(Parser $parser): void
    {
        $parser->match(Lexer::T_IDENTIFIER);
        $parser->match(Lexer::T_OPEN_PARENTHESIS);
        $this->latitude1 = $parser->ArithmeticPrimary();
        $parser->match(Lexer::T_COMMA);
        $this->longitude1 = $parser->ArithmeticPrimary();
        $parser->match(Lexer::T_COMMA);
        $this->latitude2 = $parser->ArithmeticPrimary();
        $parser->match(Lexer::T_COMMA);
        $this->longitude2 = $parser->ArithmeticPrimary();
        $parser->match(Lexer::T_CLOSE_PARENTHESIS);
    }

    /**
     * Get SQL
     *
     * @param SqlWalker $sqlWalker
     * @return string
     * @throws ASTException
     */
    public function getSql(SqlWalker $sqlWalker): string
    {
        $R = 6371;  // Rayon terrestre, en km

        $lat2 = $this->latitude2->dispatch($sqlWalker);
        $lat1 = $this->latitude1->dispatch($sqlWalker);

        // Call two additional dispatch so doctrine complete the parameters stack (careful: the order is important)
        $this->latitude1->dispatch($sqlWalker);
        $this->latitude2->dispatch($sqlWalker);

        $lon1 = $this->longitude1->dispatch($sqlWalker);
        $lon2 = $this->longitude2->dispatch($sqlWalker);

        // Latitudes et longitudes en radians
        $rLat1 = "($lat1 * PI() / 180)";
        $rLon1 = "($lon1 * PI() / 180)";
        $rLat2 = "($lat2 * PI() / 180)";
        $rLon2 = "($lon2 * PI() / 180)";

        return "2 * $R * ASIN(SQRT(POW(SIN(($rLat2 - $rLat1) / 2), 2) + COS($rLat1) * COS($rLat2) * POW(SIN(($rLon2 - $rLon1) / 2), 2)))";
    }
}
