use super::data::io::{Inputs, Outputs};
use super::element::PipelineElement;
use super::ops::prelude::*;

/// Builder for the pipelines that are actually run
///
/// TODO:
/// - Bind additional inputs if instruction has more then one and is passd without any additional
/// - allow binding to pointers to other pipelines?
/// - allow referencing earlier data
pub struct PipelineBuilder {
    elements: Vec<Box<dyn PipelineElement>>,
}

impl PipelineBuilder {
    /// Create new, empty builder
    pub fn new() -> Self {
        Self {
            elements: Vec::new(),
        }
    }

    /// Insert element into pipeline
    fn insert<T: PipelineElement + 'static>(mut self, el: T) -> Self {
        if let Some(previous_item) = self.elements.last() {
            assert_eq!(
                previous_item.signature().outputs[0],
                el.signature().inputs[0]
            );
        }
        self.elements.push(Box::new(el));
        self
    }

    /// insert string concatenattion element
    #[must_use]
    pub fn concatenate(self, sec: String) -> Self {
        self.insert(Concatenate(sec))
    }

    /// insert string uppercase element
    #[must_use]
    pub fn upper(self) -> Self {
        self.insert(Upper)
    }

    /// insert string lowercase element
    #[must_use]
    pub fn lower(self) -> Self {
        self.insert(Lower)
    }

    /// insert numeric addition element
    #[must_use]
    #[allow(
        clippy::should_implement_trait,
        reason = "is not equivalent to addition"
    )]
    pub fn add(self, sec: i32) -> Self {
        self.insert(Add(sec))
    }

    /// insert numeric subtraction element
    #[must_use]
    pub fn subtract(self, sec: i32) -> Self {
        self.insert(Subtract(sec))
    }

    /// insert stringify element
    #[must_use]
    pub fn stringify(self) -> Self {
        self.insert(Stringify)
    }

    /// Build the pipeline. Doesn't check again - `insert` should verify correctness.
    pub fn build(&self) -> Pipeline {
        let mut r = Vec::new();

        self.elements.iter().for_each(|el| r.push(el.runner()));

        Pipeline { runners: r }
    }
}

impl Default for PipelineBuilder {
    fn default() -> Self {
        Self::new()
    }
}

/// Runnable pipeline - at the core of this library
pub struct Pipeline {
    runners: Vec<fn(&Inputs) -> Outputs>,
}

impl Pipeline {
    /// run the pipeline
    pub fn run(&self, inputs: Inputs) -> Outputs {
        let mut out: Outputs = inputs.into();

        for runner in &self.runners {
            out = runner(&(&out).into());
        }

        out
    }
}
