#include "client.h"
#include "SH_types.h"
#include "SH_network_types.h"
#include "SH_ui_loader.h"
#include <QDir>
#include <QDebug>
#include <QTimer>
#include <QTimerEvent>
#include <QSettings>
#include <QCryptographicHash>
#include <QStandardPaths>
#include <QApplication>
#include <QTranslator>
#include <QDirIterator>
#include <QInputDialog>
#include <qad_locations.h>
#include <piqt.h>
#include <pisysteminfo.h>
#include <picrypt.h>
#include <pistreampacker.h>
#include <pifile.h>
#include "shs_graphic.h"
#include "ccm_SHS_shared.h"

const QString sTypeUI  = "ui";
const QString sTypeRCC = "rcc";


RemoteClient::RemoteClient(PultWindowBase * v): QObject(), PIObject(), channel(true), channel_gr(true), settings("SHS", "Pult") {
	PACKAGES;
	view = v;
	form = 0;
	kres = 0;
	force_download = sleeping = false;
	view->setChannel(&channel);
	channel.setReopenEnabled(true);
	channel.setID(generateID());
	channel_gr.setID(generateID());
	QByteArray qba = settings.value("keys").toByteArray();
	if (qba.size() >= 4) {
		QDataStream ds(&qba, QIODevice::ReadOnly);
		ds.setVersion(QDataStream::Qt_5_13);
		ds >> keys_;
	}
	QString dp;
#ifdef ANDROID
	dp = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation)[0];
#else
	dp = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
#endif
	if (dp.isEmpty()) dp = QApplication::applicationDirPath();
	else dp += "/SHS";
	dp += "/packages/Pult/";
	QDir().mkpath(dp);
	package_dir = Q2PIString(dp);
	QObject::connect(&sync, SIGNAL(objectEvent(QByteArray)), this, SLOT(objectEvent(QByteArray)));
	CONNECTU_QUEUED(&channel, disconnected, this, disconnectedP, this);
	CONNECTU_QUEUED(&channel, connected, this, connectedP, this);
	CONNECTU_QUEUED(&channel, dataReceived, this, receiveEvent, this);
	CONNECTU_QUEUED(&channel_gr, disconnected, this, grDisconnectedP, this);
	CONNECTU_QUEUED(&channel_gr, connected, this, grConnectedP, this);
	CONNECTU_QUEUED(&channel_gr, dataReceived, this, grReceiveEvent, this);
	QObject::connect(view, SIGNAL(connectRequest(QString)), this, SLOT(connectRequest(QString)));
	QObject::connect(view, SIGNAL(disconnectRequest()), this, SLOT(disconnectRequest()));
	QObject::connect(view, SIGNAL(formLoaded(QObject*,SHSUiLoader*)), this, SLOT(setForm(QObject*,SHSUiLoader*)));
	QObject::connect(view, SIGNAL(keyProbe(QString)), this, SLOT(keyProbe(QString)));
	QObject::connect(view, SIGNAL(exited()), this, SLOT(exited()));
	QObject::connect(view, SIGNAL(changeRoleRequest(int)), this, SLOT(changeRoleRequest(int)));
	QObject::connect(QApplication::instance(), SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(appStateChanged(Qt::ApplicationState)));
	ui_content = restoreData(sTypeUI);
	rcc_content = restoreData(sTypeRCC);
	timer_diff = startTimer(50);
}


RemoteClient::~RemoteClient() {
	//qDebug() << "~RemoteClient 0";
	//qDebug() << "~RemoteClient 1";
	if (timer_diff)
		killTimer(timer_diff);
	stop();
	//qDebug() << "~RemoteClient 2";
	//qDebug() << "~RemoteClient 3";
	//piDisconnect(this);
	//piDisconnect(&eth_tcp);
	//qDebug() << "~RemoteClient 4";
	//qDebug() << "~RemoteClient 5";
}


void RemoteClient::setForm(QObject * f, SHSUiLoader * loader) {
	form = f;
	sync.assign(loader);
	if (form)
		channel_gr.connectAs(channel);
	//sendGraphicsRequest();
}


void RemoteClient::keyProbe(QString key) {
	last_key = key;
	//qDebug() << "probe key" << key;
	PIByteArray ba = SHNetworkTypes::makeHeader(SHNetworkTypes::KeyProbe);
	ba << Q2PIString(last_key);
	channel.dataSend(ba);
}


void RemoteClient::connectRequest(QString addr) {
	channel.stopRead();
//	piCoutObj << "connectRequest" << ch;
	connect(addr);
}


