#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <glib.h>

#include "../include/string.h"

#include "edvtypes.h"
#include "edvdate.h"
#include "config.h"


gchar *EDVDateFormatString(
	gulong t,		/* In systime seconds */
	const gchar *format,	/* Can be NULL to just use ctime() */
	edv_date_relativity relativity
);
gchar *EDVDateStringDuration(gulong dt);

void EDVDateParseDateDMY(
	const gchar *s,
	gint *day, gint *mon, gint *year
);
void EDVDateParseDateYMD(
	const gchar *s,
	gint *year, gint *mon, gint *day
);
void EDVDateParseTime(
	const gchar *s,
	gint *hour, gint *min, gint *sec
);


#define ATOI(s)         (((s) != NULL) ? atoi(s) : 0)
#define ATOL(s)         (((s) != NULL) ? atol(s) : 0)
#define ATOF(s)         (((s) != NULL) ? atof(s) : 0.0f)
#define STRDUP(s)       (((s) != NULL) ? g_strdup(s) : NULL)

#define MAX(a,b)        (((a) > (b)) ? (a) : (b))
#define MIN(a,b)        (((a) < (b)) ? (a) : (b))
#define CLIP(a,l,h)     (MIN(MAX((a),(l)),(h)))
#define STRLEN(s)       (((s) != NULL) ? strlen(s) : 0)
#define STRISEMPTY(s)   (((s) != NULL) ? (*(s) == '\0') : TRUE)

#define MONTH_NAMES_CONICAL	{		\
 "Jan", "Feb", "Mar", "Apr", "May", "Jun",	\
 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"	\
}


/*
 *	Returns a statically allocated string containing the time
 *	specified by t in systime seconds formatted with the given
 *	string format (which can be NULL to imply use ctime() instead)
 *	and relative to the given relativitiy.
 *
 *	If relativity is EDV_DATE_RELATIVITY_CURRENT then format will be
 *	completely ignored.
 *
 *	The returned pointer must not be modified or deleted.
 *
 *	This function never returns NULL.
 */
gchar *EDVDateFormatString(
	gulong t,               /* In systime seconds */
	const gchar *format,    /* Can be NULL to just use ctime() */
	edv_date_relativity relativity
)
{
	/* Handle by relativity */
	switch(relativity)
	{
	  case EDV_DATE_RELATIVITY_CURRENT:
	    if(TRUE)
	    {
		gulong ct, dt = (gulong)time(NULL) - t;
		static gchar buf[80];


		/* Less than one second? */
		if(dt < 1)
		{
		    g_snprintf(
			buf, sizeof(buf),
			"less than 1 second ago"
		    );
		}
		/* Less than one minute? */
		else if(dt < (1 * 60))
		{
		    ct = MAX(dt / 1, 1);
		    g_snprintf(
			buf, sizeof(buf),
			"%ld second%s ago",
			ct, ((ct == 1) ? "" : "s")
		    );
		}
		/* Less than one hour? */
		else if(dt < (60 * 60))
		{
		    ct = MAX(dt / 60, 1);
		    g_snprintf(
			buf, sizeof(buf),
			"%ld minute%s ago",
			ct, ((ct == 1) ? "" : "s")
		    );
		}
		/* Less than one day? */
		else if(dt < (24 * 60 * 60))
		{
		    ct = MAX(dt / 60 / 60, 1);
		    g_snprintf(
			buf, sizeof(buf),
			"%ld hour%s ago",
			ct, ((ct == 1) ? "" : "s")
		    );
		}
		/* Less than one week? */
		else if(dt < (7 * 24 * 60 * 60))
		{
		    ct = MAX(dt / 60 / 60 / 24, 1);
		    g_snprintf(
			buf, sizeof(buf),
			"%ld day%s ago",
			ct, ((ct == 1) ? "" : "s")
		    );
		}
		/* Less than one month (30 days)? */
		else if(dt < (30 * 24 * 60 * 60))
		{
		    ct = MAX(dt / 60 / 60 / 24 / 7, 1);
		    g_snprintf(
			buf, sizeof(buf),
			"%ld week%s ago",
			ct, ((ct == 1) ? "" : "s")
		    );
		}
		/* Less than 6 months ago? */
		else if(dt < (6 * 30 * 24 * 60 * 60))
		{
		    ct = MAX(dt / 60 / 60 / 24 / 30, 1);
		    g_snprintf(
			buf, sizeof(buf),
			"%ld month%s ago",
			ct, ((ct == 1) ? "" : "s")
		    );
		}
		else
		{
		    /* All else revert to using ctime() */
		    time_t tv = (time_t)t;
		    gchar *s = ctime(&tv);

		    strncpy(
			buf,
			(s != NULL) ? s : "",
			sizeof(buf)
		    );
		    buf[sizeof(buf) - 1] = '\0';

		    s = strchr(buf, '\n');
		    if(s != NULL)
			*s = '\0';
		}

		return(buf);
	    }
	    break;

	  case EDV_DATE_RELATIVITY_ABSOLUTE:
	    if(format == NULL)
	    {
		/* No format string given so assume to use ctime() */
		time_t t2 = (time_t)t;
		return((gchar *)ctime(&t2));
	    }
	    else
	    {
		static gchar buf[80];
		time_t tv = (time_t)t;
		const struct tm *tm_ptr;

		*buf = '\0';
		tm_ptr = localtime(&tv);

		if(tm_ptr != NULL)
		    strftime(
			buf, sizeof(buf),
			format, tm_ptr
		    );
		buf[sizeof(buf) - 1] = '\0';

		return(buf);
	    }
	    break;
	}

	return("");
}

