/*****************************************************
 *
 *	tcp_time.c
 *	Copyright (C) Z-World, 2001-2002
 *	
 * Demonstrate how to talk to one or more "Time Protocol" servers (see
 * RFC868 at http://www.faqs.org/rfcs/rfc868.html) in order for the
 * Rabbit to get a reliable wall-clock time from the network.
 *
 * See also RFC2030 for the "real" way to do this -- this demo is "poor
 * man's NTP client" :-)  [One day we may have a real NTP...]
 *
 * Anyway, we use TCP/UDP to connect to a sequence of Time servers.
 * The list of servers is specified in an initialized array.
 *
 * You might like to use this as a basis for adding a reliable wall-clock
 * time to an appliance which only has a crystal oscillator and, up until
 * now, no means for setting the actual date and time.
 *
 * If you are only interested in getting the correct time on the Rabbit,
 * then you can delete the become_server() code.  Otherwise, if your
 * application is excited about all this and wants to tell the world,
 * then become_server() shows how to do this.  You disable the server
 * code by defining NO_TIMESERVER.
 *
 * Note that the RTC (Real Time Clock) is _not_ updated.  Instead, a
 * variable called 'skew' is set such that (SEC_TIMER + skew) is the
 * correct time (i.e. number of seconds since 1980).  Obviously, after
 * the initial setting is obtained from get_the_time(), the clock will
 * run at the local crystal oscillator rate, which may be +/- 100ppm
 * from the correct rate.  If keeping the time accurate is important,
 * you will need to call get_the_time() every few hours to update the
 * skew variable.
 *
 * How it works: It depends on some RFC868 time servers being set up.  The
 * nominated server(s) are queried for their UTC time.  The result
 * from each server is adjusted for the network delay (assumed to be half
 * of the round-trip time).  If there were more than 2 servers, then the
 * servers whose times were "most distant" from the average of the others
 * are discarded, until a minumum of 2 or half the original number of
 * servers remain.  The final computed time is the average of the remaining
 * servers.  If all remaining servers are within about 60 seconds of each
 * other, then the discarding process is cut short.  (The 60 second
 * value is configurable using the CLOSE_ENOUGH macro).
 *
 * If no servers can be found, the current real-time clock is used
 * (SEC_TIMER).  This is a fall-back in case the network is unavailable.
 *
 * When the time is computed, this target processor then opens its own
 * Time and Daytime servers (RFC868 and RFC867).  You can then telnet to
 * port 13 on this processor to obtain the current time, or at least
 * its best guess at the time.
 *
 * Note that UDP is used to query the servers, but the target allows both
 * TCP and UDP queries to its server sockets.
 *
 ******************************************************/

/*
 * Configurable items...
 */

// If TIMEZONE not defined, default to "UTC" (0h).
#define TIMEZONE        (-8)	// Hours difference from UTC (whole hours only)

#ifdef TIMEZONE
	#define TZNAME			"PST"	// Standard name for above timezone
#else
	#define TIMEZONE		0
	#define TZNAME			"UTC"
#endif
  
#define CLOSE_ENOUGH		60		// Discard servers who say the time is more than
										// this many seconds different from the others.
										// This could be reduced to 2 if servers are expected
										// to be tied to national standards.

//#define NO_TIMESERVER			// Disable time server code.

#define MY_IP_ADDRESS 	"10.10.6.100"
#define MY_NETMASK		"255.255.255.0"
#define MY_GATEWAY		"10.10.6.1"
#define MY_NAMESERVER	"10.10.6.1"



// Hostnames (or dotted quad IP addresses) of each time server to query.
// You can "weight" some servers as being more reliable by mentioning the
// same server in more than one entry.
// NOTE: the following addresses were obtained from hosts that were
// denoted as "open access" (http://www.eecis.udel.edu/~mills/ntp/servers.htm)
// to NTP.  We don't use NTP, but we don't think they'd mind if we used the
// old time service.  Please don't incorporate these addresses in any product
// without prior approval of the host adinistrator!
// The timestamps from these servers will be traceable to national standards.

#define NSERVERS		4

const char * server_hosts[NSERVERS] =
{
   "time-b.nist.gov"
  ,"nist1.aol-ca.truetime.com"
  ,"ntp.adelaide.edu.au"
  ,"timelord.uregina.ca"
};



#memmap xmem
#use "dcrtcp.lib"



#define TIME_PORT			37		// Binary time service (machine-readable)
#define DAYTIME_PORT		13		// ASCII time service (human-readable)

