use super::{
    build_names, BinBuildEnvironment, BinDescription, Calculator,
    FetchItem, GetCalibration, Item, Iteration, Result, Scope, SinkBin,
    SinkNames, SourceBin, SourceId, SourceNames, SourceSinkBinDescription,
    WriteDotSimple, SINK_INPUT,
};
use crate::{error, R64};
use indexmap::IndexSet;
use std::collections::VecDeque;

static BIN_TYPE: &'static str = "fifo";
static SINK_MIN_ITEMS: &'static str = "min_items";
static SINK_MAX_ITEMS: &'static str = "max_items";
static SOURCE_DELTA: &'static str = "delta";
static SOURCE_MIN: &'static str = "min";
static SOURCE_MAX: &'static str = "max";

pub struct Bin {
    scope: Scope,

    source_input: Box<FetchItem>,
    source_min_items: Box<FetchItem>,
    source_max_items: Box<FetchItem>,

    fifo: VecDeque<Item>,
    min: Option<R64>,
    max: Option<R64>,

    result_min: Item,
    result_max: Item,
    result_delta: Item,
}

impl SinkBin for Bin {}

impl SourceBin for Bin {
    fn get_source_data(&self, source: &SourceId) -> Result<Item> {
        if source.id == SOURCE_MIN {
            Ok(self.result_min.clone())
        } else if source.id == SOURCE_MAX {
            Ok(self.result_max.clone())
        } else if source.id == SOURCE_DELTA {
            Ok(self.result_delta.clone())
        } else {
            error::MissingSourceName {
                scope: self.scope.clone(),
                name: source.id.to_string(),
                bin_type: BIN_TYPE.to_string(),
            }
            .fail()
        }
    }
}

impl Calculator for Bin {
    fn calculate(&mut self, _iteration: &Iteration) -> Result<()> {
        let input = self.source_input.fetch_item(&self.scope)?;
        let min_items = self.source_min_items.fetch_item(&self.scope)?;
        let max_items = self.source_max_items.fetch_item(&self.scope)?;

        let min_items = min_items.to_usize().unwrap_or(0usize);
        let max_items = max_items.to_usize().unwrap_or(usize::max_value());

        self.fifo.push_back(input.clone());
        let dissed = {
            let mut dissed = None;
            while self.fifo.len() > max_items {
                dissed = self.fifo.pop_front()
            }
            dissed
        };

        if let Some(d) = dissed {
            if d.to_float().is_ok() {
                self.min = None;
                self.max = None;

                for v in &self.fifo {
                    match (v.to_float(), self.min) {
                        (Ok(f), None) => self.min = Some(f),
                        (Ok(f), Some(m)) if f < m => self.min = Some(f),
                        _ => {}
                    }
                    match (v.to_float(), self.max) {
                        (Ok(f), None) => self.max = Some(f),
                        (Ok(f), Some(m)) if f > m => self.max = Some(f),
                        _ => {}
                    }
                }
            }
        }

        if self.fifo.len() >= min_items {
            match (input.to_float(), self.min) {
                (Ok(f), None) => self.min = Some(f),
                (Ok(f), Some(m)) if f < m => self.min = Some(f),
                _ => {}
            }
            match (input.to_float(), self.max) {
                (Ok(f), None) => self.max = Some(f),
                (Ok(f), Some(m)) if f > m => self.max = Some(f),
                _ => {}
            }
        } else {
            self.min = None;
            self.max = None;
        }

        let (result_min, result_max, result_delta) =
            match (self.min, self.max) {
                (Some(min), Some(max)) => {
                    (Item::F64(min), Item::F64(max), Item::F64(max - min))
                }
                _ => (Item::Nothing, Item::Nothing, Item::Nothing),
            };
        self.result_min = result_min;
        self.result_max = result_max;
        self.result_delta = result_delta;
        Ok(())
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Description;

impl BinDescription for Description {
    type Bin = Bin;

    fn check_validity(
        &self,
        _scope: &Scope,
        _get_calibration: &mut GetCalibration,
    ) -> Result<()> {
        Ok(())
    }

    fn bin_type(&self) -> &'static str {
        BIN_TYPE
    }
}

impl SinkNames for Description {
    fn sink_names(&self) -> IndexSet<String> {
        build_names(&[SINK_INPUT, SINK_MIN_ITEMS, SINK_MAX_ITEMS])
    }
}

impl SourceNames for Description {
    fn source_names(&self) -> Result<IndexSet<String>> {
        Ok(build_names(&[SOURCE_MIN, SOURCE_MAX, SOURCE_DELTA]))
    }
}

impl SourceSinkBinDescription for Description {
    fn build_bin(
        &self,
        scope: &Scope,
        env: &mut BinBuildEnvironment,
    ) -> Result<Self::Bin> {
        Ok(Bin {
            scope: scope.clone(),
            source_input: env.resolve(SINK_INPUT)?,
            source_min_items: env.resolve(SINK_MIN_ITEMS)?,
            source_max_items: env.resolve(SINK_MAX_ITEMS)?,
            fifo: VecDeque::new(),
            min: None,
            max: None,
            result_min: Item::Nothing,
            result_max: Item::Nothing,
            result_delta: Item::Nothing,
        })
    }
}

impl WriteDotSimple for Description {}

#[cfg(test)]
mod tests {
    use super::Description;
    use crate::bins::{directsource, verificationsink};
    use crate::Item as I;
    use crate::{run_bin, Result};
    use indexmap::indexset;

