// Copyright (C) 2003 Dolphin Project.

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License 2.0 for more details.

// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/

// Official SVN repository and contact information can be found at
// http://code.google.com/p/dolphin-emu/

#include "FifoDataFile.h"
#include "FifoPlayer.h"

#include "Common.h"
#include "CoreTiming.h"
#include "Thread.h"

#include "HW/GPFifo.h"
#include "HW/Memmap.h"
#include "HW/SystemTimers.h"
#include "PowerPC/PowerPC.h"

#include "BPMemory.h"
#include "CommandProcessor.h"

FifoPlayer::~FifoPlayer()
{
	delete m_File;
}

bool FifoPlayer::Open(const std::string& filename)
{
	Close();

	m_File = FifoDataFile::Load(filename, false);

	if (m_File)
	{
		FifoPlaybackAnalyzer analyzer;
		analyzer.AnalyzeFrames(m_File, m_FrameInfo);

		m_FrameRangeEnd = m_File->GetFrameCount();
	}

	if (m_FileLoadedCb)
		m_FileLoadedCb();

	return (m_File != NULL);
}

void FifoPlayer::Close()
{
	delete m_File;
	m_File = NULL;

	m_FrameRangeStart = 0;
	m_FrameRangeEnd = 0;
}

bool FifoPlayer::Play()
{
	if (!m_File)
		return false;

	if (m_File->GetFrameCount() == 0)
		return false;

	m_CurrentFrame = m_FrameRangeStart;

	LoadMemory();

	// This loop replaces the CPU loop that occurs when a game is run
	while (PowerPC::GetState() != PowerPC::CPU_POWERDOWN)
	{
		if (PowerPC::GetState() == PowerPC::CPU_RUNNING)
		{
			if (m_CurrentFrame >= m_FrameRangeEnd)
			{
				m_CurrentFrame = m_FrameRangeStart;

				CoreTiming::downcount = 0;
				CoreTiming::Advance();
			}
			else
			{
				if (m_FrameWrittenCb)
					m_FrameWrittenCb();

				if (m_EarlyMemoryUpdates && m_CurrentFrame == m_FrameRangeStart)
					WriteAllMemoryUpdates();

				// Stop Fifo processing until we've written the new frame
				WriteCP(CommandProcessor::CTRL_REGISTER, 0x10);  // disable read & breakpoints, enable GP link

				// Write frame data
				WriteFrame(m_File->GetFrame(m_CurrentFrame), m_FrameInfo[m_CurrentFrame]);

				// Enable frame processing and break when done
				u16 write_ptr_lo = ReadCP(CommandProcessor::FIFO_WRITE_POINTER_LO);
				u16 write_ptr_hi = ReadCP(CommandProcessor::FIFO_WRITE_POINTER_HI);
				WriteCP(CommandProcessor::FIFO_BP_LO, write_ptr_lo);
				WriteCP(CommandProcessor::FIFO_BP_HI, write_ptr_hi);
				WriteCP(CommandProcessor::CTRL_REGISTER, 0x13);  // enable read, breakpoints & GP link

				// If necessary, wait until GP has reached the breakpoint to prevent fifo overflows
				// TODO: Can this be done any better? Dual core mode is slower than single core mode even with these conditions..
				if (m_CurrentFrame < m_FrameRangeEnd)
				{
					// Check if FIFO would be overflown when writing the next frame
					u32 CPRWDistance = (ReadCP(CommandProcessor::FIFO_RW_DISTANCE_HI)<<16) | ReadCP(CommandProcessor::FIFO_RW_DISTANCE_LO);
					CPRWDistance += m_File->GetFrame(m_CurrentFrame+1).fifoDataSize + CommandProcessor::GATHER_PIPE_SIZE;
					u32 CPFifoBase = (ReadCP(CommandProcessor::FIFO_BASE_HI)<<16) | ReadCP(CommandProcessor::FIFO_BASE_LO);
					u32 CPFifoEnd = (ReadCP(CommandProcessor::FIFO_END_HI)<<16) | ReadCP(CommandProcessor::FIFO_END_LO);

					bool bWait = (CPRWDistance > CPFifoEnd - CPFifoBase);
					while (bWait && (ReadCP(CommandProcessor::FIFO_READ_POINTER_LO) != write_ptr_lo ||
									ReadCP(CommandProcessor::FIFO_READ_POINTER_HI) != write_ptr_hi))
					{
						Common::YieldCPU();
						CoreTiming::Advance(); // Process scheduled events (esp. PixelEngine::SetFinish!)

						if (PowerPC::GetState() == PowerPC::CPU_POWERDOWN)
							break;
					}
				}

				++m_CurrentFrame;
			}
		}
	}

	return true;
}

