// Copyright 2026, Beyley Cardellio
// SPDX-License-Identifier: BSL-1.0
/*!
 * @file
 * @brief  A routine to automatically refine latency offsets of LED blink times using constellation samples.
 * @author Beyley Cardellio <ep1cm1n10n123@gmail.com>
 * @ingroup tracking
 */

#include "util/u_var.h"
#include "util/u_debug.h"

#include "math/m_api.h"

#include "os/os_time.h"

#include "t_led_sync_refinement.h"


DEBUG_GET_ONCE_LOG_OPTION(led_sync_refinement_log, "T_LED_SYNC_REFINEMENT_LOG", U_LOGGING_INFO)

#define LOG_TRACE(refinement, ...) U_LOG_IFL_T(refinement->log_level, __VA_ARGS__)
#define LOG_DEBUG(refinement, ...) U_LOG_IFL_D(refinement->log_level, __VA_ARGS__)
#define LOG_INFO(refinement, ...) U_LOG_IFL_I(refinement->log_level, __VA_ARGS__)
#define LOG_WARN(refinement, ...) U_LOG_IFL_W(refinement->log_level, __VA_ARGS__)
#define LOG_ERROR(refinement, ...) U_LOG_IFL_E(refinement->log_level, __VA_ARGS__)

//! The amount of frames to not see it for consider a device "not found" during an active search
#define MAX_SAMPLE_LATENCY 3

/*
 * Helper functions
 */

//! The amount of time of which a change under this amount is considered "good" and we can exit the binary search
static time_duration_ns
change_good_cutoff(struct t_led_sync_refinement *refinement)
{
	return MIN(refinement->current_blink_duration_ns, refinement->exposure_time_ns) / 20;
}

//! The amount of frames since the sample was applied to the controller
static uint32_t
frames_since_sample_apply(struct t_led_sync_refinement *refinement, timepoint_ns now_ns)
{
	return (uint32_t)((now_ns - refinement->last_sample_apply_time_ns) / refinement->exposure_interval_ns);
}

static void
set_offset_duration(struct t_led_sync_refinement *refinement,
                    time_duration_ns latency_offset_ns,
                    time_duration_ns fudge_offset_ns,
                    time_duration_ns blink_duration_ns)
{
	refinement->current_latency_offset_ns = latency_offset_ns;
	refinement->current_blink_fudge_ns = fudge_offset_ns;
	refinement->current_blink_duration_ns = blink_duration_ns;

	refinement->has_sample_for_driver = true;
	refinement->sample_applied = false;
	refinement->sample_for_driver = XRT_C11_COMPOUND(struct t_led_sync_sample){
	    .device_host_latency_ns = latency_offset_ns,
	    .fudge_offset_ns = fudge_offset_ns,
	    .blink_duration_ns = blink_duration_ns,
	};

	LOG_TRACE(refinement,
	          "Setting offset to %" PRId64 "ns with fudge %" PRId64 "ns and blink duration to %" PRId64 "ns",
	          latency_offset_ns, fudge_offset_ns, blink_duration_ns);
}

static void
set_centered_duration(struct t_led_sync_refinement *refinement, time_duration_ns blink_duration_ns)
{
	// Find the latency offset, the offset needing to be applied to get blink start to line up with exposure
	// start, this is the transfer latency from the controller->host.
	time_duration_ns new_latency_offset_ns = refinement->found_left_edge_ns;

	// Set the offset so that the center of the blink is at the center of the exposure, since latency and
	// fudge offset are added together
	time_duration_ns new_offset_ns =
	    ((refinement->found_right_edge_ns - refinement->found_left_edge_ns) / 2) - (blink_duration_ns / 2);

	set_offset_duration(refinement, new_latency_offset_ns, new_offset_ns, blink_duration_ns);
}

