// Local-Hyperion includes
#include "LedDeviceWled.h"

#include <utils/QStringUtils.h>
#include <utils/WaitTime.h>
#include <QThread>

#include <chrono>

// Constants
namespace {

const bool verbose = false;

// Configuration settings
const char CONFIG_ADDRESS[] = "host";
const char CONFIG_RESTORE_STATE[] = "restoreOriginalState";
const char CONFIG_BRIGHTNESS[] = "brightness";
const char CONFIG_BRIGHTNESS_OVERWRITE[] = "overwriteBrightness";
const char CONFIG_SYNC_OVERWRITE[] = "overwriteSync";

// UDP elements
const quint16 STREAM_DEFAULT_PORT = 19446;
const int UDP_MAX_LED_NUM = 490;

// WLED JSON-API elements
const int API_DEFAULT_PORT = -1; //Use default port per communication scheme

const char API_BASE_PATH[] = "/json/";
//const char API_PATH_INFO[] = "info";
const char API_PATH_STATE[] = "state";

// List of State Information
const char STATE_ON[] = "on";
const char STATE_VALUE_TRUE[] = "true";
const char STATE_VALUE_FALSE[] = "false";
const char STATE_LIVE[] = "live";

const bool DEFAULT_IS_RESTORE_STATE = false;
const bool DEFAULT_IS_BRIGHTNESS_OVERWRITE = true;
const int BRI_MAX = 255;
const bool DEFAULT_IS_SYNC_OVERWRITE = true;

constexpr std::chrono::milliseconds DEFAULT_IDENTIFY_TIME{ 2000 };

} //End of constants

LedDeviceWled::LedDeviceWled(const QJsonObject &deviceConfig)
	: ProviderUdp(deviceConfig)
	  ,_restApi(nullptr)
	  ,_apiPort(API_DEFAULT_PORT)
	  ,_isBrightnessOverwrite(DEFAULT_IS_BRIGHTNESS_OVERWRITE)
	  ,_brightness (BRI_MAX)
	  ,_isSyncOverwrite(DEFAULT_IS_SYNC_OVERWRITE)
	  ,_originalStateUdpnSend(false)
	  ,_originalStateUdpnRecv(true)
{
}

LedDeviceWled::~LedDeviceWled()
{
	delete _restApi;
	_restApi = nullptr;
}

LedDevice* LedDeviceWled::construct(const QJsonObject &deviceConfig)
{
	return new LedDeviceWled(deviceConfig);
}

bool LedDeviceWled::init(const QJsonObject &deviceConfig)
{
	bool isInitOK = false;

	// Initialise LedDevice sub-class, ProviderUdp::init will be executed later, if connectivity is defined
	if ( LedDevice::init(deviceConfig) )
	{
		// Initialise LedDevice configuration and execution environment
		int configuredLedCount = this->getLedCount();
		Debug(_log, "DeviceType   : %s", QSTRING_CSTR( this->getActiveDeviceType() ));
		Debug(_log, "LedCount     : %d", configuredLedCount);
		Debug(_log, "ColorOrder   : %s", QSTRING_CSTR( this->getColorOrder() ));
		Debug(_log, "LatchTime    : %d", this->getLatchTime());

		if (configuredLedCount > UDP_MAX_LED_NUM)
		{
			QString errorReason = QString("Device type %1 can only be run with maximum %2 LEDs!").arg(this->getActiveDeviceType()).arg(UDP_MAX_LED_NUM);
			this->setInError ( errorReason );
			return false;
		}

		_isRestoreOrigState = _devConfig[CONFIG_RESTORE_STATE].toBool(DEFAULT_IS_RESTORE_STATE);
		_isSyncOverwrite = _devConfig[CONFIG_SYNC_OVERWRITE].toBool(DEFAULT_IS_SYNC_OVERWRITE);
		_isBrightnessOverwrite = _devConfig[CONFIG_BRIGHTNESS_OVERWRITE].toBool(DEFAULT_IS_BRIGHTNESS_OVERWRITE);
		_brightness = _devConfig[CONFIG_BRIGHTNESS].toInt(BRI_MAX);

		Debug(_log, "RestoreOrigState  : %d", _isRestoreOrigState);
		Debug(_log, "Overwrite Sync.   : %d", _isSyncOverwrite);
		Debug(_log, "Overwrite Brightn.: %d", _isBrightnessOverwrite);
		Debug(_log, "Set Brightness to : %d", _brightness);

		//Set hostname as per configuration
		QString hostName = deviceConfig[ CONFIG_ADDRESS ].toString();

		//If host not configured the init fails
		if ( hostName.isEmpty() )
		{
			this->setInError("No target hostname nor IP defined");
			return false;
		}
		else
		{
			QStringList addressparts = QStringUtils::split(hostName,":", QStringUtils::SplitBehavior::SkipEmptyParts);
			_hostname = addressparts[0];
			if ( addressparts.size() > 1 )
			{
				_apiPort = addressparts[1].toInt();
			}
			else
			{
				_apiPort = API_DEFAULT_PORT;
			}

			if ( initRestAPI( _hostname, _apiPort ) )
			{
				// Update configuration with hostname without port
				_devConfig["host"] = _hostname;
				_devConfig["port"] = STREAM_DEFAULT_PORT;

				isInitOK = ProviderUdp::init(_devConfig);
				Debug(_log, "Hostname/IP  : %s", QSTRING_CSTR( _hostname ));
				Debug(_log, "Port         : %d", _port);
			}
		}
	}
	return isInitOK;
}