u32 FifoPlayer::GetFrameObjectCount()
{
	if (m_CurrentFrame < m_FrameInfo.size())
		return m_FrameInfo[m_CurrentFrame].objectStarts.size();

	return 0;
}

void FifoPlayer::SetFrameRangeStart(u32 start)
{
	if (m_File)
	{
		u32 frameCount = m_File->GetFrameCount();
		if (start > frameCount)
			start = frameCount;

		m_FrameRangeStart = start;
		if (m_FrameRangeEnd < start)
			m_FrameRangeEnd = start;

		if (m_CurrentFrame < m_FrameRangeStart)
			m_CurrentFrame = m_FrameRangeStart;
	}
}

void FifoPlayer::SetFrameRangeEnd(u32 end)
{
	if (m_File)
	{
		u32 frameCount = m_File->GetFrameCount();
		if (end > frameCount)
			end = frameCount;

		m_FrameRangeEnd = end;
		if (m_FrameRangeStart > end)
			m_FrameRangeStart = end;

		if (m_CurrentFrame >= m_FrameRangeEnd)
			m_CurrentFrame = m_FrameRangeStart;
	}
}

FifoPlayer &FifoPlayer::GetInstance()
{
	static FifoPlayer instance;
	return instance;
}

FifoPlayer::FifoPlayer() :
	m_CurrentFrame(0),
	m_FrameRangeStart(0),
	m_FrameRangeEnd(0),
	m_ObjectRangeStart(0),
	m_ObjectRangeEnd(10000),
	m_EarlyMemoryUpdates(false),
	m_FileLoadedCb(NULL),
	m_FrameWrittenCb(NULL),
	m_File(NULL)
{
}

void FifoPlayer::WriteFrame(const FifoFrameInfo &frame, const AnalyzedFrameInfo &info)
{
	// Core timing information
	m_CyclesPerFrame = SystemTimers::GetTicksPerSecond() / 60;
	m_ElapsedCycles = 0;
	m_FrameFifoSize = frame.fifoDataSize;

	// Determine start and end objects
	u32 numObjects = info.objectStarts.size();
	u32 drawStart = std::min(numObjects, m_ObjectRangeStart);
	u32 drawEnd = std::min(numObjects - 1, m_ObjectRangeEnd);

	u32 position = 0;
	u32 memoryUpdate = 0;

	// Skip memory updates during frame if true
	if (m_EarlyMemoryUpdates)
		memoryUpdate = frame.memoryUpdates.size();

	if (numObjects > 0)
	{
		u32 objectNum = 0;

		// Write fifo data skipping objects before the draw range
		while (objectNum < drawStart)
		{
			WriteFramePart(position, info.objectStarts[objectNum], memoryUpdate, frame, info);

			position = info.objectEnds[objectNum];
			++objectNum;		
		}

		// Write objects in draw range
		if (objectNum < numObjects && drawStart <= drawEnd)
		{
			objectNum = drawEnd;
			WriteFramePart(position, info.objectEnds[objectNum], memoryUpdate, frame, info);
			position = info.objectEnds[objectNum];
			++objectNum;
		}

		// Write fifo data skipping objects after the draw range
		while (objectNum < numObjects)
		{
			WriteFramePart(position, info.objectStarts[objectNum], memoryUpdate, frame, info);

			position = info.objectEnds[objectNum];
			++objectNum;
		}
	}

	// Write data after the last object
	WriteFramePart(position, frame.fifoDataSize, memoryUpdate, frame, info);

	FlushWGP();
}

