/* Copyright (c) 2013-2015 Jeffrey Pfau
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <mgba/internal/gba/dma.h>

#include <mgba/internal/gba/gba.h>
#include <mgba/internal/gba/io.h>

static void _dmaEvent(struct mTiming* timing, void* context, uint32_t cyclesLate);

static void GBADMAService(struct GBA* gba, int number, struct GBADMA* info);

static const int DMA_OFFSET[] = { 1, -1, 0, 1 };

void GBADMAInit(struct GBA* gba) {
	gba->memory.dmaEvent.name = "GBA DMA";
	gba->memory.dmaEvent.callback = _dmaEvent;
	gba->memory.dmaEvent.context = gba;
	gba->memory.dmaEvent.priority = 0x40;
}

void GBADMAReset(struct GBA* gba) {
	memset(gba->memory.dma, 0, sizeof(gba->memory.dma));
	int i;
	for (i = 0; i < 4; ++i) {
		gba->memory.dma[i].count = 0x4000;
	}
	gba->memory.dma[3].count = 0x10000;
	gba->memory.activeDMA = -1;
}
static bool _isValidDMASAD(int dma, uint32_t address) {
	if (dma == 0 && address >= BASE_CART0 && address < BASE_CART_SRAM) {
		return false;
	}
	return address >= BASE_WORKING_RAM;
}

static bool _isValidDMADAD(int dma, uint32_t address) {
	return dma == 3 || address < BASE_CART0;
}

uint32_t GBADMAWriteSAD(struct GBA* gba, int dma, uint32_t address) {
	struct GBAMemory* memory = &gba->memory;
	address &= 0x0FFFFFFE;
	if (_isValidDMASAD(dma, address)) {
		memory->dma[dma].source = address;
	} else {
		memory->dma[dma].source = 0;
	}
	return memory->dma[dma].source;
}

uint32_t GBADMAWriteDAD(struct GBA* gba, int dma, uint32_t address) {
	struct GBAMemory* memory = &gba->memory;
	address &= 0x0FFFFFFE;
	if (_isValidDMADAD(dma, address)) {
		memory->dma[dma].dest = address;
	}
	return memory->dma[dma].dest;
}

void GBADMAWriteCNT_LO(struct GBA* gba, int dma, uint16_t count) {
	struct GBAMemory* memory = &gba->memory;
	memory->dma[dma].count = count ? count : (dma == 3 ? 0x10000 : 0x4000);
}

uint16_t GBADMAWriteCNT_HI(struct GBA* gba, int dma, uint16_t control) {
	struct GBAMemory* memory = &gba->memory;
	struct GBADMA* currentDma = &memory->dma[dma];
	int wasEnabled = GBADMARegisterIsEnable(currentDma->reg);
	if (dma < 3) {
		control &= 0xF7E0;
	} else {
		control &= 0xFFE0;
	}
	currentDma->reg = control;

	if (GBADMARegisterIsDRQ(currentDma->reg)) {
		mLOG(GBA_MEM, STUB, "DRQ not implemented");
	}

	if (!wasEnabled && GBADMARegisterIsEnable(currentDma->reg)) {
		currentDma->nextSource = currentDma->source;
		if (currentDma->nextSource >= BASE_CART0 && currentDma->nextSource < BASE_CART_SRAM && GBADMARegisterGetSrcControl(currentDma->reg) < 3) {
			currentDma->reg = GBADMARegisterClearSrcControl(currentDma->reg);
		}
		currentDma->nextDest = currentDma->dest;

		uint32_t width = 2 << GBADMARegisterGetWidth(currentDma->reg);
		if (currentDma->nextSource & (width - 1)) {
			mLOG(GBA_MEM, GAME_ERROR, "Misaligned DMA source address: 0x%08X", currentDma->nextSource);
		}
		if (currentDma->nextDest & (width - 1)) {
			mLOG(GBA_MEM, GAME_ERROR, "Misaligned DMA destination address: 0x%08X", currentDma->nextDest);
		}

		GBADMASchedule(gba, dma, currentDma);
	}
	// If the DMA has already occurred, this value might have changed since the function started
	return currentDma->reg;
};

void GBADMASchedule(struct GBA* gba, int number, struct GBADMA* info) {
	switch (GBADMARegisterGetTiming(info->reg)) {
	case GBA_DMA_TIMING_NOW:
		info->when = mTimingCurrentTime(&gba->timing) + 3; // DMAs take 3 cycles to start
		info->nextCount = info->count;
		break;
	case GBA_DMA_TIMING_HBLANK:
	case GBA_DMA_TIMING_VBLANK:
		// Handled implicitly
		return;
	case GBA_DMA_TIMING_CUSTOM:
		switch (number) {
		case 0:
			mLOG(GBA_MEM, WARN, "Discarding invalid DMA0 scheduling");
			return;
		case 1:
		case 2:
			GBAAudioScheduleFifoDma(&gba->audio, number, info);
			break;
		case 3:
			// Handled implicitly
			break;
		}
	}
	GBADMAUpdate(gba);
}

void GBADMARunHblank(struct GBA* gba, int32_t cycles) {
	struct GBAMemory* memory = &gba->memory;
	struct GBADMA* dma;
	int i;
	for (i = 0; i < 4; ++i) {
		dma = &memory->dma[i];
		if (GBADMARegisterIsEnable(dma->reg) && GBADMARegisterGetTiming(dma->reg) == GBA_DMA_TIMING_HBLANK && !dma->nextCount) {
			dma->when = mTimingCurrentTime(&gba->timing) + 3 + cycles;
			dma->nextCount = dma->count;
		}
	}
	GBADMAUpdate(gba);
}

void GBADMARunVblank(struct GBA* gba, int32_t cycles) {
	struct GBAMemory* memory = &gba->memory;
	struct GBADMA* dma;
	int i;
	for (i = 0; i < 4; ++i) {
		dma = &memory->dma[i];
		if (GBADMARegisterIsEnable(dma->reg) && GBADMARegisterGetTiming(dma->reg) == GBA_DMA_TIMING_VBLANK && !dma->nextCount) {
			dma->when = mTimingCurrentTime(&gba->timing) + 3 + cycles;
			dma->nextCount = dma->count;
		}
	}
	GBADMAUpdate(gba);
}

void GBADMARunDisplayStart(struct GBA* gba, int32_t cycles) {
	struct GBAMemory* memory = &gba->memory;
	struct GBADMA* dma = &memory->dma[3];
	if (GBADMARegisterIsEnable(dma->reg) && GBADMARegisterGetTiming(dma->reg) == GBA_DMA_TIMING_CUSTOM && !dma->nextCount) {
		dma->when = mTimingCurrentTime(&gba->timing) + 3 + cycles;
		dma->nextCount = dma->count;
		GBADMAUpdate(gba);
	}
}

void _dmaEvent(struct mTiming* timing, void* context, uint32_t cyclesLate) {
	UNUSED(timing);
	UNUSED(cyclesLate);
	struct GBA* gba = context;
	struct GBAMemory* memory = &gba->memory;
	struct GBADMA* dma = &memory->dma[memory->activeDMA];
	if (dma->nextCount == dma->count) {
		dma->when = mTimingCurrentTime(&gba->timing);
	}
	if (dma->nextCount & 0xFFFFF) {
		GBADMAService(gba, memory->activeDMA, dma);
	} else {
		dma->nextCount = 0;
		bool noRepeat = !GBADMARegisterIsRepeat(dma->reg);
		noRepeat |= GBADMARegisterGetTiming(dma->reg) == GBA_DMA_TIMING_NOW;
		noRepeat |= memory->activeDMA == 3 && GBADMARegisterGetTiming(dma->reg) == GBA_DMA_TIMING_CUSTOM && gba->video.vcount == VIDEO_VERTICAL_PIXELS + 1;
		if (noRepeat) {
			dma->reg = GBADMARegisterClearEnable(dma->reg);

			// Clear the enable bit in memory
			memory->io[(REG_DMA0CNT_HI + memory->activeDMA * (REG_DMA1CNT_HI - REG_DMA0CNT_HI)) >> 1] &= 0x7FE0;
		}
		if (GBADMARegisterGetDestControl(dma->reg) == GBA_DMA_INCREMENT_RELOAD) {
			dma->nextDest = dma->dest;
		}
		if (GBADMARegisterIsDoIRQ(dma->reg)) {
			GBARaiseIRQ(gba, IRQ_DMA0 + memory->activeDMA);
		}
		GBADMAUpdate(gba);
	}
}

void GBADMAUpdate(struct GBA* gba) {
	int i;
	struct GBAMemory* memory = &gba->memory;
	uint32_t currentTime = mTimingCurrentTime(&gba->timing);
	int32_t leastTime = INT_MAX;
	memory->activeDMA = -1;
	for (i = 0; i < 4; ++i) {
		struct GBADMA* dma = &memory->dma[i];
		if (GBADMARegisterIsEnable(dma->reg) && dma->nextCount) {
			int32_t time = dma->when - currentTime;
			if (memory->activeDMA == -1 || (dma->count == dma->nextCount && time < leastTime)) {
				leastTime = time;
				memory->activeDMA = i;
			}
		}
	}

	if (memory->activeDMA >= 0) {
		mTimingDeschedule(&gba->timing, &memory->dmaEvent);
		mTimingSchedule(&gba->timing, &memory->dmaEvent, memory->dma[memory->activeDMA].when - currentTime);
	} else {
		gba->cpuBlocked = false;
	}
}

void GBADMAService(struct GBA* gba, int number, struct GBADMA* info) {
	struct GBAMemory* memory = &gba->memory;
	struct ARMCore* cpu = gba->cpu;
	uint32_t width = 2 << GBADMARegisterGetWidth(info->reg);
	int32_t wordsRemaining = info->nextCount;
	uint32_t source = info->nextSource;
	uint32_t dest = info->nextDest;
	uint32_t sourceRegion = source >> BASE_OFFSET;
	uint32_t destRegion = dest >> BASE_OFFSET;
	int32_t cycles = 2;

	gba->cpuBlocked = true;
	if (info->count == info->nextCount) {
		if (width == 4) {
			cycles += memory->waitstatesNonseq32[sourceRegion] + memory->waitstatesNonseq32[destRegion];
		} else {
			cycles += memory->waitstatesNonseq16[sourceRegion] + memory->waitstatesNonseq16[destRegion];
		}
		source &= -width;
		dest &= -width;
	} else {
		if (width == 4) {
			cycles += memory->waitstatesSeq32[sourceRegion] + memory->waitstatesSeq32[destRegion];
		} else {
			cycles += memory->waitstatesSeq16[sourceRegion] + memory->waitstatesSeq16[destRegion];
		}
	}
	info->when += cycles;

	gba->performingDMA = 1 | (number << 1);
	if (width == 4) {
		if (source) {
			memory->dmaTransferRegister = cpu->memory.load32(cpu, source, 0);
		}
		gba->bus = memory->dmaTransferRegister;
		cpu->memory.store32(cpu, dest, memory->dmaTransferRegister, 0);
	} else {
		if (sourceRegion == REGION_CART2_EX && memory->savedata.type == SAVEDATA_EEPROM) {
			if (memory->savedata.type == SAVEDATA_AUTODETECT) {
				mLOG(GBA_MEM, INFO, "Detected EEPROM savegame");
				GBASavedataInitEEPROM(&memory->savedata);
			}
			memory->dmaTransferRegister = GBASavedataReadEEPROM(&memory->savedata);
			memory->dmaTransferRegister |= memory->dmaTransferRegister << 16;
		} else if (source) {
			memory->dmaTransferRegister = cpu->memory.load16(cpu, source, 0);
			memory->dmaTransferRegister |= memory->dmaTransferRegister << 16;
		}
		if (destRegion == REGION_CART2_EX) {
			if (memory->savedata.type == SAVEDATA_AUTODETECT) {
				mLOG(GBA_MEM, INFO, "Detected EEPROM savegame");
				GBASavedataInitEEPROM(&memory->savedata);
			}
			if (memory->savedata.type == SAVEDATA_EEPROM) {
				GBASavedataWriteEEPROM(&memory->savedata, memory->dmaTransferRegister, wordsRemaining);
			}
		} else {
			cpu->memory.store16(cpu, dest, memory->dmaTransferRegister, 0);

		}
		gba->bus = memory->dmaTransferRegister;
	}
	int sourceOffset = DMA_OFFSET[GBADMARegisterGetSrcControl(info->reg)] * width;
	int destOffset = DMA_OFFSET[GBADMARegisterGetDestControl(info->reg)] * width;
	if (source) {
		source += sourceOffset;
	}
	dest += destOffset;
	--wordsRemaining;
	gba->performingDMA = 0;

	info->nextCount = wordsRemaining;
	info->nextSource = source;
	info->nextDest = dest;
	if (!wordsRemaining) {
		info->nextCount |= 0x80000000;
		if (sourceRegion < REGION_CART0 || destRegion < REGION_CART0) {
			info->when += 2;
		}
	}
	GBADMAUpdate(gba);
}