// Returned timestamps from servers.  These are stored as the number of seconds
// since Jan 1, 1900 UT.  They are later converted to "Rabbit epoch" (1980).
// There were lots of furry rabbits at that time but, strangely, no silicon ones.
// Sadly, RFC868 does not allow determination of timezone, so everything is in
// terms of UTC a.k.a. "GMT".
longword timestamps[NSERVERS];

// Flag to indicate whether valid result obtained from corresponding server.
int use_this[NSERVERS];

// Number of valid results remaining.
int nservers;

// Base "reference" epoch (local ms) for when we started our timings.
longword base_stamp;

// Base epoch from SEC_TIMER corresponding to the above
longword base_tick;

// Amount to add to SEC_TIMER to make the result equal to the average time
// derived from the servers.
longword skew;

// Seconds between RFC868 epoch (1900) and Rabbit epoch (1980)
#define EPOCH_DIFF	2524521600UL


// UDP socket we use to query the servers
udp_Socket usock;
#define USOCK_BUFSIZE 64
long usock_buf;


char * format_timestamp(longword stamp, int local)
{
	static char b[40];
	static const char * weekdays[7] =
		{ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
	static const char * months[12] =
		{ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
	static struct tm daytime;

	if (local)
		mktm(&daytime, stamp + TIMEZONE*3600L);
	else
		mktm(&daytime, stamp);
	sprintf(b, "%s, %s %d, %d %02d:%02d:%02d-%s (%dh)",
							weekdays[daytime.tm_wday],
							months[daytime.tm_mon - 1],
							daytime.tm_mday,
							daytime.tm_year + 1900,
							daytime.tm_hour,
							daytime.tm_min,
							daytime.tm_sec,
							local ? TZNAME : "UTC",
							local ? TIMEZONE : 0
							);
	return b;
}


void query_the_servers(void)
{
	auto int rc;
	auto udp_Socket * socket;
	auto int i;
	auto longword rhost;
	auto word rport;
	auto int nrx;
	auto longword rtt_stamp;
	auto longword stamp;
	auto longword timer;
	auto int rt_count;

	#GLOBAL_INIT { usock_buf = xalloc(USOCK_BUFSIZE); }
	nservers = 0;
	socket = NULL;

	// Remember starting epoch for our queries
	base_stamp = MS_TIMER;
	base_tick = SEC_TIMER;

	for (i = 0; i < NSERVERS; i++) {

		use_this[i] = 0;

		// Close the UDP socket if it was in use by previous iteration of this loop.
		if (socket) {
			sock_close(socket);
			socket = NULL;
		}

		// Resolve the hostname (this blocks)
		rhost = resolve(server_hosts[i]);
		if (!rhost) {
			printf("Could not resolve %s.\n", server_hosts[i]);
			continue;	// Try next server
		}

		// Open UDP channel.
		rc = udp_extopen(&usock, IF_DEFAULT, 0, rhost, TIME_PORT, NULL, usock_buf, USOCK_BUFSIZE);
		if (!rc) {
			printf("Could not open UDP socket to %s.\n", server_hosts[i]);
			sock_perror(&usock, NULL);
			exit(21);
		}

		socket = &usock;
		udp_set_dicmp(socket);	// Set to receive ICMP messages in-stream.
		
		// Wait for ARP to resolve the router address.  Wait up to 2 seconds.
		timer = _SET_TIMEOUT(2000);
		while (!sock_resolved(socket)) {
			tcp_tick(NULL);
			if (chk_timeout(timer))
				break;
		}
		if (!socket) {
			printf("Host %s not reachable: no router?\n", server_hosts[i]);
			continue;
		}

		rt_count = 0;
		
	retry:
		// All OK.  Send empty datagram to server after recording current
		// (local) timestamp.  This loops until the return is non-zero.
		
		rtt_stamp = MS_TIMER;	// Start time of issuing request
		rc = udp_send(socket, NULL, 0);
		if (rc < 0) {
			printf("Failed to send empty datagram to %s.\n", server_hosts[i]);
			continue;
		}

		for (;;) {
			tcp_tick(NULL);
			rc = udp_recvfrom(socket, (char *)&stamp, sizeof(stamp), &rhost, &rport);
				
			if (rc < -1) {
				printf("Failed to get response from %s:\n", server_hosts[i]);
				sock_perror(socket, NULL);
				if (rc == -3)
					printf("...probably not a TIME protocol server.\n");
				break;
			}
			if (rc >= 4)
				// Exit wait loop if got response.
				break;

			// 4 second timeout.  Retry up to 2 times, else ignore this server.  This raises
			// the possibility that we may get a response from a previous packet, thus
			// warping our perception of time.  We ignore this wrinkle in our demo, since
			// the protocol does not provide for correction of this other than waiting about
			// 4 minutes for stray packets to die in the network.
			if ((long)(MS_TIMER - rtt_stamp) > 4000) {
				rt_count++;
				if (rt_count < 2)
					goto retry;
				else {
					printf("Server %s timed out.\n", server_hosts[i]);
					sock_perror(socket, NULL);
					rc = -1;
					break;
				}
			}
		}

		if (rc < 0)
			continue;

		// Correct for round-trip time in milliseconds.
		rtt_stamp += MS_TIMER - rtt_stamp >> 1;
		
		//printf("Got %d bytes from host %s [%lX] port %u\n", rc, server_hosts[i], rhost, rport);

		stamp = intel(stamp);	// Convert to local ordering.
		
		// Add 1/2 round-trip time and correction back to common base epoch
		stamp -= (rtt_stamp - base_stamp)/ 1000;

		timestamps[i] = stamp;
		use_this[i] = 1;			// Mark as OK so far
		printf("Server %s says time is %s\n", server_hosts[i], format_timestamp(stamp - EPOCH_DIFF, 1));
		nservers++;
	}

	if (socket)
		udp_close(socket);

}


void discard_outliers(void)
{
	auto int i, j, k;
	auto int minservers;
	auto float diff;
	auto float wdiff;
	auto float mdiff;

	if (nservers >= 3)
		minservers = nservers + 1 >> 1;	// Half (rounded up) of the successful servers
	else
		minservers = nservers;

	while (nservers > minservers) {
		// Throw away the worst outliers first, until minservers remain.  The
		// "worst outlier" is the one whose sum-of-squares distance from the others
		// is greatest.
		mdiff = 0.0;
		for (i = 0; i < NSERVERS; i++)
			if (!use_this[i])
				continue;
			else {
				wdiff = 0.0;
				for (j = 0; j < NSERVERS; j++)
					if (!use_this[j] || i == j)
						continue;
					else {
						diff = (long)(timestamps[j] - timestamps[i]);
						wdiff += diff * diff;
					}
				if (wdiff > mdiff) {
					mdiff = wdiff;
					k = i;
				}
			}
		if (sqrt(mdiff) / nservers < CLOSE_ENOUGH)
			// Don't discard any more if within about 60 seconds of each other
			break;
		use_this[k] = 0;
		printf("Discarding server %d\n", k);
		nservers--;
	}
}

void compute_offset(void)
{
	auto int i;
	auto long stamp;
	auto long avg;
	
	
	// Average the timestamps of the remaining servers.  This can't be
	// done the obvious way, to avoid arithmetic overflow.

	for (i = 0; i < NSERVERS; i++)
		if (use_this[i]) {
			avg = timestamps[i];
			break;
		}

	stamp = 0;
	for (i++; i < NSERVERS; i++)
		if (use_this[i])
			stamp += timestamps[i] - avg;

	avg += stamp / nservers;

	// Convert to "Rabbit epoch" by subtracting the number of seconds between
	// 1900 and 1980.
	avg -= EPOCH_DIFF;

	// This is the amount to add to SEC_TIMER to get the "correct" time.
	skew = avg - base_tick;
}

#ifndef NO_TIMESERVER

// UDP socket for the TIME and DAYTIME service
udp_Socket usock_t;
udp_Socket usock_d;
long usock_t_buf;
long usock_d_buf;

// TCP sockets for the same 2 services
tcp_Socket tsock_t;
tcp_Socket tsock_d;
#define TSOCK_BUFSIZE	128
long tsock_t_buf;
long tsock_d_buf;


void ut_tick(void)
{
	auto longword rhost;
	auto word rport;
	auto longword stamp;

	if (udp_recvfrom(&usock_t, NULL, 0, &rhost, &rport) >= 0) {
		stamp = intel(SEC_TIMER + skew);
		udp_sendto(&usock_t, (char *)&stamp, sizeof(stamp), rhost, rport);
	}
}

void udt_tick(void)
{
	auto longword rhost;
	auto word rport;
	auto char b[64];

	if (udp_recvfrom(&usock_d, NULL, 0, &rhost, &rport) >= 0) {
		sprintf(b, "%s\r\n", format_timestamp(SEC_TIMER + skew, 1));
		udp_sendto(&usock_d, b, strlen(b), rhost, rport);
	}
}

#define TSTATE_CLOSED		0
#define TSTATE_LISTENING	1
#define TSTATE_CLOSING		2

void tt_tick(void)
{
	static int state;
	auto longword stamp;
	
	#GLOBAL_INIT { state = TSTATE_CLOSED; tsock_d_buf = xalloc(8); }

	switch (state) {
		case TSTATE_CLOSED:
			tcp_extlisten(&tsock_t, IF_DEFAULT, TIME_PORT, 0, 0, NULL, 0, tsock_d_buf, 8);
			state = TSTATE_LISTENING;
			// fall through
		case TSTATE_LISTENING:
			if (sock_established(&tsock_t)) {
				stamp = intel(SEC_TIMER + skew);
				sock_fastwrite(&tsock_t, (char *)&stamp, sizeof(stamp));	// Guaranteed to fit in tx buffer
				sock_close(&tsock_t);
				state = TSTATE_CLOSING;
			}
			break;
		case TSTATE_CLOSING:
			if (!tcp_tick(&tsock_t))
				state = TSTATE_CLOSED;
			break;
	}	
	
	
}

void tdt_tick(void)
{
	static int state;
	auto char b[64];
	auto int rc;
	
	#GLOBAL_INIT { state = TSTATE_CLOSED; tsock_d_buf = xalloc(TSOCK_BUFSIZE); }

	switch (state) {
		case TSTATE_CLOSED:
			tcp_extlisten(&tsock_d, IF_DEFAULT, DAYTIME_PORT, 0, 0, NULL, 0, tsock_d_buf, TSOCK_BUFSIZE);
			state = TSTATE_LISTENING;
			// fall through
		case TSTATE_LISTENING:
			if (sock_established(&tsock_d)) {
				sprintf(b, "%s\r\n", format_timestamp(SEC_TIMER + skew, 1));
				sock_fastwrite(&tsock_d, b, strlen(b));	// Guaranteed to fit in tx buffer
				sock_close(&tsock_d);
				state = TSTATE_CLOSING;
			}
			break;
		case TSTATE_CLOSING:
			if (!tcp_tick(&tsock_d))
				state = TSTATE_CLOSED;
			break;
	}	
}

void become_server(void)
{
	// Become a four-port server, for both TCP and UDP versions of Time (RFC868)
	// and Daytime (RFC867).  The latter is an ascii (human readable) version of
	// Time.

	auto char buf[64];
	auto int rc;
	auto word rport;
	auto longword rhost;
	auto long stamp;

	#GLOBAL_INIT { usock_t_buf = xalloc(USOCK_BUFSIZE); usock_d_buf = xalloc(USOCK_BUFSIZE); }

	// Allow TCP SYN queueing.
	tcp_reserveport(TIME_PORT);
	tcp_reserveport(DAYTIME_PORT);

	// Open UDP sockets in passive mode
	udp_extopen(&usock_t, IF_DEFAULT, TIME_PORT, 0xFFFFFFFF, 0, NULL, usock_t_buf, USOCK_BUFSIZE);
	udp_extopen(&usock_d, IF_DEFAULT, DAYTIME_PORT, 0xFFFFFFFF, 0, NULL, usock_d_buf, USOCK_BUFSIZE);
	
	for (;;) {
		tcp_tick(NULL);
		// Tick all servers over
		ut_tick();
		udt_tick();
		tt_tick();
		tdt_tick();
	}
	
	
}
#endif

get_the_time(void)
{
	// Query the Time protocol port on each server.
	query_the_servers();

	if (!nservers) {
		printf("No servers could be found - using local clock\n");
		skew = 0;
	}
	else {
		// Now that we have responses from hopefully all of the servers, determine the
		// set that we are going to average.
		discard_outliers();

		// Compute the offset that we need to add to our SEC_TIMER to get the
		// correct averaged time.
		compute_offset();
	}

}

int main()
{
	int rc;

	sock_init();

	// Find the time from the internet resources.
	get_the_time();

	printf("The time is: %s - RabbitTime!\n", format_timestamp(SEC_TIMER + skew, 1));
	
#ifndef NO_TIMESERVER
	// Now turn into a Time/Daytime protocol server.
	become_server();
#endif

	return 0;
}
