#include "SH_ui_sync.h"
#include <QApplication>
#include <QDebug>
#include <QMetaProperty>
#include <QWidget>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QBoxLayout>
#include <QGridLayout>
#include <QFormLayout>
#include <piqt.h>
#include "SH_ui_loader.h"
#include "SH_base.h"


SHUISync::SHUISync(): QObject() {
	timer = 0;
	applying = false;
	PIMap<QString, QVector<QByteArray>> property_names;
	QVector<QByteArray> al, def_sl;
	QStringList sl;
	def_sl << "enabled" << "hidden";
	

	/// Properties
	al = def_sl; al << "styleSheet" << "text";
	property_names["QLabel"] = al;
	property_names["IconedLabel"] = al;
	property_names["QLineEdit"] = al;
	property_names["CLineEdit"] = al;
	
	al = def_sl; al << "text" << "iconSize" << "checkable" << "checked";
	property_names["QToolButton"] = al;
	property_names["QPushButton"] = al;
	property_names["QRadioButton"] = al;
	property_names["QCheckBox"] = al;
	al << "state" << "downColor" << "upColor" << "grayColor" << "yesColor" << "noColor";
	property_names["SHSButton"] = al;
	
	al = def_sl; al << "value" << "minimum" << "maximum" << "singleStep";
	property_names["QSpinBox"] = al;
	property_names["QSlider"] = al;
	property_names["QScrollBar"] = al;
	property_names["QDial"] = al;
	property_names["SpinSlider"] = al;
	al << "decimals";
	property_names["QDoubleSpinBox"] = al;
	
	al = def_sl; al << "value" << "minimum" << "maximum" << "precision" << "readOnly" << "showMinMax" << "showDecInc";
	al << "titleInsideBar" << "barStyle" << "titleSize";
	property_names["SHSSlider"] = al;
	
	al = def_sl; al << "currentIndex";
	property_names["QComboBox"] = al;
	//property_names["QTabWidget"] = sl;
	//property_names["QStackedWidget"] = sl;
	//property_names["SHSTabWidget"] = sl;
	
	al = def_sl; al << "readOnly" << "time";
	property_names["SHSClock"] = al;
	
	al = def_sl; al << "readOnly" << "value" << "minimum" << "maximum" << "gradMinimum" << "gradMaximum";
	property_names["SHSThermometer"] = al;
	
	al = def_sl; al << "readOnly" << "binary" << "level" << "fillColor" << "backColor" << "angleShift" << "barSize" << "clickThreshold" << "onLevel";
	property_names["SHSLight"] = al;
	
	al = def_sl; al << "checkable" << "checked" << "readOnly" << "invertedAppearance" << "orientation" << "switchStyle" << "fillColor" << "backColor";
	property_names["SHSSwitch"] = al;
	
	al = def_sl; al << "dateTime";
	property_names["QTimeEdit"] = al;
	property_names["QDateEdit"] = al;
	property_names["QDateTimeEdit"] = al;
	
	al = def_sl; al << "historySize" << "maxVisibleTime";
	property_names["Graphic"] = al;
	property_names["SHSGraphic"] = al;

	al = def_sl; al << "currentButton";
	property_names["TouchButtFrame"] = al;

	al = def_sl; al << "title";
	property_names["QGroupBox"] = al;

	al = def_sl; al << "__hiddenTabs__";
	property_names["SHSTabWidget"] = al;


	/// Signals
	sl.clear(); sl << "textChanged(QString)" << "textEdited(QString)" << "editingFinished()" << "returnPressed()";
	signal_names["QLineEdit"] = sl;
	signal_names["CLineEdit"] = sl;
	
	sl.clear(); sl << "pressed()" << "released()" << "toggled(bool)";
	signal_names["QCheckBox"] = sl;
	signal_names["QRadioButton"] = sl;
	sl << "clicked(bool)";
	signal_names["QToolButton"] = sl;
	signal_names["QPushButton"] = sl;
	signal_names["SHSSwitch"] = sl;
	signal_names["SHSButton"] = sl;
	
	sl.clear(); sl << "valueChanged(int)";
	signal_names["QSpinBox"] = sl;
	signal_names["QSlider"] = sl;
	signal_names["QDial"] = sl;
	
	sl.clear(); sl << "valueChanged(double)";
	signal_names["QDoubleSpinBox"] = sl;
	signal_names["SpinSlider"] = sl;
	signal_names["SHSSlider"] = sl;
	
	sl.clear(); sl << "timeChanged(QTime)";
	signal_names["SHSClock"] = sl;

	sl.clear(); sl << "valueChanged(double)";
	signal_names["SHSThermometer"] = sl;
	
	sl.clear(); sl << "levelChanged(double)" << "clicked()";
	signal_names["SHSLight"] = sl;
	
	sl.clear(); sl << "dateTimeChanged(QDateTime)";
	signal_names["QTimeEdit"] = sl;
	signal_names["QDateEdit"] = sl;
	signal_names["QDateTimeEdit"] = sl;

	sl.clear(); sl << "clicked(int)";// << "toggled(int,bool)" << "clickedID(int,int)" << "toggledID(int,int,bool)";
	signal_names["TouchButtFrame"] = sl;


	/// Signal -> slot map
	signal_map["textChanged"] = "setText";
	signal_map["textEdited"] = "setText";
	signal_map["toggled"] = "setChecked";
	signal_map["valueChanged"] = "setValue";
	signal_map["timeChanged"] = "setTime";
	signal_map["levelChanged"] = "setLevel";
	//signal_map["byteLevelChanged"] = "setByteLevel";
	signal_map["timeChanged"] = "setTime";
	signal_map["dateChanged"] = "setDate";
	signal_map["dateTimeChanged"] = "setDateTime";
	

	/// slider classes, signals decimation
	slider_classes << "QSlider" << "QScrollBar" << "QDial" << "QSpinBox" << "QDoubleSpinBox"
				   << "SpinSlider" << "SHSSlider" << "SHSLight" << "SHSThermometer" << "SHSClock";


	logic.setSyncProperties(property_names);
	
}


