// license:BSD-3-Clause
// copyright-holders:Aaron Giles
/***************************************************************************

    timer.cpp

    Timer devices.

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

#include "emu.h"
#include "machine/timer.h"
#include "screen.h"


/***************************************************************************
//  DEBUGGING
***************************************************************************/

#define VERBOSE 0

#define LOG(x)  do { if (VERBOSE) logerror x; } while (0)




//**************************************************************************
//  LIVE TIMER DEVICE
//**************************************************************************

DEFINE_DEVICE_TYPE(TIMER, timer_device, "timer", "Timer")

//-------------------------------------------------
//  timer_device - constructor
//-------------------------------------------------

timer_device::timer_device(const machine_config &mconfig, const char *tag, device_t *owner, u32 clock)
	: device_t(mconfig, TIMER, tag, owner, clock),
		m_type(TIMER_TYPE_GENERIC),
		m_callback(),
		m_ptr(nullptr),
		m_start_delay(attotime::zero),
		m_period(attotime::zero),
		m_param(0),
		m_screen_tag(nullptr),
		m_screen(nullptr),
		m_first_vpos(0),
		m_increment(0),
		m_timer(nullptr),
		m_first_time(true)
{
}


//-------------------------------------------------
//  static_configure_generic - configuration
//  helper to set up a generic timer
//-------------------------------------------------

void timer_device::static_configure_generic(device_t &device, expired_delegate callback)
{
	timer_device &timer = downcast<timer_device &>(device);
	timer.m_type = TIMER_TYPE_GENERIC;
	timer.m_callback = callback;
}


//-------------------------------------------------
//  static_configure_periodic - configuration
//  helper to set up a periodic timer
//-------------------------------------------------

void timer_device::static_configure_periodic(device_t &device, expired_delegate callback, const attotime &period)
{
	timer_device &timer = downcast<timer_device &>(device);
	timer.m_type = TIMER_TYPE_PERIODIC;
	timer.m_callback = callback;
	timer.m_period = period;
}


//-------------------------------------------------
//  static_configure_scanline - configuration
//  helper to set up a scanline timer
//-------------------------------------------------

void timer_device::static_configure_scanline(device_t &device, expired_delegate callback, const char *screen, int first_vpos, int increment)
{
	timer_device &timer = downcast<timer_device &>(device);
	timer.m_type = TIMER_TYPE_SCANLINE;
	timer.m_callback = callback;
	timer.m_screen_tag = screen;
	timer.m_first_vpos = first_vpos;
	timer.m_increment = increment;
}


//-------------------------------------------------
//  static_set_callback - configuration helper
//  to set the callback
//-------------------------------------------------

void timer_device::static_set_callback(device_t &device, expired_delegate callback)
{
	timer_device &timer = downcast<timer_device &>(device);
	timer.m_callback = callback;
}


//-------------------------------------------------
//  static_set_start_delay - configuration helper
//  to set the starting delay
//-------------------------------------------------

void timer_device::static_set_start_delay(device_t &device, const attotime &delay)
{
	timer_device &timer = downcast<timer_device &>(device);
	timer.m_start_delay = delay;
}


//-------------------------------------------------
//  static_set_param - configuration helper to set
//  the integer parameter
//-------------------------------------------------

void timer_device::static_set_param(device_t &device, int param)
{
	timer_device &timer = downcast<timer_device &>(device);
	timer.m_param = param;
}


//-------------------------------------------------
//  static_set_ptr - configuration helper to set
//  the pointer parameter
//-------------------------------------------------

void timer_device::static_set_ptr(device_t &device, void *ptr)
{
	timer_device &timer = downcast<timer_device &>(device);
	timer.m_ptr = ptr;
}


//-------------------------------------------------
//  device_validity_check - validate the device
//  configuration
//-------------------------------------------------

