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

class Utils
{
    public static function shortToBytes(int $s): string
    {
        return pack('n', Utils::requireRange($s, 0, 2 ** 16 - 1));
    }

    public static function longToBytes(int $l): string
    {
        return pack('N', Utils::requireRange($l, 0, 2 ** 32 - 1));
    }

    public static function bytesToShort(string $s): int
    {
        if (2 !== strlen($s)) {
            throw new RangeException('need two bytes');
        }
        $unpackData = unpack('nshort', $s);
        if (!is_array($unpackData) || !array_key_exists('short', $unpackData) || !is_int($unpackData['short'])) {
            throw new RangeException('unable to convert bytes to short');
        }

        return $unpackData['short'];
    }

    public static function bytesToLong(string $s): int
    {
        if (4 !== strlen($s)) {
            throw new RangeException('need 4 bytes');
        }
        $unpackData = unpack('Nlong', $s);
        if (!is_array($unpackData) || !array_key_exists('long', $unpackData) || !is_int($unpackData['long'])) {
            throw new RangeException('unable to convert bytes to long');
        }

        return $unpackData['long'];
    }

    public static function bytesToIpAddress(string $s): string
    {
        if (false === $ipAddress = inet_ntop($s)) {
            throw new RangeException('unable to convert bytes to IP address');
        }

        return $ipAddress;
    }

    public static function ipAddressToBytes(string $s): string
    {
        if (false === $ipAddressBytes = inet_pton($s)) {
            throw new RangeException('unable to convert IP address to bytes');
        }

        return $ipAddressBytes;
    }

    public static function verifyUserName(string $userName): string
    {
        if (false !== strpos($userName, "\x00")) {
            throw new RangeException('User-Name MUST NOT contain 0x00');
        }

        return Utils::requireLengthRange($userName, 1, 128);
    }

    public static function verifyUserPassword(string $userPassword): string
    {
        if (false !== strpos($userPassword, "\x00")) {
            throw new RangeException('User-Password MUST NOT contain 0x00');
        }

        return Utils::requireLengthRange($userPassword, 1, 128);
    }

    public static function calculateHmac(string $inputData, string $hmacKey): string
    {
        return hash_hmac('md5', $inputData, $hmacKey, true);
    }

    public static function calculateHash(string $inputData): string
    {
        return md5($inputData, true);
    }

    public static function verifyPacketType(int $packetType): int
    {
        $supportedTypes = [
            RadiusPacket::ACCESS_REQUEST,
            RadiusPacket::ACCESS_ACCEPT,
            RadiusPacket::ACCESS_REJECT,
            RadiusPacket::ACCESS_CHALLENGE,
        ];

        if (!in_array($packetType, $supportedTypes, true)) {
            throw new RangeException(sprintf('packet type %d not supported', $packetType));
        }

        return $packetType;
    }

    public static function requireRange(int $i, ?int $l, ?int $u): int
    {
        if (null !== $l) {
            if ($i < $l) {
                throw new RangeException(sprintf('int value MUST be >= %d', $l));
            }
        }
        if (null !== $u) {
            if ($i > $u) {
                throw new RangeException(sprintf('int value MUST be <= %d', $u));
            }
        }

        return $i;
    }

    public static function requireLength(string $s, int $exactLen): string
    {
        if ($exactLen !== strlen($s)) {
            throw new RangeException(sprintf('string length MUST be exactly %d octets', $exactLen));
        }

        return $s;
    }

    public static function requireLengthRange(string $s, ?int $lowerLen, ?int $upperLen): string
    {
        $strLen = strlen($s);
        if (null !== $lowerLen) {
            $lowerLen = Utils::requireRange($lowerLen, 0, null);
            if ($strLen < $lowerLen) {
                throw new RangeException(sprintf('string length MUST be >= %d octets', $lowerLen));
            }
        }
        if (null !== $upperLen) {
            $upperLen = Utils::requireRange($upperLen, $lowerLen ?? 0, null);
            if ($strLen > $upperLen) {
                throw new RangeException(sprintf('string length MUST be <= %d octets', $upperLen));
            }
        }

        return $s;
    }

    /**
     * @return array{0:int,1:int}
     */
    public static function requireVendorAttributeId(string $vendorIdType): array
    {
        // it is a bit of a "matches more or less" situation,
        $e = explode('.', $vendorIdType);
        if (2 !== count($e)) {
            throw new RangeException('not of format ID.Type');
        }

        if (!preg_match('/^[0-9]+$/', $e[0])) {
            throw new RangeException('Vendor ID MUST be numeric');
        }
        if (!preg_match('/^[0-9]+$/', $e[1])) {
            throw new RangeException('Vendor Type MUST be numeric');
        }

        return [
            Utils::requireRange((int) $e[0], 1, 2 ** 24 - 1),
            Utils::requireRange((int) $e[1], 1, 255),
        ];
    }

    public static function hexEncode(string $s): string
    {
        return bin2hex($s);
    }

    /**
     * Make sure the input string is long enough to accomodate the `substr`
     * action.
     */
    public static function safeSubstr(string $s, int $o, ?int $l = null): string
    {
        if ($o < 0) {
            throw new RangeException('negative offset');
        }
        if (null !== $l && $l < 0) {
            throw new RangeException('negative length');
        }
        if (0 === strlen($s)) {
            throw new RangeException('empty string');
        }
        if ($o >= strlen($s)) {
            throw new RangeException(sprintf('offset "%d" does not exist in string of length "%d"', $o, strlen($s)));
        }
        if (null !== $l) {
            if ($o + $l > strlen($s)) {
                throw new RangeException(sprintf('cannot get string of length at provided offset [length=%d,offset=%d,requested_length=%d]', strlen($s), $o, $l));
            }
        }

        if (null === $l) {
            return substr($s, $o);
        }

        return substr($s, $o, $l);
    }
}