void FifoPlayer::WriteFramePart(u32 dataStart, u32 dataEnd, u32 &nextMemUpdate, const FifoFrameInfo &frame, const AnalyzedFrameInfo &info)
{
	u8 *data = frame.fifoData;

	while (nextMemUpdate < frame.memoryUpdates.size() && dataStart < dataEnd)
	{
		const MemoryUpdate &memUpdate = info.memoryUpdates[nextMemUpdate];

		if (memUpdate.fifoPosition < dataEnd)
		{
			if (dataStart < memUpdate.fifoPosition)
			{
				WriteFifo(data, dataStart, memUpdate.fifoPosition);
				dataStart = memUpdate.fifoPosition;
			}

			WriteMemory(memUpdate);

			++nextMemUpdate;
		}
		else
		{
			WriteFifo(data, dataStart, dataEnd);
			dataStart = dataEnd;
		}
	}

	if (dataStart < dataEnd)
		WriteFifo(data, dataStart, dataEnd);
}

void FifoPlayer::WriteAllMemoryUpdates()
{
	_assert_(m_File);

	for (int frameNum = 0; frameNum < m_File->GetFrameCount(); ++frameNum)
	{
		const FifoFrameInfo &frame = m_File->GetFrame(frameNum);
		for (unsigned int i = 0; i < frame.memoryUpdates.size(); ++i)
		{
			WriteMemory(frame.memoryUpdates[i]);
		}
	}
}

void FifoPlayer::WriteMemory(const MemoryUpdate& memUpdate)
{
	u8 *mem = NULL;

	if (memUpdate.address & 0x10000000)
		mem = &Memory::m_pEXRAM[memUpdate.address & Memory::EXRAM_MASK];
	else
		mem = &Memory::m_pRAM[memUpdate.address & Memory::RAM_MASK];

	memcpy(mem, memUpdate.data, memUpdate.size);
}

void FifoPlayer::WriteFifo(u8 *data, u32 start, u32 end)
{
	u32 written = start;
	u32 lastBurstEnd = end - 1;

	// Write up to 256 bytes at a time
	while (written < end)
	{
		u32 burstEnd = std::min(written + 255, lastBurstEnd);

		while (written < burstEnd)
			GPFifo::FastWrite8(data[written++]);			

		GPFifo::Write8(data[written++], 0);

		// Advance core timing
		u32 elapsedCycles = u32(((u64)written * m_CyclesPerFrame) / m_FrameFifoSize);
		u32 cyclesUsed = elapsedCycles - m_ElapsedCycles;
		m_ElapsedCycles = elapsedCycles;

		CoreTiming::downcount -= cyclesUsed;
		CoreTiming::Advance();
	}
}

void FifoPlayer::SetupFifo()
{
	WriteCP(0x02, 0);	// disable read, BP, interrupts
	WriteCP(0x04, 7);	// clear overflow, underflow, metrics

	const FifoFrameInfo& frame = m_File->GetFrame(m_CurrentFrame);

	// Set fifo bounds	
	WriteCP(0x20, frame.fifoStart);
	WriteCP(0x22, frame.fifoStart >> 16);
	WriteCP(0x24, frame.fifoEnd);
	WriteCP(0x26, frame.fifoEnd >> 16);

	// Set watermarks
	u32 fifoSize = frame.fifoEnd - frame.fifoStart;
	WriteCP(0x28, fifoSize);
	WriteCP(0x2a, fifoSize >> 16);
	WriteCP(0x2c, 0);
	WriteCP(0x2e, 0);

	// Set R/W pointers to fifo start
	WriteCP(0x30, 0);
	WriteCP(0x32, 0);
	WriteCP(0x34, frame.fifoStart);
	WriteCP(0x36, frame.fifoStart >> 16);
	WriteCP(0x38, frame.fifoStart);
	WriteCP(0x3a, frame.fifoStart >> 16);

	// Set fifo bounds
	WritePI(12, frame.fifoStart);
	WritePI(16, frame.fifoEnd);

	// Set write pointer
	WritePI(20, frame.fifoStart);
	FlushWGP();
	WritePI(20, frame.fifoStart);

	WriteCP(0x02, 17);	// enable read & GP link
}

