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

#pragma once

#include <algorithm>
#include <string>

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

#include "Core/HW/EXI_DeviceIPL.h"
#include "Core/HW/Sram.h"

#define BE64(x) (Common::swap64(x))
#define BE32(x) (Common::swap32(x))
#define BE16(x) (Common::swap16(x))
#define ArrayByteSwap(a) (ByteSwap(a, a+sizeof(u8)));

enum
{
	SLOT_A = 0,
	SLOT_B = 1,
	GCI = 0,
	SUCCESS,
	NOMEMCARD,
	OPENFAIL,
	OUTOFBLOCKS,
	OUTOFDIRENTRIES,
	LENGTHFAIL,
	INVALIDFILESIZE,
	TITLEPRESENT,
	DIRLEN = 0x7F,
	SAV = 0x80,
	SAVFAIL,
	GCS = 0x110,
	GCSFAIL,
	FAIL,
	WRITEFAIL,
	DELETE_FAIL,

	MC_FST_BLOCKS  = 0x05,
	MBIT_TO_BLOCKS = 0x10,
	DENTRY_STRLEN  = 0x20,
	DENTRY_SIZE    = 0x40,
	BLOCK_SIZE     = 0x2000,
	BAT_SIZE       = 0xFFB,

	MemCard59Mb   = 0x04,
	MemCard123Mb  = 0x08,
	MemCard251Mb  = 0x10,
	Memcard507Mb  = 0x20,
	MemCard1019Mb = 0x40,
	MemCard2043Mb = 0x80,

	CI8SHARED = 1,
	RGB5A3,
	CI8,
};

class MemoryCardBase
{
public:
	MemoryCardBase(int _card_index = 0, int sizeMb = MemCard2043Mb)
		: card_index(_card_index)
		, nintendo_card_id(sizeMb)
	{
	}
	virtual ~MemoryCardBase() {}
	virtual s32 Read(u32 address, s32 length, u8 *destaddress) = 0;
	virtual s32 Write(u32 destaddress, s32 length, u8 *srcaddress) = 0;
	virtual void ClearBlock(u32 address) = 0;
	virtual void ClearAll() = 0;
	virtual void DoState(PointerWrap &p) = 0;
	u32 GetCardId() const { return nintendo_card_id; }
	bool IsAddressInBounds(u32 address) const
	{
		return address <= (memory_card_size - 1);
	}

protected:
	int card_index;
	u16 nintendo_card_id;
	u32 memory_card_size;
};

struct GCMBlock
{
	GCMBlock()
	{
		Erase();
	}

	void Erase()
	{
		memset(block, 0xFF, BLOCK_SIZE);
	}

	u8 block[BLOCK_SIZE];
};

void calc_checksumsBE(u16 *buf, u32 length, u16 *csum, u16 *inv_csum);

#pragma pack(push,1)
struct Header           //Offset    Size    Description
{
	// Serial in libogc
	u8 serial[12];      //0x0000    12      ?
	u64 formatTime;     //0x000c    8       Time of format (OSTime value)
	u32 SramBias;       //0x0014    4       SRAM bias at time of format
	u32 SramLang;       //0x0018    4       SRAM language
	u8 Unk2[4];         //0x001c    4       ? almost always 0
	// end Serial in libogc
	u8 deviceID[2];     //0x0020    2       0 if formated in slot A 1 if formated in slot B
	u8 SizeMb[2];       //0x0022    2       Size of memcard in Mbits
	u16 Encoding;       //0x0024    2       Encoding (ASCII or japanese)
	u8 Unused1[468];    //0x0026    468     Unused (0xff)
	u16 UpdateCounter;  //0x01fa    2       Update Counter (?, probably unused)
	u16 Checksum;       //0x01fc    2       Additive Checksum
	u16 Checksum_Inv;   //0x01fe    2       Inverse Checksum
	u8 Unused2[7680];   //0x0200    0x1e00  Unused (0xff)