QJsonObject makeJSON(QLayout * l) {
	QJsonObject ret;
	if (qobject_cast<QBoxLayout*>(l)) {
		QBoxLayout * bl = qobject_cast<QBoxLayout*>(l);
		if (bl->direction() == QBoxLayout::LeftToRight ||
			bl->direction() == QBoxLayout::RightToLeft)
			ret["type"] = "row";
		else
			ret["type"] = "column";
	} else if (qobject_cast<QGridLayout*>(l)) {
		ret["type"] = "grid";
	} else if (qobject_cast<QFormLayout*>(l)) {
		ret["type"] = "form";
	}
	ret["spacing"] = l->spacing();
	QJsonObject mo;
	mo["left"  ] = l->contentsMargins().left  ();
	mo["right" ] = l->contentsMargins().right ();
	mo["top"   ] = l->contentsMargins().top   ();
	mo["bottom"] = l->contentsMargins().bottom();
	ret["margins"] = mo;
	return ret;
}
QJsonObject makeJSON(QWidget * w, const QMap<QWidget*, QList<QWidget*> > & tree) {
	QJsonObject ret;
	if (!w) return ret;
	const QMetaObject * mo = w->metaObject();
	ret["class"] = mo->className();
	ret["name" ] = w->objectName();
	for (int i = QWidget::staticMetaObject.propertyCount(); i < mo->propertyCount(); ++i) {
		QMetaProperty p = mo->property(i);
		if (!p.isDesignable()) continue;
		if (w->property(p.name()).toString().isEmpty()) continue;
		ret[p.name()] = QJsonValue::fromVariant(w->property(p.name()));
	}
	if (w->layout()) {
		ret["layout"] = makeJSON(w->layout());
	}
	QJsonArray cl;
	QList<QWidget*> wcl = tree.value(w);
	if (!wcl.isEmpty()) {
		foreach (QWidget * c, wcl) {
			cl << makeJSON(c, tree);
		}
		ret["children"] = cl;
	}
	return ret;
}


