<?php

    class SocketTransport
    {
        private Socket|null $socket = null;
        private string $host;
        private int $port;
        private int $timeout;
        private bool $blocking;
        public bool $debug = false;

        public function __construct(string $host, int $port, int $timeout = 10, bool $blocking = true)
        {
            $this->host     = $host;
            $this->port     = $port;
            $this->timeout  = $timeout;
            $this->blocking = $blocking;
        }

        // Open TCP socket
        public function open(): void
        {
            if ($this->isOpen()) {
                return;
            }

            $this->socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
            if ($this->socket === false) {
                throw new Exception("Unable to create socket: " . socket_strerror(socket_last_error()));
            }

            // Initial send/receive timeout
            socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, ["sec" => $this->timeout, "usec" => 0]);
            socket_set_option($this->socket, SOL_SOCKET, SO_SNDTIMEO, ["sec" => $this->timeout, "usec" => 0]);

            $result = @socket_connect($this->socket, $this->host, $this->port);
            if ($result === false) {
                $error = socket_strerror(socket_last_error($this->socket));
                $this->close();
                throw new Exception("Unable to connect to {$this->host}:{$this->port} - {$error}");
            }

            if ($this->blocking) {
                socket_set_block($this->socket);
            } else {
                socket_set_nonblock($this->socket);
            }

            if ($this->debug) {
                echo "Connected to {$this->host}:{$this->port}\n";
            }
        }

        public function isOpen(): bool
        {
            return $this->socket instanceof Socket;
        }

        public function close(): void
        {
            if ($this->socket instanceof Socket) {
                @socket_close($this->socket);
                $this->socket = null;
                if ($this->debug) {
                    echo "Socket closed\n";
                }
            }
        }

        // Write raw data
        public function write(string $data): void
        {
            if (!$this->isOpen()) {
                $this->open();
            }

            $length = strlen($data);
            $written = 0;

            while ($written < $length) {
                $result = @socket_write($this->socket, substr($data, $written), $length - $written);
                if ($result === false) {
                    throw new Exception("Unable to write to socket: " . socket_strerror(socket_last_error($this->socket)));
                }
                $written += $result;
            }

            if ($this->debug) {
                echo "Written to socket: " . bin2hex($data) . "\n";
            }
        }

        // Read up to $length bytes
        public function read(int $length = 1024): string
        {
            if (!$this->isOpen()) {
                $this->open();
            }

            $data = @socket_read($this->socket, $length, PHP_BINARY_READ);
            if ($data === false) {
                $err = socket_last_error($this->socket);
                if ($err === SOCKET_EAGAIN || $err === SOCKET_EWOULDBLOCK) {
                    // Resource temporarily unavailable
                    return '';
                }
                throw new Exception("Unable to read from socket: " . socket_strerror($err));
            }

            if ($this->debug && $data !== '') {
                echo "Read from socket: " . bin2hex($data) . "\n";
            }

            return $data;
        }

        // Read exactly $length bytes
        public function readAll(int $length): string
        {
            if (!$this->isOpen()) {
                $this->open();
            }

            $data = '';
            $read = 0;

            while ($read < $length) {
                $chunk = @socket_read($this->socket, $length - $read, PHP_BINARY_READ);

                if ($chunk === false) {
                    $err = socket_last_error($this->socket);
                    if ($err === SOCKET_EAGAIN || $err === SOCKET_EWOULDBLOCK) {
                        usleep(50000); // 50ms and retry
                        continue;
                    }
                    throw new Exception("Unable to read from socket: " . socket_strerror($err));
                }

                if ($chunk === '') {
                    // Connection closed
                    break;
                }

                $data .= $chunk;
                $read += strlen($chunk);
            }

            if ($this->debug) {
                echo "ReadAll from socket: " . bin2hex($data) . "\n";
            }

            return $data;
        }

        // Set receive timeout dynamically (microseconds)
        public function setRecvTimeout(int $microseconds): void
        {
            if (!$this->isOpen()) {
                $this->open();
            }

            $sec = floor($microseconds / 1000000);
            $usec = $microseconds % 1000000;

            socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, ['sec' => $sec, 'usec' => $usec]);

            if ($this->debug) {
                echo "Receive timeout set to {$sec} sec and {$usec} usec\n";
            }
        }
    }
