// This file is part of the Coral project.
// Please read the copyright notice in the LICENSE file.

#include "helper_uuid.h"

#include <cstdlib>
#include <cstdio>

#include <sys/time.h>
#include <string.h>
#include <coral/lib/assert.h>
#include <iostream>

#include "multithread.h"

namespace coral {

#define MAC_LEN_W_NUL 13

#ifdef __MINGW32__
	/* Following code by Danny Smith */
	void __stdcall GetSystemTimeAsFileTime(FILETIME*);

	/*time from 1 Jan 1601 to 1 Jan 1970 in 100ns units */
#define _W32_FT_OFFSET (116444736000000000LL) 

	typedef union {
		long long ns100; /*time since 1 Jan 1601 in 100ns units */
		FILETIME ft;
	} w32_ftv;

	int gettimeofday(struct timeval* p, struct timezone* tz /* IGNORED */){
		w32_ftv _now; 
		GetSystemTimeAsFileTime( &(_now.ft) );
		p->tv_usec=(long)((_now.ns100 / 10LL) % 1000000LL );
		p->tv_sec= (long)((_now.ns100-_W32_FT_OFFSET)/10000000LL);
		return 0;
	}

#endif

/*
 * The timestamp is a 60 bit value. For UUID version 1, this is
 * represented by Coordinated Universal Time (UTC) as a count of
 * 100-nanosecond intervals since 00:00:00.00, 15 October 1582 (the
 * date of Gregorian reform to the Christian calendar).
 */

/*
 * Assume there are buglets in this implementation,
 * the spec isn't _that_ clean on the matter.
 * http://www.opengroup.org/onlinepubs/9629399/apdxa.htm
 */

#define CLOCKSEQ_MAX (16384)

	static const char hexadecimals[] = "0123456789ABCDEF";

	static unsigned int rand_high(unsigned int max) {
		return rand() % max;
	}

	/** Needed for e.g. dummy0 devices which have no MAC address */
	static bool isValid(const char* s) {
		if (strlen(s) != 12)
			return false;
		if (!strncmp(s, "000000000000", 12)) {
			return false;
		}
		return true;
	}

	/* Finds first ethenet mac address in linux or freebsd.
	   The stupid approach. */
	static bool fill_eth_linux(char* output) {
		bool ret = false;

		std::FILE* f = popen("/sbin/ifconfig -a", "r");
		if (!f) return false;

		char buf[1024];
		while (fgets(buf, 1024, f) != NULL) {
			unsigned int h1,h2,h3,h4,h5,h6;
			char* sub;

			if (strstr(buf, "eth")
			    && (sub = strstr(buf, "HWaddr"))
			    && (sscanf(sub, "HWaddr %x:%x:%x:%x:%x:%x",
				       &h1, &h2, &h3, &h4, &h5, &h6) == 6)) {
				;
			} else if ((sub = strstr(buf, "ether"))
				   && (sscanf(sub, "ether %x:%x:%x:%x:%x:%x", 
					      &h1, &h2, &h3, &h4, &h5, &h6)
				       == 6)) {
				;
			} else {
				continue;
			}

			snprintf(output,
				 MAC_LEN_W_NUL,
				 "%02X%02X%02X%02X%02X%02X",
				 h1, h2, h3, h4, h5, h6);
			if (!isValid(output)) continue;
			ret = true;
			break;
		}
		pclose(f);
		return ret;
	}

	/* Finds first ethenet mac address in windows 2000 (maybe XP).
	   The stupid approach. */
	static bool fill_eth_windows(char* output) {
		bool ret = false;
		std::FILE* f = popen("ipconfig /all", "r");
		if (!f) return false;
	
		char buf[1024];
		while (fgets(buf, 1024, f) != NULL) {
			char* sub = strstr(buf, "Physical Address");
			if (!sub) continue;
			sub = strstr(sub, ":");
			if (!sub) continue;
			unsigned int h1,h2,h3,h4,h5,h6;
			if (sscanf(sub, ": %x-%x-%x-%x-%x-%x",
				   &h1, &h2, &h3, &h4, &h5, &h6) != 6)
				continue;
			snprintf(output, MAC_LEN_W_NUL,
				 "%02X%02X%02X%02X%02X%02X",
				 h1, h2, h3, h4, h5, h6);
			if (!isValid(output)) continue;
			ret = true;
			break;
		}
		pclose(f);
		return ret;
	}

