"""
Inscoper API Example Script: Define Channels
============================================
This script demonstrates how to define channels (imaging states) and load/run them.

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 "camera_and_microscope_and_fluo_source".
   Change it below if your configuration is named differently.
3. Device/Channel Names: Customize device and optical component names below.
"""
import os
from typing import Dict
from dataclasses import dataclass
import numpy as np
import time
import inscoper_api


@dataclass
class SubDevice:
    name: str = None # a descriptive string for the sub-device
    tag: int = None # a unique identifier within the device for this sub-device
    inscoper_type: int = None # a "purpose" type for the sub-device (what it does, e.g., CUBE, OBJECTIVE, SHUTTER...)

@dataclass
class Device:
    name: str # the name given to the device during the configuration step
    hardware_id: int # an identifier for this device model
    sub_device_map: Dict[int, SubDevice] # the collection of chosen sub-devices for this particular device instance

# Macro to all Inscoper configurations
CONFIG_PATH = os.environ['InscoperConfigsPath']

my_bridge = inscoper_api.Bridge()

config = my_bridge.readConfigFile(os.sep.join([CONFIG_PATH, "camera_and_microscope_and_fluo_source"]))
my_bridge.loadConfigFile(os.sep.join([CONFIG_PATH, "camera_and_microscope_and_fluo_source"]))
my_bridge.initDevices()

# Dictionary to store the mapping of hardware IDs to our Device objects.
device_map = {}

# Step 0: Iterate over the "Device List" from the configuration. 
# This tells us what specific devices have been added to this system's configuration.
for device in config.getDeviceList():
    driver_config = device.getDriverConfig()
    
    # In this example, we only keep devices controlled by the Inscoper Device Controller.
    if isinstance(driver_config, inscoper_api.InscoperBoxDriverConfig):
        device_name = device.getId() # e.g., "My Microscope" or "My Stage"
        device_id = driver_config.getHardwareId() # The internal hardware identifier

        sub_device_map = {}
        
        # Iterate over all sub-devices configured for this device.
        for sub_device_config in device.getSubDeviceConfigList():
            sub_device_tag = sub_device_config.getTag() # The unique tag will allow us to connect config and description
            sub_device_id = sub_device_config.getId() # Descriptive name of the sub-device
            
            # Create our SubDevice instance and map it by its tag.
            sub_device_map[sub_device_tag] = SubDevice(name=sub_device_id, tag=sub_device_tag)
            
        # Store the complete Device object (with its sub-devices) in our global map.
        device_map[device_id] = Device(name=device_name, hardware_id=device_id, sub_device_map=sub_device_map)

# At this point, device_map contains our devices, but we are missing the "purpose" of each sub-device.
print(device_map)

# Step 0: Iterate over the "Device Description List".
# This information acts as the "class definitions" (descriptions.xml) for all possible hardware supported by Inscoper,
# providing metadata like the generic type (purpose) of the sub-devices.
for device_desc in my_bridge.getDeviceDescriptionList():

    driver_desc = device_desc.getDriverDescription()
    
    # Again, we only keep devices controlled by the Inscoper Device Controller.
    if isinstance(driver_desc, inscoper_api.InscoperBoxDriverDescription):
        device_id = driver_desc.getHardwareId()
        
        # If this hardware_id matches one of the devices we found in our configuration...
        if device_id in device_map:
            current_device = device_map[device_id]
            sub_device_map = current_device.sub_device_map
            
            # Look at all possible sub-devices described for this hardware model.
            for sub_device_desc in device_desc.getSubDeviceDescriptionList():
                sub_device_tag = sub_device_desc.getTag()
                sub_device_type = sub_device_desc.getType() # The general purpose of the sub-device (e.g., CUBE, SHUTTER, etc.)
                
                # If this configured device actually uses this specific sub-device...
                if sub_device_tag in sub_device_map:
                    current_sub_device = sub_device_map[sub_device_tag]
                    
                    # Store the human-readable string of the sub-device type 
                    current_sub_device.inscoper_type = inscoper_api.Utils.subDeviceTypeToString(sub_device_type)

# --8<-- [start:main_logic]

