/*
    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/>.
*/

#include "pibinarylog.h"

#include "pidir.h"
#include "piliterals_bytes.h"
#include "piliterals_time.h"
#include "pipropertystorage.h"
#include "pitime.h"
#include "pitranslator.h"

#define PIBINARYLOG_VERSION_OLD 0x31

/*! \class PIBinaryLog
 * \brief Class for read and write binary data to logfile, and playback this data in realtime, or custom speed
 *
 * \section PIBinaryLog_sec0 Synopsis
 * Binary Log is a file with simple header, where you can read and write some binary data.
 * Any written data include special header with ID, size and timestamp.
 * This header provides separation different messages from the one file by choosing different IDs.
 * With \a filterID or special functions, like \a readBinLog() you can choose IDs what you want to read.
 * With function \a writeBinLog() or \a setDefaultID() you can choose ID that mark you data.
 * By default ID = 1, and \a filterID is empty, that mean you read any ID without filtering.
 * ThreadedRead provide you playback data, with delay that you write data.
 * You can choose different playbak modes by set \a PlayMode.
 *
 * \section PIBinaryLog_sec1 Basic usage
 * This class provide all functions of \a PIIODevice, such \a open(), \a close(),
 * \a read() ,\a write(), and threaded read/write.
 * function \a setLogDir() need to set directory for BinLog files
 * function \a createNewFile() need to create new binlog file
 * function \a restart() need start from the begining of binlog file
 *
 */

static const uchar binlog_sig[] = {'B', 'I', 'N', 'L', 'O', 'G'};

#define PIBINARYLOG_VERSION        0x32
#define PIBINARYLOG_SIGNATURE_SIZE sizeof(binlog_sig)

REGISTER_DEVICE(PIBinaryLog)

PIBinaryLog::PIBinaryLog() {
#ifdef MICRO_PIP
	setThreadedReadBufferSize(512);
#else
	setThreadedReadBufferSize(64_KiB);
#endif
	is_started = is_indexed = is_pause = false;
	create_index_on_fly                = false;
	current_index                      = -1;
	log_size                           = 0;
	f_new_path                         = nullptr;
	setPlaySpeed(1.);
	setDefaultID(1);
	setPlaySpeed(1.0);
	setPlayDelay(1_s);
	setPlayRealTime();
	setSplitTime(600_s);
	setSplitRecordCount(1000);
	setSplitFileSize(0xFFFFFF);
	setSplitMode(SplitNone);
	setLogDir(PIString());
	setFilePrefix(PIString());
	setRapidStart(false);
	file.setName("_S.PIBinLog.file");
}


PIBinaryLog::~PIBinaryLog() {
	stop();
	close();
}


bool PIBinaryLog::openDevice() {
	lastrecord.timestamp = PISystemTime();
	lastrecord.id        = 0;
	write_count          = 0;
	is_started           = false;
	is_thread_ok         = true;
	is_indexed           = false;
	is_pause             = false;
	index.clear();
	log_size = 0;
	if (mode_ == ReadWrite) {
		piCoutObj << "Error: ReadWrite mode not supported, use WriteOnly or ReadOnly"_tr("PIBinLog");
		return false;
	}
	if (path().isEmpty() && mode_ == WriteOnly) {
		if (f_new_path)
			setPath(f_new_path());
		else
			setPath(getLogfilePath(logDir(), filePrefix()));
	}
	if (path().isEmpty() && mode_ == ReadOnly) {
		PIDir ld(logDir());
		if (ld.isExists()) {
			PIVector<PIFile::FileInfo> es = ld.allEntries();
			for (const auto & i: es) {
				if (i.extension() == "binlog" && i.isFile() && i.baseName().startsWith(filePrefix())) {
					setPath(i.path);
					break;
				}
			}
		}
	}
	if (!file.open(path(), mode_)) {
		piCoutObj << "Error: Can't open file \"%1\": %2"_tr("PIBinLog").arg(path()).arg(errorString());
		return false;
	}
	setName(path());
	if (mode_ == WriteOnly) {
		file.clear();
		if (!writeFileHeader()) {
			piCoutObj << "Error: Can't write binlog file header \"%1\""_tr("PIBinLog").arg(path());
			return false;
		}
		is_started = true;
	}
	if (mode_ == ReadOnly) {
		if (file.isEmpty()) {
			piCoutObj << "Error: File is null \"%1\""_tr("PIBinLog").arg(path());
			fileError();
			return false;
		}
		if (!checkFileHeader()) {
			fileError();
			return false;
		}
		if (isEmpty()) {
			piCoutObj << "Warning: Empty BinLog file \"%1\""_tr("PIBinLog").arg(path());
			fileEnd();
		}
		play_time = 0;
		if (!rapid_start) is_started = true;
	}
	startlogtime = PISystemTime::current();
	pause_time   = PISystemTime();
	return true;
}


