#define PERL_POLLUTE

#include <EXTERN.h>
#include <perl.h>
#include <XSUB.h>


static UV dim[14]
	= { 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 28 };
static IV tweak[12]
	= { 1, 2, 4, 5, 7, 8, 9, 11, 12, 14, 15, 16 };
static IV cum_days[12]
	= { -1, 30, 58, 89, 119, 150, 180, 211, 242, 272, 303, 333 };

static bool
is_leap_year (IV y)
{
	return (y % 4 == 0) && ((y % 100 != 0) || (y % 400 == 0));
}

static IV
days_in_month (IV month, IV year)
{
	IV ret = dim [ month - 1 ];
	if (ret == 0)
		ret = is_leap_year (year) ? 29 : 28;
	return ret;
}

/* Compute the number of days since 1970.  */
static bool
ymd_to_days (IV y, IV m, IV d, IV* days)
{
	IV x;
	IV nonleap_days;
	IV leap_days_4;
	IV leap_holes_100;
	IV leap_days_400;

	if (m < 1 || m > 12 || d < 1 || (d > 28 && d > days_in_month (m, y)))
		return FALSE;

	x = (m <= 2 ? y - 1 : y);
	nonleap_days = d + cum_days [m - 1] + 365 * (y - 1970);

	leap_days_4 = (x - 1968) >> 2;
	if (x >= 1900)
		leap_holes_100 = (x - 1900) / 100;
	else
		leap_holes_100 = - (1999 - x) / 100;
	if (x >= 1600)
		leap_days_400 = (x - 1600) / 400;
	else
		leap_days_400 = - (1999 - x) / 400;

	*days = nonleap_days + leap_days_4 - leap_holes_100 + leap_days_400;
	return TRUE;
}

/* Compute year, month, and day given days_since_1970.  */
static void
days_to_ymd (IV days, IV ymd[3])
{
	IV year;
	IV month, day, quot;

	/* Shift frame of reference from 1 Jan 1970 to (the imaginary)
	   1 Mar 0AD.  */
	days += 719468;

	/* Do the math.  */

	quot = days / 146097;
	days -= 146097 * quot;
	year = 400 * quot;

	if (days == 146096)
	{
		/* Handle 29 Feb 2000, 2400, ...  */
		year += 400;
		month = 2;
		day = 29;
	}
	else
	{
		quot = days / 36524;
		days -= 36524 * quot;
		year += 100 * quot;

		quot = days / 1461;
		days -= 1461 * quot;
		year += 4 * quot;

		if (days == 1460)
		{
			year += 4;
			month = 2;
			day = 29;
		}
		else
		{
			quot = days / 365;
			days -= 365 * quot;
			year += quot;

			quot = days / 32;
			days -= 32 * quot;
			month = quot;

			day = days + tweak [month];
			days = dim [month + 2];

			if (day > days)
			{
				day -= days;
				month += 1;
			}
			if (month > 9)
			{
				month -= 9;
				year += 1;
			}
			else
				month += 3;
		}
	}
	ymd[0] = year;
	ymd[1] = month;
	ymd[2] = day;
}

static bool
d8_to_days (SV* d8, IV* days)
{
	char buf[5];
	STRLEN len;
	char* p;

	p = SvPV(d8, len);
	if (len == 8)
	{
		while (len > 0)
		{
			if (!isDIGIT(p[len - 1]))
				break;
			len--;
		}
		if (len != 0)
			return FALSE;
	}
	else
		return FALSE;

	return ymd_to_days(10*(10*(10*(p[0]-'0')+p[1]-'0')+p[2]-'0')+p[3]-'0',
			   10*(p[4]-'0')+p[5]-'0', 10*(p[6]-'0')+p[7]-'0',
			   days);
}

static SV*
days_to_date (IV days, SV* pkg)
{
        char* pack=0;
        if (SvROK (pkg)) {
            HV* stash;
            stash=SvSTASH(SvRV(pkg));
       	    return sv_bless( newRV_noinc (newSViv (days)), stash );
        } else if (SvTRUE(pkg)) {
            pack=SvPV_nolen(pkg);
        }
        return sv_bless( newRV_noinc (newSViv (days)),
			 gv_stashpv (pack == 0 ? "Date::Simple" : pack, 1));
}

static int
is_object (SV* sv)
{
	return (SvROK (sv) && SvTYPE (SvRV (sv)) == SVt_PVMG);
}

