<?php

//namespace Webpatser\Uuid;
/*
 *
 * from https://github.com/webpatser/laravel-uuid/blob/master/src/Webpatser/Uuid/Uuid.php
 */
//use Exception;

class UUID2
{
    const MD5 = 3;
    const SHA1 = 5;
    /**
     * 00001111  Clears all bits of version byte with AND.
     *
     * @var int
     */
    const CLEAR_VER = 15;

    /**
     * 00111111  Clears all relevant bits of variant byte with AND.
     *
     * @var int
     */
    const CLEAR_VAR = 63;

    /**
     * 11100000  Variant reserved for future use.
     *
     * @var int
     */
    const VAR_RES = 224;

    /**
     * 11000000  Microsoft UUID variant.
     *
     * @var int
     */
    const VAR_MS = 192;

    /**
     * 10000000  The RFC 4122 variant (this variant).
     *
     * @var int
     */
    const VAR_RFC = 128;

    /**
     * 00000000  The NCS compatibility variant.
     *
     * @var int
     */
    const VAR_NCS = 0;

    /**
     * 00010000.
     *
     * @var int
     */
    const VERSION_1 = 16;

    /**
     * 00110000.
     *
     * @var int
     */
    const VERSION_3 = 48;

    /**
     * 01000000.
     *
     * @var int
     */
    const VERSION_4 = 64;

    /**
     * 01010000.
     *
     * @var int
     */
    const VERSION_5 = 80;

    /**
     * Time (in 100ns steps) between the start of the UTC and Unix epochs.
     *
     * @var int
     */
    const INTERVAL = 0x01b21dd213814000;

    /**
     * @var string
     */
    const NS_DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';

    /**
     * @var string
     */
    const NS_URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8';

    /**
     * @var string
     */
    const NS_OID = '6ba7b812-9dad-11d1-80b4-00c04fd430c8';

    /**
     * @var string
     */
    const NS_X500 = '6ba7b814-9dad-11d1-80b4-00c04fd430c8';

    /**
     * @var string
     */
    protected static $randomFunc = 'randomMcrypt';

    protected $bytes;
    protected $hex;
    protected $string;
    protected $urn;
    protected $version;
    protected $variant;
    protected $node;
    protected $time;

    /**
     * @param string $uuid
     *
     * @throws Exception
     */
    protected function __construct($uuid)
    {
        if (!empty($uuid) && strlen($uuid) !== 16) {
            throw new Exception('Input must be a 128-bit integer.');
        }

        $this->bytes = $uuid;

        // Optimize the most common use
        $this->string = bin2hex(substr($uuid, 0, 4)).'-'.
            bin2hex(substr($uuid, 4, 2)).'-'.
            bin2hex(substr($uuid, 6, 2)).'-'.
            bin2hex(substr($uuid, 8, 2)).'-'.
            bin2hex(substr($uuid, 10, 6));
    }

    /**
     * @param int    $ver
     * @param string $node
     * @param string $ns
     *
     * @return Uuid
     *
     * @throws Exception
     */
    public static function generate($ver = 1, $node = null, $ns = null)
    {
        /* Create a new UUID based on provided data. */
        switch ((int) $ver) {
            case 1:
                return new static(static::mintTime($node));
            case 2:
                // Version 2 is not supported
                throw new Exception('Version 2 is unsupported.');
            case 3:
                return new static(static::mintName(static::MD5, $node, $ns));
            case 4:
                return new static(static::mintRand());
            case 5:
                return new static(static::mintName(static::SHA1, $node, $ns));
            default:
                throw new Exception('Selected version is invalid or unsupported.');
        }
    }

