#include "types.hpp"

#include <dv-processing/data/bounding_box_base.hpp>
#include <dv-processing/data/depth_event_base.hpp>
#include <dv-processing/data/depth_frame_base.hpp>
#include <dv-processing/data/event_base.hpp>
#include <dv-processing/data/frame_base.hpp>
#include <dv-processing/data/imu_base.hpp>
#include <dv-processing/data/landmark_base.hpp>
#include <dv-processing/data/pose_base.hpp>
#include <dv-processing/data/timed_keypoint_base.hpp>
#include <dv-processing/data/trigger_base.hpp>

#include <stdexcept>

namespace dv::Types {

static inline void makeTypeNode(const Type &t, dv::Cfg::Node n) {
	auto typeNode = n.getRelativeNode(std::string(t.identifier) + "/");

	typeNode.create<dv::CfgType::STRING>("description", t.description, {0, 2000},
		dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Type description.");

	typeNode.create<dv::CfgType::LONG>("size", static_cast<int64_t>(t.sizeOfType), {0, INT64_MAX},
		dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Type size.");
}

TypeSystem::TypeSystem() {
	auto systemTypesNode = dv::Cfg::GLOBAL.getNode("/system/types/system/");

	// Initialize placeholder types.
	auto nullType
		= Type(nullIdentifier, "Placeholder for errors.", 0, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr);
	systemTypes.push_back(nullType);
	makeTypeNode(nullType, systemTypesNode);

	auto anyType = Type(
		anyIdentifier, "Placeholder for any valid type.", 0, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr);
	systemTypes.push_back(anyType);
	makeTypeNode(anyType, systemTypesNode);

	// Initialize system types. These are always available due to
	// being compiled into the core.
	auto evtType = makeTypeDefinition<EventPacket, Event>("Array of events (polarity ON/OFF).");
	systemTypes.push_back(evtType);
	makeTypeNode(evtType, systemTypesNode);

	auto frmType = makeTypeDefinition<Frame, Frame>("Standard frame (8-bit image).");
	systemTypes.push_back(frmType);
	makeTypeNode(frmType, systemTypesNode);

	auto imuType = makeTypeDefinition<IMUPacket, IMU>("Inertial Measurement Unit data samples.");
	systemTypes.push_back(imuType);
	makeTypeNode(imuType, systemTypesNode);

	auto trigType = makeTypeDefinition<TriggerPacket, Trigger>("External triggers and special signals.");
	systemTypes.push_back(trigType);
	makeTypeNode(trigType, systemTypesNode);

	auto bboxType = makeTypeDefinition<BoundingBoxPacket, BoundingBox>("Bounding boxes for object tracking.");
	systemTypes.push_back(bboxType);
	makeTypeNode(bboxType, systemTypesNode);

	auto depthEvtType = makeTypeDefinition<DepthEventPacket, DepthEvent>("Array of events with depth.");
	systemTypes.push_back(depthEvtType);
	makeTypeNode(depthEvtType, systemTypesNode);

	auto depthFrmType = makeTypeDefinition<DepthFrame, DepthFrame>("Frame representing a depth map.");
	systemTypes.push_back(depthFrmType);
	makeTypeNode(depthFrmType, systemTypesNode);

	auto landmarkType = makeTypeDefinition<LandmarksPacket, Landmark>("Tracking landmarks.");
	systemTypes.push_back(landmarkType);
	makeTypeNode(landmarkType, systemTypesNode);

	auto poseType = makeTypeDefinition<Pose, Pose>("Camera pose.");
	systemTypes.push_back(poseType);
	makeTypeNode(poseType, systemTypesNode);

	auto timedKPType = makeTypeDefinition<TimedKeyPointPacket, TimedKeyPoint>("Key points with timestamps.");
	systemTypes.push_back(timedKPType);
	makeTypeNode(timedKPType, systemTypesNode);
}

void TypeSystem::registerModuleType(const Module *m, const Type &t) {
	std::scoped_lock lock(typesLock);

	// Register user type. Rules:
	// a) cannot have same identifier as a system type
	// b) if same type registered multiple times, just
	// add multiple times. Modules will register on
	// load/dlopen, and unregister on unload/dlclose.
	// As such, a pack/unpack will always be present if
	// any module that uses such a type is loaded at least
	// once. It is assumed types with same identifier are
	// equal or fully compatible.
	if (dv::vectorContainsIf(systemTypes, [&t](const auto &sysType) {
			return (t.id == sysType.id);
		})) {
		throw std::invalid_argument("Already present as a system type.");
	}

	// Not a system type. Check if this module already registered
	// this type before, or if it's different from already registered ones.
	if (userTypes.count(t.id)) {
		if (dv::vectorContainsIf(userTypes[t.id], [m](const auto &userType) {
				return (m == userType.first);
			})) {
			throw std::invalid_argument("User type already registered for this module.");
		}

		if (dv::vectorContainsIf(userTypes[t.id], [&t](const auto &userType) {
				return (t.sizeOfType != userType.second.sizeOfType);
			})) {
			throw std::invalid_argument("User type has different object size than already registered ones.");
		}
	}
	else {
		auto userTypesNode = dv::Cfg::GLOBAL.getNode("/system/types/user/");
		makeTypeNode(t, userTypesNode);
	}

	userTypes[t.id].emplace_back(m, t);
}

void TypeSystem::unregisterModuleTypes(const Module *m) {
	std::scoped_lock lock(typesLock);

	// Remove all types registered to this module.
	for (auto &type : userTypes) {
		vectorRemoveIf(type.second, [m](const auto &userType) {
			return (m == userType.first);
		});
	}

	// Cleanup empty vectors.
	for (auto it = userTypes.begin(); it != userTypes.end();) {
		if (it->second.empty()) {
			// Empty vector means no survivors of this type, so we can remove
			// it from the global registry too.
			uint32_t id = it->first;
			std::string identifier{};
			identifier.push_back(static_cast<char>((id >> 24) & 0xFF));
			identifier.push_back(static_cast<char>((id >> 16) & 0xFF));
			identifier.push_back(static_cast<char>((id >> 8) & 0xFF));
			identifier.push_back(static_cast<char>(id & 0xFF));

			auto userTypesNode = dv::Cfg::GLOBAL.getNode("/system/types/user/");
			userTypesNode.getRelativeNode(identifier + "/").removeNode();

			it = userTypes.erase(it);
		}
		else {
			++it;
		}
	}
}

const Type TypeSystem::getTypeInfo(std::string_view tIdentifier, const Module *m) const {
	if (tIdentifier.size() != 4) {
		throw std::invalid_argument("Identifier must be 4 characters long.");
	}

	uint32_t id = dvTypeIdentifierToId(tIdentifier.data());

	return (getTypeInfo(id, m));
}

const Type TypeSystem::getTypeInfo(const char *tIdentifier, const Module *m) const {
	if (strlen(tIdentifier) != 4) {
		throw std::invalid_argument("Identifier must be 4 characters long.");
	}

	uint32_t id = dvTypeIdentifierToId(tIdentifier);

	return (getTypeInfo(id, m));
}

const Type TypeSystem::getTypeInfo(uint32_t tId, const Module *m) const {
	std::scoped_lock lock(typesLock);

	// Search for type, first in system then user types.
	const auto sysPos = std::find_if(systemTypes.cbegin(), systemTypes.cend(), [tId](const auto &sysType) {
		return (tId == sysType.id);
	});

	if (sysPos != systemTypes.cend()) {
		// Found.
		return (*sysPos);
	}

	if (userTypes.count(tId) > 0) {
		if (m == nullptr) {
			// Generic query, just return first one.
			return (userTypes.at(tId).at(0).second);
		}
		else {
			// Specific module query (usually during setup), return specific type record.
			const auto userPos
				= std::find_if(userTypes.at(tId).cbegin(), userTypes.at(tId).cend(), [m](const auto &userType) {
					  return (m == userType.first);
				  });

			if (userPos != userTypes.at(tId).cend()) {
				// Found.
				return (userPos->second);
			}
		}
	}

	// Not found.
	char typeName[5] = {0};
	dvTypeIdToIdentifier(tId, typeName);

	throw std::out_of_range(fmt::format(FMT_STRING("Type '{:s}' not found in type system."), typeName));
}

} // namespace dv::Types
