/*
    PIP - Platform Independent Primitives
    Class for send and receive files and directories via PIBaseTransfer
    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 "pifiletransfer.h"

const char PIFileTransfer::sign[] = {'P', 'F', 'T'};

PIFileTransfer::PIFileTransfer() {
	for (uint i = 0; i < sizeof(sign); i++)
		pftheader.sig[i] = sign[i];
	pftheader.version    = __PIFILETRANSFER_VERSION;
	pftheader.session_id = 0;
	pftheader.step       = pft_None;
	dir                  = PIDir::current();
	started_ = scanning = false;
	bytes_file_all = bytes_file_cur = 0;
	CONNECT1(void, bool, this, sendFinished, this, send_finished);
	CONNECT1(void, bool, this, receiveFinished, this, receive_finished);
}


PIFileTransfer::~PIFileTransfer() {
	stop();
	work_file.close();
}


bool PIFileTransfer::send(const PIFile & file) {
	return send(file.path());
}


bool PIFileTransfer::send(const PIString & file) {
	return send(PIVector<PIFile::FileInfo>() << PIFile::fileInfo(file));
}


bool PIFileTransfer::send(const PIStringList & files) {
	PIVector<PIFile::FileInfo> fil;
	for (const auto & file: files)
		fil << PIFile::fileInfo(file);
	return send(fil);
}


bool PIFileTransfer::send(PIVector<PIFile::FileInfo> entries) {
	started_ = true;
	PIVector<PFTFileInfo> allEntries;
	for (int i = 0; i < entries.size_s(); i++) {
		allEntries << PFTFileInfo(entries[i]);
		allEntries.back().dest_path = entries[i].name();
		if (entries[i].isDir()) {
			cur_file_string = "scanning ... ";
			scan_dir.setDir(entries[i].path);
			scanning                       = true;
			PIVector<PIFile::FileInfo> fls = scan_dir.allEntries();
			scanning                       = false;
			scan_dir.up();
			// piCout << "files rel to" << d.absolutePath();
			for (uint j = 0; j < fls.size(); j++) {
				allEntries << PFTFileInfo(fls[j]);
				allEntries.back().dest_path = scan_dir.relative(fls[j].path);
				// piCout << "  abs path" << fls[j].path;
				// piCout << "  rel path" << allEntries.back().dest_path;
			}
		}
	}
	return sendFiles(allEntries);
}


bool PIFileTransfer::sendFiles(const PIVector<PFTFileInfo> & files) {
	files_ = files;
	PIStringList names;
	// piCoutObj << "prepare to send" << files_.size() << "files";
	for (int i = 0; i < files_.size_s(); i++) {
		if (names.contains(files_[i].path)) {
			files_.remove(i);
			--i;
		} else
			names << files_[i].path;
		if (files_[i].isDir()) files_[i].size = 0;
		// piCout << "prepare" << i << files_[i].path << files_[i].dest_path << files_[i].name();
	}
	randomize();
	step_mutex.lock();
	pftheader.session_id = randomi();
	sendFilesStarted();
	cur_file_string = "build session";
	desc.clear();
	desc << files_;
	pftheader.step = pft_Description;
	buildSession(PIVector<Part>() << Part(0, desc.size()));
	cur_file_string = "";
	step_mutex.unlock();
	if (!send_process()) {
		sendFilesFinished(false);
		started_ = false;
		return false;
	}
	step_mutex.lock();
	pftheader.step = pft_Data;
	PIVector<Part> pts;
	for (int i = 0; i < files_.size_s(); i++) {
		pts << Part(i + 1, files_[i].size);
	}
	buildSession(pts);
	step_mutex.unlock();
	bool ok = send_process();
	sendFilesFinished(ok);
	started_ = false;
	return ok;
}


void PIFileTransfer::processFile(int id, ullong start, PIByteArray & data) {
	// piCout << "processFile" << id << files_.size();
	if (id <= 0 || id > files_.size_s()) {
		cur_file_string = "Error: Invalid file id " + PIString::fromNumber(id);
		piCoutObj << cur_file_string;
		stopReceive();
		return;
	}
	PFTFileInfo fi  = files_[id - 1];
	bytes_file_all  = fi.size;
	bytes_file_cur  = start;
	cur_file_string = fi.dest_path;
	PIString path   = dir.absolutePath() + dir.separator + fi.dest_path;
	// piCout << "receive" << path << fi.size << data.size();
	if (fi.isDir()) {
		// piCoutObj << "make dir" << fi.entry.path;
		if (!PIDir::make(path)) {
			cur_file_string = "Error: While create directory \"" + path + "\"";
			piCoutObj << cur_file_string;
			stopReceive();
			return;
		}
	}
	if (fi.isFile()) {
		if (work_file.path() != path || !work_file.isOpened()) {
			// piCout << "file close";
			work_file.close();
			// piCout << "new file" << path << work_file.path() << work_file.isOpened();
			if (!work_file.open(path, PIIODevice::ReadWrite)) {
				cur_file_string = "Error: While open file \"" + path + "\"";
				piCoutObj << cur_file_string;
				stopReceive();
				return;
			}
			PIFile::applyFileInfo(path, fi);
			if (work_file.size() > fi.size) {
				// piCoutObj << "error size" << work_file.size() << fi.size;
				work_file.clear();
				work_file.resize(fi.size);
				// piCoutObj << "correct size" << work_file.size() << fi.size;
			}
		}
		// piCoutObj << "write file" << path << work_file.path() << work_file.size() << fi.entry.size << work_file.pos() << fi.fstart <<
		// fi.fsize;
		if (work_file.size() < (llong)start) {
			// piCoutObj << "error pos size" << work_file.pos() << fi.fstart;
			work_file.resize(start);
			// piCoutObj << "correct pos size" << work_file.pos() << fi.fstart;
		}
		if (work_file.size() > fi.size) {
			piCoutObj << "****** error size" << work_file.size() << fi.size;
			work_file.clear();
			work_file.resize(fi.size);
			piCoutObj << "****** correct size" << work_file.size() << fi.size;
		}
		// if (fi.fstart != work_file.pos()) piCoutObj << "error pos" << work_file.pos() << fi.fstart;
		work_file.seek(start);
		int rs = work_file.write(data.data(), data.size());
		if (rs != data.size_s()) {
			cur_file_string = "Error: While write file \"" + fi.path + "\" (must " + PIString::fromNumber(data.size()) + ", but write " +
			                  PIString::fromNumber(rs) + ")";
			piCoutObj << cur_file_string;
			stopReceive();
			return;
		}
	}
}


PIByteArray PIFileTransfer::buildPacket(Part p) {
	// piCoutObj << "Step" << pftheader.step;
	// piCoutObj << "session id" << pftheader.session_id;
	PIByteArray ba;
	switch (pftheader.step) {
	case pft_None: stopSend(); return PIByteArray();
	case pft_Description:
		ba.resize(p.size);
		memcpy(ba.data(), desc.data(p.start), p.size);
		return ba;
	case pft_Data:
		// piCout << "send data" << p.id << files_.size();
		PIFile::FileInfo fi = files_[p.id - 1];
		if (fi.isFile()) {
			// piCout << "send file" << fi.name() << fi.size;
			PIString path = fi.path;
			if (work_file.path() != path || !work_file.isOpened()) {
				if (!work_file.open(path, PIIODevice::ReadOnly)) {
					break_          = true;
					cur_file_string = "Error: While open file \"" + fi.path + "\"";
					piCoutObj << cur_file_string;
					stopSend();
					return PIByteArray();
				}
			}
			work_file.seek(p.start);
			ba.resize(p.size);
			int rs = work_file.read(ba.data(), ba.size());
			if ((llong)rs != (llong)p.size) {
				break_          = true;
				cur_file_string = "Error: While read file \"" + fi.path + "\" (must " + PIString::fromNumber(p.size) + ", but read " +
				                  PIString::fromNumber(rs) + ")";
				piCoutObj << cur_file_string;
				stopSend();
				return PIByteArray();
			}
		}
		cur_file_string = fi.path;
		bytes_file_all  = fi.size;
		bytes_file_cur  = p.start;
		return ba;
	}
	return ba;
}


void PIFileTransfer::receivePart(PIBaseTransfer::Part fi, PIByteArray ba, PIByteArray pheader) {
	PFTHeader h;
	// 	piCout << pheader.size() << sizeof(PFTHeader);
	pheader >> h;
	// 	piCout << h.session_id;
	PIMutexLocker ml(step_mutex);
	StepType st    = (StepType)h.step;
	pftheader.step = st;
	if (!h.check_sig()) {
		cur_file_string = "Error: Check signature: File Transfer incompatable or invalid version!";
		piCoutObj << cur_file_string;
		stopReceive();
		return;
	}
	switch (st) {
	case pft_None: break;
	case pft_Description:
		pftheader.session_id = h.session_id;
		if (desc.size() < fi.start + fi.size) desc.resize(fi.start + fi.size);
		memcpy(desc.data(fi.start), ba.data(), ba.size_s());
		break;
	case pft_Data:
		if (h.session_id == pftheader.session_id)
			processFile(fi.id, fi.start, ba);
		else
			stopReceive();
		break;
	default: break;
	}
}


PIString PIFileTransfer::curFile() const {
	PIString s = cur_file_string;
	if (scanning) return s + scan_dir.scanDir();
	return s;
}


PIByteArray PIFileTransfer::customHeader() {
	PIByteArray ba;
	ba << pftheader;
	return ba;
}


void PIFileTransfer::beginReceive() {
	if (pftheader.step == pft_None) {
		files_.clear();
		//		piCoutObj << "start receive"
		started_ = true;
		receiveFilesStarted();
	}
}


void PIFileTransfer::receive_finished(bool ok) {
	step_mutex.lock();
	StepType st = (StepType)pftheader.step;
	step_mutex.unlock();
	if (st == pft_Description) {
		bool user_ok = true;
		if (ok) {
			//			piCoutObj << desc.size() << PICoutManipulators::Hex << desc;
			desc >> files_;
			//			piCoutObj << files_;
			PIStringList files;
			for (const auto & fi: files_)
				files << fi.dest_path;
			pause();
			receiveFilesRequest(files, bytesAll(), &user_ok);
		}
		if (!ok || !user_ok) {
			pftheader.session_id--;
			piCoutObj << "receive aborted";
			receiveFilesFinished(false);
			started_ = false;
		} else
			resume();
	}
	if (st == pft_Data) {
		// piCoutObj << "file close";
		work_file.close();
		receiveFilesFinished(ok);
		started_ = false;
	}
}


void PIFileTransfer::send_finished(bool ok) {
	//	piCoutObj << "file close";
	if (pftheader.step == pft_Data) {
		work_file.close();
	}
}
