/*
	aica interface
		Handles RTC, Display mode reg && arm reset reg !
	arm7 is handled on a separate arm plugin now
*/

#include "aica_if.h"
#include "aica_mem.h"
#include "hw/holly/sb.h"
#include "hw/holly/holly_intc.h"
#include "hw/sh4/sh4_mem.h"
#include "hw/sh4/sh4_sched.h"
#include "profiler/dc_profiler.h"
#include "hw/sh4/dyna/blockmanager.h"
#include "hw/sh4/sh4_interpreter.h"
#include "hw/arm7/arm7.h"
#include "cfg/option.h"

#include <ctime>

VArray2 aica_ram;
u32 VREG;
u32 ARMRST;
u32 rtc_EN;
int dma_sched_id = -1;
u32 RealTimeClock;
int rtc_schid = -1;
u32 SB_ADST;

u32 GetRTC_now()
{
	// rtc kept static for netplay when savestate is not loaded
	if (config::GGPOEnable)
		// 1/1/70 00:00:00
		return (20 * 365 + 5) * 24 * 60 * 60;

	// The Dreamcast Epoch time is 1/1/50 00:00 but without support for time zone or DST.
	// We compute the TZ/DST current time offset and add it to the result
	// as if we were in the UTC time zone (as well as the DC Epoch)
	time_t rawtime = time(NULL);
	struct tm localtm, gmtm;
	localtm = *localtime(&rawtime);
	gmtm = *gmtime(&rawtime);
	gmtm.tm_isdst = -1;
	time_t time_offset = mktime(&localtm) - mktime(&gmtm);
	// 1/1/50 to 1/1/70 is 20 years and 5 leap days
	return (20 * 365 + 5) * 24 * 60 * 60 + rawtime + time_offset;
}

template<typename T>
T ReadMem_aica_rtc(u32 addr)
{
	switch (addr & 0xFF)
	{
	case 0:
		return (T)(RealTimeClock >> 16);
	case 4:
		return (T)(RealTimeClock & 0xFFFF);
	case 8:
		return 0;
	}

	WARN_LOG(AICA, "ReadMem_aica_rtc: invalid address %x sz %d", addr, (int)sizeof(T));
	return 0;
}
template u8 ReadMem_aica_rtc<>(u32 addr);
template u16 ReadMem_aica_rtc<>(u32 addr);
template u32 ReadMem_aica_rtc<>(u32 addr);

template<typename T>
void WriteMem_aica_rtc(u32 addr, T data)
{
	switch (addr & 0xFF)
	{
	case 0:
		if (rtc_EN)
		{
			RealTimeClock &= 0xFFFF;
			RealTimeClock |= (data & 0xFFFF) << 16;
			rtc_EN = 0;
		}
		break;
	case 4:
		if (rtc_EN)
		{
			RealTimeClock &= 0xFFFF0000;
			RealTimeClock |= data & 0xFFFF;
			//TODO: Clean the internal timer ?
		}
		break;
	case 8:
		rtc_EN = data & 1;
		break;

	default:
		WARN_LOG(AICA, "WriteMem_aica_rtc: invalid address %x sz %d data %x", addr, (int)sizeof(T), data);
		break;
	}
}
template void WriteMem_aica_rtc<>(u32 addr, u8 data);
template void WriteMem_aica_rtc<>(u32 addr, u16 data);
template void WriteMem_aica_rtc<>(u32 addr, u32 data);

template<typename T>
T ReadMem_aica_reg(u32 addr)
{
	addr &= 0x7FFF;
	if (sizeof(T) == 1)
	{
		switch (addr)
		{
		case 0x2C00:
			return (T)ARMRST;
		case 0x2C01:
			return (T)VREG;
		default:
			break;
		}
	}
	else if (addr == 0x2C00)
		return (T)((VREG << 8) | ARMRST);

	if (sizeof(T) == 4)
		return aicaReadReg<u16>(addr);
	else
		return aicaReadReg<T>(addr);
}
template u8 ReadMem_aica_reg<>(u32 addr);
template u16 ReadMem_aica_reg<>(u32 addr);
template u32 ReadMem_aica_reg<>(u32 addr);

static void ArmSetRST()
{
	ARMRST &= 1;
	aicaarm::enable(ARMRST == 0);
}

