#![doc = include_str!("../README.md")]
#![feature(uefi_std)]

use crate::context::{RootContext, SproutContext};
use crate::phases::phase;
use anyhow::{Context, Result, bail};
use log::info;
use std::collections::BTreeMap;
use std::ops::Deref;
use uefi::proto::device_path::LoadedImageDevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};

/// actions: Code that can be configured and executed by Sprout.
pub mod actions;

/// config: Sprout configuration mechanism.
pub mod config;

/// context: Stored values that can be cheaply forked and cloned.
pub mod context;

/// drivers: EFI drivers to load and provide extra functionality.
pub mod drivers;

/// entries: Boot menu entries that have a title and can execute actions.
pub mod entries;

/// extractors: Runtime code that can extract values into the Sprout context.
pub mod extractors;

/// generators: Runtime code that can generate entries with specific values.
pub mod generators;

/// phases: Hooks into specific parts of the boot process.
pub mod phases;

/// setup: Code that initializes the UEFI environment for Sprout.
pub mod setup;

/// utils: Utility functions that are used by other parts of Sprout.
pub mod utils;

/// The main entrypoint of sprout.
/// It is possible this function will not return if actions that are executed
/// exit boot services or do not return control to sprout.
fn main() -> Result<()> {
    // Initialize the basic UEFI environment.
    setup::init()?;

    // Load the configuration of sprout.
    // At this point, the configuration has been validated and the specified
    // version is checked to ensure compatibility.
    let config = config::loader::load()?;

    // Load the root context.
    // This is done in a block to ensure the release of the LoadedImageDevicePath protocol.
    let mut root = {
        let current_image_device_path_protocol = uefi::boot::open_protocol_exclusive::<
            LoadedImageDevicePath,
        >(uefi::boot::image_handle())
        .context("unable to get loaded image device path")?;
        let loaded_image_path = current_image_device_path_protocol.deref().to_boxed();
        info!(
            "loaded image path: {}",
            loaded_image_path.to_string(DisplayOnly(false), AllowShortcuts(false))?
        );
        RootContext::new(loaded_image_path)
    };

    // Insert the configuration actions into the root context.
    root.actions_mut().extend(config.actions.clone());

    // Create a new sprout context with the root context.
    let mut context = SproutContext::new(root);

    // Insert the configuration values into the sprout context.
    context.insert(&config.values);

    // Freeze the sprout context so it can be shared and cheaply cloned.
    let context = context.freeze();

    // Execute the early phase.
    phase(context.clone(), &config.phases.early).context("unable to execute early phase")?;

    // Load all configured drivers.
    drivers::load(context.clone(), &config.drivers).context("unable to load drivers")?;

    // Run all the extractors declared in the configuration.
    let mut extracted = BTreeMap::new();
    for (name, extractor) in &config.extractors {
        let value = extractors::extract(context.clone(), extractor)
            .context(format!("unable to extract value {}", name))?;
        info!("extracted value {}: {}", name, value);
        extracted.insert(name.clone(), value);
    }
    let mut context = context.fork();
    // Insert the extracted values into the sprout context.
    context.insert(&extracted);
    let context = context.freeze();

    // Execute the late phase.
    phase(context.clone(), &config.phases.startup).context("unable to execute startup phase")?;

    let mut staged_entries = Vec::new();

    // Insert all the static entries from the configuration into the entry list.
    for (_name, entry) in config.entries {
        // Associate the main context with the static entry.
        staged_entries.push((context.clone(), entry));
    }

    // Run all the generators declared in the configuration.
    for (_name, generator) in config.generators {
        let context = context.fork().freeze();

        // Add all the entries generated by the generator to the entry list.
        // The generator specifies the context associated with the entry.
        for entry in generators::generate(context.clone(), &generator)? {
            staged_entries.push(entry);
        }
    }

    // Build a list of all the final boot entries.
    let mut final_entries = Vec::new();
    for (context, entry) in staged_entries {
        let mut context = context.fork();
        // Insert the values from the entry configuration into the
        // sprout context to use with the entry itself.
        context.insert(&entry.values);
        let context = context.finalize().freeze();

        // Insert the entry configuration into final boot entries with the extended context.
        final_entries.push((context, entry));
    }

    // TODO(azenla): Implement boot menu here.
    // For now, we just print all of the entries.
    info!("entries:");
    for (index, (context, entry)) in final_entries.iter().enumerate() {
        let title = context.stamp(&entry.title);
        info!("  entry {}: {}", index + 1, title);
    }

    // Execute the late phase.
    phase(context.clone(), &config.phases.late).context("unable to execute late phase")?;

    // Pick the first entry from the list of final entries until a boot menu is implemented.
    let Some((context, entry)) = final_entries.first() else {
        bail!("no entries found");
    };

    // Execute all the actions for the selected entry.
    for action in &entry.actions {
        let action = context.stamp(action);
        actions::execute(context.clone(), &action)
            .context(format!("unable to execute action '{}'", action))?;
    }

    // Sprout doesn't necessarily guarantee anything was booted.
    // If we reach here, we will exit back to whoever called us.
    Ok(())
}
