<?php



/**

 * This file is part of the ramsey/uuid library

 *

 * For the full copyright and license information, please view the LICENSE

 * file that was distributed with this source code.

 *

 * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com>

 * @license http://opensource.org/licenses/MIT MIT

 */



declare(strict_types=1);



namespace Ramsey\Uuid\Generator;



use Ramsey\Uuid\Converter\NumberConverterInterface;

use Ramsey\Uuid\Exception\DceSecurityException;

use Ramsey\Uuid\Provider\DceSecurityProviderInterface;

use Ramsey\Uuid\Type\Hexadecimal;

use Ramsey\Uuid\Type\Integer as IntegerObject;

use Ramsey\Uuid\Uuid;



use function hex2bin;

use function in_array;

use function pack;

use function str_pad;

use function strlen;

use function substr_replace;



use const STR_PAD_LEFT;



/**

 * DceSecurityGenerator generates strings of binary data based on a local

 * domain, local identifier, node ID, clock sequence, and the current time

 */

class DceSecurityGenerator implements DceSecurityGeneratorInterface

{

    private const DOMAINS = [

        Uuid::DCE_DOMAIN_PERSON,

        Uuid::DCE_DOMAIN_GROUP,

        Uuid::DCE_DOMAIN_ORG,

    ];



    /**

     * Upper bounds for the clock sequence in DCE Security UUIDs.

     */

    private const CLOCK_SEQ_HIGH = 63;



    /**

     * Lower bounds for the clock sequence in DCE Security UUIDs.

     */

    private const CLOCK_SEQ_LOW = 0;



    /**

     * @var NumberConverterInterface

     */

    private $numberConverter;



    /**

     * @var TimeGeneratorInterface

     */

    private $timeGenerator;



    /**

     * @var DceSecurityProviderInterface

     */

    private $dceSecurityProvider;



    public function __construct(

        NumberConverterInterface $numberConverter,

        TimeGeneratorInterface $timeGenerator,

        DceSecurityProviderInterface $dceSecurityProvider

    ) {

        $this->numberConverter = $numberConverter;

        $this->timeGenerator = $timeGenerator;

        $this->dceSecurityProvider = $dceSecurityProvider;

    }



    public function generate(

        int $localDomain,

        ?IntegerObject $localIdentifier = null,

        ?Hexadecimal $node = null,

        ?int $clockSeq = null

    ): string {

        if (!in_array($localDomain, self::DOMAINS)) {

            throw new DceSecurityException(

                'Local domain must be a valid DCE Security domain'

            );

        }



        if ($localIdentifier && $localIdentifier->isNegative()) {

            throw new DceSecurityException(

                'Local identifier out of bounds; it must be a value between 0 and 4294967295'

            );

        }



        if ($clockSeq > self::CLOCK_SEQ_HIGH || $clockSeq < self::CLOCK_SEQ_LOW) {

            throw new DceSecurityException(

                'Clock sequence out of bounds; it must be a value between 0 and 63'

            );

        }



        switch ($localDomain) {

            case Uuid::DCE_DOMAIN_ORG:

                if ($localIdentifier === null) {

                    throw new DceSecurityException(

                        'A local identifier must be provided for the org domain'

                    );

                }



                break;

            case Uuid::DCE_DOMAIN_PERSON:

                if ($localIdentifier === null) {

                    $localIdentifier = $this->dceSecurityProvider->getUid();

                }



                break;

            case Uuid::DCE_DOMAIN_GROUP:

            default:

                if ($localIdentifier === null) {

                    $localIdentifier = $this->dceSecurityProvider->getGid();

                }



                break;

        }



        $identifierHex = $this->numberConverter->toHex($localIdentifier->toString());



        // The maximum value for the local identifier is 0xffffffff, or

        // 4294967295. This is 8 hexadecimal digits, so if the length of

        // hexadecimal digits is greater than 8, we know the value is greater

        // than 0xffffffff.

        if (strlen($identifierHex) > 8) {

            throw new DceSecurityException(

                'Local identifier out of bounds; it must be a value between 0 and 4294967295'

            );

        }



        $domainByte = pack('n', $localDomain)[1];

        $identifierBytes = (string) hex2bin(str_pad($identifierHex, 8, '0', STR_PAD_LEFT));



        if ($node instanceof Hexadecimal) {

            $node = $node->toString();

        }



        // Shift the clock sequence 8 bits to the left, so it matches 0x3f00.

        if ($clockSeq !== null) {

            $clockSeq = $clockSeq << 8;

        }



        $bytes = $this->timeGenerator->generate($node, $clockSeq);



        // Replace bytes in the time-based UUID with DCE Security values.

        $bytes = substr_replace($bytes, $identifierBytes, 0, 4);

        $bytes = substr_replace($bytes, $domainByte, 9, 1);



        return $bytes;

    }

}

