gled 2.28.4

gled is an application for creating animations and effects on artnet or dmx installations
use crate::output_state::ProjectState;
use crate::storage::asset::scene::color::SceneInstanceColor;
use crate::storage::asset::scene::grid::GridLocation;
use egui::mutex::Mutex;
use kanal::{Receiver, Sender, unbounded};
use once_cell::sync::{Lazy, OnceCell};
use std::collections::{HashMap, HashSet};

static SENDER: OnceCell<Sender<MidiState>> = OnceCell::new();
static SENDERS: Lazy<Mutex<Vec<Sender<MidiState>>>> = Lazy::new(|| Mutex::new(Vec::new()));

#[derive(Debug, Default, Clone, PartialEq)]
pub struct MidiState {
    pub blackout: bool,
    pub beat_progression: f32,
    pub beats_per_minute: f32,
    pub beat_flank: u8,
    pub main_dimmer: f32,
    pub highlighted_row: Option<usize>,
    pub highlighted_col: Option<usize>,
    pub selected_scene_location: GridLocation,
    pub active_scenes: HashSet<GridLocation>,
    pub flashed_scenes: HashSet<GridLocation>,
    pub available_scenes_grid: HashMap<GridLocation, SceneInstanceColor>,
    pub selected_scene_color: SceneInstanceColor,
    pub selected_scene_opacity: f32,
    pub selected_scene_input_dimmer: f32,
    pub selected_scene_beat_offset: f32,
    pub selected_scene_ignore_main_dimmer: bool,
    pub selected_scene_set_offset_on_flash: bool,
    pub scene_input_dimmer: HashMap<GridLocation, f32>,
    pub scene_opacity: HashMap<GridLocation, f32>,
    pub scene_beat_offset: HashMap<GridLocation, f32>,
    pub scene_ignore_main_dimmer: HashMap<GridLocation, bool>,
    pub scene_set_offset_on_flash: HashMap<GridLocation, bool>,
    pub scene_effect_setting_f32: HashMap<(GridLocation, usize, usize), f32>,
    pub scene_effect_opacity: HashMap<(GridLocation, usize), f32>,
    pub scene_effect_color_shift: HashMap<(GridLocation, usize), f32>,
    pub scene_effect_beat_progression: HashMap<(GridLocation, usize), f32>,
    pub scene_effect_beat_offset: HashMap<(GridLocation, usize), f32>,
    /// Speed exponent normalized to 0.0–1.0 (maps i32 -63..+64 → 0.0..1.0 via (v+63)/127).
    pub scene_effect_speed_exponent: HashMap<(GridLocation, usize), f32>,
    /// Group index stored as f32 (raw usize, not normalized — use as-is for round-trip).
    pub scene_effect_group_index: HashMap<(GridLocation, usize), f32>,
}
impl Eq for MidiState {}

impl MidiState {
    fn beat_flank_from_progression(beat_progression: f32) -> u8 {
        ((beat_progression.rem_euclid(1.0) * 4.0).floor() as u8).min(4)
    }

    fn float_value_to_f32(
        value: &crate::storage::asset::animation::config::float_value::FloatValue,
    ) -> f32 {
        match value {
            crate::storage::asset::animation::config::float_value::FloatValue::F32(value) => *value,
            crate::storage::asset::animation::config::float_value::FloatValue::Percentage(
                curve,
            ) => curve.multiplier,
            crate::storage::asset::animation::config::float_value::FloatValue::Degrees(curve) => {
                curve.multiplier
            }
        }
    }

