Documentation
use polars::error::PolarsResult;
use polars::frame::DataFrame;
use polars::lazy::dsl::Expr;
use polars::prelude::*;

#[derive(Clone, Debug, Default)]
pub struct FeatureOperator {
    feature_expressions: Vec<Expr>,
    derived_feature_expressions: Vec<Expr>,
}

impl FeatureOperator {
    pub fn new() -> FeatureOperator {
        FeatureOperator {
            feature_expressions: Vec::new(),
            derived_feature_expressions: Vec::new(),
        }
    }

    pub fn append_expression(&mut self, expression: Expr) -> &mut FeatureOperator {
        self.feature_expressions.push(expression);

        self
    }

    pub fn append_derived_expression(&mut self, expression: Expr) -> &mut FeatureOperator {
        self.derived_feature_expressions.push(expression);

        self
    }

    pub fn collect_lazy_expressions(
        &self,
        df: DataFrame,
        n_rows: Option<u32>,
        exclude_columns: Option<Vec<String>>,
    ) -> PolarsResult<DataFrame> {
        let exclude_columns = exclude_columns.unwrap_or_default();

        match n_rows {
            Some(n_rows) => df
                .lazy()
                .with_columns(&self.feature_expressions)
                .with_columns(&self.derived_feature_expressions)
                .tail(n_rows)
                .select([all().exclude(exclude_columns)])
                .collect(),
            // TODO: Optionally use collect_concurrently?
            // .collect_concurrently(),
            None => df
                .lazy()
                .with_columns(&self.feature_expressions)
                .with_columns(&self.derived_feature_expressions)
                .select([all().exclude(exclude_columns)])
                .collect(),
            // TODO: Optionally use collect_concurrently?
            // .collect_concurrently(),
        }
    }

    pub fn feature_expressions_count(&self) -> usize {
        self.feature_expressions.len()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn create_feature_operator() -> FeatureOperator {
        let mut feature_operator = FeatureOperator::new();

        let addition_expression = (col("high") + col("low")).alias("addition");
        let subtraction_expression = (col("high") - col("low")).alias("subtraction");
        let division_expression = (col("high") / col("low")).alias("division");
        let multiplication_expression = (col("high") * col("low")).alias("multiplication");

        feature_operator.append_expression(addition_expression);
        feature_operator.append_expression(subtraction_expression);
        feature_operator.append_expression(division_expression);
        feature_operator.append_expression(multiplication_expression);

        feature_operator
    }

    fn create_df() -> DataFrame {
        df!(
            "ticker" => ["AAPL", "NVDA", "MSFT", "GOOG", "AMZN"],
            "price" => [229.9, 138.93, 420.56, 166.41, 188.4],
            "high" => [231.31, 139.6, 424.04, 167.62, 189.83],
            "low" => [228.6, 136.3, 417.52, 164.78, 188.44],
        )
        .unwrap()
    }

    #[test]
    fn count_features() {
        let feature_operator = create_feature_operator();

        assert_eq!(feature_operator.feature_expressions_count(), 4);
    }

    #[test]
    fn collect_expressions() {
        let df = create_df();
        let (height, width) = df.shape();
        let feature_operator = create_feature_operator();
        let feature_count = feature_operator.feature_expressions_count();

        let df = feature_operator
            .collect_lazy_expressions(df, None, None)
            .unwrap();

        dbg!(&df);

        assert_eq!(df.shape(), (height, width + feature_count));
        assert_eq!(
            df.get_column_names(),
            &[
                "ticker",
                "price",
                "high",
                "low",
                "addition",
                "subtraction",
                "division",
                "multiplication"
            ]
        );

        let exclude_columns = vec![String::from("addition"), String::from("subtraction")];
        let df = feature_operator
            .collect_lazy_expressions(df, Some(5), Some(exclude_columns.clone()))
            .unwrap();

        assert_eq!(
            df.shape(),
            (height, width + feature_count - exclude_columns.len())
        );
        assert_eq!(
            df.get_column_names(),
            &[
                "ticker",
                "price",
                "high",
                "low",
                "division",
                "multiplication"
            ]
        );
    }

    #[test]
    fn rolling_window_expression() {
        let df = create_df();
        let mut feature_operator = FeatureOperator::new();

        let rolling_window_expression = (col("price").rolling_mean(RollingOptionsFixedWindow {
            window_size: 3,
            min_periods: 3,
            weights: None,
            center: false,
            fn_params: Default::default(),
        }))
        .alias("rolling_mean");

        feature_operator.append_expression(rolling_window_expression);
        let df = feature_operator
            .collect_lazy_expressions(df, None, None)
            .unwrap();

        dbg!(&df);

        assert!(df.get_column_names_str().to_vec().contains(&"rolling_mean"));
    }
}