use super::{
    sink_names_input, source_names_output, BinBuildEnvironment,
    BinDescription, Calculator, CalibrationSource, FetchItem,
    GetCalibration, Item, Iteration, Result, Scope, SinkBin, SinkNames,
    SourceBin, SourceId, SourceNames, SourceSinkBinDescription,
    WriteDotSimple, SINK_INPUT, SOURCE_OUTPUT,
};
use crate::sortable_float::{HasDirection, HasReverse};
use crate::{error, R64};
use indexmap::{IndexMap, IndexSet};

static BIN_TYPE: &'static str = "calibration";

pub struct Bin {
    scope: Scope,
    source_input: Box<FetchItem>,
    curve: IndexMap<R64, R64>,
    result_output: Item,
}

impl SinkBin for Bin {}

impl SourceBin for Bin {
    fn get_source_data(&self, source: &SourceId) -> Result<Item> {
        if source.id == SOURCE_OUTPUT {
            Ok(self.result_output.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)?;

        self.result_output = if let Ok(v) = input.to_float() {
            let sv = R64::from(v);
            use std::iter::FromIterator;
            let curve =
                std::collections::BTreeMap::from_iter(self.curve.iter());
            let mut before = curve.range(..sv.clone());
            let mut after = curve.range(sv.clone()..);
            let (a, b) = match (before.next_back(), after.next()) {
                (None, None) => error::InvalidCalibration {
                    scope: self.scope.clone(),
                }
                .fail()?,
                (Some(b), None) => {
                    let a = before.next_back();
                    if let Some(a) = a {
                        (a, b)
                    } else {
                        error::InvalidCalibration {
                            scope: self.scope.clone(),
                        }
                        .fail()?
                    }
                }
                (None, Some(a)) => {
                    let b = after.next();
                    if let Some(b) = b {
                        (a, b)
                    } else {
                        error::InvalidCalibration {
                            scope: self.scope.clone(),
                        }
                        .fail()?
                    }
                }
                (Some(a), Some(b)) => (a, b),
            };

            let a: (f64, f64) =
                ((*a.0).clone().into(), (*a.1).clone().into());
            let b: (f64, f64) =
                ((*b.0).clone().into(), (*b.1).clone().into());

            calculate_intersection(v, a, b)
        } else {
            Item::Nothing
        };
        Ok(())
    }
}

fn calculate_intersection(
    input: R64,
    a: (f64, f64),
    b: (f64, f64),
) -> Item {
    let (ax, ay) = a;
    let (bx, by) = b;
    let (dx, dy) = ((bx - ax), (by - ay));

    let offset = input - ax;
    let value = R64::from(ay) + offset / dx * dy;

    Item::from(value)
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Description {
    pub details: CalibrationSource,
    pub reversed: bool,
}

impl BinDescription for Description {
    type Bin = Bin;

    fn check_validity(
        &self,
        scope: &Scope,
        get_calibration: &mut GetCalibration,
    ) -> Result<()> {
        get_calibration
            .calibration(&self.details)?
            .check_direction(scope, &self.details)
    }

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

impl SinkNames for Description {
    fn sink_names(&self) -> IndexSet<String> {
        sink_names_input()
    }
}

impl SourceNames for Description {
    fn source_names(&self) -> Result<IndexSet<String>> {
        Ok(source_names_output())
    }
}

impl SourceSinkBinDescription for Description {
    fn build_bin(
        &self,
        scope: &Scope,
        env: &mut BinBuildEnvironment,
    ) -> Result<Self::Bin> {
        let curve = if self.reversed {
            env.calibration(&self.details)?.reversed()
        } else {
            env.calibration(&self.details)?
        };

        Ok(Bin {
            scope: scope.clone(),
            source_input: env.resolve(SINK_INPUT)?,
            curve,
            result_output: Item::Nothing,
        })
    }
}

impl WriteDotSimple for Description {}
