gled 2.28.4

gled is an application for creating animations and effects on artnet or dmx installations
use crate::{
    midi::normalize_controller_key,
    storage::asset::midi_controller::{MidiInputAction, MidiSceneTarget},
    ui::action::UiAction,
};
use std::collections::HashMap;

use super::{
    ActiveRuntimeMapping, MidiRuntimeSnapshot, RuntimeControllerKey,
    scene_state::scene_target_to_union,
};

fn distributed_scene_index_from_midi(value: u8, scene_count: usize) -> Option<usize> {
    if scene_count == 0 {
        return None;
    }

    Some((((usize::from(value) + 1) * scene_count).saturating_sub(1)) / 128)
}

pub(super) fn handle_input_from_snapshot(
    snapshot: &MidiRuntimeSnapshot,
    port_name: &str,
    message: &[u8],
) -> bool {
    if message.len() != 3 {
        return false;
    }

    let mapping = resolve_mapping_for_port(&snapshot.mappings_by_controller, port_name);
    let Some(mapping) = mapping else {
        return false;
    };

    let status = message[0];
    let data1 = message[1];
    let value = message[2];
    let mut handled_any = false;

    for binding in &mapping.mapping.input_bindings {
        if !trigger_matches(&binding.trigger, status, data1) {
            continue;
        }

        let action = match binding.action {
            MidiInputAction::Tap => Some(UiAction::Tap),
            MidiInputAction::SetMainDimmer => {
                Some(UiAction::SetMainDimmer(f32::from(value) / 127.0))
            }
            MidiInputAction::SetBlackout => Some(UiAction::SetBlackout(value > 0)),
            MidiInputAction::SetSpeedAdd => {
                let delta = if value > 64 {
                    f32::from(value) - 128.0
                } else {
                    f32::from(value)
                };
                Some(UiAction::SpeedAdd(delta))
            }
            MidiInputAction::SetSpeedMultiply => {
                Some(UiAction::SpeedMultiply(if value > 63 { 2.0 } else { 0.5 }))
            }
            MidiInputAction::SelectSceneDistributed => {
                distributed_scene_index_from_midi(value, snapshot.scene_locations_row_major.len())
                    .and_then(|index| snapshot.scene_locations_row_major.get(index).copied())
                    .map(UiAction::SelectSceneByLocation)
            }
            MidiInputAction::SelectScene { ref target } => match target {
                MidiSceneTarget::Selected => None,
                _ => scene_target_to_union(target).map(UiAction::SelectScene),
            },
            MidiInputAction::ToggleSceneActive { ref target } => {
                scene_target_to_union(target).map(UiAction::ToggleSceneActive)
            }
            MidiInputAction::SetSceneActive { ref target } => scene_target_to_union(target)
                .map(|target| UiAction::SetSceneActive(target, value > 0)),
            MidiInputAction::FlashScene { ref target } => scene_target_to_union(target)
                .map(|target| UiAction::SetSceneFlash(target, value > 0)),
            MidiInputAction::SetSceneOpacity { ref target } => scene_target_to_union(target)
                .map(|target| UiAction::SetSceneOpacity(target, f32::from(value) / 127.0)),
            MidiInputAction::SetSceneInputDimmer { ref target } => scene_target_to_union(target)
                .map(|target| UiAction::SetSceneInputDimmer(target, f32::from(value) / 127.0)),
            MidiInputAction::SetSceneBeatOffset { ref target } => scene_target_to_union(target)
                .map(|target| UiAction::SetSceneBeatOffset(target, f32::from(value) / 127.0)),
            MidiInputAction::SetSceneIgnoreMainDimmer { ref target } => {
                scene_target_to_union(target)
                    .map(|target| UiAction::SetSceneIgnoreMainDimmer(target, value > 63))
            }
            MidiInputAction::SetSceneSetOffsetOnFlash { ref target } => {
                scene_target_to_union(target)
                    .map(|target| UiAction::SetSceneSetOffsetOnFlash(target, value > 63))
            }
            MidiInputAction::SetSceneEffectSettingF32 {
                ref target,
                effect_index,
                setting_index,
            } => scene_target_to_union(target).map(|target| {
                UiAction::SetSceneEffectAnimationConfigF32(
                    target,
                    effect_index as usize,
                    setting_index as usize,
                    f32::from(value) / 127.0,
                )
            }),
            MidiInputAction::SetSceneEffectOpacity {
                ref target,
                effect_index,
            } => scene_target_to_union(target).map(|target| {
                UiAction::SetSceneEffectOpacity(
                    target,
                    effect_index as usize,
                    f32::from(value) / 127.0,
                )
            }),
            MidiInputAction::SetSceneEffectColorShift {
                ref target,
                effect_index,
            } => scene_target_to_union(target).map(|target| {
                UiAction::SetSceneEffectColorShift(
                    target,
                    effect_index as usize,
                    f32::from(value) / 127.0,
                )
            }),
            MidiInputAction::SetSceneEffectBeatProgression {
                ref target,
                effect_index,
            } => scene_target_to_union(target).map(|target| {
                UiAction::SetSceneEffectBeatProgression(
                    target,
                    effect_index as usize,
                    f32::from(value) / 127.0,
                )
            }),
            MidiInputAction::SetSceneEffectBeatOffset {
                ref target,
                effect_index,
            } => scene_target_to_union(target).map(|target| {
                UiAction::SetSceneEffectBeatOffset(
                    target,
                    effect_index as usize,
                    f32::from(value) / 127.0,
                )
            }),
            MidiInputAction::SetSceneEffectSpeedExponent {
                ref target,
                effect_index,
            } => scene_target_to_union(target).map(|target| {
                UiAction::SetSceneEffectSpeedExponent(
                    target,
                    effect_index as usize,
                    i32::from(value) - 63,
                )
            }),
            MidiInputAction::SetSceneEffectGroupIndex {
                ref target,
                effect_index,
            } => scene_target_to_union(target).map(|target| {
                UiAction::SetSceneEffectGroupIndex(target, effect_index as usize, value as usize)
            }),
        };

        if let Some(action) = action {
            action.enqueue();
            handled_any = true;
        }
    }

    handled_any
}

