gled 2.28.5

gled is an application for creating animations and effects on artnet or dmx installations
use crate::storage::asset::midi_controller::MidiController;
use crate::storage::{asset::midi_controller::MidiTrigger, asset_id::AssetId};
use kanal::{Receiver, Sender, unbounded};
use once_cell::sync::OnceCell;
use std::{
    collections::HashMap,
    sync::atomic::{AtomicBool, Ordering::Relaxed},
    time::{Duration, Instant},
};

static STREAM_ENABLED: AtomicBool = AtomicBool::new(false);
static LEARN_ACTIVE: AtomicBool = AtomicBool::new(false);
static SENDER: OnceCell<Sender<[u8; 3]>> = OnceCell::new();

#[derive(Clone, Copy)]
pub struct MidiLearnRequest {
    pub controller_id: AssetId<MidiController>,
    pub target: MidiLearnTarget,
}

#[derive(Clone)]
struct LearnSession {
    request: MidiLearnRequest,
    started_at: Instant,
    last_seen_at: Option<Instant>,
    stats: HashMap<(u8, u8), LearnStats>,
}

#[derive(Clone, Copy)]
struct LearnStats {
    count: u32,
    min_value: u8,
    max_value: u8,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum MidiLearnTarget {
    InputBinding(usize),
    OutputBinding(usize),
}

#[derive(Clone)]
pub struct MidiLearnCapture {
    pub controller_id: AssetId<MidiController>,
    pub target: MidiLearnTarget,
    pub trigger: MidiTrigger,
}

pub fn init() -> Receiver<[u8; 3]> {
    let (sender, receiver) = unbounded();
    let _ = SENDER.set(sender);
    receiver
}

pub struct LearnState {
    receiver: Receiver<[u8; 3]>,
    request: Option<LearnSession>,
    captured: Vec<MidiLearnCapture>,
}

impl LearnState {
    pub fn new(receiver: Receiver<[u8; 3]>) -> Self {
        Self {
            receiver,
            request: None,
            captured: Vec::new(),
        }
    }

    pub fn set_streaming_enabled(&mut self, enabled: bool) {
        set_streaming_enabled(enabled);

        if !enabled {
            self.request = None;
        }
    }

    pub fn arm(&mut self, request: MidiLearnRequest) {
        self.request.replace(LearnSession {
            request,
            started_at: Instant::now(),
            last_seen_at: None,
            stats: HashMap::new(),
        });
        LEARN_ACTIVE.store(true, Relaxed);
    }

    pub fn active_request(&self) -> Option<MidiLearnRequest> {
        self.request.as_ref().map(|session| session.request)
    }

    pub fn flush_timeouts(&mut self) {
        self.poll_captures();

        let now = Instant::now();
        let Some(session) = self.request.as_ref() else {
            return;
        };

        if !should_finalize(session, now) {
            return;
        }

        if let Some(session) = self.request.take()
            && let Some(capture) = finalize_session(session)
        {
            LEARN_ACTIVE.store(false, Relaxed);
            self.captured.push(capture);
        }
    }

    fn poll_captures(&mut self) {
        while let Ok(Some(message)) = self.receiver.try_recv() {
            let now = Instant::now();
            let mut maybe_finalized = None;

            let Some(session) = self.request.as_mut() else {
                continue;
            };

            let key = (message[0], message[1]);
            let value = message[2];
            session.last_seen_at = Some(now);
            session
                .stats
                .entry(key)
                .and_modify(|stats| {
                    stats.count += 1;
                    stats.min_value = stats.min_value.min(value);
                    stats.max_value = stats.max_value.max(value);
                })
                .or_insert(LearnStats {
                    count: 1,
                    min_value: value,
                    max_value: value,
                });

            if should_finalize(session, now) {
                maybe_finalized = self.request.take();
            }

            if let Some(session) = maybe_finalized
                && let Some(capture) = finalize_session(session)
            {
                LEARN_ACTIVE.store(false, Relaxed);
                self.captured.push(capture);
            }
        }
    }

    pub fn pop_capture(&mut self) -> Option<MidiLearnCapture> {
        self.captured.pop()
    }
}

pub fn set_streaming_enabled(enabled: bool) {
    STREAM_ENABLED.store(enabled, Relaxed);

    if !enabled {
        LEARN_ACTIVE.store(false, Relaxed);
    }
}

pub fn capture(message: &[u8]) -> bool {
    if message.len() != 3 {
        return false;
    }

    if !STREAM_ENABLED.load(Relaxed) || !LEARN_ACTIVE.load(Relaxed) {
        return false;
    }

    if let Some(sender) = SENDER.get() {
        let _ = sender.send([message[0], message[1], message[2]]);
        return true;
    }

    false
}

fn should_finalize(session: &LearnSession, now: Instant) -> bool {
    const MIN_MESSAGES: u32 = 8;
    const QUIET_TIME: Duration = Duration::from_millis(180);
    const MAX_LEARN_TIME: Duration = Duration::from_secs(2);

    let total_messages: u32 = session.stats.values().map(|stats| stats.count).sum();
    if total_messages >= MIN_MESSAGES {
        return true;
    }

    if now.duration_since(session.started_at) >= MAX_LEARN_TIME && total_messages > 0 {
        return true;
    }

    if let Some(last_seen_at) = session.last_seen_at
        && now.duration_since(last_seen_at) >= QUIET_TIME
        && total_messages > 0
    {
        return true;
    }

    false
}

fn finalize_session(session: LearnSession) -> Option<MidiLearnCapture> {
    let (status, data1) = best_trigger(&session.stats)?;

    Some(MidiLearnCapture {
        controller_id: session.request.controller_id,
        target: session.request.target,
        trigger: MidiTrigger {
            status,
            data1,
            match_data1: match_data1_for_status(status),
        },
    })
}

fn best_trigger(stats: &HashMap<(u8, u8), LearnStats>) -> Option<(u8, u8)> {
    let mut canonical: HashMap<(u8, u8), (u32, u8)> = HashMap::new();

    for (&(status, data1), message_stats) in stats {
        let canonical_data1 = if status & 0xF0 == 0xB0 && (32..=63).contains(&data1) {
            data1 - 32
        } else {
            data1
        };

        let value_span = message_stats
            .max_value
            .saturating_sub(message_stats.min_value);
        canonical
            .entry((status, canonical_data1))
            .and_modify(|(count, span)| {
                *count += message_stats.count;
                *span = (*span).max(value_span);
            })
            .or_insert((message_stats.count, value_span));
    }

    canonical
        .into_iter()
        .max_by_key(|(_, (count, span))| ((*count as u64) << 8) + (*span as u64))
        .map(|(key, _)| key)
}

fn match_data1_for_status(status: u8) -> bool {
    !matches!(status & 0xF0, 0xC0 | 0xD0 | 0xE0)
}