// Copyright 2008 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#include <algorithm>

#include "Common/ChunkFile.h"
#include "Common/CommonPaths.h"
#include "Common/CommonTypes.h"
#include "Common/FileUtil.h"
#include "Common/NandPaths.h"
#include "Common/StringUtil.h"

#include "Core/Core.h"
#include "Core/IPC_HLE/WII_IPC_HLE.h"
#include "Core/IPC_HLE/WII_IPC_HLE_Device_FileIO.h"
#include "Core/IPC_HLE/WII_IPC_HLE_Device_fs.h"


static Common::replace_v replacements;

static std::map <std::string, std::weak_ptr<File::IOFile>> openFiles;

// This is used by several of the FileIO and /dev/fs functions
std::string HLE_IPC_BuildFilename(std::string path_wii)
{
	std::string path_full = File::GetUserPath(D_SESSION_WIIROOT_IDX);

	// Replaces chars that FAT32 can't support with strings defined in /sys/replace
	for (auto& replacement : replacements)
	{
		for (size_t j = 0; (j = path_wii.find(replacement.first, j)) != path_wii.npos; ++j)
			path_wii.replace(j, 1, replacement.second);
	}

	path_full += path_wii;

	return path_full;
}

void HLE_IPC_CreateVirtualFATFilesystem()
{
	const int cdbSize = 0x01400000;
	const std::string cdbPath = Common::GetTitleDataPath(TITLEID_SYSMENU, Common::FROM_SESSION_ROOT) + "cdb.vff";
	if ((int)File::GetSize(cdbPath) < cdbSize)
	{
		// cdb.vff is a virtual Fat filesystem created on first launch of sysmenu
		// we create it here as it is faster ~3 minutes for me when sysmenu does it ~1 second created here
		const u8 cdbHDR[0x20] = {'V', 'F', 'F', 0x20, 0xfe, 0xff, 1, 0, 1, 0x40, 0, 0, 0, 0x20};
		const u8 cdbFAT[4] = {0xf0, 0xff, 0xff, 0xff};

		File::IOFile cdbFile(cdbPath, "wb");
		if (cdbFile)
		{
			cdbFile.WriteBytes(cdbHDR, 0x20);
			cdbFile.WriteBytes(cdbFAT, 0x4);
			cdbFile.Seek(0x14020, SEEK_SET);
			cdbFile.WriteBytes(cdbFAT, 0x4);
			// 20 MiB file
			cdbFile.Seek(cdbSize - 1, SEEK_SET);
			// write the final 0 to 0 file from the second FAT to 20 MiB
			cdbFile.WriteBytes(cdbHDR + 14, 1);
			if (!cdbFile.IsGood())
			{
				cdbFile.Close();
				File::Delete(cdbPath);
			}
			cdbFile.Flush();
			cdbFile.Close();
		}
	}
}

CWII_IPC_HLE_Device_FileIO::CWII_IPC_HLE_Device_FileIO(u32 _DeviceID, const std::string& _rDeviceName)
	: IWII_IPC_HLE_Device(_DeviceID, _rDeviceName, false) // not a real hardware
	, m_Mode(0)
	, m_SeekPos(0)
	, m_file()
{
	Common::ReadReplacements(replacements);
}

CWII_IPC_HLE_Device_FileIO::~CWII_IPC_HLE_Device_FileIO()
{
}

IPCCommandResult CWII_IPC_HLE_Device_FileIO::Close(u32 _CommandAddress, bool _bForce)
{
	INFO_LOG(WII_IPC_FILEIO, "FileIO: Close %s (DeviceID=%08x)", m_Name.c_str(), m_DeviceID);
	m_Mode = 0;

	// Let go of our pointer to the file, it will automatically close if we are the last handle accessing it.
	m_file.reset();

	// Close always return 0 for success
	if (_CommandAddress && !_bForce)
		Memory::Write_U32(0, _CommandAddress + 4);
	m_Active = false;
	return GetDefaultReply();
}

IPCCommandResult CWII_IPC_HLE_Device_FileIO::Open(u32 _CommandAddress, u32 _Mode)
{
	m_Mode = _Mode;
	u32 ReturnValue = 0;

	static const char* const Modes[] =
	{
		"Unk Mode",
		"Read only",
		"Write only",
		"Read and Write"
	};

	m_filepath = HLE_IPC_BuildFilename(m_Name);

	// The file must exist before we can open it
	// It should be created by ISFS_CreateFile, not here
	if (File::Exists(m_filepath) && !File::IsDirectory(m_filepath))
	{
		INFO_LOG(WII_IPC_FILEIO, "FileIO: Open %s (%s == %08X)", m_Name.c_str(), Modes[_Mode], _Mode);
		OpenFile();
		ReturnValue = m_DeviceID;
	}
	else
	{
		WARN_LOG(WII_IPC_FILEIO, "FileIO: Open (%s) failed - File doesn't exist %s", Modes[_Mode], m_filepath.c_str());
		ReturnValue = FS_FILE_NOT_EXIST;
	}

	if (_CommandAddress)
		Memory::Write_U32(ReturnValue, _CommandAddress+4);
	m_Active = true;
	return GetDefaultReply();
}

