#include <iostream>
#include <iomanip>
#include <thread>
#include <chrono>
#include <numeric>
#include <unordered_set>

#include "icsneo/icsneocpp.h"

#ifdef _MSC_VER
#pragma warning(disable : 4996) // STL time functions
#endif

static std::vector<std::shared_ptr<icsneo::Device>> findDevices(icsneo::DeviceType type)
{
	std::vector<std::shared_ptr<icsneo::Device>> ret;
	for(auto&& dev : icsneo::FindAllDevices()) {
		if(dev->getType() == type) {
			ret.push_back(dev);
		}
	}
	return ret;
}

static std::vector<icsneo::Network> supported_nets;
static std::unordered_map<icsneo::Network::NetID, icsneo::NetworkMutexEvent> network_mutex_states;
constexpr uint32_t MUTEX_PRIORITY = 12345678;
constexpr uint32_t MUTEX_TTL_MS = 2500;

static void on_mutex_event(std::shared_ptr<icsneo::Message> msg) {
	if(msg->type == icsneo::Message::Type::NetworkMutex) {
		auto nmm = std::static_pointer_cast<icsneo::NetworkMutexMessage>(msg);
		if(nmm->event.has_value() && nmm->networks.size())
		{
			auto network = *nmm->networks.begin();
			if(std::find_if(supported_nets.begin(), supported_nets.end(), [network](const icsneo::Network& net){ return net.getNetID() == network; }) != supported_nets.end())
			{
				std::cout << icsneo::Network::GetNetIDString(static_cast<icsneo::Network::NetID>(network)) << ": Mutex " << icsneo::NetworkMutexMessage::GetNetworkMutexEventString(nmm->event.value()) << std::endl;
				network_mutex_states[network] = *nmm->event;
			}
		}
	}
}

static void do_mutexed_operation(std::shared_ptr<icsneo::Device> device, icsneo::Network::NetID network, std::function<void()>&& operation)
{
	// Demonstrate taking a mutex on a single / group of networks
	auto lt = device->lockNetworks({network}, MUTEX_PRIORITY, MUTEX_TTL_MS, icsneo::NetworkMutexType::TxExclusive, on_mutex_event);
	if(lt.has_value()) {
		while(network_mutex_states.find(network) == network_mutex_states.end() || 
			network_mutex_states[network] != icsneo::NetworkMutexEvent::Acquired)
		{
			std::this_thread::sleep_for(std::chrono::milliseconds(10));
		}

		operation();

		device->unlockNetworks({network});
		while(network_mutex_states.find(network) != network_mutex_states.end() && 
			network_mutex_states[network] == icsneo::NetworkMutexEvent::Acquired)
		{
			std::this_thread::sleep_for(std::chrono::milliseconds(10));
		}
		device->removeMessageCallback(*lt);
	} else {
		std::cout << "Failed to acquire mutex for " << icsneo::Network::GetNetIDString(network) << std::endl;
	}
}

