<?php
/**
 * @copyright Copyright (c) 2010, The volkszaehler.org project
 * @package util
 * @license http://www.opensource.org/licenses/gpl-license.php GNU Public License
 */
/*
 * This file is part of volkzaehler.org
 *
 * volkzaehler.org is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * volkzaehler.org is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with volkszaehler.org. If not, see <http://www.gnu.org/licenses/>.
 */

namespace Volkszaehler\Util;

/**
 * DrUUID RFC4122 library for PHP5
 *
 * @author J. King
 * @package util
 * @link http://jkingweb.ca/code/php/lib.uuid/
 * @license Licensed under MIT license
 *
 * 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.
 */
class UUID {
	const MD5  = 3;
	const SHA1 = 5;
	const clearVer = 15;	// 00001111  Clears all bits of version byte with AND
	const clearVar = 63;	// 00111111  Clears all relevant bits of variant byte with AND
	const varRes   = 224;	// 11100000  Variant reserved for future use
	const varMS    = 192;	// 11000000  Microsft GUID variant
	const varRFC   = 128;	// 10000000  The RFC 4122 variant (this variant)
	const varNCS   = 0;		// 00000000  The NCS compatibility variant
	const version1 = 16;	// 00010000
	const version3 = 48;	// 00110000
	const version4 = 64;	// 01000000
	const version5 = 80;	// 01010000
	const interval = 0x01b21dd213814000;	// Time (in 100ns steps) between the start of the UTC and Unix epochs
	const nsDNS  = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
	const nsURL  = '6ba7b811-9dad-11d1-80b4-00c04fd430c8';
	const nsOID  = '6ba7b812-9dad-11d1-80b4-00c04fd430c8';
	const nsX500 = '6ba7b814-9dad-11d1-80b4-00c04fd430c8';
	protected static $randomFunc = 'randomTwister';
	protected static $randomSource = NULL;

	//instance properties
	protected $bytes;
	protected $hex;
	protected $string;
	protected $urn;
	protected $version;
	protected $variant;
	protected $node;
	protected $time;

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

	/**
	 * Import an existing UUID
	 *
	 * @param unkown_type $uuid
	 */
	public static function import($uuid) {
		return new self(self::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 unknown_type $a
	 * @param unknown_type $b
	 */
	public static function compare($a, $b) {
		return (self::makeBin($a, 16) == self::makeBin($b, 16));
	}

	public function __toString() {
		return $this->string;
	}

	public function __get($var) {
		switch($var) {
			case "bytes":
				return $this->bytes;
			case "hex":
				return bin2hex($this->bytes);
			case "string":
				return $this->__toString();
			case "urn":
				return "urn:uuid:".$this->__toString();
			case "version":
				return ord($this->bytes[6]) >> 4;
			case "variant":
				$byte = ord($this->bytes[8]);
				if ($byte >= self::varRes)
				return 3;
				if ($byte >= self::varMS)
				return 2;
				if ($byte >= self::varRFC)
				return 1;
				else
				return 0;
			case "node":
				if (ord($this->bytes[6])>>4==1)
				return bin2hex(substr($this->bytes,10));
				else
				return NULL;
			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
					$time = (hexdec($time) - self::interval) / 10000000;
					return $time;
				}
				else
				return NULL;
			default:
				return NULL;
		}
	}

	protected function __construct($uuid) {
		if (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));
	}

	protected static function mintTime($node = NULL) {
		/* Generates a Version 1 UUID.
		 These are derived from the time at which they were generated. */
		// 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 + self::interval;
		// Convert to a string representation
		$time = sprintf("%F", $time);
		preg_match("/^\d+/", $time, $time); //strip decimal point
		// 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 .= self::randomBytes(2);
		// set variant
		$uuid[8] = chr(ord($uuid[8]) & self::clearVar | self::varRFC);
		// set version
		$uuid[6] = chr(ord($uuid[6]) & self::clearVer | self::version1);
		// Set the final 'node' parameter, a MAC address
		if ($node)
		$node = self::makeBin($node, 6);
		if (!$node) {
			// If no node was provided or if the node was invalid,
			//  generate a random MAC address and set the multicast bit
			$node = self::randomBytes(6);
			$node[0] = pack("C", ord($node[0]) | 1);
		}
		$uuid .= $node;
		return $uuid;
	}

	protected static function mintRand() {
		/* Generate a Version 4 UUID.
		 These are derived soly from random numbers. */
		// generate random fields
		$uuid = self::randomBytes(16);
		// set variant
		$uuid[8] = chr(ord($uuid[8]) & self::clearVar | self::varRFC);
		// set version
		$uuid[6] = chr(ord($uuid[6]) & self::clearVer | self::version4);
		return $uuid;
	}

	protected static function mintName($ver, $node, $ns) {
		/* Generates a Version 3 or Version 5 UUID.
		 These are derived from a hash of a name and its namespace, in binary form. */
		if (!$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 = self::makeBin($ns, 16);
		if (!$ns)
		throw new \Exception("A binary namespace is required for Version 3 or 5 UUIDs.");
		switch($ver) {
			case self::MD5:
				$version = self::version3;
				$uuid = md5($ns.$node,1);
				break;
			case self::SHA1:
				$version = self::version5;
				$uuid = substr(sha1($ns.$node,1),0, 16);
				break;
		}
		// set variant
		$uuid[8] = chr(ord($uuid[8]) & self::clearVar | self::varRFC);
		// set version
		$uuid[6] = chr(ord($uuid[6]) & self::clearVer | $version);
		return ($uuid);
	}

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

	public static function initRandom() {
		/* Look for a system-provided source of randomness, which is usually crytographically secure.
		 /dev/urandom is tried first simply out of bias for Linux systems. */
		if (is_readable('/dev/urandom')) {
			self::$randomSource = fopen('/dev/urandom', 'rb');
			self::$randomFunc = 'randomFRead';
		}
		else if (class_exists('COM', 0)) {
			try {
				self::$randomSource = new COM('CAPICOM.Utilities.1');  // See http://msdn.microsoft.com/en-us/library/aa388182(VS.85).aspx
				self::$randomFunc = 'randomCOM';
			}
			catch(\Exception $e) {}
		}
		return self::$randomFunc;
	}

	public static function randomBytes($bytes) {
		return call_user_func(array('self', self::$randomFunc), $bytes);
	}

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

	protected static function randomFRead($bytes) {
		/* Get the specified number of random bytes using a file handle
		 previously opened with UUID::initRandom().
		 Randomness is returned as a string of bytes. */
		return fread(self::$randomSource, $bytes);
	}

	protected static function randomCOM($bytes) {
		/* Get the specified number of random bytes using Windows'
		 randomness source via a COM object previously created by UUID::initRandom().
		 Randomness is returned as a string of bytes. */
		return base64_decode(self::$randomSource->GetRandom($bytes,0)); // straight binary mysteriously doesn't work, hence the base64
	}
}

?>