#include "../interface.h"
#include <time.h>

Vdp::Vdp(_System* _sys, Bus& bus, Cpu& cpu)
:
_sys(_sys),
bus(bus),
cpu(cpu)
{
	bus.mmio.map(4, this);
	bus.mmio.map(5, this);

	frameBuffer = new u8[284 * 400];
	frames_executed = frame_count = 0;
	vblancLines = 19;
	frameCounter = 0;
}

void Vdp::setLineBuffer() {
	linePtr = frameBuffer + lineBufferCounter * 284;
	lineBufferCounter++;
	linePos = 0;
}

Vdp::VMode Vdp::getVMode( u16 _v ) {
	if (_v < screenHeight ) return VMode::DISPLAY;
	if (_v < ( screenHeight + borderBottom ) ) return VMode::BORDER_BOTTOM;
	if (_v < ( screenHeight + borderBottom + vblancLines ) ) return VMode::BLANKING;
	return VMode::BORDER_TOP;
}

void Vdp::enter() {
	
	while (true) {
		if (vmode == VMode::BLANKING) {
			if (!frameComplete) {
				_sys->render();
				cacheVScroll = regs[9];
				frameComplete = true;
			}
		}
		
		//first 256 VDP cycles
		if (vmode == VMode::DISPLAY) buildScreen();
		else {
			for(u8 col=0; col < 32; col++) { 
				processH(); processH(); processH();
				if ( (vmode != VMode::BLANKING) && ((col % 4) != 0) ) (this->*spritePreProcessing)(); //sprite preprocessing in border top and bottom
				processH(); processH(); processH(); processH(); processH();
			}
		}
		//hblanc
		if (vmode != VMode::BLANKING) buildSprite();
		else {
			for (u8 i=0; i < 70; i++) {
				processH();
			}
		}
		for (u8 i=0; i<8; i++) {
			processH();
			if (vmode != VMode::BLANKING) (this->*spritePreProcessing)();
			processH();
		}
	}
}

void Vdp::handleCpuAccess() { //emulating access window
	//this part is for sure the most unknown in SMS emulation
	bool isAccess = false; //access window for vram read, vram write, cgram write ... assumption: cgram writes are handled like vram read/writes
	bool isRegAccess = false; //access window for register writes. assumption: only delayed during active display
	
	if (access.waitJob != CpuAccess::none) {
		//vram read, vram write and cgram write access windows are based on the timing diagram found at smspower.org
		if (cacheDisplayActive && hmode == HMode::ACTIVE && vmode == VMode::DISPLAY) { //active screen
			if ( (hcounter >= 3) && ( ( (hcounter - 3) % 32) == 0) ) isAccess = true;
			if ( (hcounter >= 3) && ( ( (hcounter - 3) % 21) == 0) ) isRegAccess = true; //six consecutive "out (C), r" give you a problem, outir is no problem
		} else if ( vmode != VMode::BLANKING && hmode != HMode::ACTIVE ) { //sprite processing
			//if (hcounter == 289 || hcounter == 291 || hcounter == 293 || hcounter == 295 || hcounter == 297 || hcounter == 325 || hcounter == 323 || hcounter == 321 || hcounter == 319 || hcounter == 257 || hcounter == 259) {
			if ( (hcounter % 2) == 0 ) { //assumption: the above from the timing diagram seems not to work for an SMS VDP
				//todo: find out the access windows in this area
				isAccess = true;
			}
			isRegAccess = true;
		} else {
			isAccess = true;
			isRegAccess = true;
		}

		if (access.waitJob == CpuAccess::writeReg && isRegAccess) cpu_access();
		else if (isAccess) cpu_access();
	}
	updateDelay();
}

void Vdp::processH() {
	hcounter++;
	handleCpuAccess();
	if (vmode != VMode::BLANKING) {
		if ( (cacheDisplayActive && vmode == VMode::DISPLAY) && isSprCol(hcounter) ) {
			statusReg |= 0x20;
		} if (vmode != VMode::DISPLAY && isSprCol(hcounter) && screenHeight == 192 ) {
			statusReg |= 0x20;
		}
	}

	switch (hmode) {
		case HMode::ACTIVE:
			if (vmode == VMode::BORDER_TOP || vmode == VMode::BORDER_BOTTOM) {
				*(linePtr + linePos++) = (this->*getBackdrop)();
			}
			if (hcounter == 256) {
				hmode = HMode::RIGHT;
			}
			break;
		case HMode::RIGHT:
			if (vmode != VMode::BLANKING) {
				*(linePtr + linePos++) = (this->*getBackdrop)();
				if (hcounter == 271) setLineBuffer();
			}
			if (hcounter == 271) {
				hmode = HMode::HBLANKING;
			}
			break;
		case HMode::HBLANKING:
			if (hcounter == 318) {
				(this->*checkSpriteOverflow)();
				incrementVCounter();
				vmode = getVMode( vcounter );
				spriteInit();
				cacheHScroll = regs[8];
				cacheDisplayActive = getBit(regs[1], 6);
				cacheBackDrop = regs[7];
			}

			if (hcounter == 329) {
				hmode = HMode::LEFT;
			}
			break;
		case HMode::LEFT:
			if (vmode != VMode::BLANKING) {
				*(linePtr + linePos++) = (this->*getBackdrop)();
			}

			if (hcounter == 342) {
				hcounter = 0;
				hmode = HMode::ACTIVE;
			}
			break;
	}
	addClocks(1);
}

