#include "CKStateChunk.hpp"
#include "CKFile.hpp"
#include "CKContext.hpp"

namespace LibCmo::CK2 {

#pragma region Ctor Dtor

	CKStateChunk::CKStateChunk(CKFileVisitor* visitor, CKContext* ctx) :
		m_ClassId(CK_CLASSID::CKCID_OBJECT), m_DataDwSize(0u), m_pData(nullptr),
		m_DataVersion(CK_STATECHUNK_DATAVERSION::CHUNKDATA_CURRENTVERSION), m_ChunkVersion(CK_STATECHUNK_CHUNKVERSION::CHUNK_VERSION4),
		m_Parser { CKStateChunkStatus::IDLE, 0u, 0u, 0u },
		m_ObjectList(), m_ChunkList(), m_ManagerList(),
		m_BindFile(visitor), m_BindContext(ctx)
	{}

	CKStateChunk::CKStateChunk(const CKStateChunk& rhs) :
		m_ClassId(rhs.m_ClassId), m_DataVersion(rhs.m_DataVersion), m_ChunkVersion(rhs.m_ChunkVersion),
		m_Parser(rhs.m_Parser),
		m_ObjectList(rhs.m_ObjectList), m_ManagerList(rhs.m_ManagerList), m_ChunkList(rhs.m_ChunkList),
		m_pData(nullptr), m_DataDwSize(rhs.m_DataDwSize),
		m_BindFile(rhs.m_BindFile), m_BindContext(rhs.m_BindContext) {
		// copy buffer
		if (rhs.m_pData != nullptr) {
			this->m_pData = new CKDWORD[rhs.m_DataDwSize];
				std::memcpy(this->m_pData, rhs.m_pData, sizeof(CKDWORD) * rhs.m_DataDwSize);
		}
	}

	CKStateChunk::CKStateChunk(CKStateChunk&& rhs) :
		m_ClassId(rhs.m_ClassId), m_DataVersion(rhs.m_DataVersion), m_ChunkVersion(rhs.m_ChunkVersion),
		m_Parser(rhs.m_Parser),
		m_ObjectList(std::move(rhs.m_ObjectList)), m_ManagerList(std::move(rhs.m_ManagerList)), m_ChunkList(std::move(rhs.m_ChunkList)),
		m_pData(rhs.m_pData), m_DataDwSize(rhs.m_DataDwSize),
		m_BindFile(rhs.m_BindFile), m_BindContext(rhs.m_BindContext) {
		// set to null after steal data
		rhs.m_pData = nullptr;
		// and clear it
		rhs.Clear();
	}

	CKStateChunk& CKStateChunk::operator=(const CKStateChunk& rhs) {
		this->Clear();

		this->m_DataVersion = rhs.m_DataVersion;
		this->m_ChunkVersion = rhs.m_ChunkVersion;
		this->m_ClassId = rhs.m_ClassId;

		this->m_Parser = rhs.m_Parser;

		this->m_ObjectList = rhs.m_ObjectList;
		this->m_ManagerList = rhs.m_ManagerList;
		this->m_ChunkList = rhs.m_ChunkList;

		this->m_BindFile = rhs.m_BindFile;
		this->m_BindContext = rhs.m_BindContext;

		// copy buffer
		if (rhs.m_pData != nullptr) {
			this->m_pData = new CKDWORD[rhs.m_DataDwSize];
				std::memcpy(this->m_pData, rhs.m_pData, sizeof(CKDWORD) * rhs.m_DataDwSize);
		}
		this->m_DataDwSize = rhs.m_DataDwSize;

		return *this;
	}

	CKStateChunk& CKStateChunk::operator=(CKStateChunk&& rhs) {
		this->Clear();

		this->m_DataVersion = rhs.m_DataVersion;
		this->m_ChunkVersion = rhs.m_ChunkVersion;
		this->m_ClassId = rhs.m_ClassId;

		this->m_Parser = rhs.m_Parser;

		this->m_ObjectList = rhs.m_ObjectList;
		this->m_ManagerList = rhs.m_ManagerList;
		this->m_ChunkList = rhs.m_ChunkList;

		this->m_BindFile = rhs.m_BindFile;
		this->m_BindContext = rhs.m_BindContext;

		// steal buffer
		this->m_pData = rhs.m_pData;
		rhs.m_pData = nullptr;
		this->m_DataDwSize = rhs.m_DataDwSize;

		// clear steal chunk
		rhs.Clear();

		return *this;
	}