bool PIBinaryLog::closeDevice() {
	stopAndWait();
	pausemutex.unlock();
	logmutex.unlock();
	moveIndex(-1);
	is_indexed = false;
	index.clear();
	bool e   = isEmpty();
	log_size = 0;
	if (canWrite() && e) {
		file.remove();
		return true;
	}
	return file.close();
}


bool PIBinaryLog::threadedRead(const uchar * readed, ssize_t size) {
	// piCout << "binlog threaded read";
	if (!canRead() || isEnd()) return PIIODevice::threadedRead(readed, size);
	is_thread_ok = false;
	logmutex.lock();
	const PISystemTime lastrec_timestamp = lastrecord.timestamp;
	const int lastrec_id                 = lastrecord.id;
	logmutex.unlock();
	PISystemTime pt;
	double delay;
	switch (play_mode) {
	case PlayRealTime:
		pausemutex.lock();
		if (is_pause) {
			pausemutex.unlock();
			piMSleep(100); // TODO: rewrite with condvar
			return false;
		} else if (pause_time > PISystemTime()) {
			// startlogtime += pause_time;
			pause_time = PISystemTime();
		}
		pausemutex.unlock();
		pt = PISystemTime::current() - startlogtime;
		if (is_started) {
			if (lastrec_timestamp > pt) (lastrec_timestamp - pt).sleep();
		} else {
			startlogtime = PISystemTime::current() - lastrec_timestamp;
			is_started   = true;
		}
		break;
	case PlayVariableSpeed:
		delay = lastrec_timestamp.toMilliseconds() - play_time;
		// piCoutObj << "delay" << delay;
		double cdelay;
		int dtc;
		if (is_started) {
			if (is_pause) {
				piMSleep(100); // TODO: rewrite with condvar
				return false;
			}
			if (delay > 0) {
				cdelay = delay * play_speed; // TODO: rewrite with condvar
				dtc    = int(cdelay) / 100;  // TODO: rewrite with condvar
				if (play_speed <= 0.) dtc = 2;
				// piCout << play_speed << dtc;
				for (int j = 0; j < dtc; j++) {
					cdelay = delay * play_speed; // TODO: rewrite with condvar
					dtc    = int(cdelay) / 100;  // TODO: rewrite with condvar
					piMSleep(100);               // TODO: rewrite with condvar
					if (play_speed <= 0.) {
						dtc = 2;
						j   = 0;
					}
					// piCout << "    " << play_speed << dtc << j;
				}
				cdelay = cdelay - dtc * 100; // TODO: rewrite with condvar
				PISystemTime::fromMilliseconds(cdelay).sleep();
			}
		} else
			is_started = true;
		play_time = lastrec_timestamp.toMilliseconds();
		break;
	case PlayStaticDelay:
		if (is_started) {
			if (is_pause) {
				piMSleep(100); // TODO: rewrite with condvar
				return false;
			}
			play_delay.sleep();
		} else
			is_started = true;
		break;
	default: return false;
	}
	bool res = PIIODevice::threadedRead(readed, size);
	threadedReadRecord(PIByteArray(readed, size), lastrec_id, lastrec_timestamp);
	is_thread_ok = true;
	return res;
}