void Vdp::incrementVCounter() {

	if (++vcounter >= (_sys->_isNTSC() ? 262 : 313)  ) { //frame complete
		vcounter = 0;
		frameComplete = false;
		frame();
		
		if (updateScreenDim) {
			updateScreenDim = false;
			refreshScreenDimension();
		}
	}
}

void Vdp::buildScreen() {

	for(u8 col=0; col < 32; col++) { //active display (32 * 8) -> vdp cycles
		if (screenHeight != 192 && col == 12 ) cacheDisplayActive = getBit(regs[1], 6);
		if ( !cacheDisplayActive ) {
			for (u8 i=0; i<8; i++) {
				processH();
				if ( (i == 2) && ( (col % 4) != 0 ) ) (this->*spritePreProcessing)();
				*(linePtr + linePos++) = (this->*getBackdrop)();
			}
			continue;
		}
		processH();
		(this->*name_address_select)(col);
		processH();
		(this->*pattern_ram_load)();
		processH();
		if ( (col % 4) != 0 ) (this->*spritePreProcessing)();
		processH();
		(this->*name_ram_load)();
		processH();
		(this->*color_address_select)();
		processH();
		processH();
		(this->*pattern_address_select)(col);
		processH();
		(this->*color_ram_load)(col);
	}
}

void Vdp::buildSprite() {
	processH();
	processH();
	processH();
	processH();

	for (u8 i=0; i<4; i++) {
		processH();
		processH();
		processH();
		processH();
		processH();
		processH();

		processH();
		processH();
		processH();
		processH();
		
		processH();
		processH();
		if (i==0) clearSprCol();
		//todo: split in single operations
		(this->*calcSprite)( i );
		processH();
		processH();
		processH();
	}

	for (u8 i=0; i<6; i++) {
		processH();
	}
}

void Vdp::spriteInit() {
	spr.seekPos = 0;
	spr.count = 0;
	spr.cancel = false;
	spr.Line = (_sys->_isNTSC() ? 262 : 313) == vcounter + 1 ? 0 : vcounter + 1;
	spr.Line = getRealVCounter( spr.Line );
}

void Vdp::frame() {
	frame_count++;

#ifdef COUNT_ALL_FRAMES
	frameCounter++;
	
	//Wonsiin
	//if (frameCounter == 200) cpu.butPressed();
	//if (frameCounter == 250) cpu.butNotPressed();
	//if (frameCounter == 300) cpu.butPressed();
	//if (frameCounter == 350) cpu.butNotPressed();

	//VDP Tester
	//if (frameCounter == 400) cpu.butPressed();
	//if (frameCounter == 460) cpu.butNotPressed();
	//if (frameCounter == 490) cpu.butPressed();
	//if (frameCounter == 520) cpu.butNotPressed();
	//if (frameCounter == 800) cpu.butPressed();
	//if (frameCounter == 850) cpu.butNotPressed();
	//if (frameCounter == 900) cpu.butPressed();
	//if (frameCounter == 950) cpu.butNotPressed();
	//if (frameCounter == 990) cpu.butPressed();

	//Dizzy
	//if (frameCounter == 400) cpu.butPressed();
	//if (frameCounter == 460) cpu.butNotPressed();
	//if (frameCounter == 490) cpu.butPressed();
	//if (frameCounter == 520) cpu.butNotPressed();
	//if (frameCounter == 700) cpu.butPressed();
	//if (frameCounter == 750) cpu.butNotPressed();
	//if (frameCounter == 800) cpu.butPressed();
#endif

   time(&curr_t);
   if(curr_t != prev_t) {
	  frames_executed = frame_count;
	  frame_count = 0;
   }
   prev_t = curr_t;
}

void Vdp::clearSprCache() {
	for (int i = 0; i < 256; i++) {
		pal[i].spr = 16; //transparent
	}
}