	CKStateChunk::~CKStateChunk() {
		if (this->m_pData != nullptr)
			delete[] this->m_pData;
	}

#pragma endregion

#pragma region Self Used Data Struct

	void CKStateChunk::LockedReadBufferDeleter::operator()(const void* /*buf*/) {
		if (m_Host == nullptr) return;
		m_Host->UnLockReadBuffer(m_ConsumedSize);
	}

	void CKStateChunk::LockedReadBufferDeleter::SetConsumedSize(CKDWORD newsize) {
		m_ConsumedSize = newsize;
	}
	
	void CKStateChunk::LockedWriteBufferDeleter::operator()(const void* /*buf*/) {
		if (m_Host == nullptr) return;
		m_Host->UnLockWriteBuffer(m_ConsumedSize);
	}

	void CKStateChunk::LockedWriteBufferDeleter::SetConsumedSize(CKDWORD newsize) {
		m_ConsumedSize = newsize;
	}
	
	void CKStateChunk::BufferDeleter::operator()(const void* buf) {
		if (m_Host == nullptr) return;
		m_Host->DeleteBuffer(buf);
	}

	CKDWORD CKStateChunk::BufferDeleter::GetBufferSize() const {
		return m_BufSize;
	}

#pragma endregion
	
#pragma region Misc Funcs

	// ========== Public Funcs ==========

	const CKStateChunk::ProfileStateChunk_t CKStateChunk::GetStateChunkProfile() {
		return CKStateChunk::ProfileStateChunk_t {
			.m_ClassId = this->m_ClassId,
			.m_DataDwSize = this->m_DataDwSize,
			.m_pData = this->m_pData,

			.m_DataVersion = this->m_DataVersion,
			.m_ChunkVersion = this->m_ChunkVersion,

			.m_ObjectListSize = this->m_ObjectList.size(),
			.m_ChunkListSize = this->m_ChunkList.size(),
			.m_ManagerListSize = this->m_ManagerList.size(),

			.m_BindFile = this->m_BindFile,
			.m_BindContext = this->m_BindContext,
		};
	}
	
	const XContainer::XArray<CKStateChunk::ProfileIdentifier_t> CKStateChunk::GetIdentifiersProfile() {
		XContainer::XArray<CKStateChunk::ProfileIdentifier_t> collection;
		if (this->m_Parser.m_Status != CKStateChunkStatus::READ) return collection;

		CKDWORD pos = 0u;
		if (this->m_DataDwSize < 2u) return collection;	// impossible to have a identifier

		// iterate identifier
		while (true) {
			// add current identifier
			CKDWORD nextptr = this->m_pData[pos + 1];
			if (nextptr == 0u) {
				nextptr = this->m_DataDwSize;	// got tail. no more identifier
			}
			collection.emplace_back(CKStateChunk::ProfileIdentifier_t{
				this->m_pData[pos],
				this->m_pData + pos + 2,
				CKSizeof(CKDWORD) * (nextptr - pos - 2u)
			});

			// move to next identifier or exit
			// got tail. no more identifier
			if (this->m_pData[pos + 1] == 0u) break;

			pos = this->m_pData[pos + 1];

			// out of buffer
			if (pos + 1 >= this->m_DataDwSize) break;
		};
		return collection;

	}

	void CKStateChunk::Clear() {
		this->m_ClassId = CK_CLASSID::CKCID_OBJECT;
		this->m_DataVersion = CK_STATECHUNK_DATAVERSION::CHUNK_DEV_2_1;
		this->m_ChunkVersion = CK_STATECHUNK_CHUNKVERSION::CHUNK_VERSION4;

		this->m_Parser.m_Status = CKStateChunkStatus::IDLE;
		this->m_Parser.m_CurrentPos = 0;
		this->m_Parser.m_DataSize = 0;
		this->m_Parser.m_PrevIdentifierPos = 0;

		this->m_DataDwSize = 0;
		if (this->m_pData != nullptr) {
			delete[] this->m_pData;
			this->m_pData = nullptr;
		}

		this->m_ObjectList.clear();
		this->m_ManagerList.clear();
		this->m_ChunkList.clear();
	}

	CKDWORD CKStateChunk::GetDataSize() const {
		return CKSizeof(CKDWORD) * this->m_DataDwSize;
	}

	CK_STATECHUNK_DATAVERSION CKStateChunk::GetDataVersion() const {
		return this->m_DataVersion;
	}

	void CKStateChunk::SetDataVersion(CK_STATECHUNK_DATAVERSION version) {
		this->m_DataVersion = version;
	}

