#include "devices_discovery.hpp"

#include "modules_discovery.hpp"

#include <mutex>

// Available devices list support.
static std::mutex glAvailableDevicesLock;

void dv::DevicesUpdateListener(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
	const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
	UNUSED_ARGUMENT(userData);

	if ((event == DVCFG_ATTRIBUTE_MODIFIED) && (changeType == DVCFG_TYPE_BOOL)
		&& (std::string{changeKey} == "updateAvailableDevices") && changeValue.boolean) {
		// Get information on available devices, put it into ConfigTree.
		DevicesUpdateList();

		dvConfigNodeAttributeBooleanReset(node, changeKey);
	}
}

void dv::DevicesUpdateList() {
	std::scoped_lock lock(glAvailableDevicesLock);

	auto devicesNode    = dv::Cfg::GLOBAL.getNode("/system/devices/");
	auto newDevicesNode = dv::Cfg::GLOBAL.getNode("/system/_devices/");

	// Fetch new device list into /system/_devices/ by calling the
	// DEVICE_DISCOVERY module hooks, which will add the appropriate
	// entires to /system/_devices/, populating it with new devices.
	dv::ModulesExecuteHook(DV_HOOK_DEVICE_DISCOVERY, nullptr);

	// Classify differences, update/move new devices, remove old ones.
	const auto currDevices = devicesNode.getChildNames();
	const auto newDevices  = newDevicesNode.getChildNames();

	std::vector<std::string> devicesToAdd;

	std::set_difference(
		newDevices.begin(), newDevices.end(), currDevices.begin(), currDevices.end(), std::back_inserter(devicesToAdd));

	std::vector<std::string> devicesToRemove;

	std::set_difference(currDevices.begin(), currDevices.end(), newDevices.begin(), newDevices.end(),
		std::back_inserter(devicesToRemove));

	std::vector<std::string> devicesToUpdate;

	std::set_intersection(currDevices.begin(), currDevices.end(), newDevices.begin(), newDevices.end(),
		std::back_inserter(devicesToUpdate));

	// Remove devices not present anymore.
	for (const auto &devName : devicesToRemove) {
		devicesNode.getRelativeNode(devName + '/').removeNode();
	}

	// Copy new devices over.
	for (const auto &devName : devicesToAdd) {
		auto source      = newDevicesNode.getRelativeNode(devName + '/');
		auto destination = devicesNode.getRelativeNode(devName + '/');

		source.copyTo(destination);
	}

	// Update devices present in both. If information exists, it should
	// be more up-to-date, so we can just copy it.
	for (const auto &devName : devicesToAdd) {
		auto source      = newDevicesNode.getRelativeNode(devName + '/');
		auto destination = devicesNode.getRelativeNode(devName + '/');

		source.copyTo(destination);
	}

	newDevicesNode.removeNode();
}