void Vdp::calcFrameBorder() {
	//todo: changing screen height is currently processed with the beginning of a new frame. The real machine can do this mid frame, maybe on each scanline?
	vblancLines = 19;
	if (_sys->_isNTSC() && screenHeight == 192) {
		borderTop = 27;
		borderBottom = 24;
	} else if (_sys->_isNTSC() && screenHeight == 224) {
		borderTop = 11;
		borderBottom = 8;
	} else if (_sys->_isNTSC() && screenHeight == 240) {
		//this mode is not working on a ntsc machine.
		borderTop = 6;
		borderBottom = 16;
		vblancLines = 0;
	} else if (!_sys->_isNTSC() && screenHeight == 192) {
		borderTop = 54;
		borderBottom = 48;
	} else if (!_sys->_isNTSC() && screenHeight == 224) {
		borderTop = 38;
		borderBottom = 32;
	} else if (!_sys->_isNTSC() && screenHeight == 240) {
		borderTop = 30;
		borderBottom = 24;
	}
}

void Vdp::refreshScreenDimension() {
	u8 res = 0;
	res |= getBit(regs[0], 2) << 3;
	res |= getBit(regs[1], 3) << 2;
	res |= getBit(regs[0], 1) << 1;
	res |= getBit(regs[1], 4);

	u16 tempHeight = screenHeight;
	screenHeight = 192;

	switch(res) {
		case 0:		screenMode = MODE_0; prepareForMode0(); break; //Graphics 1
		case 1:		screenMode = MODE_1; break; //Text
		case 2:		screenMode = MODE_2; prepareForMode2(); break; //Graphics 2
		case 3:		screenMode = MODE_1_2; break;
		case 4:		screenMode = MODE_3; break; //Multicolor
		case 5:		screenMode = MODE_1_3; break;
		case 6:		screenMode = MODE_2_3; break;
		case 7:		screenMode = MODE_1_2_3; break;
		case 8:		screenMode = MODE_4; prepareForMode4(); break; //Master System
		case 9:		screenMode = MODE_1; break;
		case 10:	screenMode = MODE_4; prepareForMode4(); break;
		case 11:	screenMode = MODE_4_224; prepareForMode4();
					if (_sys->_Unit() != _System::Unit::SMS1) { 
						screenHeight = 224;
					}
					break;
		case 12:	screenMode = MODE_4; prepareForMode4(); break;
		case 13:	screenMode = MODE_1; break;
		case 14:	screenMode = MODE_4_240; prepareForMode4();
					if (_sys->_Unit() != _System::Unit::SMS1) {
						screenHeight = 240;
					}
					break;
		case 15:	screenMode = MODE_4; prepareForMode4(); break;
	}

	if (tempHeight != screenHeight) {
		calcFrameBorder();
	}
}

void Vdp::initDelay(CpuAccess newjob) {
	access.time = 7;
	access.prepareJob = newjob;
}

void Vdp::updateDelay() { //emulating VDP delay
	if (access.prepareJob == CpuAccess::none) return;
	if (access.time == 0) return;

	if (--access.time == 0) {
		access.waitJob = access.prepareJob;
		access.prepareJob = CpuAccess::none;
		access.address = commandWord & 0x3FFF;
		//access.data = dataPort;
		if (access.waitJob != writeReg) {
			u8 cmd = commandWord >> 14;
			commandWord = ( (commandWord+1) & 0x3FFF) | (cmd << 14);
		}
	}
}

void Vdp::cpu_access() {

	switch (access.waitJob) {
		case none:
			return; //nothing to do
		case readVram:
			dataPort = mem::vram.read( access.address );
			break;
		case writeVram:
			mem::vram.write(access.address, dataPort);
			break;
		case writeCgRam:
			cgRam[access.address & 0x1F] = dataPort;
			break;
		case writeReg: {
			switch( (commandWord >> 8) & 0xF) {
				default: break;
				case 0:
					regs[0] = commandWord & 0xFF;
					cpu.lineirq.activate(getBit(regs[0], 4));
					updateScreenDim = true;
					break;
				case 1:
					regs[1] = commandWord & 0xFF;
					cpu.frameirq.activate(getBit(regs[1], 5));
					updateScreenDim = true;
					break;
				case 2: regs[2] = commandWord & 0xFF; break;
				case 3: regs[3] = commandWord & 0xFF; break; 
				case 4: regs[4] = commandWord & 0xFF; break; 
				case 5: regs[5] = commandWord & 0xFF; break;
				case 6: regs[6] = commandWord & 0xFF; break;
				case 7: regs[7] = commandWord & 0xFF; break;
				case 8: regs[8] = commandWord & 0xFF; break;
				case 9: regs[9] = commandWord & 0xFF; break;
				case 0xA: regs[0xA] = commandWord & 0xFF; break;
			}
		} break;

	}

	access.waitJob = none;
}