QByteArray SHUISync::widget2JSON(SHSUiLoader * loader) {
	if (!loader) return QByteArray();
	if (!loader->loaded_tree.contains(0)) return QByteArray();
	if (loader->loaded_tree[0].isEmpty()) return QByteArray();
	QJsonDocument jd(makeJSON(loader->loaded_tree[0][0], loader->loaded_tree));
	return jd.toJson(QJsonDocument::Compact);
}


void SHUISync::assign(SHSUiLoader * loader, bool need_json) {
	if (!loader)
		reset();
	else {
		setObjectList(loader->loaded_widgets);
		json_ui.clear();
		if (need_json)
			json_ui = widget2JSON(loader);
		//piCout << Q2PIString(QString::fromUtf8(jd.toJson(QJsonDocument::Indented)));
	}
}


void SHUISync::setObjectList(const QObjectList & ol) {
	if (ol.isEmpty()) {
		reset();
		return;
	}
	child_list = ol;
	for (int i = 0; i < child_list.size(); ++i) {
		QObject * o = child_list[i];
		if (o->objectName().isEmpty() || o->objectName().startsWith("qt_")) {
			child_list.removeAt(i);
			--i;
		} else {
			UiSync::writeObjectHash(o);
			object_by_hash[UiSync::getObjectHash(o)] = o;
		}
	}
	src_props = cur_props = logic.makeObjectListProperties(child_list);
	//qDebug() << "src props =" << src_props;
	connectObjectList();
	foreach (QObject * w, child_list)  {
		QString cname = w->metaObject()->className();
		if (!signal_by_hash.contains(cname)) {
			QMap<uint, QString> hash;
			QStringList pl = signal_names.value(cname);
			foreach (const QString & s, pl)
				hash[UiSync::stringHash(s)] = s;
			signal_by_hash[cname] = hash;
			//qDebug() << cname << hash;
		}
	}
	if (timer == 0)
		timer = startTimer(100);
}


void SHUISync::reset() {
	applying = false;
	child_list.clear();
	signal_by_hash.clear();
	src_props.clear();
	cur_props.clear();
	object_by_hash.clear();
	resetRoles();
}


void SHUISync::resetRoles() {
	properties_by_role.clear();
}


void SHUISync::restoreSourceState() {
	applyObjectListProperties(src_props);
}


void SHUISync::saveForRole(int index) {
	if (index < 0) return;
	//qDebug() << "SHUISync::saveForRole" << index;
	if (properties_by_role.size() <= index)
		properties_by_role.resize(index + 1);
	auto & p(properties_by_role[index]);
	p.src_props = logic.makeObjectListProperties(child_list);
	p.override_props = logic.objectListPropertiesDiff(src_props, p.src_props);
	//piCout << "override_props for" << index << p.override_props;
}


void SHUISync::buildDiffForRoles() {
	cur_props = logic.makeObjectListProperties(child_list);
	UiSync::ObjectPropertiesMap cur;
	for (auto & p: properties_by_role) {
		cur = cur_props;
		logic.overrideObjectListProperties(cur, p.override_props);
		p.diff = logic.objectListPropertiesDiff(p.cur_props, cur);
		p.cur_props = cur;
		p.serialized_diff = Q2PIByteArray(UiSync::serializeDiff(p.diff));
	}
}


PIByteArray SHUISync::diffForRole(int index) const {
	if (index < 0 || properties_by_role.size() <= index) return {};
	return properties_by_role[index].serialized_diff;
}


QByteArray SHUISync::buildFullDiffForRole(int index) {
	if (index < 0 || properties_by_role.size() <= index) return {};
	auto & p(properties_by_role[index]);
	auto diff = logic.objectListPropertiesDiff(p.src_props, cur_props);
	logic.overrideObjectListProperties(diff, p.override_props);
	return UiSync::serializeDiff(diff);
}


