#include "accessor_factory.hpp"
#include "nano_lib_hw_strings.hpp"

#include <time.h>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <stdio.h>
#include <string>

using namespace std;


/**
 * @brief Used to convert time since epoch value to a local time string
 *
 * @param time_since_epoch_in_s  - time in seconds
 *
 */
string timeSinceEpochToLocaltimeString(uint64_t time_since_epoch_in_s) {
	char buff[128];

	chrono::duration<uint64_t, ratio<1, 1>> dur(time_since_epoch_in_s);
	auto tp = chrono::system_clock::time_point(
		chrono::duration_cast<chrono::system_clock::duration>(dur));
	time_t in_time_t = chrono::system_clock::to_time_t(tp);
	static struct tm out_time;
	struct tm *out_timep = &out_time;
#if defined(_WIN32)
	localtime_s(out_timep, &in_time_t);
#else
	localtime_r(&in_time_t, out_timep);
#endif // WIN32
	strftime(buff, sizeof(buff), "%d-%m-%Y %H:%M:%S", out_timep);
	string resDate(buff);

	return resDate;
}

/**
 * @brief Used to create a nanolib_exception from a string
 *
 * @param fault  - context description
 * @param result - the result from the operation
 *
 */
class nanolib_exception : public std::exception {
public:
	nanolib_exception(const std::string &msg) : message(msg) {
	}

	virtual char const *what() const noexcept {
		return message.c_str();
	}

private:
	std::string message;
};

/**
 * @brief Checks the result and throws an exception if unsuccessful.
 *
 * @param fault  - context description
 * @param result - the result from the operation
 *
 */
void checkResult(const char *fault, const nlc::Result &result) {
	if (result.hasError()) {
		std::string errorDesc(fault);

		errorDesc += " failed with error: ";
		errorDesc += result.getError();
		throw nanolib_exception(errorDesc);
	}
}

/**
 * @brief Checks the result and throws an exception if unsuccessful.
 *
 * @param fault  - context description
 * @param result - the result from the operation
 *
 * @return The result of the operation
 */
template <class ResultClass>
static ResultClass checkedResult(const char *fault, const ResultClass &result) {
	checkResult(fault, result);
	return result;
}

/**
 * @brief Implementation of nlc::NlcScanBusCallback
 * 
 * Used for getting status updates while scanning the bus
 * 
 */
class ScanBusCallbackImpl : public nlc::NlcScanBusCallback {
public:
	nlc::ResultVoid callback(nlc::BusScanInfo info, std::vector<nlc::DeviceId> const &devicesFound,
							 int32_t data) {
		(void)devicesFound;
		switch (info) {
		case nlc::BusScanInfo::Start:
			std::cout << "Scan started." << std::endl;
			break;

		case nlc::BusScanInfo::Progress:
			if ((data & 1) == 0) // data holds scan progress
			{
				std::cout << ".";
			}
			break;

		case nlc::BusScanInfo::Finished:
			std::cout << std::endl << "Scan finished." << std::endl;
			break;

		default:
			break;
		}

		return nlc::ResultVoid();
	}
};

/**
 * @brief Implementation of nlc::NlcLoggingCallback
 *
 * Used for getting log callbacks from nanolib logger.
 *
 */
class INlcLoggingCallback : public nlc::NlcLoggingCallback {
public:
	// IMPORTANT: the member function pointer of NlcLoggingCallback::callback gets
	// bound to a std::function pointer in between (because of SWIG not supporting std::function)
	// and has to be (manually) unset/deleted in the destructor!
	~INlcLoggingCallback() {
		nlc::NanoLibAccessor *nanolibAccessor = getNanoLibAccessor();
		nanolibAccessor->unsetLoggingCallback();
	}

	// handle stuff here (e.g. write to file)
	void callback(const std::string payload_str, const std::string formatted_str,
				  const std::string logger_name, const unsigned int log_level,
				  const std::uint64_t time_since_epoch, const size_t thread_id) {
		// do your callback stuff here ...
		// e.g. print to file or console
		cout << "#######################################################################################" << endl;
		cout << "# Payload = '" << payload_str << "'" << endl;
		// formatted_str contains a line separator (\r\n on windows or \n on linux) at the end of log message
#if defined(_WIN32)
		cout << "# Formatted string = '" << formatted_str.substr(0, formatted_str.find('\r\n')) << "'" << endl;
#else
		cout << "# Formatted string = '" << formatted_str.substr(0, formatted_str.find('\n')) << "'" << endl;
#endif //WIN32
		cout << "# Logger name = '" << logger_name << "'" << endl;
		cout << "# nlc_log_level = '" << nlc::LogLevelConverter::toString((nlc::LogLevel)log_level) << "'" << endl;
		cout << "# Local Time = '" << timeSinceEpochToLocaltimeString(time_since_epoch) << "'" << endl;
		cout << "# Thread id = '" << thread_id << "'" << endl;
		cout << "#######################################################################################" << endl;
	}
};

// main function entry point
int main()
{	// create instance of LoggingCallback
	INlcLoggingCallback nlcLoggingCallback;
	nlc::NanoLibAccessor *nanolibAccessor = getNanoLibAccessor();
	// listAvailableBusHardware will load dlls, set logging level and callback afterwards
	// so all registered plugins will be correctly set
	nlc::ResultBusHwIds result = nanolibAccessor->listAvailableBusHardware();
	nanolibAccessor->setLoggingLevel(nlc::LogLevel::Trace);
	// sink (callback) log level should always be >= the common log level
	nanolibAccessor->setLoggingCallback(&nlcLoggingCallback, nlc::LogLevel::Debug);
	// set function callback pointer again in accessor with other log level,
	// old callback gets released and new callback is set
	nanolibAccessor->setLoggingCallback(&nlcLoggingCallback, nlc::LogLevel::Trace);

	if (result.hasError()) {
		cerr << "listAvailableBusHardware failed with error: "
			 << static_cast<int>(result.getErrorCode()) << " " << result.getError() << endl;
	} else {
		auto busHardwareIds(result.getResult());

		cout << "Available hardware buses and protocols:" << endl;

		unsigned int lineNum = 0;

		for (const auto &busHardwareId : busHardwareIds) {
			cout << lineNum << ". " << busHardwareId.getName()
				 << " protocol: " << busHardwareId.getProtocol() << endl;
			lineNum++;
		}

		std::cout << std::endl;
		std::cout << "Press any key to exit ..." << std::endl;
		getchar();  
	}

	return 0;
}