void timer_device::device_validity_check(validity_checker &valid) const
{
	// type based configuration
	switch (m_type)
	{
		case TIMER_TYPE_GENERIC:
			if (m_screen_tag != nullptr || m_first_vpos != 0 || m_start_delay != attotime::zero)
				osd_printf_warning("Generic timer specified parameters for a scanline timer\n");
			if (m_period != attotime::zero || m_start_delay != attotime::zero)
				osd_printf_warning("Generic timer specified parameters for a periodic timer\n");
			break;

		case TIMER_TYPE_PERIODIC:
			if (m_screen_tag != nullptr || m_first_vpos != 0)
				osd_printf_warning("Periodic timer specified parameters for a scanline timer\n");
			if (m_period <= attotime::zero)
				osd_printf_error("Periodic timer specified invalid period\n");
			break;

		case TIMER_TYPE_SCANLINE:
			if (m_period != attotime::zero || m_start_delay != attotime::zero)
				osd_printf_warning("Scanline timer specified parameters for a periodic timer\n");
			if (m_param != 0)
				osd_printf_warning("Scanline timer specified parameter which is ignored\n");
//          if (m_first_vpos < 0)
//              osd_printf_error("Scanline timer specified invalid initial position\n");
//          if (m_increment < 0)
//              osd_printf_error("Scanline timer specified invalid increment\n");
			break;

		default:
			osd_printf_error("Invalid type specified\n");
			break;
	}
}


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

void timer_device::device_start()
{
	// fetch the screen
	if (m_screen_tag != nullptr)
		m_screen = machine().device<screen_device>(m_screen_tag);

	// allocate the timer
	m_timer = timer_alloc();

	m_callback.bind_relative_to(*owner());

	// register for save states
	save_item(NAME(m_first_time));
}


//-------------------------------------------------
//  device_reset - reset the device
//-------------------------------------------------

void timer_device::device_reset()
{
	// type based configuration
	switch (m_type)
	{
		case TIMER_TYPE_GENERIC:
		case TIMER_TYPE_PERIODIC:
		{
			// convert the period into attotime
			attotime period;
			if (m_period > attotime::zero)
			{
				period = m_period;

				// convert the start_delay into attotime
				attotime start_delay = attotime::zero;
				if (m_start_delay > attotime::zero)
					start_delay = m_start_delay;

				// allocate and start the backing timer
				m_timer->adjust(start_delay, m_param, period);
			}
			break;
		}

		case TIMER_TYPE_SCANLINE:
			if (m_screen == nullptr)
				fatalerror("timer '%s': unable to find screen '%s'\n", tag(), m_screen_tag);

			// set the timer to fire immediately
			m_first_time = true;
			m_timer->adjust(attotime::zero, m_param);
			break;
	}
}


//-------------------------------------------------
//  device_timer - handle timer expiration events
//-------------------------------------------------

void timer_device::device_timer(emu_timer &timer, device_timer_id id, int param, void *ptr)
{
	switch (m_type)
	{
		// general periodic timers just call through
		case TIMER_TYPE_GENERIC:
		case TIMER_TYPE_PERIODIC:
			if (!m_callback.isnull())
				(m_callback)(*this, m_ptr, param);
			break;


		// scanline timers have to do some additiona bookkeeping
		case TIMER_TYPE_SCANLINE:
		{
			// by default, we fire at the first position
			int next_vpos = m_first_vpos;

			// the first time through we just go with the default position
			if (!m_first_time)
			{
				// call the real callback
				int vpos = m_screen->vpos();
				if (!m_callback.isnull())
					(m_callback)(*this, m_ptr, vpos);

				// advance by the increment only if we will still be within the screen bounds
				if (m_increment != 0 && (vpos + m_increment) < m_screen->height())
					next_vpos = vpos + m_increment;
			}
			m_first_time = false;

			// adjust the timer
			m_timer->adjust(m_screen->time_until_pos(next_vpos));
			break;
		}
	}
}
