// license:BSD-3-Clause
// copyright-holders:Raphael Nabet
/***************************************************************************

    mm58274c.c

    mm58274c emulation

    Reference:
    * National Semiconductor MM58274C Microprocessor Compatible Real Time Clock
        <http://www.national.com/ds/MM/MM58274C.pdf>

    Todo:
    * Clock initialization will only work with the BwG: we need to provide
      a way to customize it.
    * Save the config to NVRAM?
    * Support interrupt pin output

    Raphael Nabet, 2002

***************************************************************************/

#include "emu.h"
#include "machine/mm58274c.h"

enum
{
	st_dcf = 0x8,       /* data-changed flag */
	st_if = 0x1,        /* interrupt flag */

	ctl_test = 0x8,     /* test mode (0=normal, 1=test) (not emulated) */
	ctl_clkstop = 0x4,  /* clock start/stop (0=run, 1=stop) */
	ctl_intsel = 0x2,   /* interrupt select (0=clock setting register, 1=interrupt register) */
	ctl_intstop = 0x1,  /* interrupt start stop (0=interrupt run, 1=interrupt stop) */

	clk_set_leap = 0xc,     /* leap year counter (0 indicates a leap year) */
	clk_set_leap_inc = 0x4, /* leap year increment */
	clk_set_pm = 0x2,       /* am/pm indicator (0 = am, 1 = pm, 0 in 24-hour mode) */
	clk_set_24 = 0x1,       /* 12/24-hour select bit (1= 24-hour mode) */

	int_ctl_rpt = 0x8,      /* 1 for repeated interrupt */
	int_ctl_dly = 0x7       /* 0 no interrupt, 1 = .1 second, 2=.5, 3=1, 4=5, 5=10, 6=30, 7=60 */
};



DEFINE_DEVICE_TYPE(MM58274C, mm58274c_device, "mm58274c", "National Semiconductor MM58274C RTC")


mm58274c_device::mm58274c_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock)
	: device_t(mconfig, MM58274C, tag, owner, clock)
	, m_mode24(0)
	, m_day1(0)
{
}


//-------------------------------------------------
//  device_start - device-specific startup
//-------------------------------------------------

void mm58274c_device::device_start()
{
	m_increment_rtc = machine().scheduler().timer_alloc(timer_expired_delegate(FUNC(mm58274c_device::rtc_increment_cb),this));
	m_increment_rtc->adjust(attotime::zero, 0, attotime::from_msec(100));
	m_interrupt_timer = machine().scheduler().timer_alloc(timer_expired_delegate(FUNC(mm58274c_device::rtc_interrupt_cb),this));

	// register for state saving
	save_item(NAME(m_mode24));
	save_item(NAME(m_day1));

	save_item(NAME(m_status));
	save_item(NAME(m_control));
	save_item(NAME(m_clk_set));
	save_item(NAME(m_int_ctl));
	save_item(NAME(m_wday));
	save_item(NAME(m_years1));
	save_item(NAME(m_years2));
	save_item(NAME(m_months1));
	save_item(NAME(m_months2));
	save_item(NAME(m_days1));
	save_item(NAME(m_days2));
	save_item(NAME(m_hours1));
	save_item(NAME(m_hours2));
	save_item(NAME(m_minutes1));
	save_item(NAME(m_minutes2));
	save_item(NAME(m_seconds1));
	save_item(NAME(m_seconds2));
	save_item(NAME(m_tenths));
}

//-------------------------------------------------
//  device_reset - device-specific reset
//-------------------------------------------------

void mm58274c_device::device_reset()
{
	system_time systime;

	/* get the current date/time from the core */
	machine().current_datetime(systime);

	m_clk_set = systime.local_time.year & 3 << 2;
	if (m_mode24)
		m_clk_set |= clk_set_24;

	/* The clock count starts on 1st January 1900 */
	m_wday = 1 + ((systime.local_time.weekday - m_day1) % 7);
	m_years1 = (systime.local_time.year / 10) % 10;
	m_years2 = systime.local_time.year % 10;
	m_months1 = (systime.local_time.month + 1) / 10;
	m_months2 = (systime.local_time.month + 1) % 10;
	m_days1 = systime.local_time.mday / 10;
	m_days2 = systime.local_time.mday % 10;
	if (!m_mode24)
	{
		/* 12-hour mode */
		if (systime.local_time.hour > 12)
		{
			systime.local_time.hour -= 12;
			m_clk_set |= clk_set_pm;
		}
		if (systime.local_time.hour == 0)
			systime.local_time.hour = 12;
	}
	m_hours1 = systime.local_time.hour / 10;
	m_hours2 = systime.local_time.hour % 10;
	m_minutes1 = systime.local_time.minute / 10;
	m_minutes2 = systime.local_time.minute % 10;
	m_seconds1 = systime.local_time.second / 10;
	m_seconds2 = systime.local_time.second % 10;
	m_tenths = 0;
	m_status = 0;
	m_control = 0;
}



