mini-film 11.2.3

Apply Lightroom-style film emulation profiles to RAW files with RawTherapee and HALD workflows.
Documentation
use super::model::{ReviewCodexJobKey, ReviewCodexScheduler, ScheduledCodexJob};
use super::prelude::*;

const REVIEW_RETOUCH_DEBOUNCE: Duration = Duration::from_secs(2);

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub(super) struct ReviewRetouchJobKey {
    pub(super) raw: PathBuf,
    pub(super) profile_index: usize,
}

#[derive(Clone, Debug)]
pub(super) struct ScheduledRetouchJob {
    pub(super) raw: PathBuf,
    pub(super) profile_index: usize,
    pub(super) output: PathBuf,
    pub(super) render_key: String,
    pub(super) due_at: Instant,
}

#[derive(Default)]
pub(super) struct ReviewRetouchScheduler {
    pub(super) state: Mutex<ReviewRetouchSchedulerState>,
    pub(super) changed: Condvar,
}

#[derive(Default)]
pub(super) struct ReviewRetouchSchedulerState {
    pub(super) pending: HashMap<ReviewRetouchJobKey, ScheduledRetouchJob>,
}

impl ReviewRetouchScheduler {
    pub(super) fn schedule(
        &self,
        raw: PathBuf,
        profile_index: usize,
        output: PathBuf,
        render_key: String,
    ) {
        self.schedule_after(
            raw,
            profile_index,
            output,
            render_key,
            REVIEW_RETOUCH_DEBOUNCE,
        );
    }

    pub(super) fn schedule_after(
        &self,
        raw: PathBuf,
        profile_index: usize,
        output: PathBuf,
        render_key: String,
        delay: Duration,
    ) {
        let key = ReviewRetouchJobKey {
            raw: raw.clone(),
            profile_index,
        };
        let job = ScheduledRetouchJob {
            raw,
            profile_index,
            output,
            render_key,
            due_at: Instant::now() + delay,
        };
        let mut state = self
            .state
            .lock()
            .unwrap_or_else(|poison| poison.into_inner());
        state.pending.insert(key, job);
        self.changed.notify_one();
    }

    pub(super) fn next_job(&self) -> ScheduledRetouchJob {
        let mut state = self
            .state
            .lock()
            .unwrap_or_else(|poison| poison.into_inner());
        loop {
            if state.pending.is_empty() {
                state = self
                    .changed
                    .wait(state)
                    .unwrap_or_else(|poison| poison.into_inner());
                continue;
            }

            let (next_key, next_due) = state
                .pending
                .iter()
                .min_by_key(|(_, job)| job.due_at)
                .map(|(key, job)| (key.clone(), job.due_at))
                .expect("pending retouch job exists");
            let now = Instant::now();
            if next_due <= now {
                return state
                    .pending
                    .remove(&next_key)
                    .expect("pending retouch job still exists");
            }
            let timeout = next_due.saturating_duration_since(now);
            let (next_state, _) = self
                .changed
                .wait_timeout(state, timeout)
                .unwrap_or_else(|poison| poison.into_inner());
            state = next_state;
        }
    }
}

impl ReviewCodexScheduler {
    pub(super) fn schedule(&self, raw: PathBuf, analysis_key: String) {
        let key = ReviewCodexJobKey { raw: raw.clone() };
        let job = ScheduledCodexJob { raw, analysis_key };
        let mut state = self
            .state
            .lock()
            .unwrap_or_else(|poison| poison.into_inner());
        state.pending.insert(key, job);
        self.changed.notify_one();
    }

    pub(super) fn next_job(&self) -> ScheduledCodexJob {
        let mut state = self
            .state
            .lock()
            .unwrap_or_else(|poison| poison.into_inner());
        loop {
            if let Some(key) = state.pending.keys().next().cloned() {
                return state
                    .pending
                    .remove(&key)
                    .expect("pending Codex job still exists");
            }
            state = self
                .changed
                .wait(state)
                .unwrap_or_else(|poison| poison.into_inner());
        }
    }
}