//! Gets the period to iterate over when searching for a latency offset.
static time_duration_ns
get_search_period_locked(struct t_led_sync_refinement *refinement)
{
	int divisor = 4;

	if (!refinement->has_exposure_time) {
		return refinement->current_blink_duration_ns / divisor;
	}

	if (refinement->phase == T_LED_SYNC_SEARCH_PHASE_FIND_INITIAL_OFFSET) {
		// If we're in initial offset search, we can use the larger of the two, since we just need to find one
		// that overlaps.
		return MAX(refinement->current_blink_duration_ns, refinement->exposure_time_ns) / divisor;
	} else {
		return MIN(refinement->current_blink_duration_ns, refinement->exposure_time_ns) / divisor;
	}
}

static void
set_phase_locked(struct t_led_sync_refinement *refinement, enum t_led_sync_phase new_phase)
{
	refinement->phase = new_phase;

	LOG_INFO(refinement, "Entering search phase: %d", new_phase);

	switch (new_phase) {
	case T_LED_SYNC_SEARCH_PHASE_FIND_INITIAL_OFFSET: {
		refinement->found_left_edge_ns = -1;
		refinement->found_right_edge_ns = -1;

		refinement->current_blink_duration_ns = refinement->options.initial_blink_duration_ns;

		set_offset_duration(refinement, 0, 0, refinement->current_blink_duration_ns);
		break;
	}
	case T_LED_SYNC_SEARCH_PHASE_FIND_RIGHT_EDGE: {
		// Left bound is the current offset
		refinement->binary_search_state.left_bound_ns = refinement->current_blink_fudge_ns;
		// Right bound is the start of the next exposure minus the blink duration, we don't want to blink into
		// the next frame, rather start at zero, since latency will always be >0.
		refinement->binary_search_state.right_bound_ns =
		    refinement->exposure_interval_ns - refinement->current_blink_duration_ns;

		LOG_DEBUG(refinement,
		          "Starting binary search for right edge, left bound at: %" PRId64
		          "ns, right bound at: %" PRId64 "ns",
		          refinement->binary_search_state.left_bound_ns,
		          refinement->binary_search_state.right_bound_ns);

		time_duration_ns new_offset_ns =
		    (refinement->binary_search_state.right_bound_ns + refinement->binary_search_state.left_bound_ns) /
		    2;
		set_offset_duration(refinement, 0, new_offset_ns, refinement->current_blink_duration_ns);

		break;
	}
	case T_LED_SYNC_SEARCH_PHASE_FIND_LEFT_EDGE: {
		// We should always have a right edge by now
		assert(refinement->found_right_edge_ns > -1);

		// Left bound is when the exposure *should* be given zero latency
		refinement->binary_search_state.left_bound_ns = 0;
		// Right bound is the found right edge
		refinement->binary_search_state.right_bound_ns = refinement->found_right_edge_ns;

		LOG_DEBUG(refinement, "Starting binary search for left edge, right edge at: %" PRId64 "ns",
		          refinement->found_right_edge_ns);

		time_duration_ns new_offset_ns =
		    (refinement->binary_search_state.right_bound_ns + refinement->binary_search_state.left_bound_ns) /
		    2;
		set_offset_duration(refinement, 0, new_offset_ns, refinement->current_blink_duration_ns);

		break;
	}
	case T_LED_SYNC_SEARCH_PHASE_REFINE_BLINK_DURATION: {
		// We should have both edges by now
		assert(refinement->found_right_edge_ns > -1);
		assert(refinement->found_left_edge_ns > -1);

		set_centered_duration(refinement, refinement->current_blink_duration_ns);

		refinement->blink_time_refinement_state.backing_off = false;
		refinement->blink_time_refinement_state.last_good_blink_duration_ns =
		    refinement->current_blink_duration_ns;

		break;
	}
	case T_LED_SYNC_SEARCH_PHASE_MAINTAIN_OFFSET: {
		// We should have both edges by now
		assert(refinement->found_right_edge_ns > -1);
		assert(refinement->found_left_edge_ns > -1);

		set_centered_duration(refinement, refinement->current_blink_duration_ns);

		LOG_DEBUG(refinement, "Maintaining offset at: %" PRId64 "ns", refinement->current_latency_offset_ns);

		break;
	}
	default: break;
	}
}

