/*! \file pibinarylog.h
 * \ingroup IO
 * \~\brief
 * \~english Binary log
 * \~russian Бинарный лог
 */
/*
	PIP - Platform Independent Primitives
	Class for write binary data to logfile, and read or playback this data
	Andrey Bychkov work.a.b@yandex.ru

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU Lesser General Public License as published by
	the Free Software Foundation, either version 3 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 Lesser General Public License for more details.

	You should have received a copy of the GNU Lesser General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#ifndef PIBINARYLOG_H
#define PIBINARYLOG_H

#include "pifile.h"

//! \ingroup IO
//! \~\brief
//! \~english  Binary log
//! \~russian  Бинарный лог
class PIP_EXPORT PIBinaryLog: public PIIODevice
{
	PIIODEVICE(PIBinaryLog, "binlog");
public:
	explicit PIBinaryLog();
	virtual ~PIBinaryLog();

	//! \brief Play modes for \a PIBinaryLog
	enum PlayMode {
		PlayRealTime       /*! Play in system realtime, default mode */ ,
		PlayVariableSpeed  /*! Play in software realtime with speed, set by \a setSpeed */ ,
		PlayStaticDelay    /*! Play with custom static delay, ignoring timestamp */
	};
	
	//! \brief Different split modes for writing \a PIBinaryLog, which can separate files by size, by time or by records count
	enum SplitMode {
		SplitNone   /*! Without separate, default mode */ ,
		SplitTime   /*! Separate files by record time */ ,
		SplitSize   /*! Separate files by size */ ,
		SplitCount  /*! Separate files by records count */
	};

	//! \brief Struct contains information about all records with same ID
	struct PIP_EXPORT BinLogRecordInfo {
		BinLogRecordInfo() {
			id = count = 0;
			minimum_size = maximum_size = 0;
		}
		int id;
		int count;
		int minimum_size;
		int maximum_size;
		PISystemTime start_time;
		PISystemTime end_time;
	};

	//! \brief Struct contains full information about Binary Log file and about all Records using map of \a BinLogRecordInfo
	struct PIP_EXPORT BinLogInfo {
		PIString path;
		int records_count;
		llong log_size;
		PISystemTime start_time;
		PISystemTime end_time;
		PIMap<int, BinLogRecordInfo> records;
		PIByteArray user_header;
	};

	//! \brief Struct contains position, ID and timestamp of record in file
	struct PIP_EXPORT BinLogIndex {
		int id;
		int data_size;
		llong pos;
		PISystemTime timestamp;
	};


	//! Current \a PlayMode
	PlayMode playMode() const {return play_mode;}

	//! Current \a SplitMode
	SplitMode splitMode() const {return split_mode;}

	//! Current directory where billogs wiil be saved
	PIString logDir() const {return property("logDir").toString();}
	
	//! Returns current file prefix
	PIString filePrefix() const {return property("filePrefix").toString();}
	
	//! Default ID, used in \a write function
	int defaultID() const {return default_id;}
	
	//! Returns current play speed
	double playSpeed() const {return play_speed > 0 ? 1. / play_speed : 0.;}

	//! Returns current play delay
	PISystemTime playDelay() const {return play_delay;}

	//! Returns current binlog file split time
	PISystemTime splitTime() const {return split_time;}

	//! Returns current binlog file split size
	llong splitFileSize() const {return split_size;}

	//! Returns current binlog file split records count
	int splitRecordCount() const {return split_count;}

	//! Returns if rapid start enabled
	bool rapidStart() const {return rapid_start;}
	
	//! Create binlog file with Filename = path
	void createNewFile(const PIString &path);
	
	//! Set \a PlayMode
	void setPlayMode(PlayMode mode) {setProperty("playMode", (int)mode);}

	//! Set \a SplitMode
	void setSplitMode(SplitMode mode) {setProperty("splitMode", (int)mode);}

	//! Set path to directory where binlogs will be saved
	void setLogDir(const PIString & path) {setProperty("logDir", path);}
	
	//! Set file prefix, used to
	void setFilePrefix(const PIString & prefix) {setProperty("filePrefix", prefix);}
	
	//! Set defaultID, used in \a write function
	void setDefaultID(int id) {setProperty("defaultID", id);}
	
	//! If enabled BinLog \a ThreadedRead starts without delay for first record, i.e. first record will be readed immediately
	void setRapidStart(bool enabled) {setProperty("rapidStart", enabled);}

	//! Set play speed to "speed", default value is 1.0x
	//! Also this function set \a playMode to \a PlayVariableSpeed
	void setPlaySpeed(double speed) {setPlayMode(PlayVariableSpeed); setProperty("playSpeed", speed);}

	//! Setting static delay between records, default value is 1 sec
	//! Also this function set \a playMode to \a PlayStaticDelay
	void setPlayDelay(const PISystemTime & delay) {setPlayMode(PlayStaticDelay); setProperty("playDelay", delay);}

	//! Set \a playMode to \a PlayRealTime
	void setPlayRealTime() {setPlayMode(PlayRealTime);}

	//! Set binlog file split time
	//! Also this function set \a splitMode to \a SplitTime
	void setSplitTime(const PISystemTime & time) {setSplitMode(SplitTime); setProperty("splitTime", time);}

	//! Set binlog file split size
	//! Also this function set \a splitMode to \a SplitSize
	void setSplitFileSize(llong size) {setSplitMode(SplitSize); setProperty("splitFileSize", size);}

	//! Set binlog file split records count
	//! Also this function set \a splitMode to \a SplitCount
	void setSplitRecordCount(int count) {setSplitMode(SplitCount); setProperty("splitRecordCount", count);}

	//! Set pause while playing via \a threadedRead or writing via write
	void setPause(bool pause);

	//! Set function wich returns new binlog file path when using split mode.
	//! Overrides internal file path generator (logdir() + prefix() + current_time()).
	//! To restore internal file path generator set this function to "nullptr".
	void setFuncGetNewFilePath(std::function<PIString()> f) {f_new_path = f;}

	//! Write one record to BinLog file, with ID = id, id must be greather than 0
	int writeBinLog(int id, PIByteArray data) {return writeBinLog(id, data.data(), data.size_s());}
	
	//! Write one record to BinLog file, with ID = id, id must be greather than 0
	int writeBinLog(int id, const void * data, int size);

	//! Write one RAW record to BinLog file, with ID = id, Timestamp = time
	int writeBinLog_raw(int id, const PISystemTime &time, const PIByteArray &data) {return writeBinLog_raw(id, time, data.data(), data.size_s());}
	int writeBinLog_raw(int id, const PISystemTime &time, const void * data, int size);

	//! Returns count of writed records
	int writeCount() const {return write_count;}
	
	//! Read one record from BinLog file, with ID = id, if id = 0 than any id will be readed
	PIByteArray readBinLog(int id = 0, PISystemTime * time = 0, int * readed_id = 0);
	
	//! Read one record from BinLog file, with ID = id, if id = 0 than any id will be readed
	int readBinLog(int id, void * read_to, int max_size, PISystemTime * time = 0, int * readed_id = 0);
	
	//! Returns binary log file size
	llong logSize() const {return log_size;}

	//! Return position in current binlog file
	llong logPos() const {return file.pos();}

	//! Return true, if position at the end of BinLog file
	bool isEnd() const {if (isClosed()) return true; return file.isEnd();}
	
	//! Returns if BinLog file is empty
	bool isEmpty() const;

	//! Returns BinLog pause status
	bool isPause() const {return is_pause;}
	
	//! Returns id of last readed record
	int lastReadedID() const {return lastrecord.id;}

	//! Returns timestamp of last readed record
	PISystemTime lastReadedTimestamp() const {return lastrecord.timestamp;}

	//! Returns timestamp of log start
	PISystemTime logStartTimestamp() const {return startlogtime;}

	//!Set custom file header, you can get it back when read this binlog
	void setHeader(const PIByteArray & header);

	//!Get custom file header
	PIByteArray getHeader();

