#ifndef __COMRPESSOR_H__
#define __COMRPESSOR_H__

#include "VrutApi.h"
#include <vector>
#include <wx/zstream.h>
#include <ctime>
#include <ratio>
#include <chrono>

namespace VRUT {

	class HighPrecisionTimer {
	private:
		std::chrono::high_resolution_clock::time_point t1;
	public:
		HighPrecisionTimer(void) {
			return;
		}
		~HighPrecisionTimer(void) {
			return;
		}
		void Start(void) {
			this->t1 = std::chrono::high_resolution_clock::now();
			return;
		}
		double End(void) const {
			std::chrono::duration<double> time_span = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::high_resolution_clock::now() - this->t1);
			return time_span.count();
		}
	};

	class MemoryOutputStream : public wxOutputStream {
	protected:

		size_t offset;
		std::vector<unsigned char> buffer;

		virtual size_t OnSysWrite(const void * data, size_t size) {
			if (size + offset > buffer.size())
				buffer.resize(size + offset);
			memcpy(buffer.data() + offset, data, size);
			offset += size;
			return size;
		}

	public:

		MemoryOutputStream(void) : wxOutputStream(), offset(0) {
		}

		virtual ~MemoryOutputStream(void) {
		}

		void Clear(void) {
			offset = 0;
		}

		const void * GetData(void) {
			return (const void*)buffer.data();
		}

		virtual size_t GetSize(void) const {
			return offset;
		}

	};

	class MemoryInputStream : public wxInputStream {
	protected:
	public:

		size_t begin;
		size_t end;
		std::vector<unsigned char> buffer;

		virtual size_t OnSysRead(void * data, size_t size) {
			if (begin < end) {
				int inc = std::min(size, end - begin);
				memcpy(data, buffer.data() + begin, inc);
				begin += inc;
				return inc;
			} else
				m_lasterror = wxSTREAM_EOF;
			return 0;
		}

		virtual void Reset(void) {
			begin = end;
			wxStreamBase::Reset();
		}

	public:

		MemoryInputStream(void) : wxInputStream(), begin(0), end(0) {
		}

		virtual ~MemoryInputStream(void) {
		}

		void Resize(size_t size) {
			buffer.resize(size);
			begin = 0;
			end = size;
		}

		virtual size_t GetSize(void) const {
			return size_t(end - begin);
		}

		const void * GetData(void) {
			return (const void*)buffer.data();
		}

		void SetData(const void * data, size_t size) {
			if (buffer.size() < size)
				buffer.resize(size);
			memcpy(buffer.data(), data, size);
			begin = 0;
			end = size;
		}

	};

	class VRUTCompressor {

	private:

		unsigned char compressionLevel;
		MemoryOutputStream * mos;
		wxZlibOutputStream * zos;
		double compressionTime;
		size_t rawDataSize;
		HighPrecisionTimer timer;
	public:

		VRUTCompressor(void) : compressionLevel(0), rawDataSize(0), compressionTime(0.0) {
			mos = new MemoryOutputStream();
			zos = new wxZlibOutputStream(*mos, compressionLevel, wxZLIB_NO_HEADER);
		}

		~VRUTCompressor(void) {
			if (zos != nullptr)
				delete zos;
			if (mos != nullptr)
				delete mos;
		}

		void Write(const void * data, size_t size) {
			this->timer.Start();
			this->zos->Write(data, size);
			this->compressionTime += this->timer.End();
			this->rawDataSize += size;
		}

		const void * Sync(size_t & compressedSize) {
			this->timer.Start();
			this->zos->Sync();
			this->compressionTime += this->timer.End();
			compressedSize = mos->GetSize();
			return mos->GetData();
		}

		size_t GetUncompressedSize(void) const {
			return this->rawDataSize;
		}

		void Clear(void) {
			this->mos->Clear();
			this->rawDataSize = 0;
			this->compressionTime = 0.0;
		}

		unsigned char GetCompressionLevel(void) {
			return compressionLevel;
		}

		void SetCompressionLevel(int compressionLevel) {
			if (this->compressionLevel != compressionLevel) {
				if (compressionLevel >= 0 && compressionLevel <= 9) {
					this->compressionLevel = compressionLevel;
					if (zos != nullptr)
						delete zos;
					if (mos != nullptr)
						delete mos;
					mos = new MemoryOutputStream();
					this->rawDataSize = 0;
					this->compressionTime = 0.0;
					zos = new wxZlibOutputStream(*mos, compressionLevel, wxZLIB_NO_HEADER);

				}
			}
		}

		double GetTimeSpentOnCompression(void) const {
			return this->compressionTime;
		}
		unsigned char MaxCompressionLevel(void) const {
			return 9;
		}

	};

	class VRUTDeCompressor {

	private:

		unsigned char compressionLevel;
		MemoryInputStream mis;
		wxZlibInputStream * zis;
		long long uncompressedSizeRemaining, initialUncompressedSize;
		double decompressionTime;
		HighPrecisionTimer timer;
		bool requestHardReset;
	public:

		void PartialReset(const bool requestHardReset = false) {
			this->mis.Reset();
			if (requestHardReset || this->requestHardReset) {
				if (zis != nullptr)
					delete zis;
				zis = new wxZlibInputStream(this->mis, wxZLIB_NO_HEADER);
				this->requestHardReset = false;
			} else
				this->zis->Reset();
			this->uncompressedSizeRemaining = 0;
			this->initialUncompressedSize = 0;
			this->decompressionTime = 0.0;
			return;
		}

		VRUTDeCompressor(void) : compressionLevel(0), zis(nullptr), uncompressedSizeRemaining(0), decompressionTime(0.0), initialUncompressedSize(0), requestHardReset(false) {
			this->zis = new wxZlibInputStream(this->mis, wxZLIB_NO_HEADER);
			return;
		}

		VRUTDeCompressor(const char * fn) : compressionLevel(0), zis(nullptr), uncompressedSizeRemaining(0), decompressionTime(0.0), initialUncompressedSize(0), requestHardReset(false) {
			this->zis = new wxZlibInputStream(this->mis, wxZLIB_NO_HEADER);
			return;
		}

		~VRUTDeCompressor(void) {
			if (zis != nullptr)
				delete zis;
		}

		void Resize(size_t size) {
			mis.Resize(size);
			if (requestHardReset) {
				if (zis != nullptr)
					delete zis;
				zis = new wxZlibInputStream(this->mis, wxZLIB_NO_HEADER);
				requestHardReset = false;
			} else
				this->zis->Reset();
		}

		/*void SetData(const void * data, size_t size)
		{
		zis->Reset();
		mis.SetData(data, size);
		}*/

		void SetUncompressedSize(const long long uncompressedSize) {
			this->uncompressedSizeRemaining = uncompressedSize;
			this->initialUncompressedSize = uncompressedSize;
			return;
		}

		const void * GetData(void) {
			return mis.GetData();
		}

		void Read(void * data, size_t size) {
			if (this->uncompressedSizeRemaining > 0) {
				this->timer.Start();
				this->zis->Read(data, size);
				this->decompressionTime += this->timer.End();
				this->uncompressedSizeRemaining -= size;
				if (uncompressedSizeRemaining == 0 && mis.begin != mis.end) {
					LOGERROR(wxString::Format(wxT("VRUTDeCompressor::Read(void*, size_t) mis.begin != mis.end : begin = %llu and end = %llu"), mis.begin, mis.end));
					requestHardReset = true; //TODO : find why wxStream doesnt use everything in the buffer, see MemoryInputStream::OnSysRead -> it is requested fewer times then it is necessary to exhaust the internal buffer
				}
			} else if (this->uncompressedSizeRemaining < 0) {
				// serious error, TODO: trace the mistake leading to this
				LOGERROR(wxT("VRUTDeCompressor::Read(void*, size_t) got negative value in VRUTDeCompressor::uncompressedSizeRemaining"));
			}
		}

		wxStreamError GetLastError(void) const {
			return this->zis->GetLastError();
		}

		unsigned char GetCompressionLevel(void) {
			return this->compressionLevel;
		}

		void SetCompressionLevel(const unsigned char compressionLevel) {
			if (compressionLevel >= 0 && compressionLevel <= 9)
				this->compressionLevel = compressionLevel;
		}

		bool CanReadCompressedStream(void) const {
			return !this->mis.buffer.empty() && (this->uncompressedSizeRemaining > 0);
		}
		bool AnyDecompressionHappened(void) const {
			return (this->initialUncompressedSize - this->uncompressedSizeRemaining) ? true : false;
		}

	};

	struct CompressionStatistics {
		size_t rawSize, compressedSize;
		double timeSpentCompressing;
		CompressionStatistics(const size_t raw = 0, const size_t compr = 0, const double time = 0.0) :
			rawSize(raw), compressedSize(compr), timeSpentCompressing(time) {
			return;
		}
		CompressionStatistics& operator += (const CompressionStatistics& arg) {
			this->rawSize += arg.rawSize;
			this->compressedSize += arg.compressedSize;
			this->timeSpentCompressing += arg.timeSpentCompressing;
			return *this;
		}
		CompressionStatistics Multiple(const double alpha) const {
			CompressionStatistics res{};
			res.rawSize = this->rawSize * alpha;
			res.compressedSize = this->compressedSize * alpha;
			res.timeSpentCompressing = this->timeSpentCompressing * alpha;
			return res;
		}
	};

	/*struct NetworkStatistics {
	NetworkStatistics(const size_t dataSize, const double time) :
	dataSize(dataSize), timeSpentSending(time) {return;}
	size_t dataSize;
	double timeSpentSending;
	};*/

	template<unsigned char LevelsOfCompression = 10>
	class CompressionStatisticsCollection {
	private:
		CompressionStatistics statistics[LevelsOfCompression], computedStatistics[LevelsOfCompression];
		size_t flushes[LevelsOfCompression];
		const double newToOldRatio;
	public:
		CompressionStatisticsCollection(const double newToOldRatio = 0.85) : newToOldRatio(newToOldRatio) {
			assert(0 <= this->newToOldRatio && this->newToOldRatio <= 1);
			for (size_t i = 0; i < LevelsOfCompression; i ++) {
				this->statistics[i] = CompressionStatistics{0, 0, 0.0};
				this->computedStatistics[i] = CompressionStatistics{0, 0, 0.0};
				this->flushes[i] = 0;
			}
			return;
		}
		void EmplaceBack(const CompressionStatistics compStat, const unsigned char compLvl) {
			this->flushes[compLvl] ++;
			this->statistics[compLvl] += compStat;
			this->computedStatistics[compLvl] += compStat.Multiple(this->newToOldRatio);
			return;
		}
		std::pair<double, double> GetTotalCompressionSpeedAndRatioAtLevel(const unsigned char compLvl) const {
			std::pair<double, double> res;
			res.first = (double)this->statistics[compLvl].rawSize / this->statistics[compLvl].timeSpentCompressing;
			res.second = (double)this->statistics[compLvl].compressedSize / (double)this->statistics[compLvl].rawSize;
			return res;
		}
		std::pair<double, double> GetComputedCompressionSpeedAndRatioAtLevel(const unsigned char compLvl) const {
			std::pair<double, double> res;
			res.first = (double)this->computedStatistics[compLvl].rawSize / this->computedStatistics[compLvl].timeSpentCompressing;
			res.second = (double)this->computedStatistics[compLvl].compressedSize / (double)this->computedStatistics[compLvl].rawSize;
			return res;
		}
		unsigned char ComputeRecommendedCompressionLevel(const double networkSpeed) const {
			unsigned char res = 0;
			double bestSpeed = networkSpeed;
			for (size_t i = 1; i < LevelsOfCompression; i ++) { // i = 0 is case when no compression is used - which is state on 2 rows right above
				if (this->flushes[i] == 0) {
					//TODO: even this has to be measured so we can choose the best one
					//LOG(wxString::Format(wxT("CompressionStatisticsCollection::ComputedRecommendedCompressionLevel(unsigned char) - zero measurements of lvl %llu performance"), i));
					continue;
				}
				const double compressorSpeed = (double)this->computedStatistics[i].rawSize / this->computedStatistics[i].timeSpentCompressing;
				const double ratio = (double)this->computedStatistics[i].compressedSize / (double)this->computedStatistics[i].rawSize;
				double expectableSpeed = 1.0 / (1.0 / compressorSpeed + ratio / networkSpeed);
				if (expectableSpeed > bestSpeed) {
					res = i;
					bestSpeed = expectableSpeed;
				}
			}
			return res;
		}
	};

	/*class NetworkStatisticsCollection {
	private:
	double computedBytesPerSecond, totalTime, NewToOldRatio;
	size_t transferredBytes, transfers;
	public:
	NetworkStatisticsCollection(const double NewToOldRatio = 1.0) : computedBytesPerSecond(0.0), totalTime(0.0), NewToOldRatio(NewToOldRatio), transferredBytes(0), transfers(0) {
	assert( 0 <= this->NewToOldRatio && this->NewToOldRatio <= 1 );
	return;
	}
	void EmplaceBack( const NetworkStatistics netStat ) {
	this->transfers ++;
	this->computedBytesPerSecond *= (1.0 - this->NewToOldRatio);
	this->computedBytesPerSecond += this->NewToOldRatio * (((double)netStat.dataSize) / netStat.timeSpentSending);
	this->transferredBytes += netStat.dataSize;
	this->totalTime += netStat.timeSpentSending;
	}
	double GetComputedSpeed() const {
	return this->computedBytesPerSecond;
	}
	double GetTotalSpeed() const {
	return (((double)this->transferredBytes) / this->totalTime);
	}
	};*/

}

#endif /* __COMRPESSOR_H__ */