template<typename T>
void WriteMem_aica_reg(u32 addr, T data)
{
	addr &= 0x7FFF;

	if (sizeof(T) == 1)
	{
		switch (addr)
		{
		case 0x2C00:
			ARMRST = data;
			INFO_LOG(AICA_ARM, "ARMRST = %02X", ARMRST);
			ArmSetRST();
			return;
		case 0x2C01:
			VREG = data;
			INFO_LOG(AICA_ARM, "VREG = %02X", VREG);
			return;
		default:
			break;
		}
	}
	else if (addr == 0x2C00)
	{
		VREG = (data >> 8) & 0xFF;
		ARMRST = data & 0xFF;
		INFO_LOG(AICA_ARM, "VREG = %02X ARMRST %02X", VREG, ARMRST);
		ArmSetRST();
		return;
	}
	if (sizeof(T) == 4)
		aicaWriteReg(addr, (u16)data);
	else
		aicaWriteReg(addr, data);
}
template void WriteMem_aica_reg<>(u32 addr, u8 data);
template void WriteMem_aica_reg<>(u32 addr, u16 data);
template void WriteMem_aica_reg<>(u32 addr, u32 data);

static int DreamcastSecond(int tag, int c, int j)
{
	RealTimeClock++;

	dc_prof_periodical();

#if FEAT_SHREC != DYNAREC_NONE
	bm_Periodical_1s();
#endif

	return SH4_MAIN_CLOCK;
}

//Init/res/term
void aica_Init()
{
	RealTimeClock = GetRTC_now();
	if (rtc_schid == -1)
		rtc_schid = sh4_sched_register(0, &DreamcastSecond);
}

void aica_Reset(bool hard)
{
	if (hard)
	{
		aica_Init();
		sh4_sched_request(rtc_schid, SH4_MAIN_CLOCK);
	}
	VREG = 0;
	ARMRST = 0;
}

void aica_Term()
{
	sh4_sched_unregister(rtc_schid);
	rtc_schid = -1;
}

static int dma_end_sched(int tag, int cycl, int jitt)
{
	u32 len = SB_ADLEN & 0x7FFFFFFF;

	if (SB_ADLEN & 0x80000000)
		SB_ADEN = 0;
	else
		SB_ADEN = 1;

	SB_ADSTAR += len;
	SB_ADSTAG += len;
	SB_ADST = 0;	// dma done
	SB_ADLEN = 0;

	// indicate that dma is not happening, or has been paused
	SB_ADSUSP |= 0x10;

	asic_RaiseInterrupt(holly_SPU_DMA);

	return 0;
}

static bool check_STAG(u32 addr)
{
#ifdef STRICT_MODE
	const u32 area = (addr >> 26) & 7;
	// Aica and G2 Ext dev #1 and #2 on area 0
	// G2 Ext dev #3 on area 5
	return area == 0 || area == 5;
#else
	return true;
#endif
}

static bool check_STAR(u32 addr)
{
#ifdef STRICT_MODE
	const u32 area = (addr >> 26) & 7;
	// System RAM and VRAM
	return area == 3 || area == 1;
#else
	return true;
#endif
}

static bool check_STAR_overrun(u32 addr)
{
#ifdef STRICT_MODE
	u32 bottom = (((SB_G2APRO >> 8) & 0x7f) << 20) | 0x08000000;
	u32 top = ((SB_G2APRO & 0x7f) << 20) | 0x080fffe0;

	return addr >= bottom && addr <= top;
#else
	return true;
#endif
}

template<u32 ENABLE, u32 START, u32 SRC, u32 DEST, u32 LEN, u32 DIR,
	HollyInterruptID interrupt, HollyInterruptID iainterrupt, HollyInterruptID ovinterrupt,
	const char *LogTag>
void Write_DmaStart(u32 addr, u32 data)
{
	u32& enableReg = SB_REGN_32(ENABLE);
	u32& startReg = SB_REGN_32(START);
	u32& sourceReg = SB_REGN_32(SRC);
	u32& destReg = SB_REGN_32(DEST);
	u32& lenReg = SB_REGN_32(LEN);
	const u32 dirReg = SB_REGN_32(DIR);

	if (!(data & 1) || enableReg == 0)
		return;
	u32 src = sourceReg;
	u32 dst = destReg;
	u32 len = lenReg & 0x7FFFFFFF;

	// STAR
	if (!check_STAR(src))
	{
		INFO_LOG(AICA, "%s: Invalid src address %08x", LogTag, src);
		startReg = 0;
		enableReg = 0;
		asic_RaiseInterrupt(iainterrupt);
		return;
	}
	// STAG
	if (!check_STAG(dst))
	{
		INFO_LOG(AICA, "%s: Invalid dst address %08x", LogTag, dst);
		startReg = 0;
		enableReg = 0;
		asic_RaiseInterrupt(iainterrupt);
		return;
	}
	// Overrun
	if (!check_STAR_overrun(src) || !check_STAR_overrun(src + len - 1))
	{
		INFO_LOG(AICA, "%s: Overrun address %08x len %x", LogTag, src, len);
		startReg = 0;
		enableReg = 0;
		asic_RaiseInterrupt(ovinterrupt);
		return;
	}

	if (dirReg == 1)
		std::swap(src, dst);
	DEBUG_LOG(AICA, "%s: DMA Write to %X from %X %d bytes", LogTag, dst, src, len);

	WriteMemBlock_nommu_dma(dst, src, len);

	if (lenReg & 0x80000000)
		enableReg = 0;
	else
		enableReg = 1;

	sourceReg += len;
	destReg += len;
	startReg = 0; // dma done
	lenReg = 0;

	asic_RaiseInterrupt(interrupt);
}