	void CARD_GetSerialNo(u32 *serial1, u32 *serial2) const
	{
		u32 _serial[8];

		for (int i = 0; i < 8; i++)
		{
			memcpy(&_serial[i], (u8 *)this + (i * 4), 4);
		}

		*serial1 = _serial[0] ^ _serial[2] ^ _serial[4] ^ _serial[6];
		*serial2 = _serial[1] ^ _serial[3] ^ _serial[5] ^ _serial[7];
	}

	// Nintendo format algorithm.
	// Constants are fixed by the GC SDK
	// Changing the constants will break memorycard support
	Header(int slot = 0, u16 sizeMb = MemCard2043Mb, bool ascii = true)
	{
		memset(this, 0xFF, BLOCK_SIZE);
		*(u16 *)SizeMb = BE16(sizeMb);
		Encoding = BE16(ascii ? 0 : 1);
		u64 rand = CEXIIPL::GetGCTime();
		formatTime = Common::swap64(rand);
		for (int i = 0; i < 12; i++)
		{
			rand = (((rand * (u64)0x0000000041c64e6dULL) + (u64)0x0000000000003039ULL) >> 16);
			serial[i] = (u8)(g_SRAM.flash_id[slot][i] + (u32)rand);
			rand = (((rand * (u64)0x0000000041c64e6dULL) + (u64)0x0000000000003039ULL) >> 16);
			rand &= (u64)0x0000000000007fffULL;
		}
		SramBias = g_SRAM.counter_bias;
		SramLang = BE32(g_SRAM.lang);
		// TODO: determine the purpose of Unk2 1 works for slot A, 0 works for both slot A and slot B
		*(u32 *)&Unk2 = 0; // = _viReg[55];  static vu16* const _viReg = (u16*)0xCC002000;
		*(u16 *)&deviceID = 0;
		calc_checksumsBE((u16 *)this, 0xFE, &Checksum, &Checksum_Inv);
	}
};