PIString PIBinaryLog::getLogfilePath(const PIString & log_dir, const PIString & prefix) {
	PIDir dir(log_dir);
	dir.setDir(dir.absolutePath());
	if (!dir.isExists()) {
		piCout << "[PIBinaryLog]"
			   << "Creating directory \"%1\""_tr("PIBinLog").arg(dir.path());
		dir.make(true);
	}
	const PIString npath = log_dir + PIDir::separator + prefix + PIDateTime::current().toString("yyyy_MM_dd__hh_mm_ss");
	PIString cnpath      = npath + ".binlog";
	int i                = 1;
	while (PIFile::isExists(cnpath)) {
		cnpath = npath + '_' + PIString::fromNumber(i) + ".binlog";
		i++;
	}
	return cnpath;
}

PIString PIBinaryLog::createNewFile() {
	if (!file.close()) return PIString();
	PIString cnpath;
	if (f_new_path)
		cnpath = f_new_path();
	else
		cnpath = getLogfilePath(logDir(), filePrefix());
	if (open(cnpath, PIIODevice::WriteOnly)) {
		newFile(file.path());
		return file.path();
	}
	piCoutObj << "Can't create new file, maybe LogDir \"%1\" is invalid"_tr("PIBinLog").arg(logDir());
	return PIString();
}


void PIBinaryLog::createNewFile(const PIString & path) {
	if (open(path, PIIODevice::WriteOnly)) {
		newFile(file.path());
	} else
		piCoutObj << "Can't create new file, maybe path \"%1\" is invalid"_tr("PIBinLog").arg(path);
}


void PIBinaryLog::setCreateIndexOnFly(bool yes) {
	create_index_on_fly = yes;
}


void PIBinaryLog::setPause(bool pause) {
	pausemutex.lock();
	is_pause = pause;
	if (pause)
		pause_time = PISystemTime::current();
	else {
		if (pause_time > PISystemTime()) pause_time = PISystemTime::current() - pause_time;
	}
	pausemutex.unlock();
}


int PIBinaryLog::writeBinLog(int id, const void * data, int size) {
	if (size <= 0 || !canWrite()) return -1;
	if (id == 0) {
		piCoutObj << "Error: can`t write with id = 0! ID must be > 0"_tr("PIBinLog");
		return -1;
	}
	if (is_pause) return 0;
	logmutex.lock();
	const int res = writeRecord(id, PISystemTime::current() - startlogtime, data, size);
	switch (split_mode) {
	case SplitSize:
		if (log_size > split_size) createNewFile();
		break;
	case SplitTime:
		if ((PISystemTime::current() - startlogtime) > split_time) createNewFile();
		break;
	case SplitCount:
		if (write_count > split_count) createNewFile();
		break;
	default: break;
	}
	logmutex.unlock();
	if (res > 0)
		return size;
	else
		return res;
}


int PIBinaryLog::writeBinLog_raw(int id, const PISystemTime & time, const void * data, int size) {
	if (size <= 0 || !canWrite()) return -1;
	logmutex.lock();
	const int res = writeRecord(id, time, data, size);
	logmutex.unlock();
	if (res > 0)
		return size;
	else
		return res;
}


PIByteArray PIBinaryLog::readBinLog(int id, PISystemTime * time, int * readed_id) {
	if (!canRead()) return PIByteArray();
	logmutex.lock();
	Record br = readRecord();
	logmutex.unlock();
	if (br.id == -1) {
		piCoutObj << "End of BinLog file"_tr("PIBinLog");
		fileEnd();
		return PIByteArray();
	}
	if ((id == 0) && (br.id != 0) && (br.id != -1)) {
		if (time) *time = br.timestamp;
		if (readed_id) *readed_id = br.id;
		return br.data;
	}
	logmutex.lock();
	while (br.id != id && !isEnd())
		br = readRecord();
	logmutex.unlock();
	if (br.id == -1) {
		piCoutObj << "End of BinLog file"_tr("PIBinLog");
		fileEnd();
		return PIByteArray();
	}
	if (br.id == id) {
		if (time) *time = br.timestamp;
		if (readed_id) *readed_id = br.id;
		return br.data;
	}
	piCoutObj << "Can't find record with id = %1"_tr("PIBinLog").arg(id);
	return PIByteArray();
}


int PIBinaryLog::readBinLog(int id, void * read_to, int max_size, PISystemTime * time, int * readed_id) {
	if (max_size <= 0 || read_to == 0) return -1;
	const PIByteArray ba = readBinLog(id, time, readed_id);
	if (ba.isEmpty()) return -1;
	int sz = piMini(max_size, ba.size());
	memcpy(read_to, ba.data(), sz);
	return sz;
}


