#include "script_connection.h"
#include <QDebug>
#include <QTimer>
#include "SH_plugins.h"
#include "SH_server_base.h"
#include "pitime.h"
#include "piserial.h"

Q_DECLARE_METATYPE(SHSScriptConnectionInputData)


SHSScriptConnection::SHSScriptConnection(QObject * parent): QObject(parent) {
	qRegisterMetaType<SHSScriptConnectionInputData>();
	conn_ = 0;
	timer = 0;
	inactive = log_add_time = false;
	log_add_dev = log_io = true;
}


SHSScriptConnection::~SHSScriptConnection() {
}


void SHSScriptConnection::assignConnection(QPIConnection * conn) {
	conn_ = conn;
	if (!conn_) return;
	if (conn_->objectName().isEmpty()) return;
	//qDebug() << "ASSIGN";
	connect(conn_, SIGNAL(qDataReceivedEvent(QString,QByteArray)), this, SLOT(connDataReceived(QString,QByteArray)), Qt::UniqueConnection);
	connect(conn_, SIGNAL(qPacketReceivedEvent(QString,QByteArray)), this, SLOT(connPacketReceived(QString,QByteArray)), Qt::UniqueConnection);
}


QString SHSScriptConnection::logPrefix(const char * type, const PIIODevice * dev) const {
	QString ret = QString(type) + ":";
	bool dash = false;
	if (log_add_dev && dev) {
		dash = true;
		PIStringList sl = conn_->deviceNames(dev);
		if (!sl.isEmpty()) {
			ret.append(" <" + PI2QString(sl[0]) + ">");
		} else
			ret.append(" <>");
	}
	if (log_add_time) {
		dash = true;
		ret.append(" " + QDateTime::currentDateTime().toString());
	}
	if (dash)
		ret.append(" -");
	return ret;
}


void SHSScriptConnection::logIO(const char * type, const PIIODevice * dev, const QString & s, const QByteArray & d) {
	QString ls = logPrefix(type, dev) + ' ' + s;
	if (!s.isEmpty()) ls += ' ';
	ls += QString::number(d.size()) + " bytes (" + d.toHex() + ')';
	if (log_io)
		qDebug().noquote() << ls;
	emit logIOEvent(ls);
}


void SHSScriptConnection::parseInputData(QList<PIIODevice * > devs, QByteArray data) {
	//piCout << "received" << Q2PIByteArray(data).toHex() << devs[0];
	logIO("rcv", devs[0], "", data);
	foreach (PIIODevice * d, devs) {
		//piCout << d;
		if (!d) continue;
		QString qfp;// = PI2QString(d->property(""));
		if (strcmp(d->className(), "PIPacketExtractor") == 0) continue;
		PIString fp;
		if (d->isPropertyExists("__SHS_fullPath__"))
			fp = d->property("__SHS_fullPath__").toString();
		else {
			fp = d->constructFullPath();
			d->setProperty("__SHS_fullPath__", fp);
		}
		QVector<SHS::Command*> & check_commands(RUNTIME->cache.commands_in[PI2QString(fp)]);
		//qDebug() << PI2QString(fp) << check_commands.size() << RUNTIME->cache.commands_in.keys();
		QVariantMap args;
		if (dev_read_stream.contains(d)) {
			DeviceReadStream & ds(dev_read_stream[d]);
			PISystemTime cst = PISystemTime::current(true);
			double dsrms = (cst - ds.last_rec).toMilliseconds();
			if ((dsrms >= 500) || (ds.stream.size_s() >= ds.max_size))
				ds.stream.clear();
			ds.last_rec = cst;
			//piCout << "add to dev_stream" << d << data.size();
			for (int bi = 0; bi < data.size(); ++bi) {
				ds.stream.push_back(uchar(data[bi]));
				if (ds.stream.size_s() < ds.min_size) continue;
				if (ds.stream.size_s() > ds.max_size)
					ds.stream.pop_front();
				bool found = true;
				//int found_bytes = 0;
				while (found) {
					found = false;
					if (ds.stream.isEmpty()) break;
					ds.qba.resize(ds.stream.size_s());
					memcpy(ds.qba.data(), ds.stream.data(), ds.stream.size());
					foreach (SHS::Command * c, check_commands) {
						bool ok = tryReceive(*c, ds.qba);
						found = found || ok;
						/*if (parseInputCommand(ds.qba, c, args)) {
							c.applyToBlock(args);
							received(c.value, c.name, args);
							found = true;
							found_bytes = c.command.size();
						}*/
					}
					if (found)
						ds.stream.clear();
						//ds.stream.remove(0, qMin<int>(found_bytes, ds.stream.size_s()));
				}
			}
		} else {
			foreach (SHS::Command * c, check_commands) {
				tryReceive(*c, data);
			}
		}
	}
}