	CK_CLASSID CKStateChunk::GetClassId() const {
		return this->m_ClassId;
	}

	void CKStateChunk::SetClassId(CK_CLASSID cid) {
		this->m_ClassId = cid;
	}

	bool CKStateChunk::Skip(CKDWORD DwordCount) {
		bool result;
		switch (this->m_Parser.m_Status) {
			case CKStateChunkStatus::READ:
				result = EnsureReadSpace(DwordCount);
				break;
			case CKStateChunkStatus::WRITE:
				result = EnsureWriteSpace(DwordCount);
				break;
			case CKStateChunkStatus::IDLE:
			default:
				result = false;
				break;
		}

		// if success, move cursor
		if (result) {
			this->m_Parser.m_CurrentPos += DwordCount;
		}
		return result;
	}

	// ========== Private Funcs ==========

	CKDWORD CKStateChunk::GetCeilDwordSize(size_t char_size) {
		return static_cast<CKDWORD>((char_size + 3) >> 2);
	}

	bool CKStateChunk::ResizeBuffer(CKDWORD new_dwsize) {
		if (new_dwsize == 0u) {
			// if reuqired size is zero, we just delete it
			if (this->m_pData != nullptr) {
				delete[] this->m_pData;
				this->m_pData = nullptr;
			}

			// set buf size
			this->m_Parser.m_DataSize = 0u;
		} else {
			// otherwise, we create a new buffer instead it
			CKDWORD* newbuf = new CKDWORD[new_dwsize];

			// we copy original data only when it has.
			if (this->m_pData != nullptr) {
				// MARK: use std::min to copy for the minilist one
				// otherwise, EnsureWriteSpace or StopWrite will crash.
				std::memcpy(newbuf, this->m_pData, sizeof(CKDWORD) * std::min(this->m_Parser.m_DataSize, new_dwsize));
				delete[] this->m_pData;
			}

			// assign new buffer
			this->m_pData = newbuf;

			// set buf size
			this->m_Parser.m_DataSize = new_dwsize;
		}

		return true;
	}

	bool CKStateChunk::EnsureWriteSpace(CKDWORD dwsize) {
		if (this->m_Parser.m_Status != CKStateChunkStatus::WRITE) return false;

		// check whether need enlarge
		CKDWORD needed = dwsize + this->m_Parser.m_CurrentPos;
		if (needed > this->m_Parser.m_DataSize) {
			// add a very enough space to buffer
			if (dwsize < 512) dwsize = 512;
			needed = dwsize + this->m_Parser.m_CurrentPos;

			// try resizing it
			if (!this->ResizeBuffer(needed)) return false;
		}

		return true;
	}

	bool CKStateChunk::EnsureReadSpace(CKDWORD dword_required) {
		return (m_Parser.m_Status == CKStateChunkStatus::READ) &&
			(this->m_Parser.m_CurrentPos + dword_required <= this->m_Parser.m_DataSize);
	}

	void CKStateChunk::AddEntry(XContainer::XArray<CKDWORD>& entry_ls, CKDWORD pos) {
		entry_ls.emplace_back(pos);
	}

	void CKStateChunk::AddEntries(XContainer::XArray<CKDWORD>& entry_ls, CKDWORD pos) {
		entry_ls.emplace_back(static_cast<CKDWORD>(-1));
		entry_ls.emplace_back(pos);
	}

#pragma endregion

#pragma region Buffer Related