    fn from_project_state(state: &ProjectState) -> Self {
        let mut scene_input_dimmer = HashMap::new();
        let mut scene_opacity = HashMap::new();
        let mut scene_beat_offset = HashMap::new();
        let mut scene_ignore_main_dimmer = HashMap::new();
        let mut scene_set_offset_on_flash = HashMap::new();
        let mut scene_effect_setting_f32 = HashMap::new();
        let mut scene_effect_opacity = HashMap::new();
        let mut scene_effect_color_shift = HashMap::new();
        let mut scene_effect_beat_progression = HashMap::new();
        let mut scene_effect_beat_offset = HashMap::new();
        let mut scene_effect_speed_exponent = HashMap::new();
        let mut scene_effect_group_index = HashMap::new();

        let project = state.project.as_ref();
        let (highlighted_row, highlighted_col) = if let Some(project) = project {
            match project.grid_highlight {
                crate::storage::asset::project::GridHighlight::Row => {
                    (Some(project.quick_row_index()), None)
                }
                crate::storage::asset::project::GridHighlight::Column => {
                    (None, Some(project.grid_width().saturating_sub(1)))
                }
                crate::storage::asset::project::GridHighlight::None => (None, None),
            }
        } else {
            (None, None)
        };

        if let Some(project) = project {
            for (location, scene_instance) in &project.scenes_instances_grid {
                scene_input_dimmer.insert(*location, scene_instance.input_dimmer);
                scene_opacity.insert(*location, scene_instance.opacity.multiplier);
                scene_beat_offset
                    .insert(*location, scene_instance.beat_progression_offset.multiplier);
                scene_ignore_main_dimmer.insert(*location, scene_instance.ignore_main_dimmer);
                scene_set_offset_on_flash.insert(*location, scene_instance.set_offset_on_flash);

                for (effect_index, effect) in scene_instance.scene.effects.iter().enumerate() {
                    let values = [
                        &effect.animation_config.float_0,
                        &effect.animation_config.float_1,
                        &effect.animation_config.float_2,
                        &effect.animation_config.float_3,
                        &effect.animation_config.float_4,
                        &effect.animation_config.float_5,
                    ];
                    for (setting_index, value) in values.into_iter().enumerate() {
                        scene_effect_setting_f32.insert(
                            (*location, effect_index, setting_index),
                            Self::float_value_to_f32(value),
                        );
                    }
                    scene_effect_opacity
                        .insert((*location, effect_index), effect.opacity.multiplier);
                    scene_effect_color_shift
                        .insert((*location, effect_index), effect.color_shift.multiplier);
                    scene_effect_beat_progression.insert(
                        (*location, effect_index),
                        effect.beat_progression.multiplier,
                    );
                    scene_effect_beat_offset.insert(
                        (*location, effect_index),
                        effect.beat_progression_offset.multiplier,
                    );
                    let speed_exp_norm = ((effect.speed_exponent as f32) + 63.0) / 127.0;
                    scene_effect_speed_exponent
                        .insert((*location, effect_index), speed_exp_norm.clamp(0.0, 1.0));
                    scene_effect_group_index
                        .insert((*location, effect_index), effect.group_index as f32);
                }
            }
        }

        let selected_scene = project.and_then(|project| {
            project
                .scenes_instances_grid
                .get(&state.selected_scene_instance)
        });

        MidiState {
            blackout: state.blackout,
            beat_progression: state.beat_progression,
            beats_per_minute: state.beats_per_minute,
            beat_flank: Self::beat_flank_from_progression(state.beat_progression),
            main_dimmer: project.map_or(1.0, |project| project.main_dimmer),
            highlighted_row,
            highlighted_col,
            selected_scene_location: state.selected_scene_instance,
            active_scenes: project.map_or_else(Default::default, |project| {
                project
                    .scenes_instances_grid
                    .iter()
                    .filter_map(
                        |(location, scene)| if scene.active { Some(*location) } else { None },
                    )
                    .collect()
            }),
            flashed_scenes: project.map_or_else(Default::default, |project| {
                project
                    .scenes_instances_grid
                    .iter()
                    .filter_map(
                        |(location, scene)| if scene.flash { Some(*location) } else { None },
                    )
                    .collect()
            }),
            available_scenes_grid: project.map_or_else(Default::default, |project| {
                project
                    .scenes_instances_grid
                    .iter()
                    .map(|(location, scene_instance)| (*location, scene_instance.color))
                    .collect()
            }),
            selected_scene_color: selected_scene
                .map_or(SceneInstanceColor::Black, |scene| scene.color),
            selected_scene_opacity: selected_scene.map_or(1.0, |scene| scene.opacity.multiplier),
            selected_scene_input_dimmer: selected_scene.map_or(1.0, |scene| scene.input_dimmer),
            selected_scene_beat_offset: selected_scene
                .map_or(0.0, |scene| scene.beat_progression_offset.multiplier),
            selected_scene_ignore_main_dimmer: selected_scene
                .is_some_and(|scene| scene.ignore_main_dimmer),
            selected_scene_set_offset_on_flash: selected_scene
                .is_some_and(|scene| scene.set_offset_on_flash),
            scene_input_dimmer,
            scene_opacity,
            scene_beat_offset,
            scene_ignore_main_dimmer,
            scene_set_offset_on_flash,
            scene_effect_setting_f32,
            scene_effect_opacity,
            scene_effect_color_shift,
            scene_effect_beat_progression,
            scene_effect_beat_offset,
            scene_effect_speed_exponent,
            scene_effect_group_index,
        }
    }
}

pub fn start() {
    #[cfg(feature = "profiling")]
    profiling::register_thread!("midi:state");

    let (sender, _receiver) = unbounded();
    SENDER.set(sender).expect("Could not set sender");

    let state_receiver = crate::output_state::new_receiver();

    let mut previous = MidiState::default();

    loop {
        let state = state_receiver
            .recv()
            .expect("Could not receive project state");
        let midi_state = MidiState::from_project_state(&state);

        if previous == midi_state {
            continue;
        }
        previous = midi_state.clone();
        SENDERS
            .lock()
            .retain(|sender| sender.send(midi_state.clone()).is_ok());
    }
}

pub fn new_receiver() -> Receiver<MidiState> {
    let (sender, receiver) = unbounded();
    SENDERS.lock().push(sender);
    receiver
}