    /**
     * Generates a Version 1 UUID.
     * These are derived from the time at which they were generated.
     *
     * @param string $node
     *
     * @return string
     */
    protected static function mintTime($node = null)
    {

        /** Get time since Gregorian calendar reform in 100ns intervals
         * This is exceedingly difficult because of PHP's (and pack()'s)
         * integer size limits.
         * Note that this will never be more accurate than to the microsecond.
         */
        $time = microtime(1) * 10000000 + static::INTERVAL;

        // Convert to a string representation
        $time = sprintf('%F', $time);

        //strip decimal point
        preg_match("/^\d+/", $time, $time);

        // And now to a 64-bit binary representation
        $time = base_convert($time[0], 10, 16);
        $time = pack('H*', str_pad($time, 16, '0', STR_PAD_LEFT));

        // Reorder bytes to their proper locations in the UUID
        $uuid = $time[4].$time[5].$time[6].$time[7].$time[2].$time[3].$time[0].$time[1];

        // Generate a random clock sequence
        $uuid .= static::randomBytes(2);

        // set variant
        $uuid[8] = chr(ord($uuid[8]) & static::CLEAR_VAR | static::VAR_RFC);

        // set version
        $uuid[6] = chr(ord($uuid[6]) & static::CLEAR_VER | static::VERSION_1);

        // Set the final 'node' parameter, a MAC address
        if (!is_null($node)) {
            $node = static::makeBin($node, 6);
        }

        // If no node was provided or if the node was invalid,
        //  generate a random MAC address and set the multicast bit
        if (is_null($node)) {
            $node = static::randomBytes(6);
            $node[0] = pack('C', ord($node[0]) | 1);
        }

        $uuid .= $node;

        return $uuid;
    }

    /**
     * Randomness is returned as a string of bytes.
     *
     * @param $bytes
     *
     * @return string
     */
    public static function randomBytes($bytes)
    {
        return call_user_func(array('static', static::initRandom()), $bytes);
    }

    /**
     * Trying for php 7 secure random generator, falling back to openSSL and Mcrypt.
     * If none of the above is found, falls back to mt_rand
     * Since laravel 4.* and 5.0 requires Mcrypt and 5.1 requires OpenSSL the fallback should never be used.
     *
     * @throws Exception
     *
     * @return string
     */
    public static function initRandom()
    {
        if (function_exists('random_bytes')) {
            return 'randomPhp7';
        } elseif (function_exists('openssl_random_pseudo_bytes')) {
            return 'randomOpenSSL';
        } elseif (function_exists('mcrypt_encrypt')) {
            return 'randomMcrypt';
        }

        // This is not the best randomizer (using mt_rand)...
        return 'randomTwister';
    }

    /**
     * Insure that an input string is either binary or hexadecimal.
     * Returns binary representation, or false on failure.
     *
     * @param string $str
     * @param int    $len
     *
     * @return string|null
     */
    protected static function makeBin($str, $len)
    {
        if ($str instanceof self) {
            return $str->bytes;
        }
        if (strlen($str) === $len) {
            return $str;
        } else {
            // strip URN scheme and namespace
            $str = preg_replace('/^urn:uuid:/is', '', $str);
        }
        // strip non-hex characters
        $str = preg_replace('/[^a-f0-9]/is', '', $str);
        if (strlen($str) !== ($len * 2)) {
            return null;
        } else {
            return pack('H*', $str);
        }
    }

    /**
     * Generates a Version 3 or Version 5 UUID.
     * These are derived from a hash of a name and its namespace, in binary form.
     *
     * @param string $ver
     * @param string $node
     * @param string $ns
     *
     * @return string
     *
     * @throws Exception
     */
    protected static function mintName($ver, $node, $ns)
    {
        if (empty($node)) {
            throw new Exception('A name-string is required for Version 3 or 5 UUIDs.');
        }

        // if the namespace UUID isn't binary, make it so
        $ns = static::makeBin($ns, 16);
        if (is_null($ns)) {
            throw new Exception('A binary namespace is required for Version 3 or 5 UUIDs.');
        }

        $version = null;
        $uuid = null;

        switch ($ver) {
            case static::MD5:
                $version = static::VERSION_3;
                $uuid = md5($ns.$node, 1);
                break;
            case static::SHA1:
                $version = static::VERSION_5;
                $uuid = substr(sha1($ns.$node, 1), 0, 16);
                break;
            default:
                // no default really required here
        }

        // set variant
        $uuid[8] = chr(ord($uuid[8]) & static::CLEAR_VAR | static::VAR_RFC);

        // set version
        $uuid[6] = chr(ord($uuid[6]) & static::CLEAR_VER | $version);

        return $uuid;
    }