void Vdp::addClocks(u8 cycles) {
	_sys->addclocks_vdp(cycles);
	_sys->sync_vdpcpu();
}

void Vdp::mmio_write(u8 pos, u8 value) {
	_sys->sync_cpuvdp();
	switch(pos) {
		case 4: write_port_BE(value); break;
		case 5: write_port_BF(value); break;
	}
}

u8 Vdp::mmio_read(u8 pos) {
	_sys->sync_cpuvdp();
	switch(pos) {
		case 4: return read_port_BE();
		case 5: return read_port_BF();
	}
}

void Vdp::write_port_BE(u8 value) {
	commandSecond = false;
	dataPort = value;

	switch( commandWord >> 14 ) {
		case 0:
		case 1:
		case 2: initDelay(CpuAccess::writeVram); break;
		case 3: initDelay(CpuAccess::writeCgRam); break;
	}
}

void Vdp::write_port_BF(u8 value) {
	if (!commandSecond) {
		commandWord &= 0xFF00 ;
		commandWord |= value ;
	} else {
		commandWord = (value << 8) | (commandWord & 0xFF);

		switch( commandWord >> 14 ) {
			case 0: initDelay(CpuAccess::readVram); break;
			case 1: break;
			case 2: access.waitJob = writeReg; break;
			case 3: break;
		}
	}
	commandSecond ^= 1;
}

u8 Vdp::read_port_BE() {
	commandSecond = false;
	initDelay(CpuAccess::readVram);
	return dataPort;
}

u8 Vdp::read_port_BF() {
	commandSecond = false;
	u8 res = (this->*resetStatusReg)();
	cpu.frameirq.resetPin();
	cpu.frameirq.resetRegister();
	cpu.frameirq.clearCache();
	cpu.lineirq.resetPin();
	cpu.lineirq.resetRegister();
	cpu.lineirq.clearCache();
	return res;
}

void Vdp::power() {
	prepareForMode4();
	mem::vram.init(0);
	commandSecond = false;
	dataPort = 0;
	commandBuffer = 0;
	commandWord = 0;
	statusReg = 0;
	access.prepareJob = CpuAccess::none;
	access.waitJob = CpuAccess::none;
	screenHeight = 192;
	screenMode = MODE_4;
	calcFrameBorder();
	vcounter = hcounter = 0;
	frameComplete = false;

	cacheVScroll = 0;
	cacheHScroll = 0;

	for (int i=0; i < 32; i++) {
		cgRam[i] = 0;
	}
	for (int i=0; i < 0xa; i++) {
		regs[i] = 0;
	}
	regs[0x0] = 0x36;
	regs[0x1] = 0x80;
	
	regs[0x2] = 0xff;
	regs[0x3] = 0xff;
	regs[0x4] = 0xff;
	regs[0x5] = 0xff;
	regs[0x6] = 0xfb;
	regs[0x7] = 0x00;

	regs[0xa] = 0xff;
	cacheBackDrop = 0;

	clearSprCache();
	spriteInit();

	lineBufferCounter = 0;
	setLineBuffer();
	hmode = HMode::ACTIVE;
	updateScreenDim = false;
	cpu.lineirq.activate(getBit(regs[0], 4));
	vmode = getVMode( vcounter );
}

u8 Vdp::getInternalVCounter( u16 v ) {
	if (_sys->_isNTSC()) {
		if (screenHeight == 192) {
			return v > 0xDA ? v - 6 : v;
		} else if (screenHeight == 224) {
			return v > 0xEA ? v - 6 : v;
		} else {
			return v > 0xFF ? v - 0x100 : v;
		}
	} else {
		if (screenHeight == 192) {
			return v > 0xF2 ? v - 0x39 : v;
		} else if (screenHeight == 224) {
			if (v > 0xFF) {
				return (v - 0x100) > 0x02 ? v - 0x100 + 0xC7 : v - 0x100;
			}
			return v;
		} else {
			if (v > 0xFF) {
				return (v - 0x100) > 0x0A ? v - 0x100 + 0xC7 : v - 0x100;
			}
			return v;
		}
	}
}

int Vdp::getRealVCounter( u16 v ) {
	if (_sys->_isNTSC()) {
		if (screenHeight == 192) {
			return v > 0xda ? v - 262 : v;
		} else if (screenHeight == 224) {
			return v > 0xea ? v - 262 : v;
		} else {
			return v > 0xff ? v - 262 : v;
		}
	} else {
		if (screenHeight == 192) {
			return v > 0xf2 ? v - 313 : v;
		} else if (screenHeight == 224) {
			return v > 0x102 ? v - 313 : v;
		} else {
			return v > 0x10a ? v - 313 : v;
		}
	}
}