bool LedDeviceWled::initRestAPI(const QString &hostname, int port)
{
	bool isInitOK = false;

	if ( _restApi == nullptr )
	{
		_restApi = new ProviderRestApi(hostname, port);
		_restApi->setBasePath( API_BASE_PATH );

		isInitOK = true;
	}
	return isInitOK;
}

QString LedDeviceWled::getOnOffRequest(bool isOn) const
{
	QString state = isOn ? STATE_VALUE_TRUE : STATE_VALUE_FALSE;
	return QString( "\"%1\":%2,\"%3\":%4" ).arg( STATE_ON, state).arg( STATE_LIVE, state);
}

QString LedDeviceWled::getBrightnessRequest(int bri) const
{
	return QString( "\"bri\":%1" ).arg(bri);
}

QString LedDeviceWled::getEffectRequest(int effect, int speed) const
{
	return QString( "\"seg\":{\"fx\":%1,\"sx\":%2}" ).arg(effect).arg(speed);
}

QString LedDeviceWled::getLorRequest(int lor) const
{
	return QString( "\"lor\":%1" ).arg(lor);
}

QString LedDeviceWled::getUdpnRequest(bool isSendOn, bool isRecvOn) const
{
	QString send = isSendOn ? STATE_VALUE_TRUE : STATE_VALUE_FALSE;
	QString recv = isRecvOn ? STATE_VALUE_TRUE : STATE_VALUE_FALSE;
	return QString( "\"udpn\":{\"send\":%1,\"recv\":%2}" ).arg(send, recv);
}

bool LedDeviceWled::sendStateUpdateRequest(const QString &request)
{
	bool rc = true;

	_restApi->setPath(API_PATH_STATE);

	httpResponse response1 = _restApi->put(QString("{%1}").arg(request));
	if ( response1.error() )
	{
		rc = false;
	}
	return rc;
}
bool LedDeviceWled::powerOn()
{
	bool on = false;
	if ( _isDeviceReady)
	{
		//Power-on WLED device
		_restApi->setPath(API_PATH_STATE);

		QString cmd = getOnOffRequest(true);

		if ( _isBrightnessOverwrite)
		{
			cmd += "," + getBrightnessRequest(_brightness);
		}

		if (_isSyncOverwrite)
		{
			Debug( _log, "Disable synchronisation with other WLED devices");
			cmd += "," + getUdpnRequest(false, false);
		}

		httpResponse response = _restApi->put(QString("{%1}").arg(cmd));
		if ( response.error() )
		{
			QString errorReason = QString("Power-on request failed with error: '%1'").arg(response.getErrorReason());
			this->setInError ( errorReason );
			on = false;
		}
		else
		{
			on = true;
		}
	}
	return on;
}

bool LedDeviceWled::powerOff()
{
	bool off = true;
	if ( _isDeviceReady)
	{
		// Write a final "Black" to have a defined outcome
		writeBlack();

		//Power-off the WLED device physically
		_restApi->setPath(API_PATH_STATE);

		QString cmd = getOnOffRequest(false);

		if (_isSyncOverwrite)
		{
			Debug( _log, "Restore synchronisation with other WLED devices");
			cmd += "," + getUdpnRequest(_originalStateUdpnSend, _originalStateUdpnRecv);
		}

		httpResponse response = _restApi->put(QString("{%1}").arg(cmd));
		if ( response.error() )
		{
			QString errorReason = QString("Power-off request failed with error: '%1'").arg(response.getErrorReason());
			this->setInError ( errorReason );
			off = false;
		}
	}
	return off;
}

