#pragma once
#include "Emu/FS/vfsDevice.h"

static const u64 g_hdd_magic = *(u64*)"PS3eHDD\0";
static const u16 g_hdd_version = 0x0001;

struct vfsHDD_Block
{
	struct
	{
		u64 is_used		: 1;
		u64 next_block	: 63;
	};
} static const g_null_block = {0}, g_used_block = {1};

struct vfsHDD_Hdr : public vfsHDD_Block
{
	u64 magic;
	u16 version;
	u64 block_count;
	u32 block_size;
};

enum vfsHDD_EntryType : u8
{
	vfsHDD_Entry_Dir = 0,
	vfsHDD_Entry_File = 1,
	vfsHDD_Entry_Link = 2,
};

struct vfsHDD_Entry : public vfsHDD_Block
{
	u64 data_block;
	vfsOpenMode access;
	vfsHDD_EntryType type;
	u64 size;
	u64 ctime;
	u64 mtime;
	u64 atime;
};

class vfsHDDManager
{
public:
	static void CreateBlock(vfsHDD_Block& block)
	{
		block.is_used = true;
		block.next_block = 0;
	}

	static void CreateEntry(vfsHDD_Entry& entry)
	{
		memset(&entry, 0, sizeof(vfsHDD_Entry));
		u64 ctime = time(nullptr);
		entry.atime = ctime;
		entry.ctime = ctime;
		entry.mtime = ctime;
		entry.access = vfsReadWrite;
		CreateBlock(entry);
	}

	static void CreateHDD(const wxString& path, u64 size, u64 block_size)
	{
		wxFile f(path, wxFile::write);

		static const u64 cur_dir_block = 1;

		vfsHDD_Hdr hdr;
		CreateBlock(hdr);
		hdr.next_block = cur_dir_block;
		hdr.magic = g_hdd_magic;
		hdr.version = g_hdd_version;
		hdr.block_count = (size + block_size) / block_size;
		hdr.block_size = block_size;
		f.Write(&hdr, sizeof(vfsHDD_Hdr));

		{
			vfsHDD_Entry entry;
			CreateEntry(entry);
			entry.type = vfsHDD_Entry_Dir;
			entry.data_block = hdr.next_block;
			entry.next_block = 0;

			f.Seek(cur_dir_block * hdr.block_size);
			f.Write(&entry, sizeof(vfsHDD_Entry));
			f.Write(".");
		}

		u8 null = 0;
		f.Seek(hdr.block_count * hdr.block_size - sizeof(null));
		f.Write(&null, sizeof(null));
	}

	void Format()
	{
	}

	void AppendEntry(vfsHDD_Entry entry)
	{
	}
};


class vfsHDDFile
{
	u64 m_info_block;
	vfsHDD_Entry m_info;
	const vfsHDD_Hdr& m_hdd_info;
	vfsLocalFile& m_hdd;
	u32 m_position;
	u64 m_cur_block;

	bool goto_block(u64 n)
	{
		vfsHDD_Block block_info;

		if(m_info.data_block >= m_hdd_info.block_count)
		{
			return false;
		}

		m_hdd.Seek(m_info.data_block * m_hdd_info.block_size);
		block_info.next_block = m_info.data_block;

		for(u64 i=0; i<n; ++i)
		{
			if(!block_info.next_block || !block_info.is_used || block_info.next_block >= m_hdd_info.block_count)
			{
				return false;
			}

			m_hdd.Seek(block_info.next_block * m_hdd_info.block_size);
			m_hdd.Read(&block_info, sizeof(vfsHDD_Block));
		}

		return true;
	}

	void RemoveBlocks(u64 start_block)
	{
		vfsHDD_Block block_info;
		block_info.next_block = start_block;

		while(block_info.next_block && block_info.is_used)
		{
			u64 offset = block_info.next_block * m_hdd_info.block_size;

			ReadBlock(offset, block_info);
			WriteBlock(offset, g_null_block);
		}
	}

	void WriteBlock(u64 block, const vfsHDD_Block& data)
	{
		m_hdd.Seek(block * m_hdd_info.block_size);
		m_hdd.Write(&data, sizeof(vfsHDD_Block));
	}

	void ReadBlock(u64 block, vfsHDD_Block& data)
	{
		m_hdd.Seek(block * m_hdd_info.block_size);
		m_hdd.Read(&data, sizeof(vfsHDD_Block));
	}

	void WriteEntry(u64 block, const vfsHDD_Entry& data)
	{
		m_hdd.Seek(block * m_hdd_info.block_size);
		m_hdd.Write(&data, sizeof(vfsHDD_Entry));
	}