bool PIBinaryLog::isEmpty() const {
	return (log_size <= llong(PIBINARYLOG_SIGNATURE_SIZE + 1));
}


void PIBinaryLog::setHeader(const PIByteArray & header) {
	user_header = header;
}


PIByteArray PIBinaryLog::getHeader() const {
	return index.info.user_header;
}


ssize_t PIBinaryLog::readDevice(void * read_to, ssize_t max_size) {
	PIMutexLocker _ml(logmutex);
	if (lastrecord.id == -1 || isEnd()) return 0;
	if (!is_thread_ok && lastrecord.id > 0) return lastrecord.data.size();
	if (!canRead()) return -1;
	if (max_size <= 0 || read_to == 0) return -1;
	Record br;
	br.id = 0;
	if (filterID.isEmpty()) {
		br = readRecord();
	} else {
		while (!filterID.contains(br.id) && !isEnd())
			br = readRecord();
	}
	if (br.id == -1) {
		fileEnd();
		piCoutObj << "End of BinLog file"_tr("PIBinLog");
		return 0;
	}
	if (br.id == 0) {
		piCoutObj << "Read record error"_tr("PIBinLog");
		return -1;
	}
	const ssize_t sz = piMini(max_size, br.data.size());
	if (sz < br.data.size_s()) piCoutObj << "too small read buffer: %1, data size: %2"_tr("PIBinLog").arg(max_size).arg(br.data.size());
	memcpy(read_to, br.data.data(), sz);
	return sz;
}


ssize_t PIBinaryLog::writeDevice(const void * data, ssize_t size) {
	return writeBinLog(default_id, data, size);
}


void PIBinaryLog::restart() {
	const bool th = isThreadedRead();
	if (th) stopThreadedRead();
	if (!canRead()) return;
	logmutex.unlock();
	lastrecord.timestamp = PISystemTime();
	lastrecord.id        = 0;
	is_thread_ok         = true;
	is_started           = !rapidStart();
	play_time            = 0;
	file.seekToBegin();
	checkFileHeader();
	moveIndex(0);
	startlogtime = PISystemTime::current();
	if (th) startThreadedRead();
}


bool PIBinaryLog::writeFileHeader() {
	if (file.write(binlog_sig, PIBINARYLOG_SIGNATURE_SIZE) <= 0) return false;
	const uchar version = PIBINARYLOG_VERSION;
	if (file.write(&version, 1) <= 0) return false;
	const uint32_t sz = user_header.size();
	file.write(&sz, 4);
	file.write(user_header);
	file.flush();
	if (create_index_on_fly) {
		index.info.user_header = user_header;
		index.info.path        = file.path();
	}
	return true;
}


bool PIBinaryLog::checkFileHeader() {
	index.info.user_header.clear();
	uchar read_sig[PIBINARYLOG_SIGNATURE_SIZE];
	for (uint i = 0; i < PIBINARYLOG_SIGNATURE_SIZE; i++)
		read_sig[i] = 0;
	if (file.read(read_sig, PIBINARYLOG_SIGNATURE_SIZE) < 0) return false;
	bool correct = true;
	for (uint i = 0; i < PIBINARYLOG_SIGNATURE_SIZE; i++)
		if (read_sig[i] != binlog_sig[i]) correct = false;
	if (!correct) {
		piCoutObj << "BinLogFile signature is corrupted or invalid file"_tr("PIBinLog");
		return false;
	}
	uchar read_version = 0;
	if (file.read(&read_version, 1) < 0) return false;
	if (read_version == PIBINARYLOG_VERSION_OLD) {
		log_size = file.size();
		return true;
	}
	if (read_version == PIBINARYLOG_VERSION) {
		log_size    = file.size();
		uint32_t sz = 0;
		file.read(&sz, 4);
		if (sz > 0) {
			index.info.user_header = file.read(sz);
		}
		return true;
	}
	if (read_version == 0) piCoutObj << "BinLogFile has invalid version"_tr("PIBinLog");
	if (read_version < PIBINARYLOG_VERSION) piCoutObj << "BinLogFile has too old verion"_tr("PIBinLog");
	if (read_version > PIBINARYLOG_VERSION) piCoutObj << "BinLogFile has too new version"_tr("PIBinLog");
	return false;
}