bool LedDeviceWled::storeState()
{
	bool rc = true;

	if ( _isRestoreOrigState || _isSyncOverwrite )
	{
		_restApi->setPath(API_PATH_STATE);

		httpResponse response = _restApi->get();
		if ( response.error() )
		{
			QString errorReason = QString("Storing device state failed with error: '%1'").arg(response.getErrorReason());
			setInError(errorReason);
			rc = false;
		}
		else
		{
			_originalStateProperties = response.getBody().object();
			DebugIf(verbose, _log, "state: [%s]", QString(QJsonDocument(_originalStateProperties).toJson(QJsonDocument::Compact)).toUtf8().constData() );

			QJsonObject udpn = _originalStateProperties.value("udpn").toObject();
			if (!udpn.isEmpty())
			{
				_originalStateUdpnSend = udpn["send"].toBool(false);
				_originalStateUdpnRecv = udpn["recv"].toBool(true);
			}
		}
	}

	return rc;
}

bool LedDeviceWled::restoreState()
{
	bool rc = true;

	if ( _isRestoreOrigState )
	{
		_restApi->setPath(API_PATH_STATE);

		_originalStateProperties[STATE_LIVE] = false;

		httpResponse response = _restApi->put(QString(QJsonDocument(_originalStateProperties).toJson(QJsonDocument::Compact)).toUtf8().constData());

		if ( response.error() )
		{
			Warning (_log, "%s restoring state failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason()));
		}
	}

	return rc;
}

QJsonObject LedDeviceWled::discover(const QJsonObject& /*params*/)
{
	QJsonObject devicesDiscovered;
	devicesDiscovered.insert("ledDeviceType", _activeDeviceType );

	QJsonArray deviceList;
	devicesDiscovered.insert("devices", deviceList);
	DebugIf(verbose, _log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData() );

	return devicesDiscovered;
}

QJsonObject LedDeviceWled::getProperties(const QJsonObject& params)
{
	DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData() );
	QJsonObject properties;

	QString hostName = params["host"].toString("");

	if ( !hostName.isEmpty() )
	{
		QString filter = params["filter"].toString("");

		// Resolve hostname and port (or use default API port)
		QStringList addressparts = QStringUtils::split(hostName,":", QStringUtils::SplitBehavior::SkipEmptyParts);
		QString apiHost = addressparts[0];
		int apiPort;

		if ( addressparts.size() > 1)
		{
			apiPort = addressparts[1].toInt();
		}
		else
		{
			apiPort   = API_DEFAULT_PORT;
		}

		initRestAPI(apiHost, apiPort);
		_restApi->setPath(filter);

		httpResponse response = _restApi->get();
		if ( response.error() )
		{
			Warning (_log, "%s get properties failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason()));
		}

		QJsonObject propertiesDetails = response.getBody().object();
		propertiesDetails.insert("maxLedCount", UDP_MAX_LED_NUM);

		properties.insert("properties", propertiesDetails);

		DebugIf(verbose, _log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData() );
	}
	return properties;
}

void LedDeviceWled::identify(const QJsonObject& params)
{
	DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData());

	QString hostName = params["host"].toString("");

	if ( !hostName.isEmpty() )
	{
		// Resolve hostname and port (or use default API port)
		QStringList addressparts = QStringUtils::split(hostName,":", QStringUtils::SplitBehavior::SkipEmptyParts);
		QString apiHost = addressparts[0];
		int apiPort;

		if ( addressparts.size() > 1)
		{
			apiPort = addressparts[1].toInt();
		}
		else
		{
			apiPort   = API_DEFAULT_PORT;
		}

		initRestAPI(apiHost, apiPort);

		_isRestoreOrigState = true;
		storeState();

		QString request = getOnOffRequest(true) + "," + getLorRequest(1) + "," + getEffectRequest(25);
		sendStateUpdateRequest(request);

		wait(DEFAULT_IDENTIFY_TIME);

		restoreState();
	}
}

int LedDeviceWled::write(const std::vector<ColorRgb> &ledValues)
{
	const uint8_t * dataPtr = reinterpret_cast<const uint8_t *>(ledValues.data());

	return writeBytes( _ledRGBCount, dataPtr);
}