fn trigger_matches(
    trigger: &crate::storage::asset::midi_controller::MidiTrigger,
    status: u8,
    data1: u8,
) -> bool {
    if trigger.status != status {
        return false;
    }

    if trigger.match_data1 && trigger.data1 != data1 {
        return false;
    }

    true
}

fn resolve_mapping_for_port(
    mappings_by_controller: &HashMap<RuntimeControllerKey, ActiveRuntimeMapping>,
    port_name: &str,
) -> Option<ActiveRuntimeMapping> {
    // Prefer exact match, then fall back to normalized match (e.g. ALSA names with numeric suffixes).
    let normalized_port_name = normalize_controller_key(port_name);
    mappings_by_controller.iter().find_map(|(key, mapping)| {
        if matches!(
            key,
            RuntimeControllerKey::PortName(name)
                if name == port_name || normalize_controller_key(name) == normalized_port_name
        ) {
            Some(mapping.clone())
        } else {
            None
        }
    })
}

#[cfg(test)]
mod tests {
    use super::distributed_scene_index_from_midi;

    #[test]
    fn distributed_scene_index_spans_full_range() {
        assert_eq!(distributed_scene_index_from_midi(0, 8), Some(0));
        assert_eq!(distributed_scene_index_from_midi(63, 8), Some(3));
        assert_eq!(distributed_scene_index_from_midi(64, 8), Some(4));
        assert_eq!(distributed_scene_index_from_midi(127, 8), Some(7));
    }

    #[test]
    fn distributed_scene_index_handles_empty_scene_list() {
        assert_eq!(distributed_scene_index_from_midi(0, 0), None);
    }
}