// This isn't theadsafe, but it's only called from the CPU thread.
void CWII_IPC_HLE_Device_FileIO::OpenFile()
{
	// On the wii, all file operations are strongly ordered.
	// If a game opens the same file twice (or 8 times, looking at you PokePark Wii)
	// and writes to one file handle, it will be able to immediately read the written
	// data from the other handle.
	// On 'real' operating systems, there are various buffers and caches meaning
	// applications doing such naughty things will not get expected results.

	// So we fix this by catching any attempts to open the same file twice and
	// only opening one file. Accesses to a single file handle are ordered.
	//
	// Hall of Shame:
	//    - PokePark Wii (gets stuck on the loading screen of Pikachu falling)
	//    - PokePark 2 (Also gets stuck while loading)
	//    - Wii System Menu (Can't access the system settings, gets stuck on blank screen)
	//    - The Beatles: Rock Band (saving doesn't work)

	// Check if the file has already been opened.
	auto search = openFiles.find(m_Name);
	if (search != openFiles.end())
	{
		m_file = search->second.lock(); // Lock a shared pointer to use.
	}
	else
	{
		std::string path = m_Name;
		// This code will be called when all references to the shared pointer below have been removed.
		auto deleter = [path](File::IOFile* ptr)
		{
			delete ptr; // IOFile's deconstructor closes the file.
			openFiles.erase(path); // erase the weak pointer from the list of open files.
		};

		// All files are opened read/write. Actual access rights will be controlled per handle by the read/write functions below
		m_file = std::shared_ptr<File::IOFile>(new File::IOFile(m_filepath, "r+b"), deleter); // Use the custom deleter from above.

		// Store a weak pointer to our newly opened file in the cache.
		openFiles[path] = std::weak_ptr<File::IOFile>(m_file);
	}
}

IPCCommandResult CWII_IPC_HLE_Device_FileIO::Seek(u32 _CommandAddress)
{
	u32 ReturnValue = FS_RESULT_FATAL;
	const s32 SeekPosition = Memory::Read_U32(_CommandAddress + 0xC);
	const s32 Mode = Memory::Read_U32(_CommandAddress + 0x10);

	if (m_file->IsOpen())
	{
		ReturnValue = FS_RESULT_FATAL;

		const s32 fileSize = (s32) m_file->GetSize();
		INFO_LOG(WII_IPC_FILEIO, "FileIO: Seek Pos: 0x%08x, Mode: %i (%s, Length=0x%08x)", SeekPosition, Mode, m_Name.c_str(), fileSize);

		switch (Mode)
		{
			case WII_SEEK_SET:
			{
				if ((SeekPosition >=0) && (SeekPosition <= fileSize))
				{
					m_SeekPos = SeekPosition;
					ReturnValue = m_SeekPos;
				}
				break;
			}

			case WII_SEEK_CUR:
			{
				s32 wantedPos = SeekPosition+m_SeekPos;
				if (wantedPos >=0 && wantedPos <= fileSize)
				{
					m_SeekPos = wantedPos;
					ReturnValue = m_SeekPos;
				}
				break;
			}

			case WII_SEEK_END:
			{
				s32 wantedPos = SeekPosition+fileSize;
				if (wantedPos >=0 && wantedPos <= fileSize)
				{
					m_SeekPos = wantedPos;
					ReturnValue = m_SeekPos;
				}
				break;
			}

			default:
			{
				PanicAlert("CWII_IPC_HLE_Device_FileIO Unsupported seek mode %i", Mode);
				ReturnValue = FS_RESULT_FATAL;
				break;
			}
		}
	}
	else
	{
		ReturnValue = FS_FILE_NOT_EXIST;
	}
	Memory::Write_U32(ReturnValue, _CommandAddress + 0x4);

	return GetDefaultReply();
}

