// Copyright 2022-2023, Collabora, Ltd.
// Copyright 2026, NVIDIA CORPORATION.
// SPDX-License-Identifier: BSL-1.0
/*!
 * @file
 * @brief  Default implementation helpers for @ref xrt_system_devices.
 * @author Jakob Bornecrantz <jakob@collabora.com>
 * @ingroup base
 */

#include "b_system_devices.h"
#include "b_hand_tracker.h"

#include "util/u_device.h"
#include "util/u_logging.h"
#include "util/u_misc.h"
#include "util/u_system_devices.h"

#include "xrt/xrt_device.h"

#include <assert.h>
#include <limits.h>
#include <stdlib.h>

/*
 *
 * Helper functions.
 *
 */

static int32_t
get_index_for_device(const struct xrt_system_devices *xsysd, const struct xrt_device *xdev)
{
	assert(xsysd->static_xdev_count <= ARRAY_SIZE(xsysd->static_xdevs));
	assert(xsysd->static_xdev_count < INT_MAX);

	if (xdev == NULL) {
		return -1;
	}

	for (int32_t i = 0; i < (int32_t)xsysd->static_xdev_count; i++) {
		if (xsysd->static_xdevs[i] == xdev) {
			return i;
		}
	}

	return -1;
}

static const char *
type_to_small_string(enum xrt_device_feature_type type)
{
	switch (type) {
	case XRT_DEVICE_FEATURE_HAND_TRACKING_LEFT: return "hand_tracking_left";
	case XRT_DEVICE_FEATURE_HAND_TRACKING_RIGHT: return "hand_tracking_right";
	case XRT_DEVICE_FEATURE_EYE_TRACKING: return "eye_tracking";
	case XRT_DEVICE_FEATURE_FACE_TRACKING: return "face_tracking";
	default: return "invalid";
	}
}

static inline void
get_hand_tracking_devices(struct xrt_system_devices *xsysd, enum xrt_hand hand, struct xrt_device *out_ht_xdevs[2])
{
#define XRT_GET_U_HT(HAND) xsysd->static_roles.hand_tracking.unobstructed.HAND
#define XRT_GET_C_HT(HAND) xsysd->static_roles.hand_tracking.conforming.HAND
	if (hand == XRT_HAND_LEFT) {
		out_ht_xdevs[0] = XRT_GET_U_HT(left);
		out_ht_xdevs[1] = XRT_GET_C_HT(left);
	} else {
		out_ht_xdevs[0] = XRT_GET_U_HT(right);
		out_ht_xdevs[1] = XRT_GET_C_HT(right);
	}
#undef XRT_GET_C_HT
#undef XRT_GET_U_HT
}

static xrt_result_t
set_hand_tracking_enabled(struct xrt_system_devices *xsysd, enum xrt_hand hand, bool enable)
{
	struct xrt_device *ht_sources[2] = {0};
	get_hand_tracking_devices(xsysd, hand, ht_sources);

	uint32_t ht_sources_size = ARRAY_SIZE(ht_sources);
	// hand-tracking data-sources can all come from the same xrt-device instance
	if (ht_sources[0] == ht_sources[1]) {
		ht_sources_size = 1;
	}

	typedef xrt_result_t (*set_feature_t)(struct xrt_device *, enum xrt_device_feature_type);
	const set_feature_t set_feature = enable ? xrt_device_begin_feature : xrt_device_end_feature;

	const enum xrt_device_feature_type ht_feature =
	    (hand == XRT_HAND_LEFT) ? XRT_DEVICE_FEATURE_HAND_TRACKING_LEFT : XRT_DEVICE_FEATURE_HAND_TRACKING_RIGHT;

	xrt_result_t xret = XRT_SUCCESS;
	for (uint32_t i = 0; i < ht_sources_size; ++i) {
		if (ht_sources[i]) {
			xret = set_feature(ht_sources[i], ht_feature);
		}
		if (xret != XRT_SUCCESS) {
			break;
		}
	}
	return xret;
}

/*
 *
 * Internal functions.
 *
 */

static void
destroy(struct xrt_system_devices *xsysd)
{
	b_system_devices_close(xsysd);
	free(xsysd);
}

static xrt_result_t
get_roles(struct xrt_system_devices *xsysd, struct xrt_system_roles *out_roles)
{
	struct b_system_devices_static *bsysds = b_system_devices_static(xsysd);

	assert(bsysds->cached.generation_id == 1);

	*out_roles = bsysds->cached;

	return XRT_SUCCESS;
}

static xrt_result_t
feature_inc(struct xrt_system_devices *xsysd, enum xrt_device_feature_type type)
{
	struct b_system_devices_static *bsysds = b_system_devices_static(xsysd);

	if (type >= XRT_DEVICE_FEATURE_MAX_ENUM) {
		return XRT_ERROR_FEATURE_NOT_SUPPORTED;
	}

	// If it wasn't zero nothing to do.
	if (!xrt_reference_inc_and_was_zero(&bsysds->feature_use[type])) {
		return XRT_SUCCESS;
	}

	xrt_result_t xret = XRT_SUCCESS;
	if (type == XRT_DEVICE_FEATURE_HAND_TRACKING_LEFT) {
		xret = set_hand_tracking_enabled(xsysd, XRT_HAND_LEFT, true);
	} else if (type == XRT_DEVICE_FEATURE_HAND_TRACKING_RIGHT) {
		xret = set_hand_tracking_enabled(xsysd, XRT_HAND_RIGHT, true);
	} else if (type == XRT_DEVICE_FEATURE_EYE_TRACKING) {
		xret = xrt_device_begin_feature(xsysd->static_roles.eyes, type);
	} else if (type == XRT_DEVICE_FEATURE_FACE_TRACKING) {
		xret = xrt_device_begin_feature(xsysd->static_roles.face, type);
	} else {
		xret = XRT_ERROR_FEATURE_NOT_SUPPORTED;
	}
	if (xret != XRT_SUCCESS) {
		return xret;
	}

	U_LOG_D("Device-feature %s in use", type_to_small_string(type));

	return XRT_SUCCESS;
}