static void
handle_find_initial_offset(struct t_led_sync_refinement *refinement)
{
	if (refinement->frames_since_last_visually_seen > MAX_SAMPLE_LATENCY) {
		const time_duration_ns search_period_ns = get_search_period_locked(refinement);
		const time_duration_ns new_fudge_offset_ns =
		    (refinement->current_blink_fudge_ns + search_period_ns) % refinement->exposure_interval_ns;

		LOG_DEBUG(refinement, "Device hasn't been seen, trying offset: %" PRId64 "ns", new_fudge_offset_ns);

		// Scan the fudge offset around instead of latency, so latency isn't jumping around at random and
		// confusing time conversion math.
		set_offset_duration(refinement, 0, new_fudge_offset_ns, refinement->current_blink_duration_ns);
	} else {
		LOG_DEBUG(refinement, "Device has been seen, moving to find edge phases");

		// We got a sample recently, try to find the edges now.
		set_phase_locked(refinement, T_LED_SYNC_SEARCH_PHASE_FIND_RIGHT_EDGE);
	}
}

static void
handle_find_right_edge(struct t_led_sync_refinement *refinement)
{
	if (refinement->frames_since_last_visually_seen > MAX_SAMPLE_LATENCY) {
		LOG_DEBUG(refinement,
		          "Device hasn't been seen for %" PRIu32
		          " frames, advancing binary search with left side closer",
		          refinement->frames_since_last_visually_seen);

		// We didn't see the device
		refinement->binary_search_state.right_bound_ns = refinement->current_blink_fudge_ns;

		time_duration_ns new_offset_ns =
		    (refinement->binary_search_state.left_bound_ns + refinement->binary_search_state.right_bound_ns) /
		    2;

		set_offset_duration(refinement, 0, new_offset_ns, refinement->current_blink_duration_ns);

	} else {
		LOG_DEBUG(refinement, "Device has been seen, advancing binary search with right side closer");

		// We saw the device
		refinement->binary_search_state.left_bound_ns = refinement->current_blink_fudge_ns;

		time_duration_ns new_offset_ns =
		    (refinement->binary_search_state.left_bound_ns + refinement->binary_search_state.right_bound_ns) /
		    2;

		set_offset_duration(refinement, 0, new_offset_ns, refinement->current_blink_duration_ns);
	}

	bool done = refinement->binary_search_state.right_bound_ns - refinement->binary_search_state.left_bound_ns <
	            change_good_cutoff(refinement);
	if (done) {
		// We found a good enough offset for the right edge, move on to finding the left edge.
		refinement->found_right_edge_ns = refinement->current_blink_fudge_ns;

		LOG_DEBUG(refinement,
		          "Found right edge at offset: %" PRId64 "ns. Difference %" PRId64
		          "ns was lower than cutoff %" PRId64 "ns, moving to find left edge phase",
		          refinement->current_blink_fudge_ns,
		          refinement->binary_search_state.right_bound_ns -
		              refinement->binary_search_state.left_bound_ns,
		          change_good_cutoff(refinement));

		set_phase_locked(refinement, T_LED_SYNC_SEARCH_PHASE_FIND_LEFT_EDGE);
	}
}

