//-------------------------------------------------------------------------------
// <copyright file="StorageController.h" 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>
//   Declarations for 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>
//-------------------------------------------------------------------------------

#ifndef STORAGECONTROLLER_H_
#define STORAGECONTROLLER_H_

#include <algorithm>
#include <list>
#include <map>
#include <ostream>
#include <stdexcept>
#include <string>
#include <vector>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/date_time/posix_time/time_serialize.hpp>
#include <boost/serialization/access.hpp>
#include <boost/serialization/list.hpp>
#include <boost/serialization/nvp.hpp>
#include <boost/serialization/split_member.hpp>
#include <boost/serialization/shared_ptr.hpp>
#include <boost/shared_array.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread/locks.hpp>
#include <boost/thread/recursive_mutex.hpp>
#include <stdint.h>
#include <string.h>
#include "CRC16.h"
#include "MD5-2.h"
#include "LqrSMACByteArray.h"
#include "LqrSMACSharedLib.h"
#include "LqrSMACDeclareException.h"

namespace LiquidRobotics {
namespace PayloadInterface {

class SHARED_LIB_PUBLIC StorageController : public boost::noncopyable
{
public:

	typedef std::string string;
	typedef SMACFramework::ByteArray::byte byte;
	typedef SMACFramework::ByteArray ByteArray;
	typedef uint32_t FileIDType;
	typedef uint32_t FileSizeType;
	typedef boost::posix_time::ptime datetime;
	typedef boost::unique_lock<StorageController> Lock;

	enum ESpecialFileID
	{
		NO_FILE_ID = 0xFFFFFFFF
	};

	class SHARED_LIB_PUBLIC Config
	{
	public:

		DECLARE_EXCEPTION(InvalidConfiguration, std::runtime_error);

		Config();
		Config(const Config& right);
		Config(const string& _stateFileName, const string& _storageDirectoryName, unsigned _maxFilesToRetain, bool _retainAllFiles);

		Config& operator=(const Config& right);
		bool operator==(const Config& right) const;

		const string& GetStateFileName() const
		{
			return stateFileName;
		}

		void SetStateFileName(const char* newFileName);
		void SetStateFileName(const string& newFileName);

		const string& GetStorageDirectoryName() const
		{
			return storageDirectory;
		}

		void SetStorageDirectoryName(const char* newFileName);
		void SetStorageDirectoryName(const string& newFileName);

		unsigned GetMaxFilesToRetain() const
		{
			return maxFilesToRetain;
		}

		void SetMaxFilesToRetain(unsigned newValue);

		bool GetRetainAllFiles() const
		{
			return retainAllFiles;
		}

		void SetRetainAllFiles(bool newValue);

		void VerifyConfig() const;

		void Load(const string& fileName);
		void Save(const string& fileName) const;

	private:

		friend class boost::serialization::access;

		template<class Archive>
		void serialize(Archive& ar, const unsigned int version)
		{
			ar & BOOST_SERIALIZATION_NVP(stateFileName);
			ar & BOOST_SERIALIZATION_NVP(storageDirectory);
			ar & BOOST_SERIALIZATION_NVP(retainAllFiles);
			ar & BOOST_SERIALIZATION_NVP(maxFilesToRetain);
		}

		string stateFileName;
		string storageDirectory;
		bool retainAllFiles;
		unsigned maxFilesToRetain;
	};

	class SHARED_LIB_PUBLIC FileState
	{
	public:

		typedef boost::shared_ptr<FileState> Ptr;
		typedef std::list<Ptr> PtrList;
		typedef std::map<string, Ptr> MapByString;
		typedef std::map<FileIDType, Ptr> MapByID;
		typedef std::pair<datetime, string> TimeAndNamePair;
		typedef std::map<TimeAndNamePair, Ptr> MapByTimeAndName;

		static inline Ptr Clone2Ptr(const FileState& fileState)
		{
			StorageController::FileState* pNew = new StorageController::FileState(fileState);
			StorageController::FileState::Ptr newPtr;

			try 		{ newPtr.reset(pNew); }
			catch(...) 	{ delete pNew; throw; }

			return newPtr;
		}

		static inline Ptr Clone(Ptr& ptr)
		{
			return Clone2Ptr(*ptr);
		}

