<?php
/**
 * Jingga
 *
 * PHP Version 8.2
 *
 * @package   phpOMS\Algorithm\PathFinding
 * @copyright Dennis Eichhorn
 * @license   OMS License 2.2
 * @version   1.0.0
 * @link      https://jingga.app
 *
 * Extended based on:
 * MIT License
 * (c) 2011-2012 Xueqiao Xu <xueqiaoxu@gmail.com>
 * (c) PathFinding.js
 */
declare(strict_types=1);

namespace phpOMS\Algorithm\PathFinding;

/**
 * Path in grids.
 *
 * @package phpOMS\Algorithm\PathFinding
 * @license OMS License 2.2
 * @link    https://jingga.app
 * @since   1.0.0
 */
class Path
{
    /**
     * Nodes in the path
     *
     * @var Node[]
     * @since 1.0.0
     */
    public array $nodes = [];

    /**
     * Grid this path belongs to
     *
     * @var Grid
     * @since 1.0.0
     */
    private Grid $grid;

    /**
     * Nodes in the path
     *
     * @var Node[]
     * @since 1.0.0
     */
    private array $expandedNodes = [];

    /**
     * Constructor.
     *
     * @param Grid $grid Grid this path belongs to
     *
     * @since 1.0.0
     */
    public function __construct(Grid $grid)
    {
        $this->grid = $grid;
    }

    /**
     * Add node to the path
     *
     * @param Node $node Node
     *
     * @return void
     *
     * @since 1.0.0
     */
    public function addNode(Node $node) : void
    {
        $this->nodes[] = $node;
    }

    /**
     * Get the path length (euclidean)
     *
     * @return float
     *
     * @since 1.0.0
     */
    public function getLength() : float
    {
        $n = \count($this->nodes);

        $dist = 0.0;

        for ($i = 1; $i < $n; ++$i) {
            $dx = $this->nodes[$i - 1]->getX() - $this->nodes[$i]->getX();
            $dy = $this->nodes[$i - 1]->getY() - $this->nodes[$i]->getY();

            $dist += \sqrt($dx * $dx + $dy * $dy);
        }

        return $dist;
    }

    /**
     * Get the incomplete node path
     *
     * @return Node[]
     *
     * @since 1.0.0
     */
    public function getPath() : array
    {
        return $this->nodes;
    }

    /**
     * Get the complete node path
     *
     * The path may only contain the jump points or pivot points.
     * In order to get every node it needs to be expanded.
     *
     * @return Node[]
     *
     * @since 1.0.0
     */
    public function expandPath() : array
    {
        if (empty($this->expandedNodes)) {
            //$reverse = \array_reverse($this->nodes);
            $reverse = $this->nodes;
            $length  = \count($reverse);

            if ($length < 2) {
                return $reverse;
            }

            $expanded = [];
            for ($i = 0; $i < $length - 1; ++$i) {
                $coord0 = $reverse[$i];
                $coord1 = $reverse[$i + 1];

                $interpolated = $this->interpolate($coord0, $coord1);
                $expanded     = \array_merge($expanded, $interpolated);
            }

            $expanded[]          = $reverse[$length - 1];
            $this->expandedNodes = $expanded;
        }

        return $this->expandedNodes;
    }

    /**
     * Find nodes in between two nodes.
     *
     * The path may only contain the jump points or pivot points.
     * In order to get every node it needs to be expanded.
     *
     * @param Node $node1 Node
     * @param Node $node2 Node
     *
     * @return Node[]
     *
     * @since 1.0.0
     */
    private function interpolate(Node $node1, Node $node2) : array
    {
        $dx = \abs($node2->getX() - $node1->getX());
        $dy = \abs($node2->getY() - $node1->getY());

        $sx = ($node1->getX() < $node2->getX()) ? 1 : -1;
        $sy = ($node1->getY() < $node2->getY()) ? 1 : -1;

        $node = $node1;
        $err  = $dx - $dy;

        $x0 = $node->getX();
        $y0 = $node->getY();

        $line = [];
        while (true) {
            if ($node->getX() === $node2->getX() && $node->getY() === $node2->getY()) {
                break;
            }

            $line[] = $node;

            $e2 = 2 * $err;

            if ($e2 > -$dy) {
                $err -= $dy;
                $x0 += $sx;
            }

            if ($e2 < $dx) {
                $err += $dx;
                $y0  += $sy;
            }

            $node = $this->grid->getNode($x0, $y0);

            if ($node === null) {
                break; // @codeCoverageIgnore
            }
        }

        return $line;
    }
}
