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

  MIOT 6530 emulation

The timer seems to follow these rules:
- When the timer flag changes from 0 to 1 the timer continues to count
  down at a 1 cycle rate.
- When the timer is being read or written the timer flag is reset.
- When the timer flag is set and the timer contents are 0, the counting
  stops.

From the operation of the KIM1 it expects the irqflag to be set whenever
the unit is reset. This is something that is not clear from the datasheet
and should be verified against real hardware.

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

#include "emu.h"
#include "mos6530.h"
#include "devlegcy.h"


/***************************************************************************
    CONSTANTS
***************************************************************************/

enum
{
	TIMER_IDLE,
	TIMER_COUNTING,
	TIMER_FINISHING
};

#define TIMER_FLAG      0x80



/***************************************************************************
    TYPE DEFINITIONS
***************************************************************************/

struct mos6530_port
{
	devcb_resolved_read8        in_port_func;
	devcb_resolved_write8       out_port_func;

	UINT8               in;
	UINT8               out;
	UINT8               ddr;
};


struct mos6530_state
{
	devcb_resolved_write_line   out_irq_func;

	mos6530_port    port[2];

	UINT8           irqstate;
	UINT8           irqenable;

	UINT8           timershift;
	UINT8           timerstate;
	emu_timer *     timer;

	UINT32          clock;
};



/***************************************************************************
    INLINE FUNCTIONS
***************************************************************************/

/*-------------------------------------------------
    get_safe_token - convert a device's token
    into a mos6530_state
-------------------------------------------------*/

INLINE mos6530_state *get_safe_token(device_t *device)
{
	assert(device != NULL);
	assert(device->type() == MOS6530);
	return (mos6530_state *)downcast<mos6530_device *>(device)->token();
}


/*-------------------------------------------------
    update_irqstate - update the IRQ state
    based on interrupt enables
-------------------------------------------------*/

INLINE void update_irqstate(device_t *device)
{
	mos6530_state *miot = get_safe_token(device);
	UINT8 out = miot->port[1].out;

	if ( miot->irqenable )
		out = ( ( miot->irqstate & TIMER_FLAG ) ? 0x00 : 0x80 ) | ( out & 0x7F );

	if (!miot->port[1].out_port_func.isnull())
		miot->port[1].out_port_func(0, out);
	else
		logerror("6530MIOT chip %s: Port B is being written to but has no handler.\n", device->tag());
}


/*-------------------------------------------------
    get_timer - return the current timer value
-------------------------------------------------*/

INLINE UINT8 get_timer(mos6530_state *miot)
{
	/* if idle, return 0 */
	if (miot->timerstate == TIMER_IDLE)
		return 0;

	/* if counting, return the number of ticks remaining */
	else if (miot->timerstate == TIMER_COUNTING)
		return miot->timer->remaining().as_ticks(miot->clock) >> miot->timershift;

	/* if finishing, return the number of ticks without the shift */
	else
		return miot->timer->remaining().as_ticks(miot->clock);
}


/***************************************************************************
    INTERNAL FUNCTIONS
***************************************************************************/

/*-------------------------------------------------
    timer_end_callback - callback to process the
    timer
-------------------------------------------------*/

static TIMER_CALLBACK( timer_end_callback )
{
	device_t *device = (device_t *)ptr;
	mos6530_state *miot = get_safe_token(device);

	assert(miot->timerstate != TIMER_IDLE);

	/* if we finished counting, switch to the finishing state */
	if (miot->timerstate == TIMER_COUNTING)
	{
		miot->timerstate = TIMER_FINISHING;
		miot->timer->adjust(attotime::from_ticks(256, miot->clock));

		/* signal timer IRQ as well */
		miot->irqstate |= TIMER_FLAG;
		update_irqstate(device);
	}

	/* if we finished finishing, switch to the idle state */
	else if (miot->timerstate == TIMER_FINISHING)
	{
		miot->timerstate = TIMER_IDLE;
		miot->timer->adjust(attotime::never);
	}
}



/***************************************************************************
    I/O ACCESS
***************************************************************************/

/*-------------------------------------------------
    mos6530_w - master I/O write access
-------------------------------------------------*/

