#include <kernel/kernel.h>
#include <kernel/driver.h>
#include <sys/error.h>

#define DEBUG
#include <kernel/debug.h>

/*!
 *  \ingroup	drivers
 *  \defgroup	cmos	CMOS/RTC
 *  @{
 */

#define PCIDEV_RTC	0x1f00

#define CMOS_CTRL	0x70
#define CMOS_DATA	0x71

#define RTC_SECOND	0
#define RTC_MINUTE	2
#define RTC_HOUR	4
#define RTC_DAYMONTH	7
#define RTC_MONTH	8
#define RTC_YEAR	9
#define RTC_STATUS_B	11

byte rtcRead(word port, byte reg)
{
	byte high_digit, low_digit;
	out(port, reg);
	high_digit = low_digit = in(port + 1);
	/* convert from BCD to binary */
	high_digit >>= 4;
	high_digit &= 0x0F;
	low_digit &= 0x0F;
	return 10 * high_digit + low_digit;
}

/*****************************************************************************
Finds the number of days between two dates in the Gregorian calendar.
- it's a leap year if the year is divisible by 4,
- UNLESS the year is also divisible by 100,
- UNLESS the year is also divisible by 400

This code divides the time between start_day/start_year and end_day/end_year
into "slices": fourcent (400-year) slices in the middle, bracketed on
either end by century slices, fouryear (4-year) slices, and year slices.

It's a highly generalized algorithm. If you call it with
	greg_to_jdn(-4713, 327, curr_day_in_year, curr_year);
you get the true Julian Day Number
(JDN; days since Nov 24, 4714 BC/BCE in [proleptic] Gregorian calendar)
Call with
	greg_to_jdn(1970, 0, curr_day_in_year, curr_year);
you get the number of days in the Unix epoch (since Jan 1, 1970)

This algorithm produces identical results to the one shown here:
	http://serendipity.magnet.ch/hermetic/cal_stud/jdn.htm
I think it's easier to see where my code came from, however.

And don't worry about all that heavy-duty math. GCC is pretty
good at folding constants, and doing strength-reduction
(shifts instead of divides, AND instead of MOD, etc.)
*****************************************************************************/
static long days_between_dates(short start_year, unsigned short start_day,
		short end_year, unsigned short end_day)
{
	short fourcents, centuries, fouryears, years;
	long days;

	fourcents = end_year / 400 - start_year / 400;
	centuries = end_year / 100 - start_year / 100 -
/* subtract from 'centuries' the centuries already accounted for by
'fourcents' */
		fourcents * 4;
	fouryears = end_year / 4 - start_year / 4 -
/* subtract from 'fouryears' the fouryears already accounted for by
'fourcents' and 'centuries' */
		fourcents * 100 - centuries * 25;
	years = end_year - start_year -
/* subtract from 'years' the years already accounted for by
'fourcents', 'centuries', and 'fouryears' */
		400 * fourcents - 100 * centuries - 4 * fouryears;
/* add it up: 97 leap days every fourcent */
	days = (365L * 400 + 97) * fourcents;
/* 24 leap days every residual century */
	days += (365L * 100 + 24) * centuries;
/* 1 leap day every residual fouryear */
	days += (365L * 4 + 1) * fouryears;
/* 0 leap days for residual years */
	days += (365L * 1) * years;
/* residual days (need the cast!) */
	days += ((long)end_day - start_day);
/* account for terminal leap year */
	if(end_year % 4 == 0 && end_day >= 60)
	{
		days++;
		if(end_year % 100 == 0)
			days--;
		if(end_year % 400 == 0)
			days++;
	}
/* xxx - what have I wrought? I don't know what's going on here,
but the code won't work properly without it */
	if(end_year >= 0)
	{
		days++;
		if(end_year % 4 == 0)
			days--;
		if(end_year % 100 == 0)
			days++;
		if(end_year % 400 == 0)
			days--;
	}
	if(start_year > 0)
		days--;
	return days;
}

/*****************************************************************************
NOTE: this function works only with local time, stored in the CMOS clock.
It knows nothing of GMT or timezones.
*****************************************************************************/
#define	EPOCH_YEAR	1970
#define	EPOCH_DAY	0 /* Jan 1 */