static void
handle_find_left_edge(struct t_led_sync_refinement *refinement)
{
	if (refinement->frames_since_last_visually_seen > MAX_SAMPLE_LATENCY) {
		LOG_DEBUG(refinement,
		          "Device hasn't been seen for %" PRIu32
		          " frames, advancing binary search with right side closer",
		          refinement->frames_since_last_visually_seen);

		// We didn't see the device
		refinement->binary_search_state.left_bound_ns =
		    refinement->current_blink_fudge_ns + refinement->current_blink_duration_ns;

		time_duration_ns new_offset_ns =
		    ((refinement->binary_search_state.left_bound_ns + refinement->binary_search_state.right_bound_ns) /
		     2);
		// Set the new offset so that we're moving the right edge of the blink to the left edge of the exposure
		set_offset_duration(refinement, 0, new_offset_ns - refinement->current_blink_duration_ns,
		                    refinement->current_blink_duration_ns);
	} else {
		LOG_DEBUG(refinement, "Device has been seen, advancing binary search with left side closer");

		// We saw the device
		refinement->binary_search_state.right_bound_ns =
		    refinement->current_blink_fudge_ns + refinement->current_blink_duration_ns;

		time_duration_ns new_offset_ns =
		    (refinement->binary_search_state.left_bound_ns + refinement->binary_search_state.right_bound_ns) /
		    2;
		// Set the new offset so that we're moving the right edge of the blink to the left edge of the exposure
		set_offset_duration(refinement, 0, new_offset_ns - refinement->current_blink_duration_ns,
		                    refinement->current_blink_duration_ns);
	}

	bool done = refinement->binary_search_state.right_bound_ns - refinement->binary_search_state.left_bound_ns <
	            change_good_cutoff(refinement);
	if (done) {
		// We found a good enough offset for the left edge, we can maintain the offset now.
		refinement->found_left_edge_ns =
		    refinement->current_blink_fudge_ns + refinement->current_blink_duration_ns;

		LOG_DEBUG(refinement,
		          "Found left edge at offset: %" PRId64 "ns. Difference %" PRId64
		          "ns was lower than cutoff %" PRId64 "ns, moving to maintain offset phase",
		          refinement->current_blink_fudge_ns,
		          refinement->binary_search_state.right_bound_ns -
		              refinement->binary_search_state.left_bound_ns,
		          change_good_cutoff(refinement));

		// We've found the latency offset, so if we're not optimizing blink duration, we can move to just
		// maintaining the offset now.
		if (refinement->options.flags & T_LED_SYNC_REFINEMENT_FLAGS_BLINK_DURATION) {
			set_phase_locked(refinement, T_LED_SYNC_SEARCH_PHASE_REFINE_BLINK_DURATION);
		} else {
			set_phase_locked(refinement, T_LED_SYNC_SEARCH_PHASE_MAINTAIN_OFFSET);
		}
	}
}

static void
handle_refine_blink_duration(struct t_led_sync_refinement *refinement)
{
	if (refinement->frames_since_last_visually_seen > MAX_SAMPLE_LATENCY) {
		LOG_DEBUG(refinement, "Device hasn't been seen for %" PRIu32 " frames, trying shorter blink duration",
		          refinement->frames_since_last_visually_seen);

		if (refinement->blink_time_refinement_state.backing_off) {
			// Backing off still didn't work, just go back to the start and hope for the best.
			time_duration_ns new_blink_duration = refinement->options.initial_blink_duration_ns;
			set_centered_duration(refinement, new_blink_duration);

			LOG_DEBUG(refinement,
			          "Backing off didn't work, resetting blink duration to initial value: %" PRId64 "ns",
			          new_blink_duration);

			set_phase_locked(refinement, T_LED_SYNC_SEARCH_PHASE_MAINTAIN_OFFSET);
			return;
		}

		// We didn't see the device, try increasing the blink duration again to back off
		time_duration_ns new_blink_duration =
		    refinement->blink_time_refinement_state.last_good_blink_duration_ns;

		set_centered_duration(refinement, new_blink_duration);
		refinement->blink_time_refinement_state.backing_off = true;

		LOG_DEBUG(refinement, "Trying to back off to last good blink duration: %" PRId64 "ns",
		          new_blink_duration);
	} else {
		if (refinement->blink_time_refinement_state.backing_off) {
			// We backed off and it worked, so now we found the minimum blink duration that doesn't cause
			// instability, we can stop refining.
			LOG_DEBUG(refinement,
			          "Back off worked, found good blink duration: %" PRId64 "ns, stopping refinement",
			          refinement->current_blink_duration_ns);
			set_phase_locked(refinement, T_LED_SYNC_SEARCH_PHASE_MAINTAIN_OFFSET);
			return;
		}

		time_duration_ns search_period_ns = get_search_period_locked(refinement);
		time_duration_ns new_blink_duration_ns = refinement->current_blink_duration_ns - search_period_ns;

		// We saw the device, this is a good blink duration, save it in case we need to back off.
		refinement->blink_time_refinement_state.last_good_blink_duration_ns =
		    refinement->current_blink_duration_ns;
		refinement->blink_time_refinement_state.backing_off = false;

		LOG_DEBUG(refinement,
		          "Device has been seen, trying shorter blink duration: %" PRId64 "ns (was %" PRId64 "ns)",
		          new_blink_duration_ns, refinement->current_blink_duration_ns);

		if (new_blink_duration_ns < refinement->options.min_blink_duration_ns) {
			LOG_DEBUG(refinement,
			          "New blink duration %" PRId64 "ns was below minimum blink duration %" PRId64
			          "ns, stopping refinement",
			          new_blink_duration_ns, refinement->options.min_blink_duration_ns);
			set_phase_locked(refinement, T_LED_SYNC_SEARCH_PHASE_MAINTAIN_OFFSET);
			return;
		}

		set_centered_duration(refinement, new_blink_duration_ns);
	}
}