	void ReadEntry(u64 block, vfsHDD_Entry& data)
	{
		m_hdd.Seek(block * m_hdd_info.block_size);
		m_hdd.Read(&data, sizeof(vfsHDD_Entry));
	}

	void ReadEntry(u64 block, vfsHDD_Entry& data, wxString& name)
	{
		m_hdd.Seek(block * m_hdd_info.block_size);
		m_hdd.Read(&data, sizeof(vfsHDD_Entry));
		m_hdd.Read(wxStringBuffer(name, GetMaxNameLen()), GetMaxNameLen());
	}

	void ReadEntry(u64 block, wxString& name)
	{
		m_hdd.Seek(block * m_hdd_info.block_size + sizeof(vfsHDD_Entry));
		m_hdd.Read(wxStringBuffer(name, GetMaxNameLen()), GetMaxNameLen());
	}

	void WriteEntry(u64 block, const vfsHDD_Entry& data, const wxString& name)
	{
		m_hdd.Seek(block * m_hdd_info.block_size);
		m_hdd.Write(&data, sizeof(vfsHDD_Entry));
		m_hdd.Write(name.c_str(), min<size_t>(GetMaxNameLen() - 1, name.Len() + 1));
	}

	__forceinline u32 GetMaxNameLen() const
	{
		return m_hdd_info.block_size - sizeof(vfsHDD_Entry);
	}

public:
	vfsHDDFile(vfsLocalFile& hdd, const vfsHDD_Hdr& hdd_info)
		: m_hdd(hdd)
		, m_hdd_info(hdd_info)
	{
	}

	~vfsHDDFile()
	{
	}

	void Open(u64 info_block)
	{
		m_info_block = info_block;
		ReadEntry(m_info_block, m_info);
		m_position = 0;
		m_cur_block = m_info.data_block;
	}

	u64 FindFreeBlock()
	{
		vfsHDD_Block block_info;

		for(u64 i = 0; i<m_hdd_info.block_count; ++i)
		{
			ReadBlock(i, block_info);

			if(!block_info.is_used)
			{
				return i;
			}
		}

		return 0;
	}

	u64 GetSize()
	{
		return m_info.size;
	}

	bool Seek(u64 pos)
	{
		if(!goto_block(pos / m_hdd_info.block_size))
		{
			return false;
		}

		m_position = pos % m_hdd_info.block_size;
		return true;
	}

	void SaveInfo()
	{
		m_hdd.Seek(m_info_block * m_hdd_info.block_size);
		m_hdd.Write(&m_info, sizeof(vfsHDD_Entry));
	}

	u64 Read(void* dst, u64 size)
	{
		if(!size)
			return 0;

		vfsDeviceLocker lock(m_hdd);

		const u32 block_size = m_hdd_info.block_size - sizeof(vfsHDD_Block);
		u64 rsize = min<u64>(block_size - m_position, size);

		vfsHDD_Block cur_block_info;
		m_hdd.Seek(m_cur_block * m_hdd_info.block_size);
		m_hdd.Read(&cur_block_info, sizeof(vfsHDD_Block));
		m_hdd.Seek(m_cur_block * m_hdd_info.block_size + sizeof(vfsHDD_Block) + m_position);
		m_hdd.Read(dst, rsize);
		size -= rsize;
		m_position += rsize;
		if(!size)
		{
			return rsize;
		}

		u64 offset = rsize;

		for(; size; size -= rsize, offset += rsize)
		{
			if(!cur_block_info.is_used || !cur_block_info.next_block || cur_block_info.next_block >= m_hdd_info.block_count)
			{
				return offset;
			}

			m_cur_block = cur_block_info.next_block;
			rsize = min<u64>(block_size, size);

			m_hdd.Seek(cur_block_info.next_block * m_hdd_info.block_size);
			m_hdd.Read(&cur_block_info, sizeof(vfsHDD_Block));

			if(m_hdd.Read((u8*)dst + offset, rsize) != rsize)
			{
				return offset;
			}
		}

		m_position = rsize;

		return offset;
	}