static xrt_result_t
feature_dec(struct xrt_system_devices *xsysd, enum xrt_device_feature_type type)
{
	struct b_system_devices_static *bsysds = b_system_devices_static(xsysd);

	if (type >= XRT_DEVICE_FEATURE_MAX_ENUM) {
		return XRT_ERROR_FEATURE_NOT_SUPPORTED;
	}

	// If it is not zero we are done.
	if (!xrt_reference_dec_and_is_zero(&bsysds->feature_use[type])) {
		return XRT_SUCCESS;
	}

	xrt_result_t xret;
	if (type == XRT_DEVICE_FEATURE_HAND_TRACKING_LEFT) {
		xret = set_hand_tracking_enabled(xsysd, XRT_HAND_LEFT, false);
	} else if (type == XRT_DEVICE_FEATURE_HAND_TRACKING_RIGHT) {
		xret = set_hand_tracking_enabled(xsysd, XRT_HAND_RIGHT, false);
	} else if (type == XRT_DEVICE_FEATURE_EYE_TRACKING) {
		// @todo When eyes are moved from the static roles, we need to end features on the old device when
		// swapping which device is in the eyes role
		xret = xrt_device_end_feature(xsysd->static_roles.eyes, type);
	} else if (type == XRT_DEVICE_FEATURE_FACE_TRACKING) {
		xret = xrt_device_end_feature(xsysd->static_roles.face, type);
	} else {
		xret = XRT_ERROR_FEATURE_NOT_SUPPORTED;
	}
	if (xret != XRT_SUCCESS) {
		return xret;
	}

	U_LOG_D("Device-feature %s no longer in use", type_to_small_string(type));

	return XRT_SUCCESS;
}

static xrt_result_t
create_hand_tracker(struct xrt_system_devices *xsysd,
                    const struct xrt_hand_tracker_create_info *info,
                    struct xrt_hand_tracker **out_xht)
{
	return b_hand_tracker_create(xsysd, info, out_xht);
}

/*
 *
 * 'Exported' functions.
 *
 */

struct b_system_devices *
b_system_devices_allocate(void)
{
	struct b_system_devices *bsysd = U_TYPED_CALLOC(struct b_system_devices);
	bsysd->base.destroy = destroy;
	bsysd->base.create_hand_tracker = create_hand_tracker;

	return bsysd;
}

void
b_system_devices_close(struct xrt_system_devices *xsysd)
{
	struct b_system_devices *bsysd = b_system_devices(xsysd);

	for (uint32_t i = 0; i < ARRAY_SIZE(bsysd->base.static_xdevs); i++) {
		xrt_device_destroy(&bsysd->base.static_xdevs[i]);
	}

	xrt_frame_context_destroy_nodes(&bsysd->xfctx);
}

struct b_system_devices_static *
b_system_devices_static_allocate(void)
{
	struct b_system_devices_static *bsysds = U_TYPED_CALLOC(struct b_system_devices_static);

	u_system_devices_populate_function_pointers(&bsysds->base.base, get_roles, destroy);
	bsysds->base.base.create_hand_tracker = create_hand_tracker;
	bsysds->base.base.feature_inc = feature_inc;
	bsysds->base.base.feature_dec = feature_dec;

	return bsysds;
}

void
b_system_devices_static_finalize(struct b_system_devices_static *bsysds,
                                 struct xrt_device *left,
                                 struct xrt_device *right,
                                 struct xrt_device *gamepad)
{
	struct xrt_system_devices *xsysd = &bsysds->base.base;
	int32_t left_index = get_index_for_device(xsysd, left);
	int32_t right_index = get_index_for_device(xsysd, right);
	int32_t gamepad_index = get_index_for_device(xsysd, gamepad);

	U_LOG_D(
	    "Devices:"
	    "\n\t%i: %p"
	    "\n\t%i: %p"
	    "\n\t%i: %p",
	    left_index, (void *)left,        //
	    right_index, (void *)right,      //
	    gamepad_index, (void *)gamepad); //

	// Consistency checking.
	assert(bsysds->cached.generation_id == 0);
	assert(left_index < 0 || left != NULL);
	assert(left_index >= 0 || left == NULL);
	assert(right_index < 0 || right != NULL);
	assert(right_index >= 0 || right == NULL);
	assert(gamepad_index < 0 || gamepad != NULL);
	assert(gamepad_index >= 0 || gamepad == NULL);

	// Completely clear the struct.
	bsysds->cached = (struct xrt_system_roles)XRT_SYSTEM_ROLES_INIT;
	bsysds->cached.generation_id = 1;
	bsysds->cached.left = left_index;
	bsysds->cached.right = right_index;
	bsysds->cached.gamepad = gamepad_index;
}
