//! The system schedule: the ordered phases an [`App`](super::App) runs each
//! frame, and the per-phase lists of systems modules attach to.

use super::App;

/// The ordered phases of one frame.
///
/// Systems are attached to a phase and run in phase order; within a phase they
/// run in registration order, so behavior is fully deterministic. The phases
/// mirror a conventional game loop:
///
/// - [`First`](Self::First) — start-of-frame bookkeeping.
/// - [`Input`](Self::Input) — gather input (Stage 7).
/// - [`PreUpdate`](Self::PreUpdate) — engine work before game logic.
/// - [`FixedUpdate`](Self::FixedUpdate) — fixed-timestep work; runs **zero or
///   more** times per frame so simulation is frame-rate independent. Physics
///   (Stage 9) lives here.
/// - [`Update`](Self::Update) — per-frame game logic.
/// - [`PostUpdate`](Self::PostUpdate) — engine work after game logic.
/// - [`Render`](Self::Render) — drawing (Stage 5 pipeline onward).
/// - [`Last`](Self::Last) — end-of-frame cleanup.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Schedule {
    First,
    Input,
    PreUpdate,
    FixedUpdate,
    Update,
    PostUpdate,
    Render,
    Last,
}

impl Schedule {
    /// The once-per-frame phases, in order (everything except `FixedUpdate`,
    /// which is driven separately by the fixed-timestep accumulator).
    pub(crate) const PER_FRAME: [Schedule; 7] = [
        Schedule::First,
        Schedule::Input,
        Schedule::PreUpdate,
        Schedule::Update,
        Schedule::PostUpdate,
        Schedule::Render,
        Schedule::Last,
    ];
}

/// One registered system: a closure plus the module that contributed it (so the
/// module can be disabled or removed).
pub(crate) struct SystemEntry {
    pub(crate) module: Option<&'static str>,
    pub(crate) run: Box<dyn FnMut(&mut App)>,
}

/// The collection of systems, grouped by phase, in registration order.
#[derive(Default)]
pub(crate) struct Systems {
    first: Vec<SystemEntry>,
    input: Vec<SystemEntry>,
    pre_update: Vec<SystemEntry>,
    fixed_update: Vec<SystemEntry>,
    update: Vec<SystemEntry>,
    post_update: Vec<SystemEntry>,
    render: Vec<SystemEntry>,
    last: Vec<SystemEntry>,
}

impl Systems {
    fn phase_mut(&mut self, phase: Schedule) -> &mut Vec<SystemEntry> {
        match phase {
            Schedule::First => &mut self.first,
            Schedule::Input => &mut self.input,
            Schedule::PreUpdate => &mut self.pre_update,
            Schedule::FixedUpdate => &mut self.fixed_update,
            Schedule::Update => &mut self.update,
            Schedule::PostUpdate => &mut self.post_update,
            Schedule::Render => &mut self.render,
            Schedule::Last => &mut self.last,
        }
    }

    fn phase(&self, phase: Schedule) -> &[SystemEntry] {
        match phase {
            Schedule::First => &self.first,
            Schedule::Input => &self.input,
            Schedule::PreUpdate => &self.pre_update,
            Schedule::FixedUpdate => &self.fixed_update,
            Schedule::Update => &self.update,
            Schedule::PostUpdate => &self.post_update,
            Schedule::Render => &self.render,
            Schedule::Last => &self.last,
        }
    }

    pub(crate) fn push(&mut self, phase: Schedule, entry: SystemEntry) {
        self.phase_mut(phase).push(entry);
    }

    pub(crate) fn total(&self) -> usize {
        Schedule::PER_FRAME
            .iter()
            .chain(std::iter::once(&Schedule::FixedUpdate))
            .map(|p| self.phase(*p).len())
            .sum()
    }

    const ALL_PHASES: [Schedule; 8] = [
        Schedule::First,
        Schedule::Input,
        Schedule::PreUpdate,
        Schedule::FixedUpdate,
        Schedule::Update,
        Schedule::PostUpdate,
        Schedule::Render,
        Schedule::Last,
    ];

    /// Drops every system contributed by `module`.
    pub(crate) fn remove_module(&mut self, module: &str) {
        for phase in Self::ALL_PHASES {
            self.phase_mut(phase).retain(|e| e.module != Some(module));
        }
    }

    /// Appends all of `other`'s systems (used to fold back systems registered
    /// while the frame was running).
    pub(crate) fn merge(&mut self, mut other: Systems) {
        for phase in Self::ALL_PHASES {
            let tail = std::mem::take(other.phase_mut(phase));
            self.phase_mut(phase).extend(tail);
        }
    }
}

/// Runs one phase: every enabled system in registration order.
///
/// The [`Systems`] are moved out of the [`App`] before phases run (so systems
/// get exclusive `&mut App` access), so `systems` and `app` here are disjoint.
/// Systems from a disabled module are skipped without being removed.
pub(crate) fn run_phase(systems: &mut Systems, app: &mut App, phase: Schedule) {
    for entry in systems.phase_mut(phase) {
        if app.is_system_enabled(entry.module) {
            (entry.run)(app);
        }
    }
}