	bool CKStateChunk::ConvertFromBuffer(const void* buf) {
		if (buf == nullptr) return false;
		this->Clear();

		// read chunk ver and data ver first
		// chunk ver always set in the 3rd BYTE in every format
		this->m_ChunkVersion = static_cast<CK_STATECHUNK_CHUNKVERSION>(
			static_cast<const CKBYTE*>(buf)[2]
			);
		// data ver always set in the 1st BYTE in every format
		this->m_DataVersion = static_cast<CK_STATECHUNK_DATAVERSION>(
			static_cast<const CKBYTE*>(buf)[0]
			);

		// switch according to chunk ver
		const CKDWORD* dwbuf = static_cast<const CKDWORD*>(buf);
		size_t bufpos = 0u;
		if (this->m_ChunkVersion < CK_STATECHUNK_CHUNKVERSION::CHUNK_VERSION2) {
			// very old file

			this->m_ClassId = static_cast<CK_CLASSID>(dwbuf[1]);
			this->m_DataDwSize = dwbuf[2];

			this->m_ObjectList.resize(dwbuf[4]);
			this->m_ChunkList.resize(dwbuf[5]);
			bufpos = 6u;

			if (this->m_DataDwSize != 0) {
				this->m_pData = new CKDWORD[this->m_DataDwSize];
				std::memcpy(this->m_pData, dwbuf + bufpos, sizeof(CKDWORD) * this->m_DataDwSize);
				bufpos += this->m_DataDwSize;
			}
			if (!this->m_ObjectList.empty()) {
				std::memcpy(this->m_ObjectList.data(), dwbuf + bufpos, sizeof(CKDWORD) * this->m_ObjectList.size());
				bufpos += this->m_ObjectList.size();
			}
			if (!this->m_ChunkList.empty()) {
				std::memcpy(this->m_ChunkList.data(), dwbuf + bufpos, sizeof(CKDWORD) * this->m_ChunkList.size());
				bufpos += this->m_ChunkList.size();
			}
			
			// no bind file
			this->m_BindFile = nullptr;

		} else if (this->m_ChunkVersion == CK_STATECHUNK_CHUNKVERSION::CHUNK_VERSION2) {
			// medium ver file

			this->m_ClassId = static_cast<CK_CLASSID>(dwbuf[1]);
			this->m_DataDwSize = dwbuf[2];

			this->m_ObjectList.resize(dwbuf[4]);
			this->m_ChunkList.resize(dwbuf[5]);
			this->m_ManagerList.resize(dwbuf[6]);
			bufpos = 7u;

			if (this->m_DataDwSize != 0) {
				this->m_pData = new CKDWORD[this->m_DataDwSize];
				std::memcpy(this->m_pData, dwbuf + bufpos, sizeof(CKDWORD) * this->m_DataDwSize);
				bufpos += this->m_DataDwSize;
			}
			if (!this->m_ObjectList.empty()) {
				std::memcpy(this->m_ObjectList.data(), dwbuf + bufpos, sizeof(CKDWORD) * this->m_ObjectList.size());
				bufpos += this->m_ObjectList.size();
			}
			if (!this->m_ChunkList.empty()) {
				std::memcpy(this->m_ChunkList.data(), dwbuf + bufpos, sizeof(CKDWORD) * this->m_ChunkList.size());
				bufpos += this->m_ChunkList.size();
			}
			if (!this->m_ManagerList.empty()) {
				std::memcpy(this->m_ManagerList.data(), dwbuf + bufpos, sizeof(CKDWORD) * this->m_ManagerList.size());
				bufpos += this->m_ManagerList.size();
			}

			// no bind file
			this->m_BindFile = nullptr;

		} else if (this->m_ChunkVersion <= CK_STATECHUNK_CHUNKVERSION::CHUNK_VERSION4) {
			// the latest file

			// re-read some extra data
			// class id located the 2nd BYTE
			this->m_ClassId = static_cast<CK_CLASSID>(
				static_cast<const CKBYTE*>(buf)[1]
				);
			// options located the 4th BYTE
			CK_STATECHUNK_CHUNKOPTIONS options = static_cast<CK_STATECHUNK_CHUNKOPTIONS>(
				static_cast<const CKBYTE*>(buf)[3]
				);

			// read normal data
			this->m_DataDwSize = dwbuf[1];
			bufpos = 2u;

			if (this->m_DataDwSize != 0) {
				this->m_pData = new CKDWORD[this->m_DataDwSize];
				std::memcpy(this->m_pData, dwbuf + bufpos, sizeof(CKDWORD) * this->m_DataDwSize);
				bufpos += this->m_DataDwSize;
			}
			if (!YYCC::EnumHelper::Has(options, CK_STATECHUNK_CHUNKOPTIONS::CHNK_OPTION_FILE)) {
				// forced no bind file
				this->m_BindFile = nullptr;
			}
			if (YYCC::EnumHelper::Has(options, CK_STATECHUNK_CHUNKOPTIONS::CHNK_OPTION_IDS)) {
				this->m_ObjectList.resize(dwbuf[bufpos]);
				bufpos += 1u;
				std::memcpy(this->m_ObjectList.data(), dwbuf + bufpos, sizeof(CKDWORD) * this->m_ObjectList.size());
				bufpos += this->m_ObjectList.size();
			}
			if (YYCC::EnumHelper::Has(options, CK_STATECHUNK_CHUNKOPTIONS::CHNK_OPTION_CHN)) {
				this->m_ChunkList.resize(dwbuf[bufpos]);
				bufpos += 1u;
				std::memcpy(this->m_ChunkList.data(), dwbuf + bufpos, sizeof(CKDWORD) * this->m_ChunkList.size());
				bufpos += this->m_ChunkList.size();
			}
			if (YYCC::EnumHelper::Has(options, CK_STATECHUNK_CHUNKOPTIONS::CHNK_OPTION_MAN)) {
				this->m_ManagerList.resize(dwbuf[bufpos]);
				bufpos += 1u;
				std::memcpy(this->m_ManagerList.data(), dwbuf + bufpos, sizeof(CKDWORD) * this->m_ManagerList.size());
				bufpos += this->m_ManagerList.size();
			}


		} else {
			// too new. can not read, skip
			return true;
		}

		return true;
	}