	u64 Write(const void* src, u64 size)
	{
		if(!size)
			return 0;

		vfsDeviceLocker lock(m_hdd);

		const u32 block_size = m_hdd_info.block_size - sizeof(vfsHDD_Block);

		if(!m_cur_block)
		{
			if(!m_info.data_block)
			{
				u64 new_block = FindFreeBlock();

				if(!new_block)
				{
					return 0;
				}

				WriteBlock(new_block, g_used_block);
				m_info.data_block = new_block;
				m_info.size = 0;
				SaveInfo();
			}

			m_cur_block = m_info.data_block;
			m_position = 0;
		}

		u64 wsize = min<u64>(block_size - m_position, size);

		vfsHDD_Block block_info;
		ReadBlock(m_cur_block, block_info);

		if(wsize)
		{
			m_hdd.Seek(m_cur_block * m_hdd_info.block_size + sizeof(vfsHDD_Block) + m_position);
			m_hdd.Write(src, wsize);
			size -= wsize;
			m_info.size += wsize;
			m_position += wsize;
			SaveInfo();

			if(!size)
				return wsize;
		}

		u64 last_block = m_cur_block;
		block_info.is_used = true;
		u64 offset = wsize;

		for(; size; size -= wsize, offset += wsize, m_info.size += wsize)
		{
			u64 new_block = FindFreeBlock();

			if(!new_block)
			{
				m_position = 0;
				SaveInfo();
				return offset;
			}

			m_cur_block = new_block;
			wsize = min<u64>(block_size, size);

			block_info.next_block = m_cur_block;
			m_hdd.Seek(last_block * m_hdd_info.block_size);
			if(m_hdd.Write(&block_info, sizeof(vfsHDD_Block)) != sizeof(vfsHDD_Block))
			{
				m_position = 0;
				SaveInfo();
				return offset;
			}

			block_info.next_block = 0;
			m_hdd.Seek(m_cur_block * m_hdd_info.block_size);
			if(m_hdd.Write(&block_info, sizeof(vfsHDD_Block)) != sizeof(vfsHDD_Block))
			{
				m_position = 0;
				SaveInfo();
				return offset;
			}
			if((m_position = m_hdd.Write((u8*)src + offset, wsize)) != wsize)
			{
				m_info.size += wsize;
				SaveInfo();
				return offset;
			}

			last_block = m_cur_block;
		}

		SaveInfo();
		m_position = wsize;
		return offset;
	}

	bool Eof() const
	{
		return m_info.size <= (m_cur_block * m_hdd_info.block_size + m_position);
	}
};

class vfsHDD : public vfsFileBase
{
	vfsHDD_Hdr m_hdd_info;
	vfsHDD_Entry m_cur_dir;
	u64 m_cur_dir_block;
	vfsHDDFile m_file;
	vfsLocalFile m_hdd_file;
	const wxString& m_hdd_path;

public:
	vfsHDD(const wxString& hdd_path)
		: m_hdd_file(hdd_path, vfsReadWrite)
		, m_file(m_hdd_file, m_hdd_info)
		, m_hdd_path(hdd_path)
	{
		m_hdd_file.Read(&m_hdd_info, sizeof(vfsHDD_Hdr));
		m_cur_dir_block = m_hdd_info.next_block;
		if(!m_hdd_info.block_size)
		{
			ConLog.Error("Bad block size!");
			m_hdd_info.block_size = 2048;
		}
		m_hdd_file.Seek(m_cur_dir_block * m_hdd_info.block_size);
		m_hdd_file.Read(&m_cur_dir, sizeof(vfsHDD_Entry));
	}

	virtual vfsDevice* GetNew()
	{
		return new vfsHDD(m_hdd_path);
	}

	__forceinline u32 GetMaxNameLen() const
	{
		return m_hdd_info.block_size - sizeof(vfsHDD_Entry);
	}

	bool SearchEntry(const wxString& name, u64& entry_block, u64* parent_block = nullptr)
	{
		u64 last_block = 0;
		u64 block = m_cur_dir_block;
		vfsHDD_Entry entry;
		wxString buf;

		while(block)
		{
			ReadEntry(block, entry, buf);

			if(name.CmpNoCase(buf) == 0)
			{
				entry_block = block;
				if(parent_block)
					*parent_block = last_block;

				return true;
			}

			last_block = block;
			block = entry.is_used ? entry.next_block : 0ULL;
		}

		return false;
	}

	int OpenDir(const wxString& name)
	{
		ConLog.Warning("OpenDir(%s)", name);
		u64 entry_block;
		if(!SearchEntry(name, entry_block))
			return -1;

		m_hdd_file.Seek(entry_block * m_hdd_info.block_size);
		vfsHDD_Entry entry;
		m_hdd_file.Read(&entry, sizeof(vfsHDD_Entry));
		if(entry.type == vfsHDD_Entry_File)
			return 1;

		m_cur_dir_block = entry.data_block;
		ReadEntry(m_cur_dir_block, m_cur_dir);

		return 0;
	}