int main() {
	// Print version
	std::cout << "libicsneo version " << icsneo::GetVersion() << std::endl;

	std::cout << "\nFinding devices... " << std::flush;
	auto devices = findDevices(icsneo::DeviceType::FIRE3);
	// You now hold the shared_ptrs for these devices, you are considered to "own" these devices from a memory perspective
	std::cout << "OK, " << devices.size() << " device" << (devices.size() == 1 ? "" : "s") << " found" << std::endl;

	if(devices.empty()) {
		auto lastError = icsneo::GetLastError();
		if(lastError.getType() != icsneo::APIEvent::Type::NoErrorFound)
			std::cout << lastError << std::endl;
		return 0;
	}

	// List off the devices
	for(auto& device : devices)
		std::cout << '\t' << device->describe() << " @ Handle " << device->getNeoDevice().handle << std::endl;
	std::cout << std::endl;

	for(auto& device : devices) {
		std::cout << "Connecting to " << device->describe() << "... ";
		bool ret = device->open();
		if(!ret) { // Failed to open
			std::cout << "FAIL" << std::endl;
			std::cout << icsneo::GetLastError() << std::endl << std::endl;
			continue;
		}
		std::cout << "OK" << std::endl;
		
		supported_nets = device->getSupportedTXNetworks();

		device->goOnline();

		// Demonstrate lock and unlock all networks
		std::cout << "Requesting global exclusive network mutex" << std::endl;
		auto lt = device->lockAllNetworks(MUTEX_PRIORITY, MUTEX_TTL_MS, icsneo::NetworkMutexType::TxExclusive, on_mutex_event);
		if(lt.has_value()) {
			// wait until we have the mutex on all networks
			auto all_nets = device->getSupportedTXNetworks();
			for(auto net = all_nets.begin(); net != all_nets.end();) {
				auto state = network_mutex_states.find(net->getNetID());
				if(state != network_mutex_states.end() && state->second == icsneo::NetworkMutexEvent::Acquired) {
					net = all_nets.erase(net);
				} else {
					++net;
				}
			}
			std::cout << "Acquired mutex for all supported networks on " << device->describe() << std::endl;

			// request release
			if(device->unlockAllNetworks()) {
				std::cout << "Requested release of all networks" << std::endl;
			} else {
				std::cout << "Failed to request release of all networks" << std::endl;
			}

			// wait until we have released the mutex on all networks
			all_nets = device->getSupportedTXNetworks();
			for(auto net = all_nets.begin(); net != all_nets.end();) {
				auto state = network_mutex_states.find(net->getNetID());
				if(state == network_mutex_states.end() || state->second == icsneo::NetworkMutexEvent::Released) {
					net = all_nets.erase(net);
				} else {
					++net;
				}
			}
			std::cout << "released mutex on all supported networks" << std::endl;
			
			device->removeMessageCallback(*lt);
		} else {
			std::cout << "Failed to acquire global mutex" << std::endl;
		}
		
		std::cout << "Doing mutexed operation on DWCAN 01" << std::endl;
		do_mutexed_operation(device, icsneo::Network::NetID::DWCAN_01, [&](){
			auto tx_message = std::make_shared<icsneo::CANMessage>();
			tx_message->arbid = 0x12;
			tx_message->isCANFD = true;
			tx_message->baudrateSwitch = true;
			tx_message->data.resize(64);
			tx_message->network = icsneo::Network::NetID::DWCAN_01;
			constexpr auto txInterval = std::chrono::microseconds(500);
			auto nextRefresh = std::chrono::steady_clock::now() + std::chrono::milliseconds(MUTEX_TTL_MS / 2);
			auto nextTx = std::chrono::steady_clock::now() + txInterval;
			size_t numTx = 0;
			while(numTx < 5000) {
				if(std::chrono::steady_clock::now() < nextTx) {
					continue;
				}
				nextTx = std::chrono::steady_clock::now() + txInterval;
				std::iota(tx_message->data.begin(), tx_message->data.end(), (uint8_t)(numTx & 0xFF));
				if(device->transmit(tx_message)) {
					++numTx;
				}
				// check the mutex periodically - every 200 messages for demonstrative purposes
				if(std::chrono::steady_clock::now() > nextRefresh) {
					auto status = device->getNetworkMutexStatus(icsneo::Network::NetID::DWCAN_01);
					if(!status) {
						std::cout << "failed to poll status of network mutex for DWCAN 01" << std::endl;
					} else {
						std::cout << "DWCAN 01 mutex TTL: " << *status->ttlMs << " ms" << std::endl;
					}
					std::cout << "refreshing mutex on DWCAN 01, current tx count: " << numTx << std::endl;
					(void)device->lockNetworks({icsneo::Network::NetID::DWCAN_01}, MUTEX_PRIORITY, MUTEX_TTL_MS, icsneo::NetworkMutexType::TxExclusive, nullptr);
					nextRefresh = std::chrono::steady_clock::now() + std::chrono::milliseconds(MUTEX_TTL_MS / 2);
				}
			}
		});
		std::cout << "Completed mutexed operation on DWCAN 01" << std::endl;

		
		std::cout << "Doing mutexed operation on Ethernet 03" << std::endl;
		do_mutexed_operation(device, icsneo::Network::NetID::ETHERNET_03, [&](){
			auto tx_message = std::make_shared<icsneo::EthernetMessage>();
			tx_message->data.resize(512);
			// set mac addresses and ethertype
			std::iota(tx_message->data.begin(), tx_message->data.begin() + 14, static_cast<uint8_t>(0x01));
			tx_message->network = icsneo::Network::NetID::ETHERNET_03;
			constexpr auto txInterval = std::chrono::microseconds(250);
			auto nextRefresh = std::chrono::steady_clock::now() + std::chrono::milliseconds(MUTEX_TTL_MS / 2);
			auto nextTx = std::chrono::steady_clock::now() + txInterval;
			size_t numTx = 0;
			while(numTx < 50000) {
				if(std::chrono::steady_clock::now() < nextTx) {
					continue;
				}
				nextTx = std::chrono::steady_clock::now() + txInterval;
				// keep the mac addresses and ethertype constant
				std::iota(tx_message->data.begin() + 14, tx_message->data.end(), (uint8_t)(numTx & 0xFF));
				if(device->transmit(tx_message)) {
					++numTx;
				}
				if(std::chrono::steady_clock::now() > nextRefresh) {
					auto status = device->getNetworkMutexStatus(icsneo::Network::NetID::ETHERNET_03);
					if(!status) {
						std::cout << "failed to poll status of network mutex for Ethernet 03" << std::endl;
					} else {
						std::cout << "Ethernet 03 mutex TTL: " << *status->ttlMs << " ms" << std::endl;
					}
					std::cout << "refreshing mutex on Ethernet 03, current tx count: " << numTx << std::endl;
					(void)device->lockNetworks({icsneo::Network::NetID::ETHERNET_03}, MUTEX_PRIORITY, MUTEX_TTL_MS, icsneo::NetworkMutexType::TxExclusive, nullptr);
					nextRefresh = std::chrono::steady_clock::now() + std::chrono::milliseconds(MUTEX_TTL_MS / 2);
				}
			}
		});
		std::cout << "Completed mutexed operation on Ethernet 03" << std::endl;

		device->close();
	}
	return 0;
}