struct DEntry
{
	static const u8 DENTRY_SIZE = 0x40;
	DEntry() { memset(this, 0xFF, DENTRY_SIZE); }
	std::string GCI_FileName() const
	{
		std::string filename = std::string((char *)Makercode, 2) + '-' + std::string((char *)Gamecode, 4) + '-' + (char *)Filename +
			".gci";
		static Common::replace_v replacements;
		if (replacements.size() == 0)
		{
			Common::ReadReplacements(replacements);
			// Cannot add \r to replacements file due to it being a line ending char
			// / might be ok, but we need to verify that this is only used on filenames
			// as it is a dir_sep
			replacements.push_back(std::make_pair('\r', std::string("__0d__")));
			replacements.push_back(std::make_pair('/', std::string("__2f__")));

		}

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

	u8 Gamecode[4];     //0x00       0x04    Gamecode
	u8 Makercode[2];    //0x04      0x02    Makercode
	u8 Unused1;         //0x06      0x01    reserved/unused (always 0xff, has no effect)
	u8 BIFlags;         //0x07      0x01    banner gfx format and icon animation (Image Key)
	                    //      Bit(s)  Description
	                    //      2       Icon Animation 0: forward 1: ping-pong
	                    //      1       [--0: No Banner 1: Banner present--] WRONG! YAGCD LIES!
	                    //      0       [--Banner Color 0: RGB5A3 1: CI8--]  WRONG! YAGCD LIES!
	                    //      bits 0 and 1: image format
	                    //      00 no banner
	                    //      01 CI8 banner
	                    //      10 RGB5A3 banner
	                    //      11 ? maybe ==00? Time Splitters 2 and 3 have it and don't have banner
	                    //
	u8 Filename[DENTRY_STRLEN]; //0x08      0x20     Filename
	u8 ModTime[4];      //0x28      0x04    Time of file's last modification in seconds since 12am, January 1st, 2000
	u8 ImageOffset[4];  //0x2c      0x04    image data offset
	u8 IconFmt[2];      //0x30      0x02    icon gfx format (2bits per icon)
	                    //      Bits    Description
	                    //      00      No icon
	                    //      01      CI8 with a shared color palette after the last frame
	                    //      10      RGB5A3
	                    //      11      CI8 with a unique color palette after itself
	                    //
	u8 AnimSpeed[2];    //0x32      0x02    Animation speed (2bits per icon) (*1)
	                    //      Bits    Description
	                    //      00      No icon
	                    //      01      Icon lasts for 4 frames
	                    //      10      Icon lasts for 8 frames
	                    //      11      Icon lasts for 12 frames
	                    //
	u8 Permissions;     //0x34      0x01    File-permissions
	                    //      Bit Permission  Description
	                    //      4   no move     File cannot be moved by the IPL
	                    //      3   no copy     File cannot be copied by the IPL
	                    //      2   public      Can be read by any game
	                    //
	u8 CopyCounter;     //0x35      0x01    Copy counter (*2)
	u8 FirstBlock[2];   //0x36      0x02    Block no of first block of file (0 == offset 0)
	u8 BlockCount[2];   //0x38      0x02    File-length (number of blocks in file)
	u8 Unused2[2];      //0x3a      0x02    Reserved/unused (always 0xffff, has no effect)
	u8 CommentsAddr[4]; //0x3c      0x04    Address of the two comments within the file data (*3)
};

struct Directory
{
	DEntry Dir[DIRLEN]; //0x0000            Directory Entries (max 127)
	u8 Padding[0x3a];
	u16 UpdateCounter;  //0x1ffa    2       Update Counter
	u16 Checksum;       //0x1ffc    2       Additive Checksum
	u16 Checksum_Inv;   //0x1ffe    2       Inverse Checksum
	Directory()
	{
		memset(this, 0xFF, BLOCK_SIZE);
		UpdateCounter = 0;
		Checksum = BE16(0xF003);
		Checksum_Inv = 0;
	}
	void Replace(DEntry d, int idx)
	{
		Dir[idx] = d;
		fixChecksums();
	}
	void fixChecksums() { calc_checksumsBE((u16 *)this, 0xFFE, &Checksum, &Checksum_Inv); }
};

struct BlockAlloc
{
	u16 Checksum;       //0x0000    2       Additive Checksum
	u16 Checksum_Inv;   //0x0002    2       Inverse Checksum
	u16 UpdateCounter;  //0x0004    2       Update Counter
	u16 FreeBlocks;     //0x0006    2       Free Blocks
	u16 LastAllocated;  //0x0008    2       Last allocated Block
	u16 Map[BAT_SIZE];  //0x000a    0x1ff8  Map of allocated Blocks
	u16 GetNextBlock(u16 Block) const;
	u16 NextFreeBlock(u16 MaxBlock, u16 StartingBlock = MC_FST_BLOCKS) const;
	bool ClearBlocks(u16 StartingBlock, u16 Length);
	void fixChecksums() { calc_checksumsBE((u16 *)&UpdateCounter, 0xFFE, &Checksum, &Checksum_Inv); }
	BlockAlloc(u16 sizeMb = MemCard2043Mb)
	{
		memset(this, 0, BLOCK_SIZE);
		// UpdateCounter = 0;
		FreeBlocks = BE16((sizeMb * MBIT_TO_BLOCKS) - MC_FST_BLOCKS);
		LastAllocated = BE16(4);
		fixChecksums();
	}
	u16 AssignBlocksContiguous(u16 length)
	{
		u16 starting = BE16(LastAllocated) + 1;
		if (length > BE16(FreeBlocks))
			return 0xFFFF;
		u16 current = starting;
		while ((current - starting + 1) < length)
		{
			Map[current - 5] = BE16(current + 1);
			current++;
		}
		Map[current - 5] = 0xFFFF;
		LastAllocated = BE16(current);
		FreeBlocks = BE16(BE16(FreeBlocks) - length);
		fixChecksums();
		return BE16(starting);
	}
};
#pragma pack(pop)

class GCIFile
{
public:
	bool LoadSaveBlocks();
	bool HasCopyProtection() const
	{
		if ((strcmp((char*)m_gci_header.Filename, "PSO_SYSTEM") == 0) ||
			(strcmp((char*)m_gci_header.Filename, "PSO3_SYSTEM") == 0) ||
			(strcmp((char*)m_gci_header.Filename, "f_zero.dat") == 0))
			return true;
		return false;
	}