# ── Step 1: Resolve the sub-device IDs for every optical component of interest ──────────────
# We need a typed handle (SubDeviceId) for each hardware component that will be controlled
# during channel switching. These handles uniquely identify a sub-device by combining the
# parent device name with the sub-device name, matching exactly what the Inscoper runtime expects.
# All variables start as None and are filled in during the scan below.
objective_turret_sub_device_id = None   # Turret that selects the objective
cube_turret_sub_device_id = None        # Filter-cube turret
tl_power_sub_device_id = None           # Power controller for the transmitted light
tl_shutter_sub_device_id = None         # Shutter inside the microscope for the transmitted light
epi_shutter_sub_device_id = None        # Shutter inside the microscope for the epi-fluorescence
led_blue_power_sub_device_id = None     # Power controller for the blue (440nm) LED
led_blue_shutter_sub_device_id = None   # Shutter inside the light source for the blue (440nm) LED

# Walk every configured device and its sub-devices.
# For each sub-device we match on two criteria:
#   - its functional type  (e.g., "SHUTTER", "OBJECTIVE", "CUBE")
#   - a keyword in its name that distinguishes the sub-device (to adapt to your system)
for device_id, device in device_map.items():
    for sub_device_tag, sub_device in device.sub_device_map.items():
        if sub_device.inscoper_type == "SHUTTER" and "TL" in sub_device.name:
            # Transmitted-light shutter: type is SHUTTER and the name contains "TL"
            tl_shutter_sub_device_id = inscoper_api.SubDeviceId(device.name, sub_device.name)
        elif sub_device.inscoper_type == "SHUTTER" and "IL" in sub_device.name and "Down" in sub_device.name:
            # Epi-fluorescence (incident-light) shutter: type is SHUTTER, path is "IL Down"
            epi_shutter_sub_device_id = inscoper_api.SubDeviceId(device.name, sub_device.name)
        elif sub_device.inscoper_type == "OBJECTIVE":
            # Objective turret: uniquely identified by the "OBJECTIVE" type
            objective_turret_sub_device_id = inscoper_api.SubDeviceId(device.name, sub_device.name)
        elif sub_device.inscoper_type == "CUBE" and "Down" in sub_device.name:
            # Filter-cube turret in the epi (downward) light path
            cube_turret_sub_device_id = inscoper_api.SubDeviceId(device.name, sub_device.name)
        elif ("Intensity" in sub_device.name or "Power" in sub_device.name) and "TL" in sub_device.name:
            # Transmitted-light intensity: name contains "Intensity" or "Power" AND "TL"
            tl_power_sub_device_id = inscoper_api.SubDeviceId(device.name, sub_device.name)
        elif ("Intensity" in sub_device.name or "Power" in sub_device.name) and "440" in sub_device.name:
            # 440 nm LED power: name contains "Intensity" or "Power" AND the wavelength "440"
            led_blue_power_sub_device_id = inscoper_api.SubDeviceId(device.name, sub_device.name)
        elif sub_device.inscoper_type == "SHUTTER" and "440" in sub_device.name:
            # 440 nm LED shutter: type is SHUTTER and the wavelength "440" appears in the name
            led_blue_shutter_sub_device_id = inscoper_api.SubDeviceId(device.name, sub_device.name)

# Diagnostic printout: verify that all expected sub-device handles were resolved.
# Any None value here indicates that the config does not expose that component,
# which would cause the channel definitions below to include a None key.
print(f"Objective turret sub device ID: {objective_turret_sub_device_id}")
print(f"Cube turret sub device ID: {cube_turret_sub_device_id}")
print(f"TL light power sub device ID: {tl_power_sub_device_id}")
print(f"TL shutter sub device ID: {tl_shutter_sub_device_id}")
print(f"Epi shutter sub device ID: {epi_shutter_sub_device_id}")
print(f"LED blue power sub device ID: {led_blue_power_sub_device_id}")
print(f"LED blue shutter sub device ID: {led_blue_shutter_sub_device_id}")

# ── Step 2: Declare the hardware state for each imaging channel ───────────────────────────────
# A channel describes the optical path, it is associated with a state of the system. It is described
# as a dictionary mapping each sub-device handle to its required value.

