<?php

declare(strict_types=1);

/*
 * Copyright (c) 2023-2024 François Kooman <fkooman@tuxed.net>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

namespace fkooman\Radius;

use fkooman\Radius\Exception\SocketException;

class PhpSocket implements SocketInterface
{
    private LoggerInterface $logger;

    /** @var resource|closed-resource|null */
    private $socketStream = null;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function open(string $serverUri, int $connectionTimeout): void
    {
        $this->logger->debug(sprintf('Server: %s', $serverUri));
        if (false === $socketStream = stream_socket_client($serverUri, $errNo, $errStr, $connectionTimeout)) {
            throw new SocketException(sprintf('unable to connect (%d) %s', $errNo, $errStr));
        }
        // "To set a timeout for reading/writing data over the socket, use the
        // stream_set_timeout(), as the timeout only applies while making
        // connecting the socket."
        // @see https://www.php.net/stream_socket_client
        stream_set_timeout($socketStream, $connectionTimeout);
        $this->socketStream = $socketStream;
    }

    public function send(RadiusPacket $radiusPacket): RadiusPacket
    {
        $writeBytes = $radiusPacket->toBytes();
        $this->logger->debug(sprintf('--> %s', Utils::hexEncode($writeBytes)));
        $this->writeBytes($writeBytes);
        $readBytes = $this->readBytes(20);
        $packetLength = Utils::bytesToShort(Utils::safeSubstr($readBytes, 2, 2));
        if (20 < $packetLength) {
            // packet length indicates there is more to fetch, i.e. attributes
            $readBytes .= $this->readBytes($packetLength - 20);
        }
        $this->logger->debug(sprintf('<-- %s', Utils::hexEncode($readBytes)));

        return RadiusPacket::fromBytes($readBytes);
    }

    public function close(): void
    {
        if (is_resource($this->socketStream)) {
            fclose($this->socketStream);
        }
    }

    private function writeBytes(string $bytesToWrite): void
    {
        if (!is_resource($this->socketStream)) {
            throw new SocketException('socket not open');
        }
        if (false === fwrite($this->socketStream, $bytesToWrite)) {
            throw new SocketException('unable to write to socket');
        }
    }

    private function readBytes(int $noOfBytes): string
    {
        if (!is_resource($this->socketStream)) {
            throw new SocketException('socket not open');
        }
        $bytesRead = fread($this->socketStream, $noOfBytes);
        if (false === $bytesRead || $noOfBytes !== strlen($bytesRead)) {
            throw new SocketException('unable to read from socket');
        }

        return $bytesRead;
    }
}