	void DoState(PointerWrap &p);
	DEntry m_gci_header;
	std::vector<GCMBlock> m_save_data;
	std::vector<u16> m_used_blocks;
	int UsesBlock(u16 blocknum);
	bool m_dirty;
	std::string m_filename;
};

class GCMemcard : NonCopyable
{
private:
	bool m_valid;
	std::string m_fileName;

	u32 maxBlock;
	u16 m_sizeMb;

	Header hdr;
	Directory dir, dir_backup, *CurrentDir, *PreviousDir;
	BlockAlloc bat, bat_backup, *CurrentBat, *PreviousBat;

	std::vector<GCMBlock> mc_data_blocks;

	u32 ImportGciInternal(FILE* gcih, const std::string& inputFile, const std::string &outputFile);
	void InitDirBatPointers();

public:

	GCMemcard(const std::string& fileName, bool forceCreation=false, bool sjis=false);
	bool IsValid() const { return m_valid; }
	bool IsAsciiEncoding() const;
	bool Save();
	bool Format(bool ascii = true, u16 SizeMb = MemCard2043Mb);
	static bool Format(u8 *card_data, bool ascii = true, u16 SizeMb = MemCard2043Mb);
	static s32 FZEROGX_MakeSaveGameValid(Header &cardheader, DEntry &direntry, std::vector<GCMBlock> &FileBuffer);
	static s32 PSO_MakeSaveGameValid(Header &cardheader, DEntry &direntry, std::vector<GCMBlock> &FileBuffer);

	u32 TestChecksums() const;
	bool FixChecksums();

	// get number of file entries in the directory
	u8 GetNumFiles() const;
	u8 GetFileIndex(u8 fileNumber) const;

	// get the free blocks from bat
	u16 GetFreeBlocks() const;

	// If title already on memcard returns index, otherwise returns -1
	u8 TitlePresent(const DEntry& d) const;

	bool GCI_FileName(u8 index, std::string &filename) const;
	// DEntry functions, all take u8 index < DIRLEN (127)
	std::string DEntry_GameCode(u8 index) const;
	std::string DEntry_Makercode(u8 index) const;
	std::string DEntry_BIFlags(u8 index) const;
	std::string DEntry_FileName(u8 index) const;
	u32 DEntry_ModTime(u8 index) const;
	u32 DEntry_ImageOffset(u8 index) const;
	std::string DEntry_IconFmt(u8 index) const;
	std::string DEntry_AnimSpeed(u8 index) const;
	std::string DEntry_Permissions(u8 index) const;
	u8 DEntry_CopyCounter(u8 index) const;
	// get first block for file
	u16 DEntry_FirstBlock(u8 index) const;
	// get file length in blocks
	u16 DEntry_BlockCount(u8 index) const;
	u32 DEntry_CommentsAddress(u8 index) const;
	std::string GetSaveComment1(u8 index) const;
	std::string GetSaveComment2(u8 index) const;
	// Copies a DEntry from u8 index to DEntry& data
	bool GetDEntry(u8 index, DEntry &dest) const;

	u32 GetSaveData(u8 index, std::vector<GCMBlock> &saveBlocks) const;

	// adds the file to the directory and copies its contents
	u32 ImportFile(DEntry& direntry, std::vector<GCMBlock> &saveBlocks);

	// delete a file from the directory
	u32 RemoveFile(u8 index);

	// reads a save from another memcard, and imports the data into this memcard
	u32 CopyFrom(const GCMemcard& source, u8 index);

	// reads a .gci/.gcs/.sav file and calls ImportFile or saves out a gci file
	u32 ImportGci(const std::string& inputFile,const std::string &outputFile);

	// writes a .gci file to disk containing index
	u32 ExportGci(u8 index, const std::string& fileName, const std::string &directory) const;

	// GCI files are untouched, SAV files are byteswapped
	// GCS files have the block count set, default is 1 (For export as GCS)
	static void Gcs_SavConvert(DEntry &tempDEntry, int saveType, int length = BLOCK_SIZE);

	// reads the banner image
	bool ReadBannerRGBA8(u8 index, u32* buffer) const;

	// reads the animation frames
	u32 ReadAnimRGBA8(u8 index, u32* buffer, u8 *delays) const;
};