IPCCommandResult CWII_IPC_HLE_Device_FileIO::Read(u32 _CommandAddress)
{
	u32 ReturnValue = FS_EACCESS;
	const u32 Address = Memory::Read_U32(_CommandAddress + 0xC); // Read to this memory address
	const u32 Size    = Memory::Read_U32(_CommandAddress + 0x10);


	if (m_file->IsOpen())
	{
		if (m_Mode == ISFS_OPEN_WRITE)
		{
			WARN_LOG(WII_IPC_FILEIO, "FileIO: Attempted to read 0x%x bytes to 0x%08x on a write-only file %s", Size, Address, m_Name.c_str());
		}
		else
		{
			INFO_LOG(WII_IPC_FILEIO, "FileIO: Read 0x%x bytes to 0x%08x from %s", Size, Address, m_Name.c_str());
			m_file->Seek(m_SeekPos, SEEK_SET); // File might be opened twice, need to seek before we read
			ReturnValue = (u32)fread(Memory::GetPointer(Address), 1, Size, m_file->GetHandle());
			if (ReturnValue != Size && ferror(m_file->GetHandle()))
			{
				ReturnValue = FS_EACCESS;
			}
			else
			{
				m_SeekPos += Size;
			}

		}
	}
	else
	{
		ERROR_LOG(WII_IPC_FILEIO, "FileIO: Failed to read from %s (Addr=0x%08x Size=0x%x) - file could not be opened or does not exist", m_Name.c_str(), Address, Size);
		ReturnValue = FS_FILE_NOT_EXIST;
	}

	Memory::Write_U32(ReturnValue, _CommandAddress + 0x4);
	return GetDefaultReply();
}

IPCCommandResult CWII_IPC_HLE_Device_FileIO::Write(u32 _CommandAddress)
{
	u32 ReturnValue = FS_EACCESS;
	const u32 Address = Memory::Read_U32(_CommandAddress + 0xC); // Write data from this memory address
	const u32 Size    = Memory::Read_U32(_CommandAddress + 0x10);

	if (m_file->IsOpen())
	{
		if (m_Mode == ISFS_OPEN_READ)
		{
			WARN_LOG(WII_IPC_FILEIO, "FileIO: Attempted to write 0x%x bytes from 0x%08x to a read-only file %s", Size, Address, m_Name.c_str());
		}
		else
		{
			INFO_LOG(WII_IPC_FILEIO, "FileIO: Write 0x%04x bytes from 0x%08x to %s", Size, Address, m_Name.c_str());
			m_file->Seek(m_SeekPos, SEEK_SET); // File might be opened twice, need to seek before we write
			if (m_file->WriteBytes(Memory::GetPointer(Address), Size))
			{
				ReturnValue = Size;
				m_SeekPos += Size;
			}
		}
	}
	else
	{
		ERROR_LOG(WII_IPC_FILEIO, "FileIO: Failed to read from %s (Addr=0x%08x Size=0x%x) - file could not be opened or does not exist", m_Name.c_str(), Address, Size);
		ReturnValue = FS_FILE_NOT_EXIST;
	}

	Memory::Write_U32(ReturnValue, _CommandAddress + 0x4);
	return GetDefaultReply();
}

IPCCommandResult CWII_IPC_HLE_Device_FileIO::IOCtl(u32 _CommandAddress)
{
	INFO_LOG(WII_IPC_FILEIO, "FileIO: IOCtl (Device=%s)", m_Name.c_str());
#if defined(_DEBUG) || defined(DEBUGFAST)
	DumpCommands(_CommandAddress);
#endif
	const u32 Parameter =  Memory::Read_U32(_CommandAddress + 0xC);
	u32 ReturnValue = 0;

	switch (Parameter)
	{
	case ISFS_IOCTL_GETFILESTATS:
		{
			if (m_file->IsOpen())
			{
				u32 m_FileLength = (u32)m_file->GetSize();

				const u32 BufferOut = Memory::Read_U32(_CommandAddress + 0x18);
				INFO_LOG(WII_IPC_FILEIO, "  File: %s, Length: %i, Pos: %i", m_Name.c_str(), m_FileLength, m_SeekPos);

				Memory::Write_U32(m_FileLength, BufferOut);
				Memory::Write_U32(m_SeekPos, BufferOut+4);
			}
			else
			{
				ReturnValue = FS_FILE_NOT_EXIST;
			}
		}
		break;

	default:
		{
			PanicAlert("CWII_IPC_HLE_Device_FileIO: Parameter %i", Parameter);
		}
		break;

	}

	Memory::Write_U32(ReturnValue, _CommandAddress + 0x4);

	return GetDefaultReply();
}


void CWII_IPC_HLE_Device_FileIO::PrepareForState(PointerWrap::Mode mode)
{
	// Temporally close the file, to prevent any issues with the savestating of /tmp
	// it can be opened again with another call to OpenFile()
	m_file.reset();
}

void CWII_IPC_HLE_Device_FileIO::DoState(PointerWrap &p)
{
	DoStateShared(p);

	p.Do(m_Mode);
	p.Do(m_SeekPos);

	m_filepath = HLE_IPC_BuildFilename(m_Name);

	// The file was closed during state (and might now be pointing at another file)
	// Open it again
	OpenFile();
}