/*
 *	Returns a statically allocated string describing the time
 *	lapsed specified by dt.
 */
gchar *EDVDateStringDuration(gulong dt)
{
	gulong ct, ct2;
	static gchar buf[80];

	/* Less than one second? */
	if(dt < 1)
	{
	    g_snprintf(
		buf, sizeof(buf),
		"less than 1 second"
	    );
	}
	/* Less than one minute? */
	else if(dt < (1 * 60))
	{
	    ct = MAX(dt / 1, 1);
	    g_snprintf(
		buf, sizeof(buf),
		"%ld second%s",
		ct, ((ct == 1) ? "" : "s")
	    );
	}
	/* Less than one hour? */
	else if(dt < (60 * 60))
	{
	    ct = MAX(dt / 60, 1);
	    ct2 = MAX(dt / 1, 1) % 60;
	    g_snprintf(
		buf, sizeof(buf),
		"%ld minute%s %ld second%s",
		ct, ((ct == 1) ? "" : "s"),
		ct2, ((ct2 == 1) ? "" : "s")
	    );
	}
	/* Less than one day? */
	else if(dt < (24 * 60 * 60))
	{
	    ct = MAX(dt / 60 / 60, 1);
	    ct2 = MAX(dt / 60, 1) % 60;
	    g_snprintf(
		buf, sizeof(buf),
		"%ld hour%s %ld minute%s",
		ct, ((ct == 1) ? "" : "s"),
		ct2, ((ct2 == 1) ? "" : "s")
	    );
	}
	/* Less than one week? */
	else if(dt < (7 * 24 * 60 * 60))
	{
	    ct = MAX(dt / 60 / 60 / 24, 1);
	    ct2 = MAX(dt / 60 / 60, 1) % 24;
	    g_snprintf(
		buf, sizeof(buf),
		"%ld day%s %ld hour%s",
		ct, ((ct == 1) ? "" : "s"),
		ct2, ((ct2 == 1) ? "" : "s")
	    );
	}
	/* Less than one month (30 days)? */
	else if(dt < (30 * 24 * 60 * 60))
	{
	    ct = MAX(dt / 60 / 60 / 24 / 7, 1);
	    ct2 = MAX(dt / 60 / 60 / 24, 1) % 7;
	    g_snprintf(
		buf, sizeof(buf),
		"%ld week%s %ld day%s",
		ct, ((ct == 1) ? "" : "s"),
		ct2, ((ct2 == 1) ? "" : "s")
	    );
	}
	/* Less than 6 months ago? */
#if 0
	else if(dt < (6 * 30 * 24 * 60 * 60))
#else
	else
#endif
	{
	    ct = MAX(dt / 60 / 60 / 24 / 30, 1);
	    ct2 = MAX(dt / 60 / 60 / 24, 1) % 30;
	    g_snprintf(
		buf, sizeof(buf),
		"%ld month%s %ld day%s",
		ct, ((ct == 1) ? "" : "s"),
		ct2, ((ct2 == 1) ? "" : "s")
	    );
	}

	return(buf);
}


/*
 *	Parses the given date string and returns the day (of the
 *	month), month, and (4-digit) year.
 *
 *	The date string must be of one of the following formats:
 *
 *	"D/M/Y"
 *	"D-M-Y"
 *	"D,M,Y"
 *	"D.M.Y"
 *	"D M Y"
 *
 *	The month M may be the month's number (1 to 12) or name of the
 *	month.
 *
 *	The return value of day is in the range of 1 to 32.
 *	The return value of mon is in the range of 1 to 12.
 */
void EDVDateParseDateDMY(
	const gchar *s,
	gint *day, gint *mon, gint *year
)
{
#define ISDATEDELIM(c)	(		\
 ((c) == '/') || ((c) == '-') ||	\
 ((c) == ',') || ((c) == '.') ||	\
 ((c) == ':') || ((c) == ';') ||	\
 ((c) == ' ') || ((c) == '\t')		\
)

	if(STRISEMPTY(s))
	{
	    if(day != NULL)
		*day = 0;
	    if(mon != NULL)
		*mon = 0;
	    if(year != NULL)
		*year = 0;
	    return;
	}

	/* Parse day */
	while(ISBLANK(*s))
	    s++;
	if(day != NULL)
	    *day = CLIP(ATOI(s), 1, 32);

	/* Parse month */
	for(; *s != '\0'; s++)
	{
	    if(ISDATEDELIM(*s))
	    {
		s++;
		break;
	    }
	}
	while(ISBLANK(*s))
	    s++;
	if(isalpha(*s))
	{
	    gint i;
	    const gchar *month_name[] = MONTH_NAMES_CONICAL;

	    for(i = 0; i < 12; i++)
	    {
		if(strcasepfx(s, month_name[i]))
		    break;
	    }
	    if(i >= 12)
		i = 0;

	    if(mon != NULL)
		*mon = i + 1;
	}
	else
	{
	    if(mon != NULL)
		*mon = CLIP(ATOI(s), 1, 12);
	}

	/* Parse year */
	for(; *s != '\0'; s++)
	{
	    if(ISDATEDELIM(*s))
	    {
		s++;
		break;
	    }
	}
	while(ISBLANK(*s))
	    s++;
	if(year != NULL)
	    *year = ATOI(s);