void RemoteClient::disconnectRequest() {
	//qDebug() << "RemoteClient" << "disconnectRequest";
	disconnectedP();
}


void RemoteClient::exited() {
	if (timer_diff)
		killTimer(timer_diff);
	timer_diff = 0;
	stop();
}


void RemoteClient::reconnect() {
	stop();
	sync.reset();
	view->disconnected();
	server_info = PacketServerInfo();
	server_name.clear();
	force_download = false;
	connect(last_addr);
}


void RemoteClient::connect(const QString & addr) {
	if (addr.isEmpty()) return;
	stop();
	last_addr = addr;
	//qDebug() << "connect to" << ip.dataAscii() << "... 1";
	//qDebug() << "RemoteClient" << "connect to" << addr << channel.isWaiting();
	channel.connectToServer(addr);
	view->connecting(PI2QString(channel.address().ipAddress()));
	//qDebug() << "connect to" << ip.dataAscii() << "... 2";
}


void RemoteClient::stop() {
	channel.stop();
	channel_gr.stop();
}


void RemoteClient::timerEvent(QTimerEvent * e) {
	if (e) {
		if (e->timerId() == timer_diff) {
			maybeCallQueuedEvents();
			return;
		}
	}
}


void RemoteClient::cacheData(const QByteArray & data, const QString & type) {
	QDir::home().mkdir("SHS");
	QFile f(QAD::userPath(QAD::ltCache, QString("form_%1").arg(type)));
	//qDebug() << "home" << QDir::homePath() << f.fileName();
	if (!f.open(QIODevice::ReadWrite)) {
		qDebug() << "Can`t open" << f.fileName() << "for write cache!";
		return;
	}
	f.resize(0);
	f.write(PI2QByteArray(PICrypt::crypt(Q2PIByteArray(data), key())));
}


QByteArray RemoteClient::restoreData(const QString & type) {
	QFile f(QAD::userPath(QAD::ltCache, QString("form_%1").arg(type)));
	if (!f.open(QIODevice::ReadOnly)) {
		qDebug() << "Can`t open" << f.fileName() << "for read cache!";
		return QByteArray();
	}
	return PI2QByteArray(PICrypt::decrypt(Q2PIByteArray(f.readAll()), key()));
}


QByteArray RemoteClient::stringHash(const QString & s) {
	return QCryptographicHash::hash(s.toUtf8(), QCryptographicHash::Sha1);
}


void RemoteClient::setStatus(QString s) {
	view->textStatus(s);
	emit statusChanged(s);
}


void RemoteClient::packageNameReceived(const PIString & pname) {
	piCoutObj << "packageNameReceived" << pname;
	if (pname.isEmpty()) {
		setStatus(tr("No package on server"));
		view->packageNoSuchOnServer(force_download);
		return;
	}
	if (PIFile::isExists(package_dir + pname)) {
		if (force_download) {
			PIFile::remove(package_dir + pname);
		} else {
			setStatus(tr("New package already downloaded"));
			//view->packageExists(PI2QString(package_dir + pname));
			emit packageReady(PI2QString(package_dir + pname), false);
			return;
		}
	}
	if (!force_download) {
		if (!view->askForDownloadNewPackage())
			return;
	}
	piCoutObj << "request package";
	PIByteArray ba = SHNetworkTypes::makeHeader(SHNetworkTypes::RemotePultPackageContentRequest);
	channel.dataSend(ba);
	view->packageDownloadStarted();
}


void RemoteClient::packageReceived(const SHPackages::Package & p) {
	piCoutObj << "packageReceived" << p.data.size_s();
	qDebug() << "packageReceived" << p.data.size_s();
	view->packageDownloadFinished();
	PIFile f(package_dir + p.name, PIIODevice::ReadWrite);
	f.clear();
	f.write(p.data);
	emit packageReady(PI2QString(package_dir + p.name), force_download);
}


void RemoteClient::keyProbe() {
	if (!server_info.isValid()) return;
	PICout(PICoutManipulators::Default) << "***" << server_info.identify.shs_version;
	if (server_info.identify.shs_version.isEmpty()) {
		setStatus(tr("Server error"));
		view->serverError(PI2QString("Too old server (\"" + server_info.identify.shs_version + "\")!"));
		return;
	}
	if (versionCompare(server_info.identify.shs_version, SHS_VERSION_NAME, 2) < 0) {
		force_download = false;
		return;
	}
	last_key = keys_.value(stringHash(server_name));
	PIByteArray ba = SHNetworkTypes::makeHeader(SHNetworkTypes::KeyProbe);
	ba << Q2PIString(last_key);
	channel.dataSend(ba);
}