	CKDWORD CKStateChunk::ConvertToBuffer(void* buf) {
		// calc size and setup options first
		// size = buffer + buffer_size + header
		CKDWORD size = (m_DataDwSize * CKSizeof(CKDWORD)) + CKSizeof(CKDWORD) + CKSizeof(CKDWORD);
		CK_STATECHUNK_CHUNKOPTIONS options = static_cast<CK_STATECHUNK_CHUNKOPTIONS>(0);

		if (!m_ObjectList.empty()) {
			size += CKSizeof(CKDWORD) * static_cast<CKDWORD>(m_ObjectList.size()) + sizeof(CKDWORD);
			YYCC::EnumHelper::Add(options, CK_STATECHUNK_CHUNKOPTIONS::CHNK_OPTION_IDS);
		}
		if (!m_ChunkList.empty()) {
			size += CKSizeof(CKDWORD) * static_cast<CKDWORD>(m_ChunkList.size()) + sizeof(CKDWORD);
			YYCC::EnumHelper::Add(options, CK_STATECHUNK_CHUNKOPTIONS::CHNK_OPTION_CHN);
		}
		if (!m_ManagerList.empty()) {
			size += CKSizeof(CKDWORD) * static_cast<CKDWORD>(m_ManagerList.size()) + sizeof(CKDWORD);
			YYCC::EnumHelper::Add(options, CK_STATECHUNK_CHUNKOPTIONS::CHNK_OPTION_MAN);
		}

		if (this->m_BindFile != nullptr) {
			YYCC::EnumHelper::Add(options, CK_STATECHUNK_CHUNKOPTIONS::CHNK_OPTION_FILE);
		}

		// if buffer provided, write it
		if (buf != nullptr) {
			// write header
			static_cast<CKBYTE*>(buf)[0] = static_cast<CKBYTE>(this->m_DataVersion);
			static_cast<CKBYTE*>(buf)[1] = static_cast<CKBYTE>(this->m_ClassId);
			static_cast<CKBYTE*>(buf)[2] = static_cast<CKBYTE>(this->m_ChunkVersion);
			static_cast<CKBYTE*>(buf)[3] = static_cast<CKBYTE>(options);

			CKDWORD* dwbuf = static_cast<CKDWORD*>(buf);
			// write buffer length
			dwbuf[1] = this->m_DataDwSize;
			size_t bufpos = 2u;
			// write buffer
			std::memcpy(dwbuf + bufpos, this->m_pData, this->m_DataDwSize * sizeof(CKDWORD));
			bufpos += this->m_DataDwSize;

			// write list
			if (!m_ObjectList.empty()) {
				dwbuf[bufpos] = static_cast<CKDWORD>(m_ObjectList.size());
				std::memcpy(dwbuf + bufpos + 1, m_ObjectList.data(), m_ObjectList.size() * sizeof(CKDWORD));
				bufpos += m_ObjectList.size() + 1;
			}
			if (!m_ChunkList.empty()) {
				dwbuf[bufpos] = static_cast<CKDWORD>(m_ChunkList.size());
				std::memcpy(dwbuf + bufpos + 1, m_ChunkList.data(), m_ChunkList.size() * sizeof(CKDWORD));
				bufpos += m_ChunkList.size() + 1;
			}
			if (!m_ManagerList.empty()) {
				dwbuf[bufpos] = static_cast<CKDWORD>(m_ManagerList.size());
				std::memcpy(dwbuf + bufpos + 1, m_ManagerList.data(), m_ManagerList.size() * sizeof(CKDWORD));
				bufpos += m_ManagerList.size() + 1;
			}

		}

		// return expected size.
		return size;
	}

#pragma endregion

}