#ifdef DOXYGEN
	//! Read one message from binlog file, with ID contains in "filterID" or any ID, if "filterID" is empty
	int read(void *read_to, int max_size);
	
	//! Write one record to BinLog file, with ID = "defaultID"
	int write(const void * data, int size);
#endif

	//! Array of ID, that BinLog can read from binlog file, when use \a read function, or in \a ThreadedRead
	PIVector<int> filterID;
	
	//! Go to begin of BinLog file
	void restart();

	//! Get binlog info \a BinLogInfo
	BinLogInfo logInfo() const {if (is_indexed) return binfo; return getLogInfo(path());}

	//! Get binlog index \a BinLogIndex, need \a createIndex before getting index
	const PIVector<BinLogIndex> & logIndex() const {return index;}
	
	//! Create index of current binlog file
	bool createIndex();

	//! Return if current binlog file is indexed
	bool isIndexed() {return is_indexed;}

	//! Find nearest record of time \"time\". Returns -1 if not indexed or time less than first record
	int posForTime(const PISystemTime & time);

	//! Go to record #index
	void seekTo(int rindex);

	//! Go to nearest record
	bool seek(const PISystemTime & time);

	//! Set position in file to reading/playing
	bool seek(llong filepos);

	//! Get current record index (position record in file)
	int pos() const {if (is_indexed) return current_index; return -1;}

	//! \handlers
	//! \{
	
	//! \fn PIString createNewFile()
	//! \brief Create new binlog file in \a logDir, if successful returns filename, else returns empty string.
	//! Filename is like  \a filePrefix + "yyyy_MM_dd__hh_mm_ss.binlog"
	
	//! \}
	//! \events
	//! \{
	
	//! \fn void fileEnd()
	//! \brief Raise on file end while reading
	
	//! \fn void fileError()
	//! \brief Raise on file creation error

	//! \fn void newFile(const PIString & filename)
	//! \brief Raise on new file created

	//! \}
	
	EVENT_HANDLER(PIString, createNewFile);
	EVENT(fileEnd);
	EVENT(fileError);
	EVENT1(newFile, const PIString &, filename);
	EVENT1(posChanged, int, pos);

	//! Get binlog info and statistic
	static BinLogInfo getLogInfo(const PIString & path);

	//! Create new binlog from part of "src" with allowed IDs and "from" to "to" file position
	static bool cutBinLog(const BinLogInfo & src, const PIString & dst, int from, int to);

	//! Create new binlog from serial splitted binlogs "src"
	static bool joinBinLogsSerial(const PIStringList & src, const PIString & dst, std::function<bool (const PIString &, PISystemTime)> progress = nullptr);