void RemoteClient::sendGraphicsRequest() {
	if (!view || !form || !channel_gr.isConnected()) return;
	SHSUiLoader * loader = view->uiLoader();
	if (loader->loaded_widgets.isEmpty()) return;
	piCout << "---" << loader->loaded_widgets.size() << loader->graphic_variables.size();
	foreach (SHSGraphic * g, loader->graphics)
		g->reset();
	PIByteArray ba = SHNetworkTypes::makeHeader(SHNetworkTypes::GraphicRequest);
	ba << Q2PIStringList(loader->graphic_variables);
	channel_gr.dataSend(ba);
}


void RemoteClient::connectedP() {
	force_download = false;
	channel.stopRead();
	//qDebug() << "connected to" << PI2QString(server_info.connect_address);
	PIByteArray ba;
	PacketIdentification p = PacketIdentification::makeLocal(SHServer::RoleGui, SHS_NETWORK_VERSION, SHS_UISYNC_VERSION);
	p.hashes.hash_ui  = piHashData((const uchar*)ui_content.data (), ui_content.size ());
	p.hashes.hash_rcc = piHashData((const uchar*)rcc_content.data(), rcc_content.size());
	ba = SHNetworkTypes::makeHeader(SHNetworkTypes::Identification);
	ba << p;
	channel.dataSend(ba);

	ba = SHNetworkTypes::makeHeader(SHNetworkTypes::ServerInfoRequest);
	channel.dataSend(ba);

//	keyProbe();
//	mutex_info.unlock();
}


void RemoteClient::disconnectedP() {
	//qDebug() << "disconnected";
	stop();
	sync.reset();
	view->disconnected();
	//ui_content.clear();
	//rcc_content.clear();
	server_info = PacketServerInfo();
	server_name.clear();
	force_download = false;
}


void RemoteClient::receiveEvent(PIByteArray data) {
	//piCout << "packetReceived" << data.size();
	//qDebug() << "packetReceived" << data.size() << QApplication::applicationState();
	PacketHeader hdr = SHNetworkTypes::takeHeader(data);
	if (hdr.type < 0) return;
	PIByteArray ba;
	PIString str;
	qDebug() << "packet type" << hdr.type;
	switch ((SHNetworkTypes::PacketType)hdr.type) {
	case SHNetworkTypes::KeyProbeResult:
		data >> kres;
		view->keyProbed(kres > 0);
		if (kres > 0) {
			keys_[stringHash(server_name)] = last_key;
			QByteArray qba; QDataStream ds(&qba, QIODevice::ReadWrite);
			ds.setVersion(QDataStream::Qt_5_13); ds << keys_;
			settings.setValue("keys", qba);
			qDebug() << "save key" << last_key;
			view->fetching();
		} else {
			view->keyRequest(server_info);
		}
		break;
	case SHNetworkTypes::GuiRestoreRCC:
		setStatus(tr("Restore RCC ..."));
		view->receivedRCC(rcc_content);
		setStatus(tr("Restore RCC ok, wait"));
		qDebug() << "restore rcc" << rcc_content.size();
		break;
	case SHNetworkTypes::GuiRestoreUI:
		setStatus(tr("Restore UI ..."));
		view->receivedUI(ui_content);
		//setStatus(tr("Restore UI ok, wait"));
		qDebug() << "restore ui" << ui_content.size();
		break;
	case SHNetworkTypes::GuiSyncRCC:
		setStatus(tr("Get RCC ..."));
		rcc_content = PI2QByteArray(data);
		cacheData(rcc_content, sTypeRCC);
		view->receivedRCC(rcc_content);
		setStatus(tr("Get RCC ok, wait"));
		qDebug() << "recv rcc" << rcc_content.size();
		break;
	case SHNetworkTypes::GuiSyncUI:
		setStatus(tr("Get UI ..."));
		ui_content = qUncompress(data.data(), data.size());
		cacheData(ui_content, sTypeUI);
		view->receivedUI(ui_content);
		//setStatus(tr("Get UI ok, wait"));
		qDebug() << "recv ui" << ui_content.size();
		break;
	case SHNetworkTypes::GuiSyncDiff:
		//setStatus(tr("Apply initial diff ..."));
		if (!data.isEmpty())
			sync.applyDiff(PI2QByteArray(data));
		//setStatus(tr("Apply initial diff ok, wait"));
		break;
	case SHNetworkTypes::ConnectingDone:
		emit serverConnected(server_info);
		ba = SHNetworkTypes::makeHeader(SHNetworkTypes::ClientReady);
		channel.dataSend(ba);
		break;
	case SHNetworkTypes::GuiSyncVersionMismatch: {
		int server_version;
		data >> server_version;
		view->syncVersionFailed(server_version);
		} break;
	case SHNetworkTypes::RemotePultPackageName:
		data >> str;
		packageNameReceived(str);
		break;
	case SHNetworkTypes::RemotePultPackageContent: {
		SHPackages::Package p;
		data >> p;
		packageReceived(p);
		} break;
	case SHNetworkTypes::ServerInfo: {
		if (!server_info.isValid()) {
			data >> server_info;
			server_name = PI2QString(server_info.name);
			server_info.address = channel.address();
			server_info.connect_address = channel.address().fullAddress();
			server_info.channel = channel.address().channel;
			view->connected(server_info);
			keyProbe();
		}
		} break;
	case SHNetworkTypes::ChangeUserRoleResult: {
		int index = 0, res = 0;
		data >> index >> res;
		if (res == 0) view->changeRoleRequestDone(index);
		else view->changeRoleRequestFailed();
		} break;
	default: break;
	}
}