	static void fill_random(char* output, const unsigned int size) {
		for (unsigned int i = 0; i < size; ++i) {
			*output++ = hexadecimals[rand_high(16)];
		}
	}

	static unsigned int clocksequence;
	static struct timeval old_timeval = {0, 0 };

	static void fill(char** output,
			 unsigned int size,
			 unsigned long long& value) {
		char* o = *output;
		/* Do it backwards. I think. The spec is incredible... */
		o += size - 1;
		for (unsigned int i = 0; i < size; ++i) {
			unsigned int h = value % 16;
			value >>= 4;
			*o-- = hexadecimals[h];
		}
		*output = o + size + 1;
	}

	static void fill_nanoseconds(char** output) {
		struct timeval now;
#ifndef NDEBUG
		unsigned int ret =
#endif
			gettimeofday(&now, NULL);
		coral_assert(ret == 0);
		unsigned int our_clocksequence;

		{
			LockMutex l(mutex_uuid);
			/* Update clock sequence? */
			if ((now.tv_sec == old_timeval.tv_sec)
			    && (now.tv_usec == old_timeval.tv_usec)) {
				++clocksequence;
				clocksequence %= CLOCKSEQ_MAX;
			}
			old_timeval.tv_sec = now.tv_sec;
			old_timeval.tv_usec = now.tv_usec;
			our_clocksequence = clocksequence;
		}

		char* o = *output;

		/*
		 * gettimeofday resolution is microseconds,
		 * not 100s of nanoseconds.
		 */

		unsigned long long all;
		{
			unsigned long long L1 = now.tv_sec;
			unsigned long long L2 = now.tv_usec;
			/*
			 * Epoch is January 1st, 1970, so we add a count of
			 * 100-nanosecond intervals since 00:00:00.00, 15
			 * October 1582 (the date of Gregorian reform to the
			 * Christian calendar).
			 */
			all = L1 * 10000000ULL
				+ L2 * 10L
				+ 122191200000000000ULL;
		}

		fill(&o, 8, all);

		/* time_mid */
		*o++ = '-';
		fill(&o, 4, all);

		/* time_high */
		*o++ = '-';

		/* Set DCE version */
		all &= ~((128+64+32+16) << 8);
		all |= (16 << 8);
		fill(&o, 4, all);

		/* Set clock high/low */
		*o++ = '-';
		unsigned long long clock_high = our_clocksequence >> 8;
		clock_high |= 128;
		clock_high &= (255-64);
		unsigned long long clock_low = our_clocksequence & 255;
		fill(&o, 2, clock_high);
		fill(&o, 2, clock_low);

		*o++ = '-';
		*output = o;
	
	}


	static char ethernet_mac[MAC_LEN_W_NUL];

	void initGetUniqueID() {
		srand(time(NULL));
		/* Init clock sequence. */
		clocksequence = rand_high(CLOCKSEQ_MAX);
		/* Get ethernet address. */
		do {
			if (fill_eth_linux(ethernet_mac))
				;
			else if (fill_eth_windows(ethernet_mac))
				;
			else 
				fill_random(ethernet_mac, MAC_LEN_W_NUL-1);
			while (!isValid(ethernet_mac)) {
				dlog << DEBUGLEVEL_DEBUG << "Couldn't determine MAC address, so initialising UUID generator with pseudorandom values!" << std::endl;
				fill_random(ethernet_mac, MAC_LEN_W_NUL-1);
			}
		
		} while (0);
		coral_assert(isValid(ethernet_mac));
		ethernet_mac[MAC_LEN_W_NUL-1] = 0;
	}

	/** Protects getUniqueID */
	INIT_MUTEX(mutex_uuid);

	uuid_t getUniqueID() {

		char buf[50];
		char* output = buf;
		*output++ = 'D';
		*output++ = 'C';
		*output++ = 'E';
		*output++ = ':';

		fill_nanoseconds(&output);

		snprintf(output, MAC_LEN_W_NUL, "%s", ethernet_mac);
		return std::string(buf);
	}

}