WRITE8_DEVICE_HANDLER( mos6530_w )
{
	mos6530_state *miot = get_safe_token(device);

	/* if A2 == 1, we are writing to the timer */
	if (offset & 0x04)
	{
		static const UINT8 timershift[4] = { 0, 3, 6, 10 };
		attotime curtime = space.machine().time();
		INT64 target;

		/* A0-A1 contain the timer divisor */
		miot->timershift = timershift[offset & 3];

		/* A3 contains the timer IRQ enable */
		if (offset & 8)
			miot->irqenable |= TIMER_FLAG;
		else
			miot->irqenable &= ~TIMER_FLAG;

		/* writes here clear the timer flag */
		if (miot->timerstate != TIMER_FINISHING || get_timer(miot) != 0xff)
			miot->irqstate &= ~TIMER_FLAG;
		update_irqstate(device);

		/* update the timer */
		miot->timerstate = TIMER_COUNTING;
		target = curtime.as_ticks(miot->clock) + 1 + (data << miot->timershift);
		miot->timer->adjust(attotime::from_ticks(target, miot->clock) - curtime);
	}

	/* if A2 == 0, we are writing to the I/O section */
	else
	{
		/* A1 selects the port */
		mos6530_port *port = &miot->port[(offset >> 1) & 1];

		/* if A0 == 1, we are writing to the port's DDR */
		if (offset & 1)
			port->ddr = data;

		/* if A0 == 0, we are writing to the port's output */
		else
		{
			UINT8 olddata = port->out;
			port->out = data;

			if ( ( offset & 2 ) && miot->irqenable )
			{
				olddata = ( ( miot->irqstate & TIMER_FLAG ) ? 0x00 : 0x80 ) | ( olddata & 0x7F );
				data = ( ( miot->irqstate & TIMER_FLAG ) ? 0x00 : 0x80 ) | ( data & 0x7F );
			}

			if (!port->out_port_func.isnull())
				port->out_port_func(0, data);
			else
				logerror("6530MIOT chip %s: Port %c is being written to but has no handler.  PC: %08X - %02X\n", device->tag(), 'A' + (offset & 1), space.machine().firstcpu->pc(), data);
		}
	}
}


/*-------------------------------------------------
    mos6530_r - master I/O read access
-------------------------------------------------*/

READ8_DEVICE_HANDLER( mos6530_r )
{
	mos6530_state *miot = get_safe_token(device);
	UINT8 val = 0;

	/* if A2 == 1 and A0 == 1, we are reading interrupt flags */
	if ((offset & 0x05) == 0x05)
	{
		val = miot->irqstate;
	}

	/* if A2 == 1 and A0 == 0, we are reading the timer */
	else if ((offset & 0x05) == 0x04)
	{
		val = get_timer(miot);

		/* A3 contains the timer IRQ enable */
		if (offset & 8)
			miot->irqenable |= TIMER_FLAG;
		else
			miot->irqenable &= ~TIMER_FLAG;

		/* implicitly clears the timer flag */
		if (miot->timerstate != TIMER_FINISHING || val != 0xff)
			miot->irqstate &= ~TIMER_FLAG;
		update_irqstate(device);
	}

	/* if A2 == 0 and A0 == anything, we are reading from ports */
	else
	{
		/* A1 selects the port */
		mos6530_port *port = &miot->port[(offset >> 1) & 1];

		/* if A0 == 1, we are reading the port's DDR */
		if (offset & 1)
			val = port->ddr;

		/* if A0 == 0, we are reading the port as an input */
		else
		{
			UINT8   out = port->out;

			if ( ( offset & 2 ) && miot->irqenable )
				out = ( ( miot->irqstate & TIMER_FLAG ) ? 0x00 : 0x80 ) | ( out & 0x7F );

			/* call the input callback if it exists */
			if (!port->in_port_func.isnull())
			{
				port->in = port->in_port_func(0);
			}
			else
				logerror("6530MIOT chip %s: Port %c is being read but has no handler.  PC: %08X\n", device->tag(), 'A' + (offset & 1), space.machine().firstcpu->pc());

			/* apply the DDR to the result */
			val = (out & port->ddr) | (port->in & ~port->ddr);
		}
	}
	return val;
}


/*-------------------------------------------------
    mos6530_porta_in_set - set port A input
    value
-------------------------------------------------*/

void mos6530_porta_in_set(device_t *device, UINT8 data, UINT8 mask)
{
	mos6530_state *miot = get_safe_token(device);
	miot->port[0].in = (miot->port[0].in & ~mask) | (data & mask);
}