static SV*
new_for_cmp (SV* left, SV* right, int croak_on_fail)
{
	dSP;
	SV* ret;

	/* Comparing date with non-date.
	   Try to convert the right side to a date.  */
	EXTEND (sp, 2);
	PUSHMARK(sp);
	PUSHs (left);
	PUSHs (right);
	PUTBACK;
	perl_call_method (croak_on_fail ? "new" : "_new", G_SCALAR);
	SPAGAIN;
	ret = POPs;
	if (croak_on_fail && ! is_object (ret))
	{
		PUSHMARK(sp);
		PUSHs (left);
		PUSHs (right);
		PUTBACK;
		perl_call_pv ("Date::Simple::_inval", G_VOID);
		SPAGAIN;
	}
	return ret;
}

MODULE = Date::Simple	PACKAGE = Date::Simple

SV*
_ymd(obj_or_class, y, m, d)
	SV* obj_or_class
	IV y
	IV m
	IV d
	CODE:
	{
		IV days;
		if (ymd_to_days (y, m, d, &days))
			RETVAL = days_to_date (days, obj_or_class);
		else
			XSRETURN_UNDEF;
	}
	OUTPUT:
	RETVAL

SV*
_d8(obj_or_class, d8)
	SV* obj_or_class
	SV* d8
	CODE:
	{
		IV days;
		if (d8_to_days (d8, &days))
			RETVAL = days_to_date (days, obj_or_class);
		else
			XSRETURN_UNDEF;
	}
	OUTPUT:
	RETVAL

bool
leap_year(y)
	IV y
	CODE:
	{
		RETVAL = is_leap_year (y);
	}
	OUTPUT:
	RETVAL

IV
days_in_month(y, m)
	IV y
	IV m
	CODE:
	{
		if (m < 1 || m > 12)
			croak ("days_in_month: month out of range (%d)",
			       (int) m);
		RETVAL = days_in_month (m, y);
	}
	OUTPUT:
	RETVAL

IV
validate(ysv, m, d)
	SV* ysv
	IV m
	IV d
	CODE:
	{
		IV y;
		y = SvIV (ysv);
		if ((IV) SvNV (ysv) != y)
			RETVAL = 0;
		else if (m < 1 || m > 12)
			RETVAL = 0;
		else if (d < 1 || d > days_in_month (m, y))
			RETVAL = 0;
		else
			RETVAL = 1;
	}
	OUTPUT:
	RETVAL

void
ymd_to_days(y, m, d)
	IV y
	IV m
	IV d
	CODE:
	{
		IV days;
		if (! ymd_to_days (y, m, d, &days))
			XSRETURN_UNDEF;
		else
			XSRETURN_IV (days);
	}

SV*
days_since_1970(date)
	SV* date
	CODE:
	{
		if (SvROK(date))
			RETVAL = SvREFCNT_inc (SvRV(date));
		else
			XSRETURN_UNDEF;
	}
	OUTPUT:
	RETVAL

void
days_to_ymd(days)
	IV days
	PPCODE:
	{
		IV ymd[3];
		days_to_ymd (days, ymd);
		EXTEND (sp, 3);
		PUSHs (sv_2mortal (newSViv (ymd[0])));
		PUSHs (sv_2mortal (newSViv (ymd[1])));
		PUSHs (sv_2mortal (newSViv (ymd[2])));
	}

IV
year(date)
	SV* date
	CODE:
	{
		IV ymd[3];
		if (! SvROK (date))
			XSRETURN_UNDEF;

		days_to_ymd (SvIV (SvRV (date)), ymd);
		RETVAL = ymd[0];
	}
	OUTPUT:
	RETVAL

IV
month(date)
	SV* date
	CODE:
	{
		IV ymd[3];
		if (! SvROK (date))
			XSRETURN_UNDEF;

		days_to_ymd (SvIV (SvRV (date)), ymd);
		RETVAL = ymd[1];
	}
	OUTPUT:
	RETVAL

IV
day(date)
	SV* date
	CODE:
	{
		IV ymd[3];
		if (! SvROK (date))
			XSRETURN_UNDEF;

		days_to_ymd (SvIV (SvRV (date)), ymd);
		RETVAL = ymd[2];
	}
	OUTPUT:
	RETVAL



SV*
as_iso(date, ...)
	SV* date
	CODE:
	{
		IV ymd[3];
		if (! SvROK (date))
			XSRETURN_UNDEF;

		days_to_ymd (SvIV (SvRV (date)), ymd);
		RETVAL = newSVpvf ("%04d-%02d-%02d", (int) ymd[0] % 10000,
				   (int) ymd[1], (int) ymd[2]);
	}
	OUTPUT:
	RETVAL


