<?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;

/**
 * Grid of nodes.
 *
 * @package phpOMS\Algorithm\PathFinding
 * @license OMS License 2.2
 * @link    https://jingga.app
 * @since   1.0.0
 */
class Grid
{
    /**
     * Grid system containing all nodes
     *
     * @var array<int, array<int, Node>>
     * @since 1.0.0
     */
    private array $nodes = [[]];

    /**
     * Create a grid from an array
     *
     * [
     *      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,],
     *      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0,],
     *      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,],
     *      [0, 0, 9, 9, 9, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0,],
     *      [0, 0, 0, 0, 9, 9, 9, 9, 9, 0, 9, 0, 0, 0, 0,],
     *      [0, 0, 1, 0, 9, 0, 0, 0, 0, 0, 9, 0, 9, 9, 9,],
     *      [0, 0, 0, 0, 9, 0, 0, 9, 9, 9, 9, 0, 0, 0, 0,],
     *      [0, 0, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,],
     *      [0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 9, 9, 9, 9, 0,],
     *      [0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0,],
     *      [0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 9, 9, 0, 0, 0,],
     *      [0, 0, 0, 0, 0, 9, 9, 9, 0, 0, 9, 2, 0, 0, 0,],
     *      [0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 9, 9, 0, 0, 0,],
     *      [0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0,],
     *      [0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0,],
     * ]
     *
     * @param array<int, int[]> $gridArray Grid defined in an array (0 = empty, 1 = start, 2 = end, 9 = not walkable)
     * @param string            $node      Node type name
     *
     * @return Grid
     *
     * @since 1.0.0
     */
    public static function createGridFromArray(array $gridArray, string $node) : self
    {
        $grid = new self();
        foreach ($gridArray as $y => $yRow) {
            foreach ($yRow as $x => $xElement) {
                if ($xElement === 0 || $xElement === 1 || $xElement === 2) {
                    /** @var \phpOMS\Algorithm\PathFinding\Node $empty */
                    $empty = new $node($x, $y, 1.0, true);
                    $grid->setNode($x, $y, $empty);
                } elseif ($xElement === 9) {
                    /** @var \phpOMS\Algorithm\PathFinding\Node $wall */
                    $wall = new $node($x, $y, 1.0, false);
                    $grid->setNode($x, $y, $wall);
                }
            }
        }

        return $grid;
    }

    /**
     * Set node at position
     *
     * @param int  $x    X-Coordinate
     * @param int  $y    Y-Coordinate
     * @param Node $node Node to set
     *
     * @return void
     *
     * @since 1.0.0
     */
    public function setNode(int $x, int $y, Node $node) : void
    {
        $this->nodes[$y][$x] = $node;
    }

    /**
     * Get node at position
     *
     * @param int $x X-Coordinate
     * @param int $y Y-Coordinate
     *
     * @return null|Node
     *
     * @since 1.0.0
     */
    public function getNode(int $x, int $y) : ?Node
    {
        if (!isset($this->nodes[$y]) || !isset($this->nodes[$y][$x])) {
            return null;
        }

        return $this->nodes[$y][$x];
    }

    /**
     * Is node walkable"
     *
     * @param int $x X-Coordinate
     * @param int $y Y-Coordinate
     *
     * @return bool
     *
     * @since 1.0.0
     */
    public function isWalkable(int $x, int $y) : bool
    {
        return isset($this->nodes[$y]) && isset($this->nodes[$y][$x]) && $this->nodes[$y][$x]->isWalkable;
    }

    /**
     * Get neighbors of node
     *
     * @param Node $node     Node to get neighbors from
     * @param int  $movement Allowed movements
     *
     * @return Node[]
     *
     * @since 1.0.0
     */
    public function getNeighbors(Node $node, int $movement) : array
    {
        $x = $node->getX();
        $y = $node->getY();

        $neighbors = [];

        $s0 = false;
        $s1 = false;
        $s2 = false;
        $s3 = false;
        $d0 = false;
        $d1 = false;
        $d2 = false;
        $d3 = false;

        if ($this->isWalkable($x, $y - 1)) {
            $neighbors[] = $this->getNode($x, $y - 1);
            $s0          = true;
        }

        if ($this->isWalkable($x + 1, $y)) {
            $neighbors[] = $this->getNode($x + 1, $y);
            $s1          = true;
        }

        if ($this->isWalkable($x, $y + 1)) {
            $neighbors[] = $this->getNode($x, $y + 1);
            $s2          = true;
        }

        if ($this->isWalkable($x - 1, $y)) {
            $neighbors[] = $this->getNode($x - 1, $y);
            $s3          = true;
        }

        if ($movement === MovementType::STRAIGHT) {
            /** @var Node[] $neighbors */
            return $neighbors;
        }

        if ($movement === MovementType::DIAGONAL_NO_OBSTACLE) {
            $d0 = $s3 && $s0;
            $d1 = $s0 && $s1;
            $d2 = $s1 && $s2;
            $d3 = $s2 && $s3;
        } elseif ($movement === MovementType::DIAGONAL_ONE_OBSTACLE) {
            $d0 = $s3 || $s0;
            $d1 = $s0 || $s1;
            $d2 = $s1 || $s2;
            $d3 = $s2 || $s3;
        } elseif ($movement === MovementType::DIAGONAL) {
            $d0 = true;
            $d1 = true;
            $d2 = true;
            $d3 = true;
        }

        if ($d0 && $this->isWalkable($x - 1, $y - 1)) {
            $neighbors[] = $this->getNode($x - 1, $y - 1);
        }

        if ($d1 && $this->isWalkable($x + 1, $y - 1)) {
            $neighbors[] = $this->getNode($x + 1, $y - 1);
        }

        if ($d2 && $this->isWalkable($x + 1, $y + 1)) {
            $neighbors[] = $this->getNode($x + 1, $y + 1);
        }

        if ($d3 && $this->isWalkable($x - 1, $y + 1)) {
            $neighbors[] = $this->getNode($x - 1, $y + 1);
        }

        /** @var Node[] $neighbors */
        return $neighbors;
    }
}