/*-------------------------------------------------
    mos6530_portb_in_set - set port B input
    value
-------------------------------------------------*/

void mos6530_portb_in_set(device_t *device, UINT8 data, UINT8 mask)
{
	mos6530_state *miot = get_safe_token(device);
	miot->port[1].in = (miot->port[1].in & ~mask) | (data & mask);
}


/*-------------------------------------------------
    mos6530_porta_in_get - return port A input
    value
-------------------------------------------------*/

UINT8 mos6530_porta_in_get(device_t *device)
{
	mos6530_state *miot = get_safe_token(device);
	return miot->port[0].in;
}


/*-------------------------------------------------
    mos6530_portb_in_get - return port B input
    value
-------------------------------------------------*/

UINT8 mos6530_portb_in_get(device_t *device)
{
	mos6530_state *miot = get_safe_token(device);
	return miot->port[1].in;
}


/*-------------------------------------------------
    mos6530_porta_in_get - return port A output
    value
-------------------------------------------------*/

UINT8 mos6530_porta_out_get(device_t *device)
{
	mos6530_state *miot = get_safe_token(device);
	return miot->port[0].out;
}


/*-------------------------------------------------
    mos6530_portb_in_get - return port B output
    value
-------------------------------------------------*/

UINT8 mos6530_portb_out_get(device_t *device)
{
	mos6530_state *miot = get_safe_token(device);
	return miot->port[1].out;
}


/***************************************************************************
    DEVICE INTERFACE
***************************************************************************/

static DEVICE_START( mos6530 )
{
	mos6530_state *miot = get_safe_token(device);
	const mos6530_interface *intf = (const mos6530_interface*)device->static_config();

	/* validate arguments */
	assert(device != NULL);
	assert(device->tag() != NULL);

	/* set static values */
	miot->clock = device->clock();

	/* resolve callbacks */
	miot->port[0].in_port_func.resolve(intf->in_pa_func, *device);
	miot->port[1].in_port_func.resolve(intf->in_pb_func, *device);
	miot->port[0].out_port_func.resolve(intf->out_pa_func, *device);
	miot->port[1].out_port_func.resolve(intf->out_pb_func, *device);

	/* allocate timers */
	miot->timer = device->machine().scheduler().timer_alloc(FUNC(timer_end_callback), (void *)device);

	/* register for save states */
	device->save_item(NAME(miot->port[0].in));
	device->save_item(NAME(miot->port[0].out));
	device->save_item(NAME(miot->port[0].ddr));
	device->save_item(NAME(miot->port[1].in));
	device->save_item(NAME(miot->port[1].out));
	device->save_item(NAME(miot->port[1].ddr));

	device->save_item(NAME(miot->irqstate));
	device->save_item(NAME(miot->irqenable));

	device->save_item(NAME(miot->timershift));
	device->save_item(NAME(miot->timerstate));
}


static DEVICE_RESET( mos6530 )
{
	mos6530_state *miot = get_safe_token(device);

	/* reset I/O states */
	miot->port[0].out = 0;
	miot->port[0].ddr = 0;
	miot->port[1].out = 0;
	miot->port[1].ddr = 0;

	/* reset IRQ states */
	miot->irqenable = 0;
	miot->irqstate = TIMER_FLAG;
	update_irqstate(device);

	/* reset timer states */
	miot->timershift = 0;
	miot->timerstate = TIMER_IDLE;
	miot->timer->adjust(attotime::never);
}


const device_type MOS6530 = &device_creator<mos6530_device>;

mos6530_device::mos6530_device(const machine_config &mconfig, const char *tag, device_t *owner, UINT32 clock)
	: device_t(mconfig, MOS6530, "MOS6530", tag, owner, clock, "mos6530", __FILE__)
{
	m_token = global_alloc_clear(mos6530_state);
}

//-------------------------------------------------
//  device_config_complete - perform any
//  operations now that the configuration is
//  complete
//-------------------------------------------------

void mos6530_device::device_config_complete()
{
}

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

void mos6530_device::device_start()
{
	DEVICE_START_NAME( mos6530 )(this);
}

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

void mos6530_device::device_reset()
{
	DEVICE_RESET_NAME( mos6530 )(this);
}