static void
handle_maintain_offset(struct t_led_sync_refinement *refinement)
{}

static void
handle_timing_event_locked(struct t_led_sync_refinement *refinement,
                           const struct t_timing_event_camera_exposure_start *event)
{
	if (event->exposure_time_ns > 0) {
		refinement->has_exposure_time = true;
		refinement->exposure_time_ns = event->exposure_time_ns;
	}

	assert(event->frame_period_ns > 0); // No frame period present. Please estimate this externally.

	refinement->exposure_interval_ns = event->frame_period_ns;
	refinement->frames_since_last_visually_seen++;
	refinement->current_sequence_id = event->sequence_id;

	// We have an exposure interval now, start searching!
	if (refinement->phase == T_LED_SYNC_SEARCH_PHASE_INIT) {
		set_phase_locked(refinement, T_LED_SYNC_SEARCH_PHASE_FIND_INITIAL_OFFSET);
	}

	// Not applied yet
	if (!refinement->sample_applied) {
		return;
	}

	timepoint_ns now_ns = os_monotonic_get_ns();

	// Let the device settle for a few frames after applying a sample before we start trying to refine again
	if (frames_since_sample_apply(refinement, now_ns) < refinement->options.settle_frames) {
		return;
	}

	bool trigger_resync = refinement->frames_since_last_visually_seen >
	                          (refinement->options.time_to_resync_ns / refinement->exposure_interval_ns) &&
	                      refinement->phase > T_LED_SYNC_SEARCH_PHASE_FIND_INITIAL_OFFSET;
	// If we're past the initial find and we haven't seen the controller in some amount of
	// time, start searching for an offset again.
	if (trigger_resync) {
		LOG_DEBUG(refinement, "Triggering resync due to not being visually seen for %" PRIu32 " frames",
		          refinement->frames_since_last_visually_seen);
		set_phase_locked(refinement, T_LED_SYNC_SEARCH_PHASE_FIND_INITIAL_OFFSET);
	}

	switch (refinement->phase) {
	case T_LED_SYNC_SEARCH_PHASE_INIT: return;
	case T_LED_SYNC_SEARCH_PHASE_FIND_INITIAL_OFFSET: {
		handle_find_initial_offset(refinement);
		break;
	}
	case T_LED_SYNC_SEARCH_PHASE_FIND_RIGHT_EDGE: {
		handle_find_right_edge(refinement);
		break;
	}
	case T_LED_SYNC_SEARCH_PHASE_FIND_LEFT_EDGE: {
		handle_find_left_edge(refinement);
		break;
	}
	case T_LED_SYNC_SEARCH_PHASE_REFINE_BLINK_DURATION: {
		handle_refine_blink_duration(refinement);
		break;
	}
	case T_LED_SYNC_SEARCH_PHASE_MAINTAIN_OFFSET: {
		handle_maintain_offset(refinement);
		break;
	}
	default: break;
	}
}