SV*
as_d8(date, ...)
	SV* date
	CODE:
	{
		IV ymd[3];
		if (! SvROK (date))
			XSRETURN_UNDEF;

		days_to_ymd (SvIV (SvRV (date)), ymd);
		RETVAL = newSVpvf ("%04d%02d%02d", (int)ymd[0] % 10000,
				   (int) ymd[1], (int) ymd[2]);
	}
	OUTPUT:
	RETVAL

void
as_ymd(date)
	SV* date
	PPCODE:
	{
		IV ymd[3];
		if (! SvROK (date))
			XSRETURN_EMPTY;

		days_to_ymd (SvIV (SvRV (date)), ymd);
		EXTEND (sp, 3);
		PUSHs (sv_2mortal (newSViv (ymd[0])));
		PUSHs (sv_2mortal (newSViv (ymd[1])));
		PUSHs (sv_2mortal (newSViv (ymd[2])));
	}

SV*
_add(date, diff, ...)
	SV* date
	IV diff
	CODE:
	{
	        dSP;

	        SV* new_date;
		SV* format;

		IV days;

		if (! is_object (date))
			XSRETURN_UNDEF;

		days = SvIV (SvRV (date)) + diff;

		new_date = sv_bless(newRV_noinc(newSViv(days)),
				    SvSTASH(SvRV(date)));

		PUSHMARK(SP);
		XPUSHs(date);
		PUTBACK;

		call_method("default_format", G_SCALAR);

		SPAGAIN;

		format = POPs;

		PUSHMARK(SP);
		XPUSHs(new_date);
		XPUSHs(format);
		PUTBACK;

		call_method("default_format", G_DISCARD);

		RETVAL = new_date;

	}
	OUTPUT:
                RETVAL

SV*
_subtract(left, right, reverse)
	SV* left
	SV* right
	SV* reverse
	CODE:
	{
		if (! is_object (left))
			XSRETURN_UNDEF;

		if (SvTRUE (reverse))
			croak ("Can't subtract a date from a non-date");

		if (SvROK (right))
		{
			IV diff = SvIV (SvRV (left)) - SvIV (SvRV (right));
			RETVAL = newSViv (diff);
		}
		else
		{
			IV days = SvIV (SvRV (left)) - SvIV (right);
			SV* new_date = sv_bless (newRV_noinc (newSViv (days)),
						 SvSTASH (SvRV (left)));
			SV* format;

			dSP;

			PUSHMARK(SP);
			XPUSHs(left);
			PUTBACK;

			call_method("default_format", G_SCALAR);

			SPAGAIN;

			format = POPs;

			PUSHMARK(SP);
			XPUSHs(new_date);
			XPUSHs(format);
			PUTBACK;

			call_method("default_format", G_DISCARD);

			RETVAL = new_date;
		}
	}
	OUTPUT:
	RETVAL

IV
_compare(left, right, reverse)
	SV* left
	SV* right
	bool reverse
	CODE:
	{
		IV diff;

		if (! is_object (left))
			XSRETURN_UNDEF;

		if (! is_object (right))
			right = new_for_cmp (left, right, 1);

		diff = SvIV (SvRV (left)) - SvIV (SvRV (right));
		RETVAL = diff > 0 ? 1 : (diff < 0 ? -1 : 0);

		if (reverse)
			RETVAL = -RETVAL;
	}
	OUTPUT:
	RETVAL

SV*
_eq(left, right, reverse)
	SV* left
	SV* right
	bool reverse
	CODE:
	{
		if (! is_object (left))
			XSRETURN_UNDEF;

		if (! is_object (right))
			right = new_for_cmp (left, right, 0);

		if (! is_object (right))
			XSRETURN_NO;

		if (SvIV (SvRV (left)) == SvIV (SvRV (right)))
			XSRETURN_YES;
		else
			XSRETURN_NO;
	}
	OUTPUT:
	RETVAL

SV*
_ne(left, right, reverse)
	SV* left
	SV* right
	bool reverse
	CODE:
	{
		if (! is_object (left))
			XSRETURN_UNDEF;

		if (! is_object (right))
			right = new_for_cmp (left, right, 0);

		if (! is_object (right))
			XSRETURN_YES;

		if (SvIV (SvRV (left)) == SvIV (SvRV (right)))
			XSRETURN_NO;
		else
			XSRETURN_YES;
	}
	OUTPUT:
	RETVAL

IV
day_of_week(date)
	SV* date
	CODE:
	{
		IV days;
		if (! SvROK (date))
			XSRETURN_UNDEF;

		RETVAL = (SvIV (SvRV (date)) + 4) % 7;
		if (RETVAL < 0)
			RETVAL += 7;
	}
	OUTPUT:
	RETVAL