    #[test]
    fn simulate() -> Result<()> {
        use crate::Item::*;

        let input = directsource::Description {
            columns: indexset![
                "input".to_string(),
                "min_items".to_string(),
                "max_items".to_string(),
            ],
            rows: vec![
                vec![Nothing, Nothing, Nothing],
                vec![Nothing, I::from(5f64), I::from(5f64)],
                vec![Nothing, I::from(5f64), I::from(5f64)],
                vec![I::from(1f64), I::from(5f64), I::from(5f64)],
                vec![I::from(1f64), I::from(5f64), I::from(5f64)],
                vec![I::from(0f64), I::from(5f64), I::from(5f64)],
                vec![I::from(0f64), I::from(5f64), I::from(5f64)],
                vec![I::from(0f64), I::from(5f64), I::from(5f64)],
                vec![I::from(0f64), I::from(5f64), I::from(5f64)],
                vec![I::from(0f64), I::from(5f64), I::from(5f64)],
                vec![I::from(0f64), I::from(5f64), I::from(5f64)],
                vec![I::from(1f64), I::from(5f64), I::from(5f64)],
                vec![I::from(-1f64), I::from(5f64), I::from(5f64)],
                vec![I::from(-1f64), I::from(5f64), I::from(5f64)],
                vec![I::from(-1f64), I::from(5f64), I::from(5f64)],
                vec![I::from(-1f64), I::from(5f64), I::from(5f64)],
                vec![I::from(-1f64), I::from(5f64), I::from(5f64)],
                vec![I::from(-1f64), I::from(5f64), I::from(5f64)],
                vec![I::from(-1f64), I::from(5f64), I::from(5f64)],
                vec![I::from(10f64), I::from(5f64), I::from(5f64)],
                vec![I::from(-9f64), I::from(5f64), I::from(5f64)],
            ]
            .into(),
        };
        let verification = verificationsink::Description {
            columns: indexset![
                "min".to_string(),
                "max".to_string(),
                "delta".to_string(),
            ],
            expected: vec![
                vec![Nothing, Nothing, Nothing],
                vec![Nothing, Nothing, Nothing],
                vec![Nothing, Nothing, Nothing],
                vec![Nothing, Nothing, Nothing],
                vec![I::from(1f64), I::from(1f64), I::from(0f64)],
                vec![I::from(0f64), I::from(1f64), I::from(1f64)],
                vec![I::from(0f64), I::from(1f64), I::from(1f64)],
                vec![I::from(0f64), I::from(1f64), I::from(1f64)],
                vec![I::from(0f64), I::from(1f64), I::from(1f64)],
                vec![I::from(0f64), I::from(0f64), I::from(0f64)],
                vec![I::from(0f64), I::from(0f64), I::from(0f64)],
                vec![I::from(0f64), I::from(1f64), I::from(1f64)],
                vec![I::from(-1f64), I::from(1f64), I::from(2f64)],
                vec![I::from(-1f64), I::from(1f64), I::from(2f64)],
                vec![I::from(-1f64), I::from(1f64), I::from(2f64)],
                vec![I::from(-1f64), I::from(1f64), I::from(2f64)],
                vec![I::from(-1f64), I::from(-1f64), I::from(0f64)],
                vec![I::from(-1f64), I::from(-1f64), I::from(0f64)],
                vec![I::from(-1f64), I::from(-1f64), I::from(0f64)],
                vec![I::from(-1f64), I::from(10f64), I::from(11f64)],
                vec![I::from(-9f64), I::from(10f64), I::from(19f64)],
            ]
            .into(),
        };

        run_bin(&input, &Description {}, &verification)
    }
}