static void
handle_constellation_sample_locked(struct t_led_sync_refinement *refinement,
                                   const struct t_constellation_tracker_sample *sample)
{
	refinement->frames_since_last_visually_seen = 0;
}

/*
 * Exported functions
 */

int
t_led_sync_refinement_init(struct t_led_sync_refinement *refinement,
                           const struct t_led_sync_refinement_options *options)
{
	int result;

	(*refinement) = XRT_C11_COMPOUND(struct t_led_sync_refinement){
	    .initialized = false,

	    .log_level = debug_get_log_option_led_sync_refinement_log(),

	    .options = *options,

	    .has_exposure_time = false,
	    .exposure_time_ns = 0,

	    .exposure_interval_ns = 0,

	    // Push an initial sample
	    .has_sample_for_driver = true,
	    .sample_for_driver =
	        {
	            .device_host_latency_ns = 0,
	            .fudge_offset_ns = 0,
	            .blink_duration_ns = options->initial_blink_duration_ns,
	        },
	    .sample_applied = false,

	    .phase = T_LED_SYNC_SEARCH_PHASE_INIT,
	    .current_latency_offset_ns = 0,
	    .current_blink_fudge_ns = 0,
	    .current_blink_duration_ns = options->initial_blink_duration_ns,
	    .frames_since_last_visually_seen = 0,
	    .current_sequence_id = 0,
	    .last_sample_apply_time_ns = 0,

	    .found_left_edge_ns = -1,
	    .found_right_edge_ns = -1,

	    .binary_search_state =
	        {
	            .left_bound_ns = 0,
	            .right_bound_ns = 0,
	        },
	};

	result = os_mutex_init(&refinement->lock);
	if (result < 0) {
		return result;
	}

	refinement->initialized = true;

	u_var_add_root(refinement, "LED Sync Refinement", true);

	u_var_add_gui_header(refinement, NULL, "Options");
	u_var_add_ro_i32(refinement, (int32_t *)&refinement->options.flags, "flags");
	u_var_add_ro_i64_ns(refinement, &refinement->options.initial_blink_duration_ns, "initial_blink_duration_ns");
	u_var_add_ro_i64_ns(refinement, &refinement->options.min_blink_duration_ns, "min_blink_duration_ns");
	u_var_add_ro_i64_ns(refinement, &refinement->options.max_blink_duration_ns, "max_blink_duration_ns");
	u_var_add_ro_i64_ns(refinement, &refinement->options.time_to_resync_ns, "time_to_resync_ns");
	u_var_add_ro_u32(refinement, &refinement->options.settle_frames, "settle_frames");

	u_var_add_gui_header(refinement, NULL, "Exposure Info");
	u_var_add_bool(refinement, &refinement->has_exposure_time, "Has Exposure Time");
	u_var_add_ro_i64_ns(refinement, &refinement->exposure_time_ns, "Exposure Time (ns)");
	u_var_add_ro_i64_ns(refinement, &refinement->exposure_interval_ns, "Exposure Interval (ns)");

	u_var_add_gui_header(refinement, NULL, "Driver Sample");
	u_var_add_bool(refinement, &refinement->has_sample_for_driver, "Has Sample For Driver");
	u_var_add_ro_i64_ns(refinement, &refinement->sample_for_driver.fudge_offset_ns, "Sample Fudge Offset (ns)");
	u_var_add_ro_i64_ns(refinement, &refinement->sample_for_driver.blink_duration_ns, "Sample Blink Duration (ns)");

	u_var_add_gui_header(refinement, NULL, "State");
	u_var_add_ro_u32(refinement, (uint32_t *)&refinement->phase, "phase");
	u_var_add_ro_i64_ns(refinement, &refinement->current_latency_offset_ns, "current_latency_offset_ns");
	u_var_add_ro_i64_ns(refinement, &refinement->current_blink_fudge_ns, "current_blink_fudge_ns");
	u_var_add_ro_i64_ns(refinement, &refinement->current_blink_duration_ns, "current_blink_duration_ns");
	u_var_add_ro_u32(refinement, &refinement->frames_since_last_visually_seen, "frames_since_last_visually_seen");
	u_var_add_ro_u32(refinement, &refinement->current_sequence_id, "current_sequence_id");
	u_var_add_ro_i64_ns(refinement, &refinement->last_sample_apply_time_ns, "last_sample_apply_time_ns");
	u_var_add_ro_i64_ns(refinement, &refinement->found_left_edge_ns, "found_left_edge_ns");
	u_var_add_ro_i64_ns(refinement, &refinement->found_right_edge_ns, "found_right_edge_ns");
	u_var_add_ro_i64_ns(refinement, &refinement->binary_search_state.left_bound_ns, "binary_search.left_bound_ns");
	u_var_add_ro_i64_ns(refinement, &refinement->binary_search_state.right_bound_ns,
	                    "binary_search.right_bound_ns");

	return 0;
}

