//--------------------------------------------------------------------------------
// <copyright file="StorageController.cpp" company="LiquidRobotics">
//
// Copyright (c) 2014 Liquid Robotics, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// This software is associated with a release.  Liquid Robotics makes no
// representation that the design, interfaces or component distribution will
// remain unchanged in future releases.
//
// </copyright>
//
// <summary>
//   Implementation of StorageController, a control class that implements storage,
//   listing, reading and writing of a set of files that have WGMS properties.
// </summary>
//
// <owner>Mike Cookson</owner>
//--------------------------------------------------------------------------------

#include "StorageController.h"
#include <iostream>
#include <fstream>
#include <utility>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <boost/archive/xml_iarchive.hpp>
#include <boost/archive/xml_oarchive.hpp>
#include <boost/filesystem.hpp>
#include <boost/foreach.hpp>
#include "LqrSMACPrintfBuffer.h"
#include "DoThrow.h"
#include "nsextend.h"

using namespace std;
using namespace boost::filesystem;
using namespace LiquidRobotics::PayloadInterface;
using namespace LiquidRobotics::SMACFramework;


#define EXIST_THROW(MAPTYPE,dataMap,item,itemStr,fieldName) 									\
	do { 																						\
		MAPTYPE::const_iterator it = dataMap.find(item); 										\
		if ((it != dataMap.end()) && (!overwrite || it->second->GetReadOnly())) 				\
		{ 																						\
			stringstream stm; 																	\
			stm << "A file with " 																\
			    << fieldName 																	\
			    << "=" 																			\
			    << itemStr 																		\
			    << " already exists in storage controller at " 									\
			    << config.GetStorageDirectoryName(); 											\
																								\
			if (!overwrite) 																	\
				DoThrow<FileState::CannotCreateFileExists>(__FILE__, __LINE__, stm.str()); 		\
			else 																				\
				DoThrow<FileState::CannotOverwriteReadOnly>(__FILE__, __LINE__, stm.str()); 	\
		} 																						\
	} while(0)

#define UNLINK_MATCHES(MAPTYPE,dataMap,item)													\
	do {																						\
		MAPTYPE::iterator it = dataMap.find(item);												\
		if (it != dataMap.end()) Unlink(it->second);											\
	} while(0)


