Utility classes

2019
/**
 * Used in :
 * - 2019/5
 * - 2019/7
 * - 2019/9
 * - 2019/11
 * - 2019/13
 * - 2019/15
 * - 2019/17
 * - 2019/19
 * - 2019/21
 * - 2019/23
 * - 2019/25
 */

/**
 * Incode computer class
 * @param Set $ops The program operations
 * @param array|int List of pre-defined input to use. "getInput" method can be overwritten to have dynamic inputs
 */
#[AllowDynamicProperties]
class Intcode
{
    public function __construct($program, $input = []) {
        $this->ops = $program instanceof Set ? (array) $program : (array) $program->numbers();
        $this->i = 0;
        $this->output = new Set([]);
        $this->input = new Set((array) $input);
        $this->relative_base = 0;
        $this->ended = false;
        $this->stop = false;
    }

    /**
     * Get the next operation to run, with its parameters
     *
     * @return array
     */
    public function getNextOP()
    {
        $op = str_split("00000" . $this->ops[$this->i]);
        $modes = array_reverse(array_map("intval", array_slice($op, 0, -2)));
        $op = (int) implode("", array_slice($op, -2));

        // Number of parameters for each commands
        $pcount = match ($op) {
            1, 2 => 3,
            3, 4 => 1,
            5, 6 => 2,
            7, 8 => 3,
            9    => 1,
            99   => 0
        };

        // Get the parameters based on the paramters modes
        $params = [];
        for ($k = 0; $k < $pcount; $k++) {
            $value = $this->ops[$this->i + $k + 1] ?? 0;
            switch ($modes[$k] ?? 0) {
                case 0: // Position mode
                    $params[] = $this->ops[$value] ?? 0;
                    break;
                case 1: // Immediate mode
                    $params[] = $value;
                    break;
                case 2: // Relative mode
                    $params[] = $this->ops[$this->relative_base + $value] ?? 0;
                    break;
            }
        }

        // Move the op pointer
        $this->i += 1 + $pcount;
        return [$op, $params, set(array_slice($modes, 0, $pcount))];
    }

    /**
     * Executre the next operation
     *
     * @return bool Can continue (not waiting for input or stopped)
     */
    public function next()
    {
        $this->stop = false;

        // Get OP code and parameters
        [$op, $p, $modes] = $this->getNextOP();

        // Last parameter is write position, offset if relative mode
        $write_pos  = $this->ops[$this->i - 1];
        $write_mode = $modes->last();
        if ($write_mode == 2) {
            $write_pos += $this->relative_base;
        }

        // Hangs waiting for an input
        if ($op == 3 && ($input = $this->getInput()) === null) {
            $this->i -= 2;
            return false;
        }

        // Execute
        match ($op) {
            1 => $this->ops[$write_pos] = $p[0] + $p[1],
            2 => $this->ops[$write_pos] = $p[0] * $p[1],
            3 => $this->ops[$write_pos] = $input,
            4 => $this->sendOutput($p[0]),
            5 => $this->i = $p[0] ? $p[1] : $this->i,
            6 => $this->i = $p[0] ? $this->i : $p[1],
            7 => $this->ops[$write_pos] = $p[0] < $p[1] ? 1 : 0,
            8 => $this->ops[$write_pos] = $p[0] == $p[1] ? 1 : 0,
            9 => $this->relative_base += $p[0],
            default => null
        };

        if ($op == 99) {
            $this->ended = true;
            return false;
        }

        return !$this->stop;
    }

    /**
     * Run the program, updating the pointer, the output and the state
     *
     * @return mixed
     */
    public function run()
    {
        while ($this->next()) {}
        return $this->onStop();
    }

    /**
     * Output a value
     *
     * @param int $value
     * @return void
     */
    public function sendOutput($value)
    {
        $this->output[] = $value;
        return $value;
    }

    /**
     * Get an input
     *
     * @return int
     */
    public function getInput()
    {
        return $this->input->shift();
    }

    /**
     * Return value when the Intcode computer halts
     *
     * @return mixed
     */
    public function onStop()
    {
        return $this->output->last();
    }
}