bool SHSScriptConnection::tryReceive(SHS::Command & cmd, QByteArray data) {
	SHPluginBasePacket * plugin = (SHPluginBasePacket * )cmd.packet_cmd.plugin;
	//qDebug() << "tryReceive" << cmd.name << data.toHex() << plugin;
	if (!plugin) return false;
	int recsize = plugin->receiveSize();
	if (recsize > 0) {
		if (data.size() < recsize)
			return false;
		if (data.size() > recsize)
			data = data.left(recsize);
	}
	QVariantMap args;
	SHPluginBasePacket::ReceiveResults rr = plugin->receivePacket(*(cmd.root), *(cmd.parent), cmd, data, args);
	if (rr.testFlag(SHPluginBasePacket::rrInvoke))
		emit received(&cmd, args);
	return rr.testFlag(SHPluginBasePacket::rrParsed);
}


void SHSScriptConnection::initSerials(const PIStringList & devs) {
	piCout << "init serials" << devs;
#ifdef LINUX
	piForeachC (PIString & d, devs) {
		piCout << "[initSerials] Prepare" << d;
		system(("stty -F " + d + " -hup").dataAscii());
	}
#endif
}


void SHSScriptConnection::sendCommand(int id, const QVector<QVariant> & args) {
	//qDebug() << "sendCommand" << id << args;
	//rec_mutex.lock();
	if (inactive) {
		//rec_mutex.unlock();
		return;
	}
	//rec_mutex.unlock();
	SHS::Command * src_cmd = RUNTIME->cache.command_by_id.value(id, 0);
	if (!src_cmd || !conn_) return;
	SHPluginBasePacket * plugin = (SHPluginBasePacket*)src_cmd->packet_cmd.plugin;
	if (!plugin) return;

	//qDebug() << "sendCommand" << src_cmd->name << args << plugin << src_cmd->arguments.value("_cmd").toByteArray().toHex();

	QVariantMap user_args;
	int _msz = piMini(args.size(), src_cmd->user_arguments.size());
	for (int i = 0; i < _msz; ++i)
		user_args[src_cmd->user_arguments[i]] = args[i];

	SHS::Command cmd(*src_cmd);
	addToMapOverwrite(cmd.arguments, user_args);
	QByteArray data = plugin->sendPacket(*(cmd.root), *(cmd.parent), cmd);
	if (cmd.block_plugin)
		((SHPluginBaseBlock*)cmd.block_plugin)->applyCommand(*(cmd.root), *(cmd.parent), cmd);
	//qDebug() << "sendCommand" << src_cmd->name << PI2QStringList(src_cmd->devices) << data.toHex();
	if (data.isEmpty()) return;
	if (src_cmd->devices.isEmpty()) return;

	PIString path = src_cmd->devices[0];
	PIIODevice * dev = conn_->deviceByFullPath(path);
	logIO("snd", dev, "\"" + cmd.name + "\" to \"" + PI2QString(path) + "\"", data);
	sendToDevice(dev, data);
}


void SHSScriptConnection::sendToDevice(PIIODevice * dev, const QByteArray & data) {
	if (!dev) return;
	//piCout << "SEND to" << dev;
	if (dev_send_stream.contains(dev)) {
		auto & ds(dev_send_stream[dev]);
		if (ds.send_queue)
			ds.send_queue->enqueue(Q2PIByteArray(data));
	} else
		conn_->write(dev, Q2PIByteArray(data));
}


void SHSScriptConnection::sendToAll(const QByteArray & data) {
	logIO("snd", nullptr, "broadcast", data);
	auto dl = conn_->boundedDevices();
	for (auto * d: dl)
		sendToDevice(d, data);
}