void FifoPlayer::LoadMemory()
{
	Memory::Clear();

	SetupFifo();

	u32 *regs = m_File->GetBPMem();
	for (int i = 0; i < FifoDataFile::BP_MEM_SIZE; ++i)
	{
		if (ShouldLoadBP(i))
			LoadBPReg(i, regs[i]);
	}

	regs = m_File->GetCPMem();
	LoadCPReg(0x30, regs[0x30]);
	LoadCPReg(0x40, regs[0x40]);
	LoadCPReg(0x50, regs[0x50]);
	LoadCPReg(0x60, regs[0x60]);

	for (int i = 0; i < 8; ++i)
	{
		LoadCPReg(0x70 + i, regs[0x70 + i]);
		LoadCPReg(0x80 + i, regs[0x80 + i]);
		LoadCPReg(0x90 + i, regs[0x90 + i]);
	}
	
	for (int i = 0; i < 16; ++i)
	{
		LoadCPReg(0xa0 + i, regs[0xa0 + i]);
		LoadCPReg(0xb0 + i, regs[0xb0 + i]);
	}

	regs = m_File->GetXFMem();
	for (int i = 0; i < FifoDataFile::XF_MEM_SIZE; i += 16)
		LoadXFMem16(i, &regs[i]);

	regs = m_File->GetXFRegs();
	for (int i = 0; i < FifoDataFile::XF_REGS_SIZE; ++i)
		LoadXFReg(i, regs[i]);

	FlushWGP();
}

u16 FifoPlayer::ReadCP(u32 address)
{
    return Memory::Read_U16(0xCC000000 | address);
}

void FifoPlayer::WriteCP(u32 address, u16 value)
{
	Memory::Write_U16(value, 0xCC000000 | address);
}

void FifoPlayer::WritePI(u32 address, u32 value)
{
	Memory::Write_U32(value, 0xCC003000 | address);
}

void FifoPlayer::FlushWGP()
{
	// Send 31 0s through the WGP
	for (int i = 0; i < 7; ++i)
		GPFifo::Write32(0, 0);
	GPFifo::Write16(0, 0);
	GPFifo::Write8(0, 0);

	GPFifo::ResetGatherPipe();
}

void FifoPlayer::LoadBPReg(u8 reg, u32 value)
{
	GPFifo::Write8(0x61, 0); // load BP reg

	u32 cmd = (reg << 24) & 0xff000000;
	cmd |= (value & 0x00ffffff);
	GPFifo::Write32(cmd, 0);
}

void FifoPlayer::LoadCPReg(u8 reg, u32 value)
{
	GPFifo::Write8(0x08, 0); // load CP reg
	GPFifo::Write8(reg, 0);
	GPFifo::Write32(value, 0);
}

void FifoPlayer::LoadXFReg(u16 reg, u32 value)
{
	GPFifo::Write8(0x10, 0); // load XF reg
	GPFifo::Write32((reg & 0x0fff) | 0x1000, 0); // load 4 bytes into reg
	GPFifo::Write32(value, 0);
}

void FifoPlayer::LoadXFMem16(u16 address, u32 *data)
{
	// Loads 16 * 4 bytes in xf memory starting at address
	GPFifo::Write8(0x10, 0); // load XF reg
	GPFifo::Write32(0x000f0000 | (address & 0xffff), 0); // load 16 * 4 bytes into address
	for (int i = 0; i < 16; ++i)
		GPFifo::Write32(data[i], 0);
}

bool FifoPlayer::ShouldLoadBP(u8 address)
{
	switch (address)
	{
	case BPMEM_SETDRAWDONE:
	case BPMEM_PE_TOKEN_ID:
	case BPMEM_PE_TOKEN_INT_ID:
	case BPMEM_TRIGGER_EFB_COPY:
	case BPMEM_LOADTLUT1:
	case BPMEM_PERF1:
		return false;
	}

	return true;
}