    /**
     * Generate a Version 4 UUID.
     * These are derived solely from random numbers.
     * generate random fields.
     *
     * @return string
     */
    protected static function mintRand()
    {
        $uuid = static::randomBytes(16);
        // set variant
        $uuid[8] = chr(ord($uuid[8]) & static::CLEAR_VAR | static::VAR_RFC);
        // set version
        $uuid[6] = chr(ord($uuid[6]) & static::CLEAR_VER | static::VERSION_4);

        return $uuid;
    }

    /**
     * Import an existing UUID.
     *
     * @param string $uuid
     *
     * @return Uuid
     */
    public static function import($uuid)
    {
        return new static(static::makeBin($uuid, 16));
    }

    /**
     * Compares the binary representations of two UUIDs.
     * The comparison will return true if they are bit-exact,
     * or if neither is valid.
     *
     * @param string $a
     * @param string $b
     *
     * @return string|string
     */
    public static function compare($a, $b)
    {
        if (static::makeBin($a, 16) == static::makeBin($b, 16)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Get the specified number of random bytes, using random_bytes().
     * Randomness is returned as a string of bytes.
     *
     * Requires Php 7, or random_compact polyfill
     *
     * @param $bytes
     *
     * @return mixed
     */
    protected static function randomPhp7($bytes)
    {
        return random_bytes($bytes);
    }

    /**
     * Get the specified number of random bytes, using openssl_random_pseudo_bytes().
     * Randomness is returned as a string of bytes.
     *
     * @param $bytes
     *
     * @return mixed
     */
    protected static function randomOpenSSL($bytes)
    {
        return openssl_random_pseudo_bytes($bytes);
    }

    /**
     * Get the specified number of random bytes, using mcrypt_create_iv().
     * Randomness is returned as a string of bytes.
     *
     * @param $bytes
     *
     * @return string
     */
    protected static function randomMcrypt($bytes)
    {
        return mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM);
    }

    /**
     * Get the specified number of random bytes, using mt_rand().
     * Randomness is returned as a string of bytes.
     *
     * @param int $bytes
     *
     * @return string
     */
    protected static function randomTwister($bytes)
    {
        $rand = '';
        for ($a = 0; $a < $bytes; ++$a) {
            $rand .= chr(mt_rand(0, 255));
        }

        return $rand;
    }

    /**
     * @param string $var
     *
     * @return string|string|number|number|number|number|number|null|number|null|null
     */
    public function __get($var)
    {
        switch ($var) {
            case 'bytes':
                return $this->bytes;
            // no break
            case 'hex':
                return bin2hex($this->bytes);
            // no break
            case 'string':
                return $this->__toString();
            // no break
            case 'urn':
                return 'urn:uuid:'.$this->__toString();
            // no break
            case 'version':
                return ord($this->bytes[6]) >> 4;
            // no break
            case 'variant':
                $byte = ord($this->bytes[8]);
                if ($byte >= static::VAR_RES) {
                    return 3;
                } elseif ($byte >= static::VAR_MS) {
                    return 2;
                } elseif ($byte >= static::VAR_RFC) {
                    return 1;
                } else {
                    return 0;
                }
            // no break
            case 'node':
                if (ord($this->bytes[6]) >> 4 == 1) {
                    return bin2hex(substr($this->bytes, 10));
                } else {
                    return null;
                }
            // no break
            case 'time':
                if (ord($this->bytes[6]) >> 4 == 1) {
                    // Restore contiguous big-endian byte order
                    $time = bin2hex($this->bytes[6].$this->bytes[7].$this->bytes[4].$this->bytes[5].
                        $this->bytes[0].$this->bytes[1].$this->bytes[2].$this->bytes[3]);
                    // Clear version flag
                    $time[0] = '0';

                    // Do some reverse arithmetic to get a Unix timestamp
                    return (hexdec($time) - static::INTERVAL) / 10000000;
                } else {
                    return null;
                }
            // no break
            default:
                return null;
            // no break
        }
    }

    /**
     * Return the UUID.
     *
     * @return string
     */
    public function __toString()
    {
        return $this->string;
    }
}