protected:
	PIString constructFullPathDevice() const override;
	void configureFromFullPathDevice(const PIString & full_path) override;
	PIPropertyStorage constructVariantDevice() const override;
	void configureFromVariantDevice(const PIPropertyStorage & d) override;
	ssize_t readDevice(void *read_to, ssize_t max_size) override;
	ssize_t writeDevice(const void * data, ssize_t size) override;
	bool openDevice() override;
	bool closeDevice() override;
	void propertyChanged(const char * s) override;
	bool threadedRead(const uchar *readed, ssize_t size) override;
	DeviceInfoFlags deviceInfoFlags() const override {return PIIODevice::Reliable;}

private:
	struct PIP_EXPORT BinLogRecord {
		int id;
		int size;
		PISystemTime timestamp;
		PIByteArray data;
	};

	bool writeFileHeader();
	bool checkFileHeader();
	BinLogRecord readRecord();
	static void parseLog(PIFile *f, BinLogInfo *info, PIVector<BinLogIndex> * index);
	void moveIndex(int i);
	static PIString getLogfilePath(const PIString & log_dir, const PIString & prefix);

	PIVector<BinLogIndex> index;
	PIMap<llong, int> index_pos;
	BinLogInfo binfo;

	PlayMode play_mode;
	SplitMode split_mode;
	PIFile file;
	BinLogRecord lastrecord;
	PISystemTime startlogtime, play_delay, split_time, pause_time;
	mutable PIMutex logmutex, pausemutex;
	double play_time, play_speed;
	llong split_size, log_size;
	int write_count, split_count, default_id, current_index;
	bool is_started, is_thread_ok, is_indexed, rapid_start, is_pause;
	PIByteArray user_header;
	std::function<PIString()> f_new_path;
};

//! \relatesalso PICout \brief Output operator PIBinaryLog::BinLogInfo to PICout
inline PICout operator <<(PICout s, const PIBinaryLog::BinLogInfo & bi) {
	s.space();
	s.saveAndSetControls(0);
	s << "[PIBinaryLog] " << bi.path << "\n";
	if (bi.log_size < 0) {
		s << "invalid file path";
		s.restoreControls();
		return s;
	}
	if (bi.log_size == 0) {
		s << "Invalid empty file";
		s.restoreControls();
		return s;
	}	if (bi.records_count < 0 && bi.records_count > -4) {
		s << "Invalid file or corrupted signature";
		s.restoreControls();
		return s;
	}
	if (bi.records_count < -3) {
		s << "Invalid binlog version";
		s.restoreControls();
		return s;
	}
	s << "read records " << bi.records_count << " in " << bi.records.size() << " types, log size " << bi.log_size;
	s << "\nlog start " << bi.start_time << " , log end " << bi.end_time;
	PIVector<int> keys = bi.records.keys();
	for (int i : keys) {
		PIBinaryLog::BinLogRecordInfo bri = bi.records.at(i);
		s << "\n  record id " << bri.id << " , count " << bri.count;
		s << "\n    record start " << bri.start_time << " , end " << bri.end_time;
		s << "\n    record size " << bri.minimum_size << " - " << bri.maximum_size;
	}
	s.restoreControls();
	return s;
}

#endif // PIBINARYLOG_H