		DECLARE_EXCEPTION(CannotCreateFileExists, std::runtime_error);
		DECLARE_EXCEPTION(CannotOverwriteReadOnly, std::runtime_error);
		DECLARE_EXCEPTION(FileNotFound, std::runtime_error);
		DECLARE_EXCEPTION(FileCRCCheckError, std::runtime_error);
		DECLARE_EXCEPTION(FileMD5CheckError, std::runtime_error);
		DECLARE_EXCEPTION(FileHasNotBeenChecked, std::runtime_error);
		DECLARE_EXCEPTION(FileBoundsError, std::runtime_error);
		DECLARE_EXCEPTION(FileCheckValuesNotEstablished, std::runtime_error);

		template<unsigned FixedStorageLength>
		class FixedStorage
		{
		public:

			FixedStorage(const string& storageInit)
			{
				Set(storageInit);
			}

			const char* Get() const
			{
				return storage;
			}

			char* Get()
			{
				return storage;
			}

			unsigned Length() const
			{
				return strnlen(storage, FixedStorageLength);
			}

			void Set(const string& storageInit)
			{
				int len = std::min(storageInit.length(), FixedStorageLength);

				strncpy(storage, &storageInit.at(0), len);
				storage[len] = '\0';
			}

			void Set(const char* storageInit)
			{
				int len = std::min(strlen(storageInit), FixedStorageLength);

				strncpy(storage, storageInit, len);
				storage[len] = '\0';
			}

		private:

			char storage[FixedStorageLength+1];
		};

		class SHARED_LIB_PUBLIC FileIOError : public std::runtime_error
		{
		public:

			SHARED_LIB_PUBLIC FileIOError(int _savedErrno, const string& _fileName, const string& whatArg);

			const int savedErrno;
			const FixedStorage<300> fileName;
		};

		class SHARED_LIB_PUBLIC MissingSCDirectory : public std::runtime_error
		{
		public:

			SHARED_LIB_PUBLIC MissingSCDirectory(const string& _dirName, const string& whatArg);

			const FixedStorage<300> dirName;
		};

		class SHARED_LIB_PUBLIC FileSystemFull : public std::runtime_error
		{
		public:

			SHARED_LIB_PUBLIC FileSystemFull(const string& _fileName, const string& whatArg);

			const FixedStorage<300> fileName;
		};

		// Default constructor (used for serialization?)
		FileState();

		// Constructor with a known directory and actual file name; the reported file name is determined based
		// on the reported names already in the list and the characteristics and check values are determined by
		// the file itself
		FileState(StorageController& _sc, const string& _actualFileName, bool readOnly = false);

		// Constructor using reported name as the basis for an actual filename, creates the file according to specifications
		FileState(StorageController& _sc, const string& _reportedFileName,
				FileIDType _fileID,	FileSizeType _fileSize, datetime _creationTime, datetime _modifiedTime, bool _readOnly,
				const CRC16& _crc, const MD5::Entity& _md5, bool overwrite = false, bool ignoreReadOnly = false);

		// Copy constructor (copies all fields)
		FileState(const FileState& right);

		// Assignment operator (overwrites all fields with values from another instance)
		FileState& operator=(const FileState& right);

		// Equality test operator
		bool operator==(const FileState& right) const;

		// Inequality test operator
		bool operator!=(const FileState& right) const;

		// Reads the content of the file and sets the check values accordingly (sets or clears the "checked" flag)
		//void AssignFileChecks();

		// Sets the check values according to the parameters passed in (clears the "checked' flag if these values
		// are different)
		void AssignFileChecks(const CRC16& crc, const MD5::Entity& _md5);

		// Check the file content vs. the programmed check values (crc and MD5) and set the "checked" flag
		// if successful (clears the checked flag otherwise)
		void CheckFile(const string& scDir);

		// Read data from the file and optionally verify that the file has been checked before starting
		void ReadFile(ByteArray& result, const string& scDir, FileSizeType offset, FileSizeType maxSize, bool verifyChecked = true);

		// Writes data to the file (clears the checked flag)
		void WriteFile(const string& scDir, uint32_t offset, const ByteArray& data);

		// get/set ActualFileName property
		const string& GetActualFileName() const		{ return actualFileName; }
		void SetActualFileName(const char* newValue);
		void SetActualFileName(const string& newValue);

		// get file path with specified directory
		string GetActualFilePath(const string& scDir, bool mustExist = false, bool testForOverwrite = false, bool overwriteEnabled = false, bool overrideReadOnly = false) const;
		static string GetActualFilePath(const string& scDir, const string& actualFileName, bool readOnly, bool mustExist = false, bool testForOverwrite = false, bool overwriteEnabled = false, bool overrideReadOnly = false);