#undef ISDATEDELIM
}

/*
 *	Same as EDVDateParseDateDMY() except that the order is parsed
 *	as (4-digit) year, month, day (of month).
 */
void EDVDateParseDateYMD(
	const gchar *s,
	gint *year, gint *mon, gint *day
)
{
#define ISDATEDELIM(c)	(		\
 ((c) == '/') || ((c) == '-') ||	\
 ((c) == ',') || ((c) == '.') ||	\
 ((c) == ':') || ((c) == ';') ||	\
 ((c) == ' ') || ((c) == '\t')		\
)

	if(STRISEMPTY(s))
	{
	    if(day != NULL)
		*day = 0;
	    if(mon != NULL)
		*mon = 0;
	    if(year != NULL)
		*year = 0;
	    return;
	}

	/* Parse year */
	while(ISBLANK(*s))
	    s++;
	if(year != NULL)
	    *year = ATOI(s);

	/* Parse month */
	for(; *s != '\0'; s++)
	{
	    if(ISDATEDELIM(*s))
	    {
		s++;
		break;
	    }
	}
	while(ISBLANK(*s))
	    s++;
	if(isalpha(*s))
	{
	    gint i;
	    const gchar *month_name[] = MONTH_NAMES_CONICAL;

	    for(i = 0; i < 12; i++)
	    {
		if(strcasepfx(s, month_name[i]))
		    break;
	    }
	    if(i >= 12)
		i = 0;

	    if(mon != NULL)
		*mon = i + 1;
	}
	else
	{
	    if(mon != NULL)
		*mon = CLIP(ATOI(s), 1, 12);
	}

	/* Parse day */
	for(; *s != '\0'; s++)
	{
	    if(ISDATEDELIM(*s))
	    {
		s++;
		break;
	    }
	}
	while(ISBLANK(*s))
	    s++;
	if(day != NULL)
	    *day = CLIP(ATOI(s), 1, 32);
#undef ISDATEDELIM
}

/*
 *	Parses the given time string and returns the hour, minutes,
 *	and seconds.
 *
 *	The time string must be of one of the following formats:
 *
 *      "H:M:S"
 *	"H M S"
 *
 *	To indicate 12-hour time a "AM" or "PM" may be postfixed to
 *	the time string.
 *
 */
void EDVDateParseTime(
	const gchar *s,
	gint *hour, gint *min, gint *sec
)
{
#define ISTIMEDELIM(c)	(		\
 ((c) == '/') || ((c) == '-') ||	\
 ((c) == ',') || ((c) == '.') ||	\
 ((c) == ':') || ((c) == ';') ||	\
 ((c) == ' ') || ((c) == '\t')		\
)

	if(STRISEMPTY(s))
	{
	    if(hour != NULL)
		*hour = 0;
	    if(min != NULL)
		*min = 0;
	    if(sec != NULL)
		*sec = 0;
	    return;
	}

	/* Parse hour */
	while(ISBLANK(*s))
	    s++;
	/* Seek past leading 0 */
	if(*s == '0')
	{
	    if(isdigit(s[1]))
		s++;
	}
	if(hour != NULL)
	    *hour = CLIP(ATOI(s), 0, 23);

	/* Parse minutes */
	for(; *s != '\0'; s++)
	{
	    if(ISTIMEDELIM(*s))
	    {
		s++;
		break;
	    }
	}
	while(ISBLANK(*s))
	    s++;
	/* Seek past leading 0 */
	if(*s == '0')
	{
	    if(isdigit(s[1]))
		s++;
	}
	if(min != NULL)
	    *min = CLIP(ATOI(s), 0, 59);

	/* Parse seconds */
	for(; *s != '\0'; s++)
	{
	    if(ISTIMEDELIM(*s))
	    {
		s++;
		break;
	    }
	}
	while(ISBLANK(*s))
	    s++;
	/* Seek past leading 0 */
	if(*s == '0')
	{
	    if(isdigit(s[1]))
		s++;
	}
	if(sec != NULL)
	    *sec = CLIP(ATOI(s), 0, 59);

	/* Is there a PM postfix? */
	while(isdigit(*s))
	    s++;
	if((strchr(s, 'P') != NULL) ||
	   (strchr(s, 'p') != NULL)
	)
	{
	    /* There is a PM postfix, add 12 hours */
	    if(hour != NULL)
		*hour += 12;
	}
#undef ISTIMEDELIM
}