void RemoteClient::grConnectedP() {
	qDebug() << "graphics connected to" << PI2QString(server_info.name);

	PIByteArray ba = SHNetworkTypes::makeHeader(SHNetworkTypes::Identification);
	PacketIdentification p = PacketIdentification::makeLocal(SHServer::RoleGraphics, SHS_NETWORK_VERSION, SHS_UISYNC_VERSION);
	ba << p;
	channel_gr.dataSend(ba);
	ba = SHNetworkTypes::makeHeader(SHNetworkTypes::ClientReady);
	channel_gr.dataSend(ba);

	//view->connectedGraphics();
	sendGraphicsRequest();
}


void RemoteClient::grDisconnectedP() {
}


void RemoteClient::grReceiveEvent(PIByteArray data) {
	PacketHeader hdr = SHNetworkTypes::takeHeader(data);
	if (hdr.type < 0) return;
	SHS::PacketGraphicData gd;
	QList<QPair<SHSGraphic*, int>> gvm;
	PIString vname;
	//qDebug() << "packet type" << type;
	switch ((SHNetworkTypes::PacketType)hdr.type) {
	case SHNetworkTypes::GraphicData:
		data >> gd;
		gvm = view->uiLoader()->variables_map.value(PI2QString(gd.name));
		foreach (auto gl, gvm) {
			SHSGraphic * g = gl.first;
			if (g) {
				QPolygonF pol; pol.reserve(gd.data.size_s());
				PIPair<ullong, double> tp;
				for (int i = 0; i < gd.data.size_s(); ++i) {
					tp = gd.data[i];
					pol << QPointF(tp.first, tp.second);
				}
				g->addPoints(pol, gl.second, true);
				//if (g->isAllDone())
				//g->updateGraphics();
			}
		}
		break;
	case SHNetworkTypes::GraphicDone:
		data >> vname;
		gvm = view->uiLoader()->variables_map.value(PI2QString(vname));
		foreach (auto gl, gvm) {
			if (gl.first)
				gl.first->graphicDone(PI2QString(vname));
		}
		break;
	default: break;
	}
}


void RemoteClient::objectEvent(QByteArray data) {
	PIByteArray ed = piqt(data), hdr = SHNetworkTypes::makeHeader(SHNetworkTypes::GuiSyncEvent);
	ed.insert(0, hdr);
	channel.dataSend(ed);
}


void RemoteClient::appStateChanged(Qt::ApplicationState state) {
#ifdef ANDROID
	if (state == Qt::ApplicationActive && sleeping) {
		sleeping = false;
		//qDebug() << "RemoteClient" << "reconnect to" << ip_.dataAscii();
		QTimer::singleShot(100, this, SLOT(reconnect()));
		//view->disconnected();
	}
	if (state == Qt::ApplicationSuspended && channel.isConnected()) {
		sleeping = true;
		stop();
	}
	qDebug() << "RemoteClient" << "appStateChanged" << state;
#endif
}


void RemoteClient::changeRoleRequest(int index) {
	ServerUserRole role;
	role.index = index;
	bool ok = false;
	QString password = QInputDialog::getText(nullptr, tr("SHS Pult"), tr("Input password:"), QLineEdit::Password, "", &ok);
	if (!ok) return;
	role.password_hash = QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha256);
	PIByteArray ba = SHNetworkTypes::makeHeader(SHNetworkTypes::ChangeUserRole);
	ba << role;
	channel.dataSend(ba);
}


const PIByteArray & RemoteClient::key() {
	static PIByteArray ret = PICrypt::hash("SHS_cache");
	return ret;
}