		// get/set ReportedFileName property
		const string& GetReportedFileName() const	{ return reportedFileName; }
		void SetReportedFileName(const char* newValue);
		void SetReportedFileName(const string& newValue);

		// get/set FileID property
		FileIDType GetFileID() const				{ return fileID; }
		void SetFileID(FileIDType newValue);
		void SetFileIDToDefault();

		// get/set FileSize property
		FileSizeType GetFileSize() const			{ return fileSize; }
		void SetFileSize(FileSizeType newValue);

		// get/set CreationTime property
		datetime GetCreationTime() const			{ return creationTime; }
		void SetCreationTime(datetime newValue);

		// get/set ModifiedTime property
		datetime GetModifiedTime() const			{ return modifiedTime; }
		void SetModifiedTime(datetime newValue);

		// get/set FileChecked property
		bool GetFileChecked() const					{ return fileWasChecked; }
		void SetFileChecked(bool newValue = true);

		// get/set ReadOnly property
		bool GetReadOnly() const					{ return fileReadOnly; }
		void SetReadOnly(bool newValue = true);

		// get/set CRC property
		CRC16 GetCRC() const;
		void SetCRC(const CRC16& newValue);

		// get calculated CRC
		CRC16 GetCalculatedCRC() const;

		// get/set MD5 property
		MD5::Entity GetMD5() const;
		void SetMD5(const MD5::Entity& newValue);

		// get calculated MD5
		MD5::Entity GetCalculatedMD5() const;

		// get/set dirty state
		bool GetDirty() const						{ return dirty; }
		bool GetDirtyAndClear()						{ bool result = dirty; dirty = false; return result; }
		void SetDirty(bool set = true)				{ dirty = set; }

		// suggests the reported filename based on the reported name, calls PermuteFileName if the file
		// already exists
		static string SuggestActualFileName(const string& reportedFileName, const string& scDirectory,
													bool permute = true, const MapByString* _reportedNameList = NULL);
		static string SuggestReportedFileName(const string& scDirectory, const string& actualFileName,
													const MapByString& _reportedNameList);
		static string PermuteFileName(const string& reportedFileName, const string& scDirectory,
													const MapByString* _reportedNameList = NULL);
		static bool PermutedFileName(const string& baseName, const string& ext, int tryNum, string& permutedBaseName);

	private:

		friend class boost::serialization::access;

		template<class Archive>
		void serialize(Archive& ar, const unsigned int version)
		{
			ar & BOOST_SERIALIZATION_NVP(actualFileName);
			ar & BOOST_SERIALIZATION_NVP(reportedFileName);
			ar & BOOST_SERIALIZATION_NVP(fileID);
			ar & BOOST_SERIALIZATION_NVP(fileSize);
			ar & BOOST_SERIALIZATION_NVP(creationTime);
			ar & BOOST_SERIALIZATION_NVP(modifiedTime);
			ar & BOOST_SERIALIZATION_NVP(fileWasChecked);
			ar & BOOST_SERIALIZATION_NVP(fileReadOnly);
			ar & BOOST_SERIALIZATION_NVP(crc);
			ar & BOOST_SERIALIZATION_NVP(crcSet);
			ar & BOOST_SERIALIZATION_NVP(crcCalc);
			ar & BOOST_SERIALIZATION_NVP(crcCalcSet);
			ar & BOOST_SERIALIZATION_NVP(md5);
			ar & BOOST_SERIALIZATION_NVP(md5Set);
			ar & BOOST_SERIALIZATION_NVP(md5Calc);
			ar & BOOST_SERIALIZATION_NVP(md5CalcSet);
			ar & BOOST_SERIALIZATION_NVP(firstCheckIncomplete);
		}