QByteArray SHUISync::buildTransitionDiff(int index_from, int index_to) {
	if (index_from < 0 || properties_by_role.size() <= index_from) return {};
	if (index_to < 0 || properties_by_role.size() <= index_to) return {};
	const auto & from(properties_by_role[index_from].src_props);
	const auto & to(properties_by_role[index_to].src_props);
	return UiSync::serializeDiff(logic.objectListPropertiesDiff(from, to));
}


void SHUISync::timerEvent(QTimerEvent * ) {
	//qDebug() << signals_queue.size();
	if (signals_queue.isEmpty()) return;
	QHash<QWidget * , QHash<QString, QVariant> >::const_iterator i;
	for (i = signals_queue.constBegin(); i != signals_queue.constEnd(); ++i) {
		QHash<QString, QVariant>::const_iterator j;
		for (j = i.value().constBegin(); j != i.value().constEnd(); ++j) {
			sendSignal(i.key(), j.key(), j.value());
		}
	}
	signals_queue.clear();
}


QByteArray SHUISync::buildDiff(bool from_src) {
	if (child_list.isEmpty()) return QByteArray();
	UiSync::ObjectPropertiesMap diff;
	if (from_src)
		diff = logic.objectListPropertiesDiff(src_props, cur_props);
	else {
		UiSync::ObjectPropertiesMap new_props = logic.makeObjectListProperties(child_list);
		diff = logic.objectListPropertiesDiff(cur_props, new_props);
		cur_props = new_props;
	}
	//qDebug() << "diff =" << diff;
	return UiSync::serializeDiff(diff);
}


QByteArray SHUISync::convertFromVersion3(QByteArray data) {
	if (data.size() < 4) return {};
	UiSync::ObjectPropertiesMap ret;

	typedef QPair<uint, QVariant> IndexedPropertyV3;

	auto in = qDeserialize<QMap<uint, QList<IndexedPropertyV3>>>(data, QDataStream::Qt_5_13);
	QMapIterator<uint, QList<IndexedPropertyV3>> it(in);
	while (it.hasNext()) {
		it.next();

		auto & map(ret[it.key()]);
		for (const auto & i: it.value())
			map[i.first] = i.second;

	}

	return UiSync::serializeDiff(ret);
}


void SHUISync::applyDiff(QByteArray data) {
	if (child_list.isEmpty() || data.isEmpty()) return;
	applyObjectListProperties(UiSync::deserializeDiff(data));
}


void SHUISync::applyObjectProperties(QObject * o, const UiSync::ObjectProperties & income) {
	if (!o) return;
	//qDebug() << "applyObjectProperties" << o << income.size();
	QString cname = o->metaObject()->className();
	const QMap<uint, QString> & properties_by_bash(logic.propertiesByClassname(cname));
	if (!properties_by_bash.isEmpty()) {
		auto it = income.makeIterator();
		while (it.next()) {
			if (!it.value().isValid()) continue;
			QString pname = properties_by_bash.value(it.key());
			if (pname.isEmpty()) continue;
			if (slider_classes.contains(cname)) {
				QWidget * w = qobject_cast<QWidget*>(o);
				/// ignore current tracking slider
				if (w) {
					//qDebug() << w << pname << w->underMouse() << qApp->mouseButtons();
					if (w->underMouse() && (qApp->mouseButtons() != 0))
						if (pname.contains("value", Qt::CaseInsensitive) || pname.contains("level", Qt::CaseInsensitive))
							continue;
				}
			}
			o->setProperty(pname.toLatin1(), it.value());
		}
	}
	//qDebug() << "applyObjectProperties done";
}


void SHUISync::applyObjectListProperties(const UiSync::ObjectPropertiesMap & income) {
	applying = true;
	auto it = income.makeIterator();
	while (it.next()) {
		QObject * o = object_by_hash.value(it.key());
		applyObjectProperties(o, it.value());
	}
	applying = false;
}


void SHUISync::parseSignature(QString sig, QString * fname, QString * arg) {
	fname->clear();
	arg->clear();
	int i = sig.indexOf("(");
	if (i < 0) return;
	*fname = sig.left(i);
	*arg = sig.mid(i + 1, sig.size() - i - 2);
}