PIBinaryLog::Record PIBinaryLog::readRecord() {
	// piCoutObj << "readRecord pos =" << file.pos();
	logmutex.lock();
	PIByteArray ba;
	Record br;
	lastrecord.id = 0;
	lastrecord.data.clear();
	lastrecord.timestamp = PISystemTime();
	ba.resize(sizeof(Record) - sizeof(PIByteArray));
	if (file.read(ba.data(), ba.size_s()) > 0) {
		ba >> br.id >> br.size >> br.timestamp;
	} else {
		br.id = -1;
		logmutex.unlock();
		// piCoutObj << "readRecord done" << br.id;
		return br;
	}
	if ((br.id != 0) && (br.id != -1) && (br.size > 0)) {
		ba.resize(br.size);
		if (file.read(ba.data(), ba.size_s()) > 0)
			br.data = ba;
		else
			br.id = 0;
	} else
		br.id = 0;
	lastrecord = br;
	if (br.id == 0) fileError();
	moveIndex(index.index_pos.value(file.pos(), -1));
	logmutex.unlock();
	// piCoutObj << "readRecord done";
	return br;
}


int PIBinaryLog::writeRecord(int id, PISystemTime time, const void * data, int size) {
	PIByteArray logdata;
	logdata << id << size << time << PIMemoryBlock(data, size);
	if (create_index_on_fly) {
		BinLogIndex bi;
		bi.id        = id;
		bi.data_size = size;
		bi.pos       = file.pos();
		bi.timestamp = time;
		index.index << bi;
		auto & rinfo(index.info.records[id]);
		if (rinfo.count == 0) {
			rinfo.id         = id;
			rinfo.start_time = rinfo.end_time = time;
			rinfo.minimum_size = rinfo.maximum_size = size;
		} else {
			rinfo.start_time   = piMin(rinfo.start_time, time);
			rinfo.end_time     = piMax(rinfo.end_time, time);
			rinfo.minimum_size = piMin(rinfo.minimum_size, size);
			rinfo.maximum_size = piMax(rinfo.maximum_size, size);
		}
		++rinfo.count;
		if (index.info.records_count == 0) {
			index.info.start_time = index.info.end_time = time;
		} else {
			index.info.start_time = piMin(index.info.start_time, time);
			index.info.end_time   = piMax(index.info.end_time, time);
		}
		++index.info.records_count;
		is_indexed = true;
	}
	const int ret = file.write(logdata.data(), logdata.size());
	file.flush();
	write_count++;
	log_size = file.size();
	if (create_index_on_fly) {
		index.info.log_size = log_size;
	}
	return ret;
}