attotime mm58274c_device::interrupt_period_table(int val)
{
	switch(val)
	{
		case 0: return attotime::from_msec(0);
		case 1: return attotime::from_msec(100);
		case 2: return attotime::from_msec(500);
		case 3: return attotime::from_seconds(1);
		case 4: return attotime::from_seconds(5);
		case 5: return attotime::from_seconds(10);
		case 6: return attotime::from_seconds(30);
		case 7: return attotime::from_seconds(60);
		default: fatalerror("out of range\n");
	}
}

uint8_t mm58274c_device::read(offs_t offset)
{
	int reply;

	offset &= 0xf;

	switch (offset)
	{
		case 0x00:   /* Control Register */
			reply = m_status;
			m_status = 0;
			break;

		case 0x01:   /* Tenths of Seconds */
			reply = m_tenths;
			break;

		case 0x02:   /* Units Seconds */
			reply = m_seconds2;
			break;

		case 0x03:   /* Tens Seconds */
			reply = m_seconds1;
			break;

		case 0x04:  /* Units Minutes */
			reply = m_minutes2;
			break;

		case 0x05:   /* Tens Minutes */
			reply = m_minutes1;
			break;

		case 0x06:   /* Units Hours */
			reply = m_hours2;
			break;

		case 0x07:   /* Tens Hours */
			reply = m_hours1;
			break;

		case 0x08:   /* Units Days */
			reply = m_days2;
			break;

		case 0x09:   /* Tens Days */
			reply = m_days1;
			break;

		case 0x0a:   /* Units Months */
			reply = m_months2;
			break;

		case 0x0b:   /* Tens Months */
			reply = m_months1;
			break;

		case 0x0c:   /* Units Years */
			reply = m_years2;
			break;

		case 0x0d:   /* Tens Years */
			reply = m_years1;
			break;

		case 0x0e:   /* Day of Week */
			reply = m_wday;
			break;

		case 0x0f:   /* Clock Setting & Interrupt Registers */
			if (m_control & ctl_intsel) /* interrupt register */
				reply = m_int_ctl;
			else    /* clock setting register */
			{
				if (m_clk_set & clk_set_24) /* 24-hour mode */
					reply = m_clk_set & ~clk_set_pm;
				else    /* 12-hour mode */
					reply = m_clk_set;
			}
			break;

		default:
			reply = 0;
			break;
	}

	return reply;
}


void mm58274c_device::write(offs_t offset, uint8_t data)
{
	offset &= 0xf;
	data &= 0xf;

	switch (offset)
	{
		case 0x00:   /* Control Register (test mode and interrupt not emulated) */
			if ((!(m_control & ctl_intstop)) && (data & ctl_intstop))   /* interrupt stop */
				m_interrupt_timer->enable(0);
			else if ((m_control & ctl_intstop) && (!(data & ctl_intstop)))  /* interrupt run */
			{
				attotime period = interrupt_period_table(m_int_ctl & int_ctl_dly);

				m_interrupt_timer->adjust(period, 0, m_int_ctl & int_ctl_rpt ? period : attotime::zero);
			}
			if (data & ctl_clkstop) /* stopping the clock clears the tenth counter */
				m_tenths = 0;
			m_control = data;
			break;

		case 0x01:   /* Tenths of Seconds: cannot be written */
			break;

		case 0x02:   /* Units Seconds */
			m_seconds2 = data;
			break;

		case 0x03:   /* Tens Seconds */
			m_seconds1 = data;
			break;

		case 0x04:   /* Units Minutes */
			m_minutes2 = data;
			break;

		case 0x05:   /* Tens Minutes */
			m_minutes1 = data;
			break;

		case 0x06:   /* Units Hours */
			m_hours2 = data;
			break;

		case 0x07:   /* Tens Hours */
			m_hours1 = data;
			break;

		case 0x08:   /* Units Days */
			m_days2 = data;
			break;

		case 0x09:   /* Tens Days */
			m_days1 = data;
			break;

		case 0x0a:   /* Units Months */
			m_months2 = data;
			break;

		case 0x0b:   /* Tens Months */
			m_months1 = data;
			break;

		case 0x0c:   /* Units Years */
			m_years2 = data;
			break;

		case 0x0d:   /* Tens Years */
			m_years1 = data;
			break;

		case 0x0e:   /* Day of Week */
			m_wday = data;
			break;

		case 0x0f:   /* Clock Setting & Interrupt Registers */
			if (m_control & ctl_intsel) /* interrupt register (not emulated) */
			{
				m_int_ctl = data;
				if (!(m_control & ctl_intstop)) /* interrupt run */
				{
					attotime period = interrupt_period_table(m_int_ctl & int_ctl_dly);

					m_interrupt_timer->adjust(period, 0, m_int_ctl & int_ctl_rpt ? period : attotime::zero);
				}
			}
			else    /* clock setting register */
			{
				m_clk_set = data;
#if 0
				if (m_clk_set & clk_set_24) /* 24-hour mode */
					m_clk_set &= ~clk_set_pm;
#endif
			}
			break;
	}
}