	bool Rename(const wxString& from, const wxString& to)
	{
		u64 entry_block;
		if(!SearchEntry(from, entry_block))
		{
			return false;
		}

		vfsHDD_Entry entry;
		ReadEntry(entry_block, entry);
		WriteEntry(entry_block, entry, to);

		return true;
	}

	u64 FindFreeBlock()
	{
		vfsHDD_Block block_info;

		for(u64 i = 0; i<m_hdd_info.block_count; ++i)
		{
			ReadBlock(i, block_info);

			if(!block_info.is_used)
			{
				return i;
			}
		}

		return 0;
	}

	void WriteBlock(u64 block, const vfsHDD_Block& data)
	{
		m_hdd_file.Seek(block * m_hdd_info.block_size);
		m_hdd_file.Write(&data, sizeof(vfsHDD_Block));
	}

	void ReadBlock(u64 block, vfsHDD_Block& data)
	{
		m_hdd_file.Seek(block * m_hdd_info.block_size);
		m_hdd_file.Read(&data, sizeof(vfsHDD_Block));
	}

	void WriteEntry(u64 block, const vfsHDD_Entry& data)
	{
		m_hdd_file.Seek(block * m_hdd_info.block_size);
		m_hdd_file.Write(&data, sizeof(vfsHDD_Entry));
	}

	void ReadEntry(u64 block, vfsHDD_Entry& data)
	{
		m_hdd_file.Seek(block * m_hdd_info.block_size);
		m_hdd_file.Read(&data, sizeof(vfsHDD_Entry));
	}

	void ReadEntry(u64 block, vfsHDD_Entry& data, wxString& name)
	{
		m_hdd_file.Seek(block * m_hdd_info.block_size);
		m_hdd_file.Read(&data, sizeof(vfsHDD_Entry));
		m_hdd_file.Read(wxStringBuffer(name, GetMaxNameLen()), GetMaxNameLen());
	}

	void ReadEntry(u64 block, wxString& name)
	{
		m_hdd_file.Seek(block * m_hdd_info.block_size + sizeof(vfsHDD_Entry));
		m_hdd_file.Read(wxStringBuffer(name, GetMaxNameLen()), GetMaxNameLen());
	}

	void WriteEntry(u64 block, const vfsHDD_Entry& data, const wxString& name)
	{
		m_hdd_file.Seek(block * m_hdd_info.block_size);
		m_hdd_file.Write(&data, sizeof(vfsHDD_Entry));
		m_hdd_file.Write(name.c_str(), min<size_t>(GetMaxNameLen() - 1, name.Len() + 1));
	}

	bool Create(vfsHDD_EntryType type, const wxString& name)
	{
		if(HasEntry(name))
		{
			return false;
		}

		u64 new_block = FindFreeBlock();

		ConLog.Write("CREATION ENTRY AT 0x%llx", new_block);
		if(!new_block)
		{
			return false;
		}

		WriteBlock(new_block, g_used_block);

		{
			vfsHDD_Entry new_entry;
			vfsHDDManager::CreateEntry(new_entry);
			new_entry.next_block = 0;
			new_entry.type = type;

			if(type == vfsHDD_Entry_Dir)
			{
				u64 block_cur = FindFreeBlock();

				if(!block_cur)
				{
					return false;
				}

				WriteBlock(block_cur, g_used_block);

				u64 block_last = FindFreeBlock();

				if(!block_last)
				{
					return false;
				}

				WriteBlock(block_last, g_used_block);

				vfsHDD_Entry entry_cur, entry_last;
				vfsHDDManager::CreateEntry(entry_cur);
				vfsHDDManager::CreateEntry(entry_last);

				entry_cur.type = vfsHDD_Entry_Dir;
				entry_cur.data_block = block_cur;
				entry_cur.next_block = block_last;

				entry_last.type = vfsHDD_Entry_Dir;
				entry_last.data_block = m_cur_dir_block;
				entry_last.next_block = 0;

				new_entry.data_block = block_cur;

				WriteEntry(block_cur, entry_cur, ".");
				WriteEntry(block_last, entry_last, "..");
			}

			WriteEntry(new_block, new_entry, name);
		}

		{
			u64 block = m_cur_dir_block;

			vfsHDD_Block tmp;
			while(block)
			{
				ReadBlock(block, tmp);

				if(!tmp.next_block)
					break;

				block = tmp.next_block;
			}

			tmp.next_block = new_block;
			WriteBlock(block, tmp);
		}

		return true;
	}