# Transmitted-light (brightfield) channel:
#   - Objective position 0  (e.g., 10× brightfield objective)
#   - Cube slot 0           (empty / BF cube, no fluorescence filter)
#   - TL intensity at 30%   (moderate illumination for brightfield)
#   - TL shutter OPEN  (1)  / Epi shutter CLOSED (0)
#   - Blue LED OFF          (power 0, shutter 0)
TL_channel_description = {objective_turret_sub_device_id: 0,
             cube_turret_sub_device_id: 0,
             tl_power_sub_device_id: 30,
             tl_shutter_sub_device_id: 1,
             epi_shutter_sub_device_id: 0,
             led_blue_power_sub_device_id: 0,
             led_blue_shutter_sub_device_id: 0
             }

# Blue fluorescence channel (440 nm excitation):
#   - Objective position 1  (e.g., 20× fluorescence objective)
#   - Cube slot 2           (fluorescence cube matching the 440 nm excitation / emission)
#   - TL shutter CLOSED (0) / TL power 0  → transmitted light is off
#   - Epi shutter OPEN  (1) → epi-fluorescence path is active
#   - Blue LED at full power (100) with its shutter OPEN (1)
blue_channel_description = {objective_turret_sub_device_id: 1,
             cube_turret_sub_device_id: 2,
             tl_power_sub_device_id: 0,
             tl_shutter_sub_device_id: 0,
             epi_shutter_sub_device_id: 1,
             led_blue_power_sub_device_id: 100,
             led_blue_shutter_sub_device_id: 1}

# "Off" (safe) channel: every light source and shutter is driven to its inactive state.
# This is used as a safe park position between experiments or at the end of a run
# to avoid unnecessary sample exposure or hardware wear.
off_channel = {objective_turret_sub_device_id: 0,
             cube_turret_sub_device_id: 0,
             tl_power_sub_device_id: 0,
             tl_shutter_sub_device_id: 0,
             epi_shutter_sub_device_id: 0,
             led_blue_power_sub_device_id: 0,
             led_blue_shutter_sub_device_id: 0}

# ── Step 3: Build Status objects from the channel descriptions ───────────────────────────────
# inscoper_api.Status is the representation of a complete hardware state snapshot.
# Each (sub_device_id, value) pair is registered via addParam() to the Status object.

TL_status = inscoper_api.Status()
for sub_device_id, value in TL_channel_description.items():
    TL_status.addParam(sub_device_id, value)

blue_status = inscoper_api.Status()
for sub_device_id, value in blue_channel_description.items():
    blue_status.addParam(sub_device_id, value)

off_status = inscoper_api.Status()
for sub_device_id, value in off_channel.items():
    off_status.addParam(sub_device_id, value)

# ── Step 4: Register the channels as three sequences ─────────────────────
# After loading, the bridge holds three addressable sequences:
#   index 0 → brightfield (TL) channel
#   index 1 → blue fluorescence channel
#   index 2 → all-off (safe park)
my_bridge.loadSequence(0, "DeviceUpdate", [TL_status])
my_bridge.loadSequence(1, "DeviceUpdate", [blue_status])
my_bridge.loadSequence(2, "DeviceUpdate", [off_status])

# ── Step 5: Run a two-cycle acquisition alternating between channels ─────────────────────────
# dispatching the hardware command, without waiting for motion/shutter settling.
# A 5-second sleep is inserted between channel switches to give the hardware time to stabilize
# before the next acquisition would be triggered by an external imaging call.
for i in range(0,2):
    my_bridge.runSequence(0, False)   # Switch to brightfield and acquire (external trigger)
    time.sleep(5)                     # Wait for the microscope to settle in TL mode
    my_bridge.runSequence(1, False)   # Switch to blue fluorescence and acquire
    time.sleep(5)                     # Wait for the microscope to settle in fluorescence mode

# ── Step 6: Park the microscope in the safe "off" state ──────────────────────────────────────
# After the acquisition loop, drive all channels to their inactive setpoints.
# This closes every shutter and disables every light source, protecting the sample
# from prolonged illumination and putting the hardware in a known idle state.
my_bridge.runSequence(2, False)
# --8<-- [end:main_logic]

my_bridge.close()
