"""
Inscoper API Example Script: Triggered Acquisition
==================================================
This script performs a hardware-triggered time-series acquisition.

Configuration Prerequisites:
---------------------------
1. Environment Variable: Ensure 'InscoperConfigsPath' is defined in your environment
   and points to your Inscoper configurations folder.
2. Configuration Name: This script assumes a hardware configuration named "triggered_camera_and_microscope_and_fluo_source".
   Change it below if your configuration is named differently.
3. Device/Channel Names: Customize device and trigger names below.
"""
import os
import time
import numpy as np
import matplotlib.pyplot as plt
import inscoper_api


# --8<-- [start:main_logic]


class CustomImageListener(inscoper_api.ImageListener):
    """
    A custom image listener that receives images from the bridge as they are acquired.
    It tracks and prints the elapsed time between consecutive frames (Delta t)
    and the total elapsed time since the acquisition started, which is crucial
    for verifying the hardware timing accuracy in triggered workflows.
    """
    def __init__(self):
        super().__init__()
        self._received_images = []
        self._image_counter = 0
        self._start_time = None
        self._last_image_time = None

    def handleNewImage(self, image):
        current_time = time.time()
        if self._start_time is None:
            self._start_time = current_time
            self._last_image_time = current_time
            delta_t = 0.0
            total_time = 0.0
        else:
            delta_t = current_time - self._last_image_time
            total_time = current_time - self._start_time
            self._last_image_time = current_time

        print(f"Image {self._image_counter} received! (Delta t: {delta_t:.3f}s, Total time: {total_time:.3f}s)")
        self._image_counter += 1
        self._received_images.append(image)

    def getReceivedImages(self):
        return self._received_images

# ── Step 1: Initialize the API bridge and load hardware configuration
# Macro to all Inscoper configurations
CONFIG_PATH = os.environ['InscoperConfigsPath']

my_bridge = inscoper_api.Bridge()
my_listener = CustomImageListener()
my_bridge.addImageListener(my_listener)


# Read and load the specific hardware configuration designed for triggered acquisitions
config_name = "triggered_camera_and_microscope_and_fluo_source"
my_bridge.readConfigFile(os.sep.join([CONFIG_PATH, config_name]))
my_bridge.loadConfigFile(os.sep.join([CONFIG_PATH, config_name]))

# Initialize all loaded physical and virtual devices
my_bridge.initDevices()


# ── Step 2: Start the camera in external trigger mode
# To perform triggered acquisitions, the camera must be started with the 'EXTERNAL_PARAMS' tag.
# In this mode, the camera awaits level-trigger pulses to control its exposure time.
start_cam_status = inscoper_api.Status()
start_cam_status.addTag("MyCamera")
start_cam_status.addTag("EXTERNAL_PARAMS")

my_bridge.loadSequence(0, "StartCamera", [start_cam_status])
my_bridge.runSequence(0, False)
print("Camera started in external trigger mode.")


# ── Step 3: Create and configure a virtual Time Device
# Time series acquisitions require precise, hardware-timed intervals between frames.
# This is achieved by creating a virtual Time Device. The recipe.xml must define matching 
# 'StartTimerRecipeId' and 'WaitTimerRecipeId' timer recipes to drive the hardware timer.
time_index = 0
my_bridge.createTimeDevice(time_index, "StartTimerRecipeId", "WaitTimerRecipeId")

# Retrieve the sub-device handle associated with the wait timer for the configured time index.
wait_timer_sub_device_id = my_bridge.getWaitTimerSubDeviceId(time_index)


# ── Step 4: Define the time-series sequence via frame Status objects
# We construct a list of Status states, where each Status defines the parameters
# and tags required for a single frame in the time-series acquisition.
time_series_status = []
number_of_frames = 10

for i in range(number_of_frames):
    frame_status = inscoper_api.Status()
    
    # Tag sequence boundaries to manage timer activation correctly
    if i == 0:
        frame_status.addTag("SequenceStart")
    elif i == number_of_frames - 1:
        frame_status.addTag("SequenceEnd")

    # Set exposure time (LevelTrigger parameter) on the camera trigger sub-device.
    # Unit: microseconds. Here, '20000' µs corresponds to a 20 ms exposure.
    frame_status.addParam(inscoper_api.SubDeviceId("MyCameraTrigger", "LevelTrigger"), "20000")

    # Set the wait interval between frames on the virtual wait timer sub-device.
    # Unit: microseconds. Here, '1000000' µs corresponds to a 1.0 second interval.
    frame_status.addParam(wait_timer_sub_device_id, "1000000")
    
    # Tag to reset the hardware timer at the beginning of this frame's recipe cycle
    frame_status.addTag("ResetRelativeTimer")
    
    # Tag the specific camera device and indicate that it uses external level triggering
    frame_status.addTag("MyCamera")
    frame_status.addTag("EXTERNAL_PARAMS")

    time_series_status.append(frame_status)


# ── Step 5: Load and run the acquisition sequence
# Load the time-series statuses into the "Acquisition" recipe index.
# The bridge handles the synchronous execution of this sequence, ensuring precise timing.
my_bridge.loadSequence(1, "Acquisition", time_series_status)

print("Executing acquisition sequence...")
t0 = time.time()
my_bridge.runSequence(1, False)
t1 = time.time()
print("Acquisition sequence executed successfully.")
print(f"Total hardware execution time: {t1 - t0:.3f} seconds.")


# ── Step 6: Retrieve and display the acquired images
received_images = my_listener.getReceivedImages()

if received_images:
    N = len(received_images)
    cols = int(np.ceil(np.sqrt(N)))
    rows = int(np.ceil(N / cols))
    
    fig, axes = plt.subplots(rows, cols, figsize=(cols * 3, rows * 3))
    if N == 1:
        axes = np.array([axes])
    else:
        axes = axes.flatten()
        
    for idx, image in enumerate(received_images):
        image_data = image.getImageData()
        image_width = image.getWidth()
        image_height = image.getHeight()
        pixel_size_bytes = image.getPixelSize()
        bit_depth = image.getBitDepth()
        bytes_per_value = bit_depth // 8
        values_per_pixel = pixel_size_bytes // bytes_per_value
        img = np.reshape(image_data, (image_height, image_width, values_per_pixel))
        
        ax = axes[idx]
        ax.imshow(img[:, :, 0], cmap="gray")
        ax.set_title(f"Frame {idx}")
        ax.axis("off")
        
    # Hide any unused subplots in the plotting grid
    for idx in range(N, len(axes)):
        axes[idx].axis("off")
        
    plt.tight_layout()
    plt.show()
else:
    print("No images received to plot.")

# --8<-- [end:main_logic]

my_bridge.close()