void PIBinaryLog::parseLog(PIFile * f, PIBinaryLog::BinLogInfo * info, PIVector<PIBinaryLog::BinLogIndex> * index) {
	if (!info && !index) return;
	if (info) {
		info->log_size      = -1;
		info->records_count = 0;
		info->records.clear();
	}
	if (index) index->clear();
	if (f == 0) return;
	if (!f->canRead()) return;
	if (info) {
		info->path     = f->path();
		info->log_size = f->size();
	}
	uchar read_sig[PIBINARYLOG_SIGNATURE_SIZE];
	for (uint i = 0; i < PIBINARYLOG_SIGNATURE_SIZE; i++)
		read_sig[i] = 0;
	if (f->read(read_sig, PIBINARYLOG_SIGNATURE_SIZE) < 0) {
		if (info) info->records_count = -1;
		return;
	}
	for (uint i = 0; i < PIBINARYLOG_SIGNATURE_SIZE; i++) {
		if (read_sig[i] != binlog_sig[i]) {
			if (info) info->records_count = -2;
			return;
		}
	}
	uchar read_version = 0;
	if (f->read(&read_version, 1) < 0) {
		if (info) info->records_count = -3;
		return;
	}
	if (read_version == 0) {
		if (info) info->records_count = -4;
		return;
	}
	if (read_version < PIBINARYLOG_VERSION_OLD) {
		if (info) info->records_count = -5;
		return;
	}
	if (read_version > PIBINARYLOG_VERSION) {
		if (info) info->records_count = -6;
		return;
	}
	if (read_version == PIBINARYLOG_VERSION) {
		uint32_t sz = 0;
		f->read(&sz, 4);
		if (sz > 0) {
			PIByteArray user_hdr = f->read(sz);
			if (info) info->user_header.swap(user_hdr);
		}
	}
	PIByteArray ba;
	Record br;
	br.id                     = 0;
	br.size                   = 0;
	bool first                = true;
	constexpr size_t hdr_size = sizeof(Record) - sizeof(PIByteArray);
	ba.resize(hdr_size);
	while (1) {
		ba.resize(hdr_size);
		{
			if (f->read(ba.data(), ba.size()) > 0) {
				ba >> br.id >> br.size >> br.timestamp;
			} else
				break;
			if (info) {
				if (info->log_size - f->pos() >= br.size) {
					f->seek(f->pos() + br.size);
				}
			} else
				break;
		}
		if (br.id > 0) {
			if (index) {
				BinLogIndex bl_ind;
				bl_ind.id        = br.id;
				bl_ind.data_size = br.size;
				bl_ind.pos       = f->pos() - br.size - hdr_size;
				bl_ind.timestamp = br.timestamp;
				index->append(bl_ind);
			}
			if (info) {
				info->records_count++;
				if (first) {
					info->start_time = br.timestamp;
					first            = false;
				}
				BinLogRecordInfo & bri(info->records[br.id]);
				bri.count++;
				if (bri.id == 0) {
					bri.id           = br.id;
					bri.minimum_size = bri.maximum_size = br.size;
					bri.start_time                      = br.timestamp;
				} else {
					bri.end_time = br.timestamp;
					if (bri.minimum_size > br.size) bri.minimum_size = br.size;
					if (bri.maximum_size < br.size) bri.maximum_size = br.size;
				}
			}
		}
	}
	if (info) info->end_time = br.timestamp;
}


void PIBinaryLog::moveIndex(int i) {
	if (is_indexed) {
		current_index = i;
		posChanged(current_index);
	}
}


PIBinaryLog::BinLogInfo PIBinaryLog::getLogInfo(const PIString & path) {
	BinLogInfo bi;
	bi.path          = path;
	bi.records_count = 0;
	PIFile tfile;
	if (!tfile.open(path, PIIODevice::ReadOnly)) return bi;
	parseLog(&tfile, &bi, 0);
	return bi;
}


bool PIBinaryLog::cutBinLog(const PIBinaryLog::BinLogInfo & src, const PIString & dst, int from, int to) {
	PIBinaryLog slog;
	if (!slog.open(src.path, PIIODevice::ReadOnly)) {
		piCout << "[PIBinaryLog]"
			   << "Error, can't open \"%1\""_tr("PIBinLog").arg(src.path);
		return false;
	}
	const PIVector<int> ids = src.records.keys();
	slog.seekTo(from);
	PIBinaryLog dlog;
	dlog.createNewFile(dst);
	if (!dlog.isOpened()) {
		piCout << "[PIBinaryLog]"
			   << "Error, can't create \"%1\""_tr("PIBinLog").arg(dst);
		return false;
	}
	bool first = true;
	Record br;
	PISystemTime st;
	PITimeMeasurer tm;
	while (!slog.isEnd() && ((slog.pos() <= to) || to < 0)) {
		br = slog.readRecord();
		if (first) {
			st    = br.timestamp;
			first = false;
		}
		if (ids.contains(br.id)) {
			if (dlog.writeBinLog_raw(br.id, br.timestamp - st, br.data) <= 0) {
				piCout << "[PIBinaryLog]"
					   << "Error, can't write to file \"%1\""_tr("PIBinLog").arg(dst);
				return false;
			}
		}
		if (tm.elapsed_s() > 1) {
			tm.reset();
			piCout << "[PIBinaryLog]"
				   << "process" << PITime::fromSystemTime(br.timestamp).toString();
		}
	}
	return true;
}