void
t_led_sync_refinement_destroy(struct t_led_sync_refinement *refinement)
{
	if (!refinement->initialized) {
		return;
	}

	os_mutex_destroy(&refinement->lock);
}

void
t_led_sync_push_timing_event(struct t_led_sync_refinement *refinement,
                             const struct t_timing_event_camera_exposure_start *event)
{
	assert(refinement->initialized);

	os_mutex_lock(&refinement->lock);
	handle_timing_event_locked(refinement, event);
	os_mutex_unlock(&refinement->lock);
}

void
t_led_sync_push_constellation_sample(struct t_led_sync_refinement *refinement,
                                     const struct t_constellation_tracker_sample *sample)
{
	assert(refinement->initialized);

	os_mutex_lock(&refinement->lock);
	handle_constellation_sample_locked(refinement, sample);
	os_mutex_unlock(&refinement->lock);
}

bool
t_led_sync_get_sample(struct t_led_sync_refinement *refinement, struct t_led_sync_sample *out_sample)
{
	assert(refinement->initialized);

	os_mutex_lock(&refinement->lock);
	{
		if (!refinement->has_sample_for_driver) {
			os_mutex_unlock(&refinement->lock);
			return false;
		}

		*out_sample = refinement->sample_for_driver;
		refinement->has_sample_for_driver = false;
	}
	os_mutex_unlock(&refinement->lock);

	return true;
}

void
t_led_sync_mark_latest_sample_applied(struct t_led_sync_refinement *refinement, timepoint_ns apply_time_ns)
{
	assert(refinement->initialized);

	os_mutex_lock(&refinement->lock);
	refinement->sample_applied = true;
	refinement->last_sample_apply_time_ns = apply_time_ns;
	os_mutex_unlock(&refinement->lock);
}

void
t_led_sync_update_minimum_blink_time(struct t_led_sync_refinement *refinement, time_duration_ns new_minimum_ns)
{
	assert(refinement->initialized);

	os_mutex_lock(&refinement->lock);
	{
		refinement->options.min_blink_duration_ns = new_minimum_ns;
		LOG_TRACE(refinement, "Updated minimum blink duration to: %" PRId64 "ns", new_minimum_ns);

		if (refinement->current_blink_duration_ns < refinement->options.min_blink_duration_ns) {
			set_offset_duration(refinement, refinement->current_latency_offset_ns,
			                    refinement->current_blink_fudge_ns,
			                    refinement->options.min_blink_duration_ns);

			LOG_DEBUG(refinement, "Current blink duration was below minimum, updated to: %" PRId64 "ns",
			          refinement->options.min_blink_duration_ns);
		}
	}
	os_mutex_unlock(&refinement->lock);
}