void SHSScriptConnection::prepareConnection() {
	stop();
	if (!conn_) return;
	PIVector<PIIODevice*> cdl = conn_->boundedDevices();
	PIStringList devs;
	for (PIIODevice * d: cdl) {
		if (!d) continue;
		//piCout << "prepare for" << d;
		if (d->isTypeOf<PISerial>()) {
			PIString pl = d->path(), p = pl;
			if (!pl.startsWith("/") && !pl.startsWith("com")) {
				PIVector<PISerial::DeviceInfo> idevs = PISerial::availableDevicesInfo();
				piForeachC (PISerial::DeviceInfo & di, idevs) {
					if (di.id() == pl) {
						p = di.path;
						break;
					}
				}
			}
			devs << p;
		}

		if (d->infoFlags()[PIIODevice::Sequential]) {
			DeviceReadStream & drs(dev_read_stream[d]);
			drs.last_rec = PISystemTime::current(true);
		}

		auto dnl = conn_->deviceNames(d);
		if (dnl.isEmpty()) continue;
		QString dname = PI2QString(dnl[0]);
		auto dp = RUNTIME->server_project.deviceParameterByName(dname);
		if (!dp.isValid()) continue;
		DeviceSendStream & ds(dev_send_stream[d]);
		ds.init(conn_, d, dp);

	}
	initSerials(devs);
}


void SHSScriptConnection::setInactive(bool yes) {
	rec_mutex.lock();
	inactive = yes;
	rec_mutex.unlock();
}


void SHSScriptConnection::start() {
	auto it = dev_send_stream.makeIterator();
	while (it.next())
		it.value().start();
}


void SHSScriptConnection::stop() {
	auto it = dev_send_stream.makeIterator();
	while (it.next()) {
		it.value().stop();
		it.value().destroy();
	}
	dev_send_stream.clear();
	dev_read_stream.clear();
}


void SHSScriptConnection::connDataReceived(QString dev, QByteArray data) {
	//qDebug() << "recv" << dev << data;
	SHSScriptConnectionInputData d(QList<PIIODevice * >() << conn_->deviceByFullPath(piqt(dev)), data);
	QMetaObject::invokeMethod(this, "qDataReceived", Qt::QueuedConnection, Q_ARG(SHSScriptConnectionInputData, d));
}


void SHSScriptConnection::connPacketReceived(QString filter, QByteArray data) {
	PIVector<PIIODevice*> devs = conn_->filterBoundedDevices(piqt(filter));
	QList<PIIODevice * > dl;
	piForeach (PIIODevice * d, devs)
		dl << d;
	SHSScriptConnectionInputData d(dl, data);
	QMetaObject::invokeMethod(this, "qDataReceived", Qt::QueuedConnection, Q_ARG(SHSScriptConnectionInputData, d));
}




void SHSScriptConnection::DeviceSendStream::init(QPIConnection * conn, PIIODevice * dev, const SHS::DeviceParameters & params) {
	destroy();
	// queue for 5 seconds
	double period = piMaxd(0.1, params.send_time_grid_period_ms);
	send_queue = new PIBlockingQueue<PIByteArray>(piClampi(5. * 1000. / period, 2, 0xFFFF));
	send_timer = new PITimer();
	send_timer->setInterval(period);
	CONNECTL(send_timer, tickEvent, ([this,conn,dev,period](void*,int){
		if (!send_queue) return;
		auto data = send_queue->poll(piMaxi(period / 2, 1));
		if (!data.isEmpty()) {
			conn->write(dev, data);
			//piCout << "ds snd to" << dev->path() << data.size() << "bytes" << data.toHex();
		}
	}));
}


void SHSScriptConnection::DeviceSendStream::destroy() {
	if (send_queue) {
		delete send_queue;
		send_queue = nullptr;
	}
	if (send_timer) {
		send_timer->stop();
		delete send_timer;
		send_timer = nullptr;
	}
}


void SHSScriptConnection::DeviceSendStream::start() {
	if (send_timer) send_timer->start();
}


void SHSScriptConnection::DeviceSendStream::stop() {
	if (send_timer) send_timer->stop();
}