static void Write_SB_ADST(u32 addr, u32 data)
{
	//0x005F7800	SB_ADSTAG	RW	AICA:G2-DMA G2 start address 
	//0x005F7804	SB_ADSTAR	RW	AICA:G2-DMA system memory start address 
	//0x005F7808	SB_ADLEN	RW	AICA:G2-DMA length 
	//0x005F780C	SB_ADDIR	RW	AICA:G2-DMA direction 
	//0x005F7810	SB_ADTSEL	RW	AICA:G2-DMA trigger select 
	//0x005F7814	SB_ADEN		RW	AICA:G2-DMA enable
	//0x005F7818	SB_ADST		RW	AICA:G2-DMA start
	//0x005F781C	SB_ADSUSP	RW	AICA:G2-DMA suspend 
	
	if ((data & 1) == 1 && (SB_ADST & 1) == 0)
	{
		if (SB_ADEN == 1)
		{
			u32 src = SB_ADSTAR;
			u32 dst = SB_ADSTAG;
			u32 len = SB_ADLEN & 0x7FFFFFFF;

			if (!check_STAR(src))
			{
				INFO_LOG(AICA, "AICA-DMA : Invalid src address %08x", src);
				SB_ADST = 0;
				SB_ADEN = 0;
				asic_RaiseInterrupt(holly_AICA_ILLADDR);
				return;
			}
			if (!check_STAG(dst))
			{
				INFO_LOG(AICA, "AICA-DMA : Invalid dst address %08x", dst);
				SB_ADST = 0;
				SB_ADEN = 0;
				asic_RaiseInterrupt(holly_AICA_ILLADDR);
				return;
			}
			// Overrun
			if (!check_STAR_overrun(src) || !check_STAR_overrun(src + len - 1))
			{
				INFO_LOG(AICA, "AICA-DMA : Overrun address %08x len %x", src, len);
				SB_ADST = 0;
				SB_ADEN = 0;
				asic_RaiseInterrupt(holly_AICA_OVERRUN);
				return;
			}

			if (SB_ADDIR == 1)
			{
				//swap direction
				std::swap(src, dst);
				DEBUG_LOG(AICA, "AICA-DMA : SB_ADDIR==1 DMA Read to 0x%X from 0x%X %x bytes", dst, src, SB_ADLEN);
			}
			else
				DEBUG_LOG(AICA, "AICA-DMA : SB_ADDIR==0:DMA Write to 0x%X from 0x%X %x bytes", dst, src, SB_ADLEN);

			WriteMemBlock_nommu_dma(dst, src, len);

			// indicate that dma is in progress
			SB_ADST = 1;
			SB_ADSUSP &= ~0x10;

			// Schedule the end of DMA transfer interrupt
			int cycles = len * (SH4_MAIN_CLOCK / 2 / G2_BUS_CLOCK);       // 16 bits @ 25 MHz
			if (cycles < SH4_TIMESLICE / 2)
				dma_end_sched(0, 0, 0);
			else
				sh4_sched_request(dma_sched_id, cycles);
		}
	}
}

u32 Read_SB_ADST(u32 addr)
{
	// Le Mans and Looney Tunes sometimes send the same dma transfer twice after checking SB_ADST == 0.
	// To avoid this, we pretend SB_ADST is still set when there is a pending aica-dma interrupt.
	// This is only done once.
	if ((SB_ISTNRM & (1 << (u8)holly_SPU_DMA)) && !(SB_ADST & 2))
	{
		SB_ADST |= 2;
		return 1;
	}
	else
	{
		SB_ADST &= ~2;
		return SB_ADST;
	}
}

template<u32 STAG, HollyInterruptID iainterrupt, const char *LogTag>
void Write_SB_STAG(u32 addr, u32 data)
{
	u32& stagReg = sb_regs[(STAG - SB_BASE) / 4].data32;
	stagReg = data & 0x1FFFFFE0;

	if (!check_STAG(data))
	{
		INFO_LOG(AICA, "%s Write_SB_STAG: Invalid address %08x", LogTag, data);
		asic_RaiseInterrupt(iainterrupt);
	}
}

