/* -----------------------------------------------------------------------------
	GSFramework
	Copyright 2001-2013 Emmanuel Julien. All Rights Reserved.
----------------------------------------------------------------------------- */


		#include <cstring>
		#include "zlib.h"
		#include "archive/archive.h"
		#include "metafile/nml.h"
		#include "filesystem/filesystem.h"
		#include "thread/mutex.h"
		#include "platform.h"
		#include "log/log.h"

		using namespace GS;
		using namespace GS::NML;


//------------------------------------------------------------------------------
#define	__CorrectOffsetPadding	\
{\
	if	(offset_padding)\
	{\
		size_t error = handle->Tell() % offset_padding;\
		if	(error)\
			handle->Seek((long)(offset_padding - error));\
	}\
}
bool				ArchiveIndex::Load(const char *uri)
{
	index.Clear();

	__LOG_V__ << "Loading archive index '" << uri << "'...\n";

	File file;
	if	(!Parser::Load(uri, file))
		return false;

	// Grab index tag.
	Tag *index_tag = file.GetTag("Index;");
	if	(!index_tag)
		__ERR__(__LOG_E__ << "No archive index tag found in '" << uri << "'.\n", false)

	// Pool for entries.
	NMLTagForeach(t, *index_tag)
		if	(t->name == "Entry")
		{
			Tag	*id_tag = t->GetTypedTag("ID;", Variant::VariantString),
				*compressed_tag = t->GetTypedTag("CompLen", Variant::VariantInteger),
				*length_tag = t->GetTypedTag("Len", Variant::VariantInteger),
				*method_tag = t->GetTypedTag("Method", Variant::VariantString),
				*offset_tag = t->GetTypedTag("Offset", Variant::VariantInteger);

			if	(id_tag && compressed_tag && length_tag && method_tag)
			{
				// Import the entry.
				if	(ArchiveEntry *entry = new ArchiveEntry)
				{
					entry->path = id_tag->GetString();
					entry->length = length_tag->GetInteger();
					entry->compressed_length = compressed_tag->GetInteger();
					entry->offset = offset_tag->GetInteger();

					String method(method_tag->GetString());

							if	(method == "Raw")
						entry->method = ArchiveEntry::MethodRaw;
					else	if	(method == "Zlib")
						entry->method = ArchiveEntry::MethodZLibCompress;	

					index.Add(entry);
				}
				else
					__LOG_E__ << "Failed to allocate new archive index entry.\n";
			}
			else
				__LOG_E__ << "Incomplete index entry.\n";
		}
		else
			__LOG_W__ << "Unexpected tag <" << t->name << "> in <Index>.\n";

	__LOG_V__ << "Done, found " << index.GetCount() << " entries.\n";
	return true;
}
bool				ArchiveIndex::Save(const char *uri)
{
	File file;
	Tag *index_tag = file.AddRoot("Index");

	ListForeachPtr(ArchiveEntry *, entry, index)
	{
		// Create entry.
		Tag *entry_tag = index_tag->AddChild("Entry");

		entry_tag->AddChild("ID", entry->path.c_str());
		entry_tag->AddChild("CompLen", (int)entry->compressed_length);
		entry_tag->AddChild("Len", (int)entry->length);
		entry_tag->AddChild("Offset", (int)entry->offset);

		switch (entry->method)
		{
			case ArchiveEntry::MethodRaw:
				entry_tag->AddChild("Method", "Raw");
				break;

			case ArchiveEntry::MethodZLibCompress:
				entry_tag->AddChild("Method", "Zlib");
				break;
		}
	}

	return Parser::Save(uri, file);
}
ArchiveEntry		*ArchiveIndex::FindEntry(const char *alias) const
{
	ListForeachPtr(ArchiveEntry *, e, index)
		if	(e->path == alias)
			return e;
	return NULL;
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
bool				Archive::LoadIndex(const char *uri)
{	return index.Load(uri);	}
bool				Archive::SaveIndex(const char *uri)
{	return index.Save(uri);	}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
bool				Archive::OpenRead(const char *uri, const char *idx)
{
	Close();
	if	(!uri)
		__ERR__(__LOG_E__ << "No archive to open.\n", false)

	if	(!(handle = Platform::Get().io->Open(uri)))
		__ERR__(__LOG_E__ << "Failed to open archive '" << uri << "'.\n", false)			

	// Check archive header.
	__CorrectOffsetPadding

	uint magic_word = handle->Read <uint> ();

	if	(magic_word == 0x4E415244)			// 'NARD' (padding support).
	{
		__CorrectOffsetPadding
		offset_padding = handle->Read <int> ();
		__CorrectOffsetPadding
		size_padding = handle->Read <int> ();
		revision = EnhancedLegacy;
	}
	else	if	(magic_word == 0x4E415243)	// 'NARC' backward compatibility.
	{
		offset_padding = 0;
		size_padding = 0;
		revision = Legacy;
	}
	else
	{
		__LOG_E__ << "Invalid archive type '" << uri << "'.\n";
		Close();
		return false;
	}

	// Open or create index if none provided.
	if	(!idx)
	{
		size_t alen = handle->GetSize();

		if	(verbose)
			__LOG__ << "No index provided, please wait while scanning archive...\n";

		while (handle->Tell() < alen)
		{
			// Fetch alias.
			char tmp[512];
			__CorrectOffsetPadding
			uint tsz = handle->Read <uint> ();

			if	(tsz > 511)
				break;

			else
			{
				handle->Read((void *)tmp, tsz);
				tmp[tsz] = 0;
			}

			// Attributes.
			__CorrectOffsetPadding
			char cmp = handle->Read <uchar> () & 1;
			__CorrectOffsetPadding
			uint len = handle->Read <uint> (), clen = len;
			if	(cmp)
			{
				__CorrectOffsetPadding
				clen = handle->Read <uint> ();
			}

			// Store in index.
			if	(ArchiveEntry *entry = new ArchiveEntry)
			{
				entry->path = tmp;
				entry->method = cmp;
				entry->length = len;
				entry->compressed_length = clen;
				entry->offset = handle->Tell();
				index.index.Add(entry);

				if	(verbose)
				{
					__LOG__ << "New entry: '" << entry->path << "'\n";
					__LOG__ << "[method = " << entry->method << ", len = " << (uint)entry->length << ", clen = " << (uint)entry->compressed_length << ", offset = " << (uint)entry->offset << "].\n";
				}
			}
			else
				__LOG_E__ << "Could not allocate new index entry.\n";

			handle->Seek(clen);
		}

		if	(verbose)
			__LOG__ << "Done, index table built.\n\n";
	}
	else
	{
		LoadIndex(idx);

		if	(verbose)
			ListForeachPtr(ArchiveEntry *, entry, index.index)
			{
				__LOG__ << "New entry: '" << entry->path << "'\n";
				__LOG__ << "[method = " << entry->method << ", len = " << (uint)entry->length << ", clen = " << (uint)entry->compressed_length << ", offset = " << (uint)entry->offset << "].\n";
			}
	}
	return true;
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
bool				Archive::CreateNew(const char *uri)
{
	Close();

	if	(!(handle = Platform::Get().io->Open(uri, IO::ModeWrite)))
		__ERR__(__LOG_E__ << "Failed to open '" << uri << "' as output archive.\n", false)

	// Output header.
	__CorrectOffsetPadding
	handle->Write <uint> (0x4E415244);	// 'NARD'
	__CorrectOffsetPadding
	handle->Write <int> (offset_padding);
	__CorrectOffsetPadding
	handle->Write <int> (size_padding);

	append_mode = true;
	return true;
}
void				Archive::Close()
{
	if	(append_mode)
	{
		// EOF marker.
		handle->Write <uint> (0xffffffff);

		// Size padding.
		size_t size = handle->Tell();
		size_t pad_count = size_padding ? size_padding - size % size_padding : 0;

		for	(size_t n = 0; n < pad_count; ++n)
			handle->Write <uchar> (0xff);	// End of archive marker (entry id length > 512).
	}

	// Close archive.
	handle = NULL;
	index.index.Clear();

	if	(append_mode)
		__LOG__ << "Archive complete, closing.\n";
	append_mode = false;
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
bool				Archive::FileRead(const char *path, void *out)
{
	if	(append_mode)
		__ERR__(__LOG_E__ << "Cannot load '" << path << "' (r/w mode error).\n", false)

	ArchiveEntry *entry = Exists(path);
	if	(!entry)
		__ERR__(__LOG_E__ << "Could not find '" << path << "'.\n", false)

	Threading::MutexLock lock(access_mutex);

	size_t h_cursor = handle->Tell();

	switch (entry->method)
	{
		case ArchiveEntry::MethodRaw:
			handle->Seek(entry->offset, IO::Base::SeekStart);		// Note: Should be padded to the right offset already.
			if	(handle->Read(out, entry->length) != entry->length)
				__LOG_E__ << "Failed to load raw source.\n";								
			break;

		case ArchiveEntry::MethodZLibCompress:
		{
			handle->Seek(entry->offset, IO::Base::SeekStart);		// Note: Should be padded to the right offset already.

			Array <char> cp((uint)(entry->compressed_length + 16));
			if	(!cp)
				__ERR__(__LOG_E__ << "Failed to allocated compressed memory support.\n", false)

			if	(handle->Read((void *)cp, entry->compressed_length) != entry->compressed_length)
				__ERR__(__LOG_E__ << "Failed to load compressed source.\n", false)

			uLong out_len = (uLong)entry->length;
			uncompress((Bytef *)out, &out_len, (Bytef *)&cp[0], (uLong)entry->compressed_length);
		}
		break;
	}

	handle->Seek(h_cursor, IO::Base::SeekStart);
	return true;
}
//-----------------------------------------------------------------

//------------------------------------------------------------------------------
ArchiveEntry		*Archive::MemoryBlockWrite(const char *alias, const void *in, size_t len, int level)
{
	if	(!append_mode || !len)
		__ERR__(__LOG_E__ << "Cannot write '" << alias << "' (r/w mode error).\n", NULL)

	Threading::MutexLock lock(access_mutex);

	// Ensure alias uniqueness.
	if	(ArchiveEntry *e = index.FindEntry(alias))
		return e;

	// Sync index.
	ArchiveEntry *entry = new ArchiveEntry;
	if	(!entry)
		__ERR__(__LOG_E__ << "Failed to allocate new index entry.\n", NULL)
	entry->path = alias;
	index.index.Add(entry);

	// Compress and add to archive.
	Array <char> out;
	size_t clen;

	// Output alias.
	__CorrectOffsetPadding
	handle->Write <uint> (std::strlen(alias));
	__CorrectOffsetPadding
	handle->Write((const void *)alias, std::strlen(alias));
	if	(verbose)
		__LOG_H__ << "Adding '" << alias << "' to archive...\n";

	// Output data, ZLIB needs destination to be at least source * 100.1% + 12 bytes.
	bool add_raw = true;

	if	(level >= 0)
	{
		clen = len + (len / 1000 + 1) + 12;

		if	(out.Allocate((uint)clen) && (compress2((Bytef *)&out[0], (uLongf *)&clen, (const Bytef *)in, (uLong)len, level) == Z_OK))
		{
			// @TODO compression method as a bit flag here.
			__CorrectOffsetPadding
			handle->Write <uchar> (1);
			__CorrectOffsetPadding
			handle->Write <uint> (len);
			__CorrectOffsetPadding
			handle->Write <uint> (clen);

			// Write compressed block.
			__CorrectOffsetPadding
			entry->method = ArchiveEntry::MethodZLibCompress;
			entry->offset = handle->Tell();
			entry->length = len;
			entry->compressed_length = clen;

			if	(verbose)
				__LOG__ << "Was " << (uint)(len / 1000) << " KB, is now " << (uint)(clen / 1000) << " KB (" << (uint)(clen * 100 / len) <<"%).\n";

			handle->Write((const void *)out, clen);
			add_raw = false;
		}
		else
			if	(verbose)
				__LOG__ << "No compression achieved for '" << alias << "', adding uncompressed.\n";
	}

	if	(add_raw)
	{
		__CorrectOffsetPadding
		handle->Write <uchar> (0);
		__CorrectOffsetPadding
		handle->Write <uint> (len);

		// Write raw block.
		__CorrectOffsetPadding

		entry->method = ArchiveEntry::MethodRaw;
		entry->offset = handle->Tell();
		entry->length = len;
		entry->compressed_length = 0;

		handle->Write((const void *)in, len);
	}
	return entry;
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
ArchiveEntry		*Archive::FileWrite(const char *path, const char *alias, int level)
{
	Array <char> data;
	if	(!Platform::Get().io->FileLoad(path, data))
		return NULL;
	return MemoryBlockWrite(alias ? alias : path, &data[0], data.GetSize(), level);
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
Archive::Archive()
{
	append_mode = false;
	offset_padding = 0;
	size_padding = 0;

	access_mutex = new Threading::Mutex;
}
Archive::~Archive()
{
	Close();
}
//------------------------------------------------------------------------------
