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

#include <chrono>
#include <cstring>
#include <memory>
#include <string>

#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
#include "Common/FileUtil.h"
#include "Common/Thread.h"
#include "Common/Logging/Log.h"
#include "Core/ConfigManager.h"
#include "Core/Core.h"
#include "Core/HW/GCMemcard.h"
#include "Core/HW/GCMemcardRaw.h"

#define SIZE_TO_Mb (1024 * 8 * 16)
#define MC_HDR_SIZE 0xA000

MemoryCard::MemoryCard(const std::string& filename, int _card_index, u16 sizeMb)
	: MemoryCardBase(_card_index, sizeMb)
	, m_filename(filename)
{
	File::IOFile pFile(m_filename, "rb");
	if (pFile)
	{
		// Measure size of the existing memcard file.
		memory_card_size = (u32)pFile.GetSize();
		nintendo_card_id = memory_card_size / SIZE_TO_Mb;
		m_memcard_data = std::make_unique<u8[]>(memory_card_size);
		memset(&m_memcard_data[0], 0xFF, memory_card_size);

		INFO_LOG(EXPANSIONINTERFACE, "Reading memory card %s", m_filename.c_str());
		pFile.ReadBytes(&m_memcard_data[0], memory_card_size);
	}
	else
	{
		// Create a new 128Mb memcard
		nintendo_card_id = sizeMb;
		memory_card_size = sizeMb * SIZE_TO_Mb;

		m_memcard_data = std::make_unique<u8[]>(memory_card_size);
		// Fills in MC_HDR_SIZE bytes
		GCMemcard::Format(&m_memcard_data[0], m_filename.find(".JAP.raw") != std::string::npos, sizeMb);
		memset(&m_memcard_data[MC_HDR_SIZE], 0xFF, memory_card_size - MC_HDR_SIZE);

		INFO_LOG(EXPANSIONINTERFACE, "No memory card found. A new one was created instead.");
	}

	// Class members (including inherited ones) have now been initialized, so
	// it's safe to startup the flush thread (which reads them).
	m_flush_buffer = std::make_unique<u8[]>(memory_card_size);
	m_flush_thread = std::thread(&MemoryCard::FlushThread, this);
}

MemoryCard::~MemoryCard()
{
	if (m_flush_thread.joinable())
	{
		m_flush_trigger.Set();

		m_flush_thread.join();
	}
}

void MemoryCard::FlushThread()
{
	if (!SConfig::GetInstance().bEnableMemcardSdWriting)
	{
		return;
	}

	Common::SetCurrentThreadName(
		StringFromFormat("Memcard %d flushing thread", card_index).c_str());

	const auto flush_interval = std::chrono::seconds(15);

	while (true)
	{
		// If triggered, we're exiting.
		// If timed out, check if we need to flush.
		bool do_exit = m_flush_trigger.WaitFor(flush_interval);
		if (!do_exit)
		{
			bool is_dirty = m_dirty.TestAndClear();
			if (!is_dirty)
			{
				continue;
			}
		}

		// Opening the file is purposefully done each iteration to ensure the
		// file doesn't disappear out from under us after the first check.
		File::IOFile pFile(m_filename, "r+b");

		if (!pFile)
		{
			std::string dir;
			SplitPath(m_filename, &dir, nullptr, nullptr);
			if (!File::IsDirectory(dir))
			{
				File::CreateFullPath(dir);
			}
			pFile.Open(m_filename, "wb");
		}

		// Note - pFile may have changed above, after ctor
		if (!pFile)
		{
			PanicAlertT(
				"Could not write memory card file %s.\n\n"
				"Are you running Dolphin from a CD/DVD, or is the save file maybe write protected?\n\n"
				"Are you receiving this after moving the emulator directory?\nIf so, then you may "
				"need to re-specify your memory card location in the options.",
				m_filename.c_str());

			// Exit the flushing thread - further flushes will be ignored unless
			// the thread is recreated.
			return;
		}

		{
			std::unique_lock<std::mutex> l(m_flush_mutex);
			memcpy(&m_flush_buffer[0], &m_memcard_data[0], memory_card_size);
		}
		pFile.WriteBytes(&m_flush_buffer[0], memory_card_size);

		if (!do_exit)
		{
			Core::DisplayMessage(
				StringFromFormat("Wrote memory card %c contents to %s",
				card_index ? 'B' : 'A', m_filename.c_str()).c_str(),
				4000);
		}
		else
		{
			return;
		}
	}
}

void MemoryCard::MakeDirty()
{
	m_dirty.Set();
}

s32 MemoryCard::Read(u32 srcaddress, s32 length, u8 *destaddress)
{
	if (!IsAddressInBounds(srcaddress))
	{
		PanicAlertT("MemoryCard: Read called with invalid source address (0x%x)",
					srcaddress);
		return -1;
	}

	memcpy(destaddress, &m_memcard_data[srcaddress], length);
	return length;
}

s32 MemoryCard::Write(u32 destaddress, s32 length, u8 *srcaddress)
{
	if (!IsAddressInBounds(destaddress))
	{
		PanicAlertT("MemoryCard: Write called with invalid destination address (0x%x)",
					destaddress);
		return -1;
	}

	{
		std::unique_lock<std::mutex> l(m_flush_mutex);
		memcpy(&m_memcard_data[destaddress], srcaddress, length);
	}
	MakeDirty();
	return length;
}

void MemoryCard::ClearBlock(u32 address)
{
	if (address & (BLOCK_SIZE - 1) || !IsAddressInBounds(address))
	{
		PanicAlertT("MemoryCard: ClearBlock called on invalid address (0x%x)",
			address);
		return;
	}
	else
	{
		std::unique_lock<std::mutex> l(m_flush_mutex);
		memset(&m_memcard_data[address], 0xFF, BLOCK_SIZE);
	}
	MakeDirty();
}

void MemoryCard::ClearAll()
{
	{
		std::unique_lock<std::mutex> l(m_flush_mutex);
		memset(&m_memcard_data[0], 0xFF, memory_card_size);
	}
	MakeDirty();
}

void MemoryCard::DoState(PointerWrap &p)
{
	p.Do(card_index);
	p.Do(memory_card_size);
	p.DoArray(&m_memcard_data[0], memory_card_size);
}
