// ---------------------------------------------------------------------------
// 
//   Lily Engine alpha
// 
//   Copyright (c) 2010 Clifford Jolly
//     http://cliffjolly.com
//     cliff@cliffjolly.com
// 
// ---------------------------------------------------------------------------
// 
//   This program is free software; you can redistribute it and/or modify
//   it under the terms of the GNU General Public License as published by
//   the Free Software Foundation; either version 2 of the License, or
//   (at your option) any later version.
//  
//   This program is distributed in the hope that it will be useful, but
//   WITHOUT ANY WARRANTY; without even the implied warranty of
//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
//   General Public License for more details.
// 
//   You should have received a copy of the GNU General Public License
//   along with this program; if not, write to the Free Software
//   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
//   USA
// -------------------------- END HEADER -------------------------------------

#include <malloc.h>
#include <cassert>
#include <iostream>
#include <cstring>
using namespace std;

#include "filesystem.h"
#include "archive.h"

namespace FileSystem {

	struct FileHeader {
		// Name of the file.
		char name[512];

		// Length of the file (little endian, 32 bits).
		// Does not include the size of this header.
		unsigned int length;
	};

	Archive::Archive(const std::string &fileName, bool writeMode) {

		failed = false;	

		if(writeMode) {
			archiveFile.open(fileName.c_str(), ios::out | ios::binary);
		} else {
			archiveFile.open(fileName.c_str(), ios::in | ios::binary);
		}
		this->writeMode = writeMode;
		myFileName = fileName;

		if(archiveFile.fail()) {
			failed = true;
			return;
		}

		// Scan through the whole file and build up the TOC.
		if(!writeMode) {
			rebuildTableOfContents();
			closeArchiveFile();
		}
	}

	Archive::~Archive(void) {
		closeArchiveFile();
	}

	void Archive::openArchiveFile(void) {
		if(!archiveFile.is_open()) {
			archiveFile.open(myFileName.c_str(), ios::binary | (writeMode ? ios::out : ios::in) );
		}
	}
	void Archive::closeArchiveFile(void) {
		if(archiveFile.is_open()) {
			archiveFile.close();
		}
	}

	void Archive::addFile(const std::string &fileName, const char *data, int length) {
		if(failed) return;
		assert(archiveFile.is_open());
		assert(writeMode);

		/*
		// First, see if the file is already in the archive.
		if(tableOfContents[fileName]) {
			// If it is, remove it.
			deleteFile(fileName);
		}
		*/

		// Go to the end of the archive.
		archiveFile.seekp(0, ios_base::end);
		unsigned int currentEnd = archiveFile.tellp();

		// Make the filename consistent.
		string fixedFileName = fixFileName(fileName);

		// Write the header.
		FileHeader *header = (FileHeader*)calloc(1, sizeof(FileHeader));
		assert(header);
		strncpy(header->name, fixedFileName.c_str(), 511);
		header->length = length;
		archiveFile.write((char*)header, sizeof(FileHeader));
		free(header);

		// Write the data.
		archiveFile.write(data, length);

		// Update TOC.
		tableOfContents[fileName] = currentEnd;
	}

	/*
	void Archive::deleteFile(const std::string &fileName) {
		// TODO: Maybe implement this.
	}
	*/

	char *Archive::loadFile(const std::string &fileName, int *length, bool addNullTerminator) {
		if(failed) return NULL;
		assert(!writeMode);
		openArchiveFile();

		string fixedFileName = fixFileName(fileName);

		// Get our offset of the header in the archive.
		unsigned int offset;
		std::map<std::string, unsigned int>::iterator iter =
			tableOfContents.find(fixedFileName);

		if(iter == tableOfContents.end()) {
			return NULL; // Not in this archive!
		}

		offset = iter->second;

		// Go to the offset in the archive and read in the header.
		FileHeader header;
		archiveFile.seekg(offset, ios_base::beg);
		archiveFile.read((char*)&header, sizeof(FileHeader));

		// Make a place to put the data.
		char *data = new char[header.length + (addNullTerminator ? 1 : 0)];
		if(!data) return NULL; // Too big?
		
		// TODO: We should probably put an upper limit on the data size
		//   regardless of what we read from the archive. Allocating too
		//   much memory can be really bad.

		// Read in the data.
		archiveFile.read(data, header.length);
		*length = header.length;

		if(addNullTerminator) {
			data[*length] = 0;
			(*length)++;
		}

		closeArchiveFile();
		return data;
	}