template<u32 STAR, HollyInterruptID iainterrupt, const char *LogTag>
void Write_SB_STAR(u32 addr, u32 data)
{
	u32& starReg = sb_regs[(STAR - SB_BASE) / 4].data32;
	starReg = data & 0x1FFFFFE0;

	if (!check_STAR(data))
	{
		INFO_LOG(AICA, "%s Write_SB_STAR: Invalid address %08x", LogTag, data);
		asic_RaiseInterrupt(iainterrupt);
	}
}

void Write_SB_G2APRO(u32 addr, u32 data)
{
	if ((data >> 16) == 0x4659)
		SB_G2APRO = data & 0x00007f7f;
}

extern const char AICA_TAG[] = "G2-AICA DMA";
extern const char EXT1_TAG[] = "G2-EXT1 DMA";
extern const char EXT2_TAG[] = "G2-EXT2 DMA";
extern const char DDEV_TAG[] = "G2-DDev DMA";

void aica_sb_Init()
{
	// G2-DMA registers

	// AICA
	sb_rio_register(SB_ADST_addr, RIO_FUNC, &Read_SB_ADST, &Write_SB_ADST);
	sb_rio_register(SB_ADSTAR_addr, RIO_WF, nullptr, &Write_SB_STAR<SB_ADSTAR_addr, holly_AICA_ILLADDR, AICA_TAG>);
	sb_rio_register(SB_ADSTAG_addr, RIO_WF, nullptr, &Write_SB_STAG<SB_ADSTAG_addr, holly_AICA_ILLADDR, AICA_TAG>);

	// G2 Ext device #1
	sb_rio_register(SB_E1ST_addr, RIO_WF, nullptr, &Write_DmaStart<SB_E1EN_addr, SB_E1ST_addr, SB_E1STAR_addr, SB_E1STAG_addr, SB_E1LEN_addr,
			SB_E1DIR_addr, holly_EXT_DMA1, holly_EXT1_ILLADDR, holly_EXT1_OVERRUN, EXT1_TAG>);
	sb_rio_register(SB_E1STAR_addr, RIO_WF, nullptr, &Write_SB_STAR<SB_E1STAR_addr, holly_EXT1_ILLADDR, EXT1_TAG>);
	sb_rio_register(SB_E1STAG_addr, RIO_WF, nullptr, &Write_SB_STAG<SB_E1STAG_addr, holly_EXT1_ILLADDR, EXT1_TAG>);

	// G2 Ext device #2
	sb_rio_register(SB_E2ST_addr, RIO_WF, nullptr, &Write_DmaStart<SB_E2EN_addr, SB_E2ST_addr, SB_E2STAR_addr, SB_E2STAG_addr, SB_E2LEN_addr,
			SB_E2DIR_addr, holly_EXT_DMA2, holly_EXT2_ILLADDR, holly_EXT2_OVERRUN, EXT2_TAG>);
	sb_rio_register(SB_E2STAR_addr, RIO_WF, nullptr, &Write_SB_STAR<SB_E2STAR_addr, holly_EXT2_ILLADDR, EXT2_TAG>);
	sb_rio_register(SB_E2STAG_addr, RIO_WF, nullptr, &Write_SB_STAG<SB_E2STAG_addr, holly_EXT2_ILLADDR, EXT2_TAG>);

	// G2 Ext device #3
	sb_rio_register(SB_DDST_addr, RIO_WF, nullptr, &Write_DmaStart<SB_DDEN_addr, SB_DDST_addr, SB_DDSTAR_addr, SB_DDSTAG_addr, SB_DDLEN_addr,
			SB_DDDIR_addr, holly_DEV_DMA, holly_DEV_ILLADDR, holly_DEV_OVERRUN, DDEV_TAG>);
	sb_rio_register(SB_DDSTAR_addr, RIO_WF, nullptr, &Write_SB_STAR<SB_DDSTAR_addr, holly_DEV_ILLADDR, DDEV_TAG>);
	sb_rio_register(SB_DDSTAG_addr, RIO_WF, nullptr, &Write_SB_STAG<SB_DDSTAG_addr, holly_DEV_ILLADDR, DDEV_TAG>);

	sb_rio_register(SB_G2APRO_addr, RIO_WO_FUNC, nullptr, &Write_SB_G2APRO);

	dma_sched_id = sh4_sched_register(0, &dma_end_sched);
}

void aica_sb_Reset(bool hard)
{
	if (hard) {
		SB_ADST = 0;
		SB_G2APRO = 0x7f00;
	}
}

void aica_sb_Term()
{
	sh4_sched_unregister(dma_sched_id);
	dma_sched_id = -1;
}