		string actualFileName;			// the name of the file on disk (does not include directory name)
		string reportedFileName;		// the name of the file as reported (11-character 8/3 name)
		FileIDType fileID;				// ID of the file
		FileSizeType fileSize;			// size of the file
		datetime creationTime;			// date/time that the file was created
		datetime modifiedTime;			// date/time that the file was modified
		bool fileWasChecked;			// true if the file was checked, false if it has not been checked yet
		bool fileReadOnly;				// true if file is marked read only, false if the file is writeable
		CRC16 crc;						// 16-bit CRC that should match the file content
		bool crcSet;					// true if CRC has been configured, false if it has not been configured
		CRC16 crcCalc;					// 16-bit CRC that should match the file content
		bool crcCalcSet;				// true if CRC has been configured, false if it has not been configured
		MD5::Entity md5;				// MD5 that should match the file content
		bool md5Set;					// true if the MD5 has been configured, false if it has not been configured
		MD5::Entity md5Calc;			// MD5 that should match the file content
		bool md5CalcSet;				// true if the MD5 has been configured, false if it has not been configured
		bool firstCheckIncomplete;		// true if the first check has not been made, so file attributes are not updated
		bool dirty;						// true if there are unapplied changes, false if not (for use by external actors)
	};

	StorageController(const Config& _config);

	virtual ~StorageController();

	virtual void AddFile(const string& actualFileName, FileIDType newFileID = (FileIDType) NO_FILE_ID);
	virtual void CheckAllFiles(bool deleteOnFailure);
	virtual void CheckFile(FileIDType fileID, const string& reportedFileName, bool deleteOnFailure = false);
	virtual void CreateFile(const FileState& newFileState, bool overwrite = false);
	virtual void DeleteFile(FileIDType fileID, const string& reportedFileName, bool overrideReadOnly = false);
	virtual const Config& GetConfig() const;
	virtual FileState::Ptr GetFileState(const string& actualFileName);
	virtual FileState::PtrList GetFileStateList(FileIDType fileIDSpec, const string& reportedFileNameSpec);
	virtual void ReadFile(ByteArray& result, FileIDType fileID, FileSizeType offset, FileSizeType maxSize);
	virtual void ReadFile(ByteArray& result, const string& reportedFileName, FileSizeType offset, FileSizeType maxSize);
	virtual void WriteFile(FileIDType fileID, const string& reportedFileName, FileSizeType offset, const ByteArray& data);

	// Lockable concept used internally and usable by external actors with StorageController::Lock to allow for protection
	// across multiple operations and to update the file state upon unlock
	void lock();
	bool try_lock();
	void unlock();

private:

	class DateTimeRelinker
	{
	public:

		DateTimeRelinker(StorageController& _sc, FileState::Ptr _item)
		: sc(_sc)
		, item(_item)
		, oldDateTime(_item->GetModifiedTime())
		{
		}

		~DateTimeRelinker()
		{
			sc.DateTimeRelink(item, oldDateTime);
		}

	private:

		StorageController& sc;
		FileState::Ptr item;
		datetime oldDateTime;
	};

	friend class FileState;
	friend class DateTimeRelinker;

	typedef FileState::PtrList FileStateList;
	typedef FileState::MapByString FileStateByString;
	typedef FileState::MapByID FileStateByID;
	typedef FileState::TimeAndNamePair TimeAndNamePair;
	typedef FileState::MapByTimeAndName FileStateByTimeAndName;
	typedef boost::recursive_mutex Mutex;

	Mutex* pMutex;
	int mutexNestingLevel;
	bool dirty;
	const Config config;
	FileStateList fileStateList;
	FileStateByString fileStateByActualName;
	FileStateByString fileStateByReportedName;
	FileStateByID fileStateByID;
	FileStateByTimeAndName fileStateByModifiedTime;

	void DoFileCheck(FileState::Ptr fileState, bool deleteOnFailure = false);
	void RescanFileStateList();
	void Unlink(FileState::Ptr fileState, bool throwOnError = false);
	void UpdateStateFile();
	FileState::Ptr GetSingleMatch(const char* fileName, int lineNum, FileIDType fileID, const string& reportedFileName);
	void DateTimeRelink(FileState::Ptr& item, datetime oldDateTime);

	friend class boost::serialization::access;

	template<class Archive>
	void save(Archive& ar, const unsigned int version) const
	{
		ar & BOOST_SERIALIZATION_NVP(config);
		ar & BOOST_SERIALIZATION_NVP(fileStateList);
	}

	template<class Archive>
	void load(Archive& ar, const unsigned int version)
	{
		ar & BOOST_SERIALIZATION_NVP(config);
		ar & BOOST_SERIALIZATION_NVP(fileStateList);

		RescanFileStateList();
	}

	BOOST_SERIALIZATION_SPLIT_MEMBER()
};

SHARED_LIB_PUBLIC std::ostream& operator<<(std::ostream& stm, const StorageController::Config& config);

}
}

#endif /* STORAGECONTROLLER_H_ */
