// STL includes
#include <cstring>
#include <cstdio>
#include <iostream>
#include <cerrno>

// Linux includes
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

// Local Hyperion includes
#include "ProviderSpi.h"
#include <utils/Logger.h>

// qt includes
#include <QDir>

// Constants
namespace {
	const bool verbose = false;

	// SPI discovery service
	const char DISCOVERY_DIRECTORY[] = "/dev/";
	const char DISCOVERY_FILEPATTERN[] = "spidev*";

} //End of constants

ProviderSpi::ProviderSpi(const QJsonObject &deviceConfig)
	: LedDevice(deviceConfig)
	, _deviceName("/dev/spidev0.0")
	, _baudRate_Hz(1000000)
	, _fid(-1)
	, _spiMode(SPI_MODE_0)
	, _spiDataInvert(false)
{
	memset(&_spi, 0, sizeof(_spi));
	_latchTime_ms = 1;
}

ProviderSpi::~ProviderSpi()
{
}

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

	// Initialise sub-class
	if ( LedDevice::init(deviceConfig) )
	{
		_deviceName    = deviceConfig["output"].toString(_deviceName);
		_baudRate_Hz   = deviceConfig["rate"].toInt(_baudRate_Hz);
		_spiMode       = deviceConfig["spimode"].toInt(_spiMode);
		_spiDataInvert = deviceConfig["invert"].toBool(_spiDataInvert);

		Debug(_log, "_baudRate_Hz [%d], _latchTime_ms [%d]", _baudRate_Hz, _latchTime_ms);
		Debug(_log, "_spiDataInvert [%d], _spiMode [%d]", _spiDataInvert, _spiMode);

		isInitOK = true;
	}
	return isInitOK;
}

int ProviderSpi::open()
{
	int retval = -1;
	QString errortext;
	_isDeviceReady = false;

	const int bitsPerWord = 8;

	_fid = ::open(QSTRING_CSTR(_deviceName), O_RDWR);

	if (_fid < 0)
	{
		errortext = QString ("Failed to open device (%1). Error message: %2").arg(_deviceName, strerror(errno));
		retval = -1;
	}
	else
	{
		if (ioctl(_fid, SPI_IOC_WR_MODE, &_spiMode) == -1 || ioctl(_fid, SPI_IOC_RD_MODE, &_spiMode) == -1)
		{
			retval = -2;
		}
		else
		{
			if (ioctl(_fid, SPI_IOC_WR_BITS_PER_WORD, &bitsPerWord) == -1 || ioctl(_fid, SPI_IOC_RD_BITS_PER_WORD, &bitsPerWord) == -1)
			{
				retval = -4;
			}
			else
			{
				if (ioctl(_fid, SPI_IOC_WR_MAX_SPEED_HZ, &_baudRate_Hz) == -1 || ioctl(_fid, SPI_IOC_RD_MAX_SPEED_HZ, &_baudRate_Hz) == -1)
				{
					retval = -6;
				}
				else
				{
					// Everything OK -> enable device
					_isDeviceReady = true;
					retval = 0;
				}
			}
		}
		if ( retval < 0 )
		{
			errortext = QString ("Failed to open device (%1). Error Code: %2").arg(_deviceName).arg(retval);
		}
	}

	if ( retval < 0 )
	{
		this->setInError( errortext );
	}

	return retval;
}

int ProviderSpi::close()
{
	// LedDevice specific closing activities
	int retval = 0;
	_isDeviceReady = false;

	// Test, if device requires closing
	if ( _fid > -1 )
	{
		// Close device
		if ( ::close(_fid) != 0 )
		{
			Error( _log, "Failed to close device (%s). Error message: %s", QSTRING_CSTR(_deviceName),  strerror(errno) );
			retval = -1;
		}
	}
	return retval;
}

int ProviderSpi::writeBytes(unsigned size, const uint8_t * data)
{
	if (_fid < 0)
	{
		return -1;
	}

	uint8_t * newdata {nullptr};

	_spi.tx_buf = __u64(data);
	_spi.len    = __u32(size);

	if (_spiDataInvert)
	{
		newdata = static_cast<uint8_t *>(malloc(size));
		for (unsigned i = 0; i<size; i++) {
			newdata[i] = data[i] ^ 0xff;
		}
		_spi.tx_buf = __u64(newdata);
	}

	int retVal = ioctl(_fid, SPI_IOC_MESSAGE(1), &_spi);
	ErrorIf((retVal < 0), _log, "SPI failed to write. errno: %d, %s", errno,  strerror(errno) );

	free (newdata);

	return retVal;
}

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

	QJsonArray deviceList;

	QDir deviceDirectory (DISCOVERY_DIRECTORY);
	QStringList deviceFilter(DISCOVERY_FILEPATTERN);
	deviceDirectory.setNameFilters(deviceFilter);
	deviceDirectory.setSorting(QDir::Name);
	QFileInfoList deviceFiles = deviceDirectory.entryInfoList(QDir::System);

	QFileInfoList::const_iterator deviceFileIterator;
	for (deviceFileIterator = deviceFiles.constBegin(); deviceFileIterator != deviceFiles.constEnd(); ++deviceFileIterator)
	{
		QJsonObject deviceInfo;
		deviceInfo.insert("deviceName", (*deviceFileIterator).fileName().remove(0,6));
		deviceInfo.insert("systemLocation", (*deviceFileIterator).absoluteFilePath());
		deviceList.append(deviceInfo);
	}
	devicesDiscovered.insert("devices", deviceList);

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

	return devicesDiscovered;
}