bool PIBinaryLog::joinBinLogsSerial(const PIStringList & src,
                                    const PIString & dst,
                                    std::function<bool(const PIString &, PISystemTime)> progress) {
	PIBinaryLog slog;
	PIBinaryLog dlog;
	PISystemTime dtime;
	PISystemTime lt;
	PITimeMeasurer tm;
	bool first = true;
	for (const PIString & fn: src) {
		if (!slog.open(fn, PIIODevice::ReadOnly)) {
			piCout << "[PIBinaryLog]"
				   << "Error, can't open \"%1\""_tr("PIBinLog").arg(fn);
			return false;
		}
		if (first) {
			first = false;
			dlog.setHeader(slog.getHeader());
			dlog.createNewFile(dst);
			if (!dlog.isOpened()) {
				piCout << "[PIBinaryLog]"
					   << "Error, can't create \"%1\""_tr("PIBinLog").arg(dst);
				return false;
			}
			piCout << "[PIBinaryLog]"
				   << "Start join binlogs to \"%1\""_tr("PIBinLog").arg(dst);
		} else {
			dtime = lt;
		}
		tm.reset();
		Record br;
		PISystemTime st;
		while (!slog.isEnd()) {
			br = slog.readRecord();
			if (br.data.isEmpty() || br.id == 0 || br.id == -1) continue;
			st = br.timestamp;
			lt = dtime + br.timestamp;
			if (dlog.writeBinLog_raw(br.id, lt, br.data) <= 0) {
				piCout << "[PIBinaryLog]"
					   << "Error, can't write to file \"%1\""_tr("PIBinLog").arg(dst);
				return false;
			}
			if (tm.elapsed_s() > 0.1) {
				tm.reset();
				if (progress) {
					if (!progress(fn, lt)) {
						slog.close();
						dlog.close();
						PIFile::remove(dlog.path());
						return false;
					}
				} else {
					piCout << "[PIBinaryLog]"
						   << "process" << PITime::fromSystemTime(lt).toString();
				}
			}
		}
		slog.close();
		// piCout << "[PIBinaryLog]" << "complete" << fn;
	}
	piCout << "[PIBinaryLog]"
		   << "Finish join binlogs, total time %1"_tr("PIBinLog").arg(PITime::fromSystemTime(lt).toString());
	return true;
}


bool PIBinaryLog::createIndex() {
	logmutex.lock();
	const llong cp = file.pos();
	file.seekToBegin();
	index.clear();
	parseLog(&file, &index.info, &index.index);
	index.makeIndexPos();
	file.seek(cp);
	is_indexed = !index.index.isEmpty();
	logmutex.unlock();
	return is_indexed;
}


int PIBinaryLog::posForTime(const PISystemTime & time) {
	int ci = -1;
	for (uint i = 0; i < index.index.size(); i++) {
		if (time <= index.index[i].timestamp && (filterID.contains(index.index[i].id) || filterID.isEmpty())) {
			ci = i;
			break;
		}
	}
	return ci;
}


void PIBinaryLog::seekTo(int rindex) {
	// piCoutObj << "seekTo";
	logmutex.lock();
	pausemutex.lock();
	if (rindex < index.index.size_s() && rindex >= 0) {
		file.seek(index.index[rindex].pos);
		moveIndex(index.index_pos.value(file.pos(), -1));
		// double prev_pt = play_time;
		play_time            = index.index[rindex].timestamp.toMilliseconds();
		lastrecord.timestamp = index.index[rindex].timestamp;
		if (play_mode == PlayRealTime) {
			startlogtime = PISystemTime::current() - lastrecord.timestamp;
		}
	}
	// piCoutObj << "seekTo done";
	pausemutex.unlock();
	logmutex.unlock();
}


bool PIBinaryLog::seek(const PISystemTime & time) {
	const int ci = posForTime(time);
	if (ci >= 0) {
		seekTo(ci);
		return true;
	}
	return false;
}


bool PIBinaryLog::seek(llong filepos) {
	int ci = -1;
	for (uint i = 0; i < index.index.size(); i++) {
		if (filepos <= index.index[i].pos && (filterID.contains(index.index[i].id) || filterID.isEmpty())) {
			ci = i;
			break;
		}
	}
	if (ci >= 0) {
		seekTo(ci);
		return true;
	}
	return false;
}