void SHUISync::connectObjectList() {
	for (QObject * w: child_list) {
		QStringList sl = signal_names.value(w->metaObject()->className());
		//qDebug() << "obj" << w->objectName();
		QString fn, an, fan;
		for (const QString & s: sl) {
			//qDebug() << "try" << s;
			parseSignature(s, &fn, &an);
			fan = an.isEmpty() ? "void" : an;
			fan[0] = fan[0].toUpper();
			//qDebug() << "connect" << QString("2%1").arg(s) << "to" << QString("1signal%1(%2)").arg(fan, an);
			connect(w, QString("2%1").arg(s).toLatin1(), this, QString("1signal%1(%2)").arg(fan, an).toLatin1(), Qt::DirectConnection);
		}
	}
}


void SHUISync::receiveObjectEvent(QByteArray data) {
	if (child_list.isEmpty()) return;
	if (data.isEmpty()) return;
	//qDebug() << "receiveObjectEvent" << data.size();
	int oi = -1;
	uint signal_hash = 0;
	QVariant v;
	QDataStream s(data);
	s.setVersion(QDataStream::Qt_4_8);
	s >> oi >> signal_hash >> v;
	if (oi < 0 || oi >= child_list.size() || signal_hash == 0) return;
	QObject * w = child_list[oi];
	QString signame = signal_by_hash[w->metaObject()->className()].value(signal_hash);
	signame = signame.left(signame.indexOf("("));
	signame = signal_map.value(signame, signame);
	//qDebug() << "invoke" << w->objectName() << signame << v;
	if (!v.isValid())
		QMetaObject::invokeMethod(w, signame.toLatin1());
	else {
		switch (v.type()) {
		case QVariant::Bool:     QMetaObject::invokeMethod(w, signame.toLatin1(), Q_ARG(bool, v.toBool())); break;
		case QVariant::Int:      QMetaObject::invokeMethod(w, signame.toLatin1(), Q_ARG(int, v.toInt())); break;
		case QVariant::Double:   QMetaObject::invokeMethod(w, signame.toLatin1(), Q_ARG(double, v.toDouble())); break;
		case QVariant::String:   QMetaObject::invokeMethod(w, signame.toLatin1(), Q_ARG(QString, v.toString())); break;
		case QVariant::Time:     QMetaObject::invokeMethod(w, signame.toLatin1(), Q_ARG(QTime, v.toTime())); break;
		case QVariant::Date:     QMetaObject::invokeMethod(w, signame.toLatin1(), Q_ARG(QDate, v.toDate())); break;
		case QVariant::DateTime: QMetaObject::invokeMethod(w, signame.toLatin1(), Q_ARG(QDateTime, v.toDateTime())); break;
		//case : arg = Q_ARG(, v.to()); break;
		default: return;
		}
	}
}


void SHUISync::signalReveived(QVariant v) {
	if (child_list.isEmpty()) return;
	if (applying) return;
	QObject * src = sender();
	if (!src) return;
	int si = senderSignalIndex();
	if (si < 0) return;
	QString signal_name = src->metaObject()->method(si).
#if (QT_VERSION >= 0x050000)
		methodSignature();
#else
		signature();
#endif
	//sname = sname.left(sname.indexOf("("));
	//qDebug() << "signal" << sname << stringHash(sname) << "from" << src->objectName() << "value" << v;
	if (slider_classes.contains(src->metaObject()->className()) && signal_name.contains("changed", Qt::CaseInsensitive))
		signals_queue[(QWidget*)src][signal_name] = v;
	else
		sendSignal((QWidget*)src, signal_name, v);
}


void SHUISync::sendSignal(QWidget * w, const QString & signal_name, const QVariant & v) {
	QByteArray ba;
	QDataStream s(&ba, QIODevice::ReadWrite);
	s.setVersion(QDataStream::Qt_4_8);
	s << (int)child_list.indexOf((QWidget*)w) << UiSync::stringHash(signal_name) << v;
	emit objectEvent(ba);
}