	bool GetFirstEntry(u64& block, vfsHDD_Entry& entry, wxString& name)
	{
		if(!m_cur_dir_block)
		{
			return false;
		}

		ReadEntry(m_cur_dir_block, entry, name);
		block = entry.is_used ? entry.next_block : 0;

		return true;
	}

	bool GetNextEntry(u64& block, vfsHDD_Entry& entry, wxString& name)
	{
		if(!block)
		{
			return false;
		}

		ReadEntry(block, entry, name);

		block = entry.is_used ? entry.next_block : 0;
		return true;
	}

	virtual bool Open(const wxString& path, vfsOpenMode mode = vfsRead)
	{
		const char* s = path.c_str();
		u64 from = 0;
		u64 pos = 0;
		u64 file_pos = -1;

		do
		{
			if(s[pos] == '\\' || s[pos] == '\0')
			{
				if(file_pos != -1)
				{
					return false;
				}

				if(from != -1)
				{
					if(pos - from > 1)
					{
						int res = OpenDir(wxString(s + from, pos));
						if(res == -1)
						{
							return false;
						}
						
						if(res == 1)
						{
							file_pos = from;
						}
					}

					from = pos;
				}
				else
				{
					from = pos;
				}
			}
		}
		while(s[pos++] != '\0');

		if(file_pos == -1)
		{
			return false;
		}

		u64 file_block;
		if(!SearchEntry(wxString(s + file_pos), file_block))
		{
			return false;
		}

		ConLog.Write("ENTRY FINDED AT 0x%llx", file_block);
		m_file.Open(file_block);

		return vfsFileBase::Open(path, mode);
	}

	bool HasEntry(const wxString& name)
	{
		u64 file_block;
		if(!SearchEntry(name, file_block))
		{
			return false;
		}

		return true;
	}

	void RemoveBlocksDir(u64 start_block)
	{
		wxString name;
		u64 block = start_block;
		vfsHDD_Entry entry;

		while(block)
		{
			ReadEntry(block, entry, name);
			WriteBlock(block, g_null_block);

			if(entry.type == vfsHDD_Entry_Dir && name != "." && name != "..")
			{
				ConLog.Warning("removing sub folder '%s'", name);
				RemoveBlocksDir(entry.data_block);
			}
			else if(entry.type == vfsHDD_Entry_File)
			{
				RemoveBlocksFile(entry.data_block);
			}

			block = entry.next_block;
		}
	}

	void RemoveBlocksFile(u64 start_block)
	{
		u64 block = start_block;
		vfsHDD_Block block_data;

		while(block)
		{
			ReadBlock(block, block_data);
			WriteBlock(block, g_null_block);

			block = block_data.next_block;
		}
	}

	bool RemoveEntry(const wxString& name)
	{
		u64 entry_block, parent_entry;
		if(!SearchEntry(name, entry_block, &parent_entry))
		{
			return false;
		}

		vfsHDD_Entry entry;
		ReadEntry(entry_block, entry);
		if(entry.type == vfsHDD_Entry_Dir)
		{
			RemoveBlocksDir(entry.data_block);
		}
		else if(entry.type == vfsHDD_Entry_File)
		{
			RemoveBlocksFile(entry.data_block);
		}

		if(parent_entry)
		{
			u64 next = entry.next_block;
			ReadEntry(parent_entry, entry);
			entry.next_block = next;
			WriteEntry(parent_entry, entry);
		}
		WriteBlock(entry_block, g_null_block);
		return true;
	}

	virtual bool Create(const wxString& path)
	{
		return false;
	}

	virtual u32 Write(const void* src, u32 size)
	{
		return vfsFileBase::Write(src, m_file.Write(src, size));
	}

	virtual u32 Read(void* dst, u32 size)
	{
		return vfsFileBase::Read(dst, m_file.Read(dst, size));
	}

	virtual u64 Seek(s64 offset, vfsSeekMode mode = vfsSeekSet)
	{
		switch(mode)
		{
		case vfsSeekCur:
			m_file.Seek(Tell() + offset);
		break;

		case vfsSeekSet:
			m_file.Seek(offset);
		break;

		case vfsSeekEnd:
			m_file.Seek(m_file.GetSize() + offset);
		break;
		}

		return vfsFileBase::Seek(offset, mode);
	}

	virtual bool Eof()
	{
		return m_file.Eof();
	}

	virtual u64 GetSize()
	{
		return m_file.GetSize();
	}
};