int PIBinaryLog::pos() const {
	if (is_indexed) return current_index;
	return -1;
}


PIByteArray PIBinaryLog::saveIndex() const {
	if (!is_indexed) return PIByteArray();
	return piSerialize(index);
}


bool PIBinaryLog::loadIndex(PIByteArray saved) {
	if (!canRead() || saved.isEmpty()) return false;
	index = piDeserialize<CompleteIndex>(saved);
	index.makeIndexPos();
	is_indexed = index.index.isNotEmpty();
	return is_indexed;
}


PIString PIBinaryLog::constructFullPathDevice() const {
	PIString ret;
	ret += logDir() + ":" + filePrefix() + ":" + PIString::fromNumber(defaultID()) + ":";
	switch (play_mode) {
	case PlayRealTime: ret += "RT"; break;
	case PlayVariableSpeed: ret += PIString::fromNumber(playSpeed()) + "X"; break;
	case PlayStaticDelay: ret += PIString::fromNumber(playDelay().toMilliseconds()) + "M"; break;
	default: ret += "RT"; break;
	}
	return ret;
}


void PIBinaryLog::configureFromFullPathDevice(const PIString & full_path) {
	const PIStringList pl = full_path.split(":");
	for (int i = 0; i < pl.size_s(); ++i) {
		const PIString p(pl[i].trimmed());
		switch (i) {
		case 0: setLogDir(p); break;
		case 1: setFilePrefix(p); break;
		case 2: setDefaultID(p.toInt()); break;
		case 3:
			if (p.toUpperCase() == "RT") setPlayRealTime();
			if (p.toUpperCase().right(1) == "X") setPlaySpeed((p.left(p.size() - 1)).toDouble());
			if (p.toUpperCase().right(1) == "M") setPlayDelay(PISystemTime::fromMilliseconds((p.left(p.size() - 1)).toDouble()));
			break;
		}
	}
	// piCoutObj << "configured";
}


PIPropertyStorage PIBinaryLog::constructVariantDevice() const {
	PIPropertyStorage ret;
	PIVariantTypes::Enum e;
	ret.addProperty("log dir", PIVariantTypes::Dir(logDir()));
	ret.addProperty("file prefix", filePrefix());
	ret.addProperty("default ID", defaultID());
	e << "real-time"
	  << "variable speed"
	  << "static delay";
	e.selectValue((int)playMode());
	ret.addProperty("play mode", e);
	ret.addProperty("play speed", playSpeed());
	ret.addProperty("play delay", playDelay().toMilliseconds());
	return ret;
}


void PIBinaryLog::configureFromVariantDevice(const PIPropertyStorage & d) {
	setLogDir(d.propertyValueByName("log dir").toString());
	setFilePrefix(d.propertyValueByName("file prefix").toString());
	setDefaultID(d.propertyValueByName("default ID").toInt());
	setPlaySpeed(d.propertyValueByName("play speed").toDouble());
	setPlayDelay(PISystemTime::fromMilliseconds(d.propertyValueByName("play delay").toDouble()));
	setPlayMode((PlayMode)d.propertyValueByName("play mode").toEnum().selectedValue());
}


void PIBinaryLog::propertyChanged(const char * s) {
	default_id      = property("defaultID").toInt();
	rapid_start     = property("rapidStart").toBool();
	play_mode       = (PlayMode)property("playMode").toInt();
	const double ps = property("playSpeed").toDouble();
	play_speed      = ps > 0. ? 1. / ps : 0.;
	play_delay      = property("playDelay").toSystemTime();
	split_mode      = (SplitMode)property("splitMode").toInt();
	split_time      = property("splitTime").toSystemTime();
	split_size      = property("splitFileSize").toLLong();
	split_count     = property("splitRecordCount").toInt();
	// piCoutObj << "propertyChanged" << s << play_mode;
}


void PIBinaryLog::CompleteIndex::clear() {
	info = BinLogInfo();
	index.clear();
	index_pos.clear();
}


void PIBinaryLog::CompleteIndex::makeIndexPos() {
	index_pos.clear();
	for (uint i = 0; i < index.size(); i++)
		index_pos[index[i].pos] = i;
}