namespace LiquidRobotics {
namespace PayloadInterface {

StorageController::StorageController(const Config& _config)
: pMutex(NULL)
, mutexNestingLevel(0)
, dirty(false)
, config(_config)
{
	pMutex = new Mutex;

	try
	{
		ifstream ifs(config.GetStateFileName().c_str());

		if (ifs.good())
		{
			boost::archive::xml_iarchive xia(ifs);
			xia >> BOOST_SERIALIZATION_NVP(fileStateList);
		}
	}
	catch(runtime_error& rtError)
	{
		fileStateList.clear();
	}

	RescanFileStateList();
}

StorageController::~StorageController()
{
	if (pMutex) { delete pMutex; pMutex = NULL; }
}

void StorageController::lock()
{
	if (!pMutex)
		DoThrow<runtime_error>(__FILE__, __LINE__, "Mutex has not been created");

	pMutex->lock();
	++mutexNestingLevel;
}

bool StorageController::try_lock()
{
	if (!pMutex)
		DoThrow<runtime_error>(__FILE__, __LINE__, "Mutex has not been created");

	bool result = pMutex->try_lock();

	if (result)
		++mutexNestingLevel;

	return result;
}

void StorageController::unlock()
{
	if (!pMutex)
		DoThrow<runtime_error>(__FILE__, __LINE__, "Mutex has not been created");

	if (!mutexNestingLevel)
		DoThrow<runtime_error>(__FILE__, __LINE__, "Unlocking instance with a zero nesting level");

	if (!--mutexNestingLevel)
		UpdateStateFile();

	pMutex->unlock();
}


void StorageController::AddFile(const string& actualFileName, FileIDType newFileID)
{
	Lock lock(*this);

	if ((newFileID != NO_FILE_ID) && (fileStateByID.find(newFileID) != fileStateByID.end()))
	{
		stringstream stm;
		stm << "A file with fileID=" << ultos(newFileID, 16, 8)
			<< " already exists in storage controller at " << config.GetStorageDirectoryName();

		DoThrow<FileState::CannotCreateFileExists>(__FILE__, __LINE__, stm.str());
	}

	FileState::Ptr newItem;
	newItem.reset(new FileState(*this, actualFileName));

	newItem->SetFileID(newFileID);

	try
	{
		dirty = true;
		fileStateList.push_back(newItem);
		fileStateByActualName[newItem->GetActualFileName()] = newItem;
		fileStateByReportedName[newItem->GetReportedFileName()] = newItem;

		if (newFileID != NO_FILE_ID)
			fileStateByID[newFileID] = newItem;

		const TimeAndNamePair tnpair(newItem->GetModifiedTime(), newItem->GetActualFileName());
		fileStateByModifiedTime[tnpair] = newItem;
	}
	catch(...)
	{
		Unlink(newItem);
		throw;
	}
}

void StorageController::CheckAllFiles(bool deleteOnFailure)
{
	Lock lock(*this);
	FileStateList listCopy = fileStateList;
	bool crcFail = false;
	bool md5Fail = false;

	BOOST_FOREACH(FileState::Ptr filePtr, listCopy)
	{
		bool fail = false;

		try
		{
			filePtr->CheckFile(config.GetStorageDirectoryName());
		}
		catch(FileState::FileMD5CheckError& md5Failure)
		{
			fail = true;
			crcFail = true;
		}
		catch(FileState::FileCRCCheckError& crcFailure)
		{
			fail = true;
			md5Fail = true;
		}
		catch(FileState::FileNotFound& fileNotFoundFailure)
		{
			fail = true;
		}
		catch(...)
		{
			if (filePtr->GetDirtyAndClear()) dirty = true;
			throw;
		}

		if (filePtr->GetDirtyAndClear()) dirty = true;

		if (fail && deleteOnFailure)
			Unlink(filePtr);
	}

	if (crcFail)
		DoThrow<FileState::FileCRCCheckError>(__FILE__, __LINE__, "One or more files failed CRC check");

	if (md5Fail)
		DoThrow<FileState::FileMD5CheckError>(__FILE__, __LINE__, "One or more files failed MD5 check");
}

void StorageController::CheckFile(FileIDType fileID, const string& reportedFileName, bool deleteOnFailure)
{
	Lock lock(*this);
	FileState::Ptr filePtr = GetSingleMatch(__FILE__, __LINE__, fileID, reportedFileName);

	try
	{
		filePtr->CheckFile(config.GetStorageDirectoryName());
	}
	catch(FileState::FileMD5CheckError& md5Failure)
	{
		if (filePtr->GetDirtyAndClear()) dirty = true;
		if (deleteOnFailure) Unlink(filePtr);
		throw;
	}
	catch(FileState::FileCRCCheckError& crcFailure)
	{
		if (filePtr->GetDirtyAndClear()) dirty = true;
		if (deleteOnFailure) Unlink(filePtr);
		throw;
	}
	catch(...)
	{
		if (filePtr->GetDirtyAndClear()) dirty = true;
		throw;
	}

	if (filePtr->GetDirtyAndClear()) dirty = true;
}

void StorageController::CreateFile(const FileState& newFileState, bool overwrite)
{
	Lock lock(*this);

	// verify that file ID does not exist
	const FileIDType fileID = newFileState.GetFileID();
	const TimeAndNamePair tnpair(newFileState.GetModifiedTime(), newFileState.GetActualFileName());

	if (fileID != NO_FILE_ID)
	{
		string message = ultos(fileID, 16, 8);
		EXIST_THROW(FileStateByID, fileStateByID, newFileState.GetFileID(), message, "File ID");
	}

	// verify that actual file name does not exist
	{
		string s = newFileState.GetActualFileName();
		EXIST_THROW(FileStateByString, fileStateByActualName, s, s, "Actual File Name");
	}

	// verify that reported file name does not exist
	{
		string s = newFileState.GetReportedFileName();
		EXIST_THROW(FileStateByString, fileStateByReportedName, s, s, "Reported File Name");
	}

	// verify modified date and name do not exist
	{
		stringstream stm;

		stm << "<" << tnpair.first << "," << tnpair.second << ">";

		string message = stm.str();
		EXIST_THROW(FileStateByTimeAndName, fileStateByModifiedTime, tnpair, message, "Modified Date And Name");
	}

	// since we got this far, go back and clear out any match on file ID, actual file name, reported file name
	// modified date and name
	if (fileID != NO_FILE_ID)
		UNLINK_MATCHES(FileStateByID, fileStateByID, fileID);

	UNLINK_MATCHES(FileStateByString, fileStateByActualName, newFileState.GetActualFileName());
	UNLINK_MATCHES(FileStateByString, fileStateByReportedName, newFileState.GetReportedFileName());
	UNLINK_MATCHES(FileStateByTimeAndName, fileStateByModifiedTime, tnpair);

	// now re-add this item
	FileState::Ptr newItem(FileState::Clone2Ptr(newFileState));

	try
	{
		dirty = true;
		fileStateList.push_back(newItem);
		fileStateByActualName[newFileState.GetActualFileName()] = newItem;
		fileStateByReportedName[newFileState.GetReportedFileName()] = newItem;
		fileStateByID[fileID] = newItem;
		fileStateByModifiedTime[tnpair] = newItem;
	}
	catch(...)
	{
		Unlink(newItem);
		throw;
	}
}

const StorageController::Config& StorageController::GetConfig() const
{
	return config;
}

StorageController::FileState::Ptr StorageController::GetSingleMatch(const char* fileName, int lineNum, FileIDType fileID, const string& reportedFileName)
{
	FileState::Ptr result;
	FileStateByID::iterator foundID;

	if ((foundID = fileStateByID.find(fileID)) == fileStateByID.end())
	{
		const bool emptyName = (reportedFileName.find_first_not_of(' ') == string::npos);
		FileStateByString::iterator foundStr;

		//cout << "GetSingleMatch for 0x" << ultos(fileID, 16, 8) << " '" << reportedFileName << "'" << endl
		//     << "reported name keys:" << endl;

		//for (FileStateByString::const_iterator it = fileStateByReportedName.begin(); it != fileStateByReportedName.end(); ++it)
		//	cout << "    '" << it->first << "'" << endl;

		if (emptyName
		 || ((foundStr = fileStateByReportedName.find(reportedFileName)) == fileStateByReportedName.end()))
		{
			stringstream msg;
			msg << "Could not find fileID=0x" << ultos(fileID, 16, 8) << " fileName='" << reportedFileName << "'";

			DoThrow<FileState::FileNotFound>(fileName, lineNum, msg.str());
		}
		else
			result = foundStr->second;
	}
	else
		result = foundID->second;

	return result;
}

void StorageController::DeleteFile(FileIDType fileID, const string& reportedFileName, bool overrideReadOnly)
{
	Lock lock(*this);

	FileState::Ptr filePtr = GetSingleMatch(__FILE__, __LINE__, fileID, reportedFileName);

	if (filePtr->GetReadOnly() && !overrideReadOnly)
	{
		stringstream msg;
		msg << "Could not delete file with ID=0x" << ultos(fileID, 16, 8) << " fileName='" << reportedFileName << "'";

		DoThrow<FileState::CannotOverwriteReadOnly>(__FILE__, __LINE__, msg.str());

	}

	Unlink(filePtr);
}

StorageController::FileState::Ptr StorageController::GetFileState(const string& actualFileName)
{
	Lock lock(*this);

	FileState::Ptr result;
	FileStateByString::iterator foundStr = fileStateByActualName.find(actualFileName);

	if (foundStr == fileStateByActualName.end())
	{
		stringstream msg;
		msg << "Could not find state information for actualFileName='" << actualFileName << "'";

		DoThrow<FileState::FileNotFound>(__FILE__, __LINE__, msg.str());
	}

	return FileState::Clone(foundStr->second);
}

StorageController::FileState::PtrList StorageController::GetFileStateList(FileIDType fileIDSpec, const string& reportedFileNameSpec)
{
	Lock lock(*this);
	FileState::PtrList result;

	if ((fileIDSpec == NO_FILE_ID) && (reportedFileNameSpec.find_first_not_of(' ') == string::npos))
		BOOST_FOREACH(FileState::Ptr filePtr, fileStateList)
			result.push_back(FileState::Clone(filePtr));

	else
	{
		FileState::Ptr filePtr = GetSingleMatch(__FILE__, __LINE__, fileIDSpec, reportedFileNameSpec);

		result.push_back(FileState::Clone(filePtr));
	}

	return result;
}

void StorageController::ReadFile(ByteArray& result, FileIDType fileID, FileSizeType offset, FileSizeType maxSize)
{
	Lock lock(*this);

	result.clear();

	FileState::Ptr filePtr = GetSingleMatch(__FILE__, __LINE__, fileID, "");

	filePtr->ReadFile(result, config.GetStorageDirectoryName(), offset, maxSize);
	if (filePtr->GetDirtyAndClear()) dirty = true;
}

void StorageController::ReadFile(ByteArray& result, const string& reportedFileName, FileSizeType offset, FileSizeType maxSize)
{
	Lock lock(*this);

	result.clear();
	FileState::Ptr filePtr = GetSingleMatch(__FILE__, __LINE__, NO_FILE_ID, reportedFileName);

	filePtr->ReadFile(result, config.GetStorageDirectoryName(), offset, maxSize);
	if (filePtr->GetDirtyAndClear()) dirty = true;
}

void StorageController::WriteFile(FileIDType fileID, const string& reportedFileName, FileSizeType offset, const ByteArray& data)
{
	Lock lock(*this);

	FileState::Ptr filePtr = GetSingleMatch(__FILE__, __LINE__, fileID, reportedFileName);
	DateTimeRelinker dtRelinker(*this, filePtr);

	filePtr->WriteFile(config.GetStorageDirectoryName(), offset, data);
	if (filePtr->GetDirtyAndClear()) dirty = true;
}

void StorageController::DateTimeRelink(FileState::Ptr& item, datetime oldDateTime)
{
	datetime newDateTime = item->GetModifiedTime();

	if (newDateTime != oldDateTime)
	{
		const string actual = item->GetActualFileName();
		const TimeAndNamePair tnpair0(oldDateTime, actual);
		const TimeAndNamePair tnpair1(newDateTime, actual);

		if (fileStateByModifiedTime.erase(tnpair0))
			fileStateByModifiedTime[tnpair1] = item;

		dirty = true;
	}
}

void StorageController::Unlink(FileState::Ptr item, bool throwOnError)
{
	Lock lock(*this);

	// note: for the purposes of the "dirty" flag, the only thing that matters is if the file state
	// list changes
	string actualFilePath;
	string reportedFileName;
	uint32_t fileID = -1;

	for (FileStateList::iterator it = fileStateList.begin(); it != fileStateList.end(); ++it)
		if (item == (*it))
		{
			fileID = (*it)->GetFileID();
			reportedFileName = (*it)->GetReportedFileName();
			actualFilePath = (*it)->GetActualFilePath(config.GetStorageDirectoryName(), true, true, true, true);
			fileStateList.erase(it);
			dirty = true;
			break;
		}

	fileStateByActualName.erase(item->GetActualFileName());
	fileStateByReportedName.erase(item->GetReportedFileName());
	fileStateByID.erase(item->GetFileID());

	const TimeAndNamePair tnpair(item->GetModifiedTime(), item->GetActualFileName());
	fileStateByModifiedTime.erase(tnpair);

	if (actualFilePath.length())
	{
		int status;
		int errSave;

		do
		{
			status = unlink(actualFilePath.c_str());
			errSave = errno;
		} while((status == -1) && (errSave == EINTR));

		if ((status == -1) && throwOnError)
		{
			switch(errSave)
			{
			case ENOENT:
				{
					stringstream msg;
					msg << "unlink() reported file not found for fileID=0x" << ultos(fileID, 16, 8)
						<< " fileName='" << reportedFileName
						<< "' fullPath='" << actualFilePath;

					DoThrow<FileState::FileNotFound>(__FILE__, __LINE__, msg.str());
				}

			default:
				{
					stringstream msg;
					msg << "unlink() reported failed for fileID=0x" << ultos(fileID, 16, 8)
						<< " fileName='" << reportedFileName
						<< "' fullPath='" << actualFilePath;

					string formattedMsg = FormatMessage(__FILE__, __LINE__, msg.str());
					throw FileState::FileIOError(errSave, actualFilePath, formattedMsg);
				}
			}
		}
	}
}

void StorageController::RescanFileStateList()
{
	Lock lock(*this);

	fileStateByActualName.clear();
	fileStateByReportedName.clear();
	fileStateByID.clear();
	fileStateByModifiedTime.clear();

	BOOST_FOREACH(FileState::Ptr ptr, fileStateList)
	{
		fileStateByActualName[ptr->GetActualFileName()] = ptr;

		string reported = ptr->GetReportedFileName();
		fileStateByReportedName[reported] = ptr;

		FileIDType fid = ptr->GetFileID();

		if (fid != ((FileIDType) NO_FILE_ID))
			fileStateByID[fid] = ptr;

		FileState::TimeAndNamePair modpair(ptr->GetModifiedTime(), reported);

		fileStateByModifiedTime[modpair] = ptr;
	}

}

void StorageController::UpdateStateFile()
{
	if (dirty)
	{
		try
		{
			ofstream ofs(config.GetStateFileName().c_str());

			if (ofs.good())
			{
				boost::archive::xml_oarchive xoa(ofs);
				xoa << BOOST_SERIALIZATION_NVP(fileStateList);
				dirty = false;
			}
		}
		catch(...)
		{
		}
	}
}

void StorageController::DoFileCheck(FileState::Ptr fileState, bool deleteOnFailure)
{
	try
	{
		fileState->CheckFile(config.GetStorageDirectoryName());
	}
	catch(...)
	{
		if (deleteOnFailure)
			Unlink(fileState);

		throw;
	}

	if (fileState->GetDirtyAndClear()) dirty = true;
}

}
}