// Set RTC interrupt flag
TIMER_CALLBACK_MEMBER(mm58274c_device::rtc_interrupt_cb)
{
	m_status |= st_if;
}


// Increment RTC clock (timed interrupt every 1/10s)
TIMER_CALLBACK_MEMBER(mm58274c_device::rtc_increment_cb)
{
	if (!(m_control & ctl_clkstop))
	{
		m_status |= st_dcf;

		if ((++m_tenths) == 10)
		{
			m_tenths = 0;

			if ((++m_seconds2) == 10)
			{
				m_seconds2 = 0;

				if ((++m_seconds1) == 6)
				{
					m_seconds1 = 0;

					if ((++m_minutes2) == 10)
					{
						m_minutes2 = 0;

						if ((++m_minutes1) == 6)
						{
							m_minutes1 = 0;

							if ((++m_hours2) == 10)
							{
								m_hours2 = 0;

								m_hours1++;
							}

							/* handle wrap-around */
							if ((!(m_clk_set & clk_set_24))
									&& ((m_hours1*10 + m_hours2) == 12))
							{
								m_clk_set ^= clk_set_pm;
							}
							if ((!(m_clk_set & clk_set_24))
									&& ((m_hours1*10 + m_hours2) == 13))
							{
								m_hours1 = 0;
								m_hours2 = 1;
							}

							if ((m_clk_set & clk_set_24)
								&& ((m_hours1*10 + m_hours2) == 24))
							{
								m_hours1 = m_hours2 = 0;
							}

							/* increment day if needed */
							if ((m_clk_set & clk_set_24)
								? ((m_hours1*10 + m_hours2) == 0)
								: (((m_hours1*10 + m_hours2) == 12)
									&& (!(m_clk_set & clk_set_pm))))
							{
								int days_in_month;

								if ((++m_days2) == 10)
								{
									m_days2 = 0;

									m_days1++;
								}

								if ((++m_wday) == 8)
									m_wday = 1;

								{
									static const int days_in_month_array[] =
									{
										31,28,31, 30,31,30,
										31,31,30, 31,30,31
									};

									if (((m_months1*10 + m_months2) != 2) || (m_clk_set & clk_set_leap))
										days_in_month = days_in_month_array[m_months1*10 + m_months2 - 1];
									else
										days_in_month = 29;
								}


								if ((m_days1*10 + m_days2) == days_in_month+1)
								{
									m_days1 = 0;
									m_days2 = 1;

									if ((++m_months2) == 10)
									{
										m_months2 = 0;

										m_months1++;
									}

									if ((m_months1*10 + m_months2) == 13)
									{
										m_months1 = 0;
										m_months2 = 1;

										m_clk_set = (m_clk_set & ~clk_set_leap)
															| ((m_clk_set + clk_set_leap_inc) & clk_set_leap);

										if ((++m_years2) == 10)
										{
											m_years2 = 0;

											if ((++m_years1) == 10)
												m_years1 = 0;
										}
									}
								}
							}
						}
					}
				}
			}
		}
	}
}