unsigned long sys_time(void)
{
	static const unsigned short days_to_date[12] =
	{
/*              jan  feb  mar  apr  may  jun  jul  aug  sep  oct  nov  dec */
		0,
		31,
		31 + 28,
		31 + 28 + 31,
		31 + 28 + 31 + 30,
		31 + 28 + 31 + 30 + 31,
		31 + 28 + 31 + 30 + 31 + 30,
		31 + 28 + 31 + 30 + 31 + 30 + 31,
		31 + 28 + 31 + 30 + 31 + 30 + 31 + 31,
		31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30,
		31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31,
		31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30
	};
	unsigned short day, month, hour, minute, second;
	unsigned long time;
	short year;

/* b2=0 BCD mode, vs. binary (binary mode seems to be buggy)
b1=1	24-hour mode, vs. 12-hour mode

### - This could be done once when the OS initializes,
instead of being done on every syscall. */
	out(CMOS_CTRL, RTC_STATUS_B);
	out(CMOS_DATA, (in(CMOS_DATA) & ~6) | 2);
/* wait for stored time value to stop changing */
	out(CMOS_CTRL, 10);
	while(in(CMOS_DATA) & 128)
		/* nothing */;
/* get year (0-99)	xxx - OH NO, Y2K!
	year = read_cmos(9) + 1900; */
	year = rtcRead(CMOS_CTRL, RTC_YEAR) + 2000;
/* get month (1-12) */
	month = rtcRead(CMOS_CTRL, RTC_MONTH);
/* get date (1-31) */
	day = rtcRead(CMOS_CTRL, RTC_DAYMONTH);
	TRACE3("Current date: MM/DD/YYYY=%u/%u/%u\n",
		month, day, year);
/* convert date to (0-30) */
	day--;
/* convert month to (0-11), convert to days-to-date, add */
	day += days_to_date[month - 1];
	TRACE2("Epoch day/year=%u/%u\n", EPOCH_DAY, EPOCH_YEAR);
	TRACE2("Current day/year=%u/%u\n", day, year);
/* convert to MJDN (days since Jan 1, 1970) */
	time = days_between_dates(EPOCH_YEAR, EPOCH_DAY, year, day);
/* read current time */
	hour = rtcRead(CMOS_CTRL, RTC_HOUR); /* 0-23 */
	minute = rtcRead(CMOS_CTRL, RTC_MINUTE); /* 0-59 */
	second = rtcRead(CMOS_CTRL, RTC_SECOND); /* 0-59 */
	TRACE3("Current time is %u:%u:%u\n", hour, minute, second);
/* convert time to hours, add current hour */
	time *= 24;
	time += hour;
/* convert to minutes, add current minute */
	time *= 60;
	time += minute;
/* convert to seconds, add current second */
	time *= 60;
	time += second;

	return time;
}

bool rtcRequest(device_t* dev, request_t* req)
{
	qword* qw;

	switch (req->code)
	{
	case DEV_REMOVE:
		hndFree(dev);

	case DEV_OPEN:
	case DEV_CLOSE:
		hndSignal(req->event, true);
		return true;

	case DEV_READ:
		if (req->params.read.length < sizeof(qword))
		{
			req->result = EBUFFER;
			return false;
		}

		qw = (qword*) req->params.read.buffer;
		*qw = sys_time();
		hndSignal(req->event, true);
		return true;
	}

	req->result = ENOTIMPL;
	return false;
}

device_t* cmosAddDevice(driver_t* drv, const wchar_t* name, device_config_t* cfg)
{
	device_t* dev;

	TRACE2("cmos: %x/%x\n", cfg->vendor_id, cfg->device_id);
	if (cfg->vendor_id != 0xffff)
		return NULL;

	switch (cfg->device_id)
	{
	case PCIDEV_RTC:
		dev = hndAlloc(sizeof(device_t), NULL);
		dev->request = rtcRequest;
		dev->driver = drv;

		TRACE2("CMOS %s installed; time = %u\n", name, sys_time());
		return dev;
	}

	return NULL;
}

bool STDCALL INIT_CODE drvInit(driver_t* drv)
{
	drv->add_device = cmosAddDevice;
	return true;
}

//@}