//! Configuring `takeaway`.

//----------- Config -----------------------------------------------------------

use core::num::NonZeroUsize;

use crate::{Queue, Task};

/// Configuration for a [`Queue`].
///
/// This is a collection of the settings for a [`Queue`] that need to be
/// determined before it can be created.  It provides a builder-pattern API
/// for easy use, ending in [`Config::build()`] to create the final [`Queue`].
///
/// To create a [`Config`], use [`Default`] if the `std` feature is enabled;
/// otherwise, decide on a number of workers and use [`Config::new()`].  All
/// available settings are described below.
///
/// # Settings
///
/// The following settings are currently defined:
///
/// ## Number of Workers
///
/// The number of workers used to execute tasks.  Precisely this many
/// [`Worker`]s will be associated with the [`Queue`]; it is the caller's
/// responsibility to drive them all.
///
/// [`Worker`]: crate::Worker
///
/// `takeaway` is intended for CPU-heavy task processing, so it's
/// not recommended to use more workers than the host machine can run
/// simultaneously.  If you want to perform CPU-heavy work _alongside_
/// `takeaway`, you could configure a smaller number of workers.
///
/// This is configured in [`Config::new()`].  If the `std` feature is enabled,
/// [an implementation][impl-default] of [`Default`] is provided which sets it
/// to [`std::thread::available_parallelism()`].  The configured value can be
/// accessed through [`Config::num_workers()`].
///
/// [impl-default]: #impl-Default-for-Config
///
/// ## Batch Size
///
/// The ideal size to group tasks in for processing.  `takeaway` has to share
/// information about tasks between different workers, and this incurs a
/// communication / synchronization cost.  To amortize this, `takeaway` operates
/// on _batches_ of tasks, and this setting controls the maximum size of each
/// batch.  `takeaway` will aim to group tasks in the largest batches possible,
/// i.e. it will aim for this size.
///
/// The default batch size is 64.  Reducing the batch size can improve task
/// distribution, e.g. getting high-priority tasks to be executed faster, at the
/// cost of increased communication overhead.  Increasing the batch size will
/// have the opposite effect, worsening task distribution but reducing overhead.
/// Only consider tuning the batch size if `takeaway` has a performance impact
/// on your program, or if you notice high-priority tasks getting delayed.
///
/// This can be configured by [`Config::with_batch_size()`], and the configured
/// value can be accessed through [`Config::batch_size()`].
///
/// ## One-shot Mode
///
/// `takeaway` can be used in two distinct modes, depending on your program's
/// needs: *daemon mode* (the default), where `takeaway` will never shut down
/// (automatically) because it expects new tasks to be introduced at any time;
/// and *one-shot mode*, where `takeaway` will shut down the moment all known
/// tasks are complete.
///
/// In one-shot mode, `takeaway` expects to be launched with an initial set of
/// tasks; as these tasks are executed, they may spawn sub-tasks as well.  Tasks
/// are not expected to be added externally.  As soon as all tasks (i.e. the
/// initial set and their descendants) finish, [`Queue::shutdown()`] will be
/// called.
///
/// In either mode, you can call [`Queue::shutdown()`] at any time to shut down
/// the system manually.  If one-shot mode does not quite meet your needs, you
/// can track completion yourself and initiate a shutdown appropriately.  You
/// may also wish to initiate a shutdown in response to CTRL-C.
///
/// This can be configured by [`Config::with_oneshot()`], and the configured
/// value can be accessed through [`Config::oneshot()`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Config {
    /// The number of workers.
    pub(crate) num_workers: NonZeroUsize,

    /// The batch size.
    pub(crate) batch_size: NonZeroUsize,

    /// Whether one-shot mode is enabled.
    pub(crate) oneshot: bool,
}

impl Config {
    /// Construct a new [`Config`].
    ///
    /// The only required configuration is the number of workers that will be
    /// used.  See the documentation on [`Config`] for a complete description.
    ///
    /// With regards to other settings:
    /// - The batch size is set to 64.
    /// - One-shot mode is disabled.
    ///
    /// When the `std` feature is enabled, [an implementation][impl-default] of
    /// [`Default`] is provided, which sets the number of workers to the number
    /// of CPUs detected (or, more strictly, the estimated parallelism of the
    /// system).  Use it when possible.
    ///
    /// [impl-default]: #impl-Default-for-Config
    ///
    /// # Panics
    ///
    /// Panics if `num_workers > 8192`.
    pub const fn new(num_workers: NonZeroUsize) -> Self {
        assert!(
            num_workers.get() <= 8192,
            "'takeaway' does not support using more than 8192 workers"
        );

        let batch_size = NonZeroUsize::new(64).expect("64 != 0");

        Self {
            num_workers,
            batch_size,
            oneshot: false,
        }
    }

    /// The number of workers.
    ///
    /// This is the number of workers that will process tasks in the system.
    /// See the documentation on [`Config`] for a complete description.
    #[inline]
    pub const fn num_workers(&self) -> NonZeroUsize {
        self.num_workers
    }

    /// Select the queue's batch size.
    ///
    /// Tasks in the system will be processed in batches of this size.  See the
    /// documentation on [`Config`] for a complete description.
    ///
    /// # Panics
    ///
    /// Panics if `batch_size > 8192` or `!batch_size.is_power_of_two()`.
    pub const fn with_batch_size(self, batch_size: NonZeroUsize) -> Self {
        assert!(
            batch_size.get() <= 8192,
            "'takeaway' does not support batches bigger than 8192"
        );
        assert!(
            batch_size.is_power_of_two(),
            "'takeaway' requires power-of-two batch sizes"
        );

        Self { batch_size, ..self }
    }

    /// The queue's batch size.
    ///
    /// Tasks in the system will be processed in batches of this size.  See the
    /// documentation on [`Config`] for a complete description.
    ///
    /// # Invariants
    ///
    /// The batch size is a power of two.
    #[inline]
    pub const fn batch_size(&self) -> NonZeroUsize {
        self.batch_size
    }

    /// Enable or disable one-shot mode.
    ///
    /// In one-shot mode, the task system will shut down once all known tasks
    /// are complete.  See the documentation on [`Config`] for more details.
    pub const fn with_oneshot(self, oneshot: bool) -> Self {
        Self { oneshot, ..self }
    }

    /// Whether one-shot mode is enabled.
    ///
    /// In one-shot mode, the task system will shut down once all known tasks
    /// are complete.  See the documentation on [`Config`] for more details.
    #[inline]
    pub const fn oneshot(&self) -> bool {
        self.oneshot
    }

    /// Construct a [`Queue`] with this configuration.
    ///
    /// This is equivalent to [`Queue::new()`]; pick whichever one is more
    /// idiomatic in your context.
    #[inline]
    pub fn build<T: Task>(self) -> Queue<T> {
        Queue::new(self)
    }
}

#[cfg(feature = "std")]
impl Default for Config {
    /// The default task queue configuration.
    ///
    /// This will estimate the appropriate number of workers to use from
    /// [`std::thread::available_parallelism()`].  See its documentation for a
    /// description of its caveats.  For manual control, use [`Config::new()`].
    ///
    /// A summary of the settings:
    /// - The number of workers will be detected as described above.
    /// - The batch size will be set to 64.
    /// - One-shot mode will be disabled.
    ///
    /// # Panics
    ///
    /// Panics if [`std::thread::available_parallelism()`] fails, or if it
    /// reports a number greater than 8192.
    fn default() -> Self {
        Self::new(std::thread::available_parallelism().unwrap())
    }
}