    char *Archive::loadFileHead(const std::string &fileName, int headerLength) {

        // Same as above, but only reads the first headerLength bytes.

        if(failed) return NULL;
        assert(!writeMode);
        openArchiveFile();

        string fixedFileName = fixFileName(fileName);

        // Get our offset of the header in the archive.
        unsigned int offset;
        std::map<std::string, unsigned int>::iterator iter =
            tableOfContents.find(fixedFileName);

        if(iter == tableOfContents.end()) {
            return NULL; // Not in this archive!
        }

        offset = iter->second;

        // Go to the offset in the archive and read in the header.
        FileHeader header;
        archiveFile.seekg(offset, ios_base::beg);
        archiveFile.read((char*)&header, sizeof(FileHeader));

        // Make a place to put the data.
        char *data = new char[headerLength];
        if(!data) return NULL; // Too big?

        memset(data, 0, headerLength);

        // Read in the data.
        archiveFile.read(data, header.length < headerLength ? header.length : headerLength);

        closeArchiveFile();
        return data;
    }

	void Archive::rebuildTableOfContents(void) {
		
		tableOfContents.clear();
		tableOfDirectories.clear();
		
		assert(!writeMode);
		openArchiveFile();

		if(failed) return;

		archiveFile.seekg(0, ios_base::beg);

		// Clear whatever last EOF or error might be sitting around.
		archiveFile.clear();
		
		std::map<std::string, std::map<std::string, bool> > directoriesInDirectories;
		
		while(!archiveFile.eof()) {
			
			FileHeader header;
			int currentPos = archiveFile.tellg();
			
			archiveFile.read((char*)&header, sizeof(FileHeader));

			if(archiveFile.fail()) {
				archiveFile.clear();
				break;
			}

			tableOfContents[header.name] = currentPos;
			
			// Add it to the appropriate directory entry.
			string dirName = getParentName(header.name);
			string baseName = header.name + dirName.size();
			
			// Get rid of that last '/'.
			if(baseName.size()) {
				baseName = baseName.c_str() + 1;
			}
			
			directoryContents[dirName].push_back(baseName);

			while(dirName.size()) {
				string parentName = getParentName(dirName);
				directoriesInDirectories[parentName][getBaseName(dirName)] = true;
				dirName = parentName;
			}
			
			// Next file.
			archiveFile.seekg(header.length, ios_base::cur);
		}

		// Add all the directories found to the directory directories.

		// These iterator loops look ugly as hell. Can I use the 'auto'
		// keyword yet? Ughh...
		for(std::map<std::string, std::map<std::string, bool> >::iterator outerIterator = directoriesInDirectories.begin(); outerIterator != directoriesInDirectories.end(); outerIterator++) {
			for(std::map<std::string, bool>::iterator innerIterator = (*outerIterator).second.begin(); innerIterator != (*outerIterator).second.end(); innerIterator++) {
				
				string outerDir = (*outerIterator).first;
				string innerDir = (*innerIterator).first;
				
				//cout << "Dir in Dir: " << outerDir << " / " << innerDir << endl;
				subdirectories[outerDir].push_back(innerDir);
				
				if(outerDir.size()) outerDir = outerDir + "/";
				tableOfDirectories[outerDir + innerDir] = true;
				
			}
		}
		
		closeArchiveFile();
	}

	bool Archive::getFileExists(const std::string &fileName) {
		if(failed) return false;
		
		string fixedFileName = fixFileName(fileName);

		std::map<std::string, unsigned int>::iterator iter =
			tableOfContents.find(fixedFileName);

		return !(iter == tableOfContents.end());
	}
	
	bool Archive::getDirExists(const std::string &dirName) {
		if(failed) return false;
		
		string fixedDirName = fixFileName(dirName);
		
		std::map<std::string, bool>::iterator iter =
			tableOfDirectories.find(fixedDirName);
			
		return !(iter == tableOfDirectories.end());
	}

	unsigned int Archive::getFileSize(const std::string &fileName) {
		if(failed) return 0;
		assert(!writeMode);
		openArchiveFile();

		string fixedFileName = fixFileName(fileName);
		
		if(!getFileExists(fixedFileName)) {
			return 0;
		}

		FileHeader header;

		archiveFile.seekg(tableOfContents[fileName], ios_base::beg);
		archiveFile.read((char*)&header, sizeof(FileHeader));
		
		closeArchiveFile();
		return header.length;

	}

	void Archive::getFileList(std::vector<std::string> &fileList) {
		std::map<std::string, unsigned int>::iterator iter;

		for(iter = tableOfContents.begin(); iter != tableOfContents.end(); iter++) {
			fileList.push_back(iter->first);
		}
	}
	
	void Archive::getFileListForDir(const std::string &dirName, std::vector<std::string> &fileList) {
		
		// In case anyone decided to be cute and use "." as the
		// directory name...
		string fixedDirName = fixFileName(dirName);
		
		fileList.insert(fileList.end(), subdirectories[fixedDirName].begin(), subdirectories[fixedDirName].end());
		fileList.insert(fileList.end(), directoryContents[fixedDirName].begin(), directoryContents[fixedDirName].end());
	}

	const std::string &Archive::getMyFileName(void) {
		// Should we return the fixed version of this?
		return myFileName;
	}

	bool Archive::getFailed(void) {
		return failed;
	}

}
