#[macro_use]
extern crate log;

extern crate arnalisa;
extern crate env_logger;
extern crate serde_json;
extern crate snafu;
extern crate structopt;

use arnalisa::bins::{BinDescription, SourceNames, WriteDot};
use indexmap::IndexMap;
use snafu::{Backtrace, OptionExt, ResultExt, Snafu};
use std::fs::File;
use std::path::{Path, PathBuf};
use structopt::StructOpt;

fn main() {
    let mut builder = env_logger::Builder::new();
    builder.parse_filters("arnalisa=info");
    if let Ok(rust_log) = std::env::var("RUST_LOG") {
        builder.parse_filters(&rust_log);
    }
    builder.init();

    if let Err(err) = run(Arguments::from_args()) {
        eprintln!("{}", err);
        std::process::exit(1);
    }
}

fn run(args: Arguments) -> Result<()> {
    match args.command {
        Command::Check(args) => {
            let description_file = Path::new(&args.file).to_path_buf();
            let description_path = description_file
                .parent()
                .context(InvalidParentDirectory {
                    path: args.file.to_string(),
                })?
                .to_path_buf();

            let mut calibration = CalibrationResolver {
                description_directory: description_path.clone(),
                calibration_directory: args.calibration_path.clone(),
            };

            load_and_check_description(&args.file, &mut calibration)?;
            Ok(())
        }
        Command::DumpDot(args) => {
            let description = load_and_check_description(
                &args.file,
                &mut UnitCalibration {},
            )?;
            description
                .write_dot(&mut std::io::stdout(), "pipeline")
                .context(Arnalisa)?;
            Ok(())
        }
        Command::Run(args) => {
            let description_file = Path::new(&args.file).to_path_buf();
            let description_path = description_file
                .parent()
                .context(InvalidParentDirectory {
                    path: args.file.to_string(),
                })?
                .to_path_buf();

            let mut calibration = CalibrationResolver {
                description_directory: description_path.clone(),
                calibration_directory: args.calibration_path.clone(),
            };

            let bin_description =
                load_and_check_description(&args.file, &mut calibration)?;
            let source_description =
                arnalisa::bins::jsonlsource::Description {
                    file_path: args.input.to_string(),
                };
            let sink_description = arnalisa::bins::cborsink::Description {
                file_path: args.output.clone(),
                sinks: bin_description.source_names().context(Arnalisa)?,
            };

            arnalisa::run_bin_with_calibration(
                &source_description,
                &bin_description,
                &sink_description,
                &calibration,
            )
            .context(Arnalisa)?;

            Ok(())
        }
    }
}

#[derive(StructOpt, Debug)]
struct CheckArguments {
    /// The arnalisa definition file
    file: String,
    /// The calibration data path. Only required if the pipeline contains
    /// calibration lookup which gets resolved by the calibration id.
    #[structopt(long = "calibration-path")]
    calibration_path: Option<String>,
}

#[derive(StructOpt, Debug)]
struct DumpDotArguments {
    /// The arnalisa definition file
    file: String,
}

#[derive(StructOpt, Debug)]
struct RunArguments {
    /// The arnalisa definition file
    file: String,
    /// The data input file
    input: String,
    /// The data output file
    output: Option<String>,
    /// The calibration data path. Only required if the pipeline contains
    /// calibration lookup which gets resolved by the calibration id.
    #[structopt(long = "calibration-path")]
    calibration_path: Option<String>,
}

#[derive(StructOpt, Debug)]
enum Command {
    /// Check a pipeline file for consistency
    #[structopt(name = "check")]
    Check(CheckArguments),
    /// Dump a dot diagram
    #[structopt(name = "dump-dot")]
    DumpDot(DumpDotArguments),
    /// Run a pipeline, driven by an input file
    #[structopt(name = "run")]
    Run(RunArguments),
}

#[derive(StructOpt, Debug)]
#[structopt(name = "arnalisa")]
struct Arguments {
    #[structopt(subcommand)]
    command: Command,
}

#[derive(Clone)]
struct CalibrationResolver {
    pub description_directory: PathBuf,
    pub calibration_directory: Option<String>,
}

impl arnalisa::GetCalibration for CalibrationResolver {
    fn calibration(
        &mut self,
        mode: &arnalisa::CalibrationSource,
    ) -> arnalisa::Result<IndexMap<arnalisa::R64, arnalisa::R64>> {
        use arnalisa::CalibrationSource;
        use arnalisa::R64;
        match *mode {
            CalibrationSource::File { ref path } => {
                let file = self.description_directory.join(path);
                Self::load_calibration_from_file(&file)
            }
            CalibrationSource::Embedded { ref curve } => Ok(curve
                .iter()
                .map(|&(x, y)| (R64::from(x), R64::from(y)))
                .collect::<IndexMap<R64, R64>>()),
            CalibrationSource::Identifier { ref id } => {
                if let Some(ref dir) = self.calibration_directory {
                    let file =
                        Path::new(dir).join(id).join("calibration.json");
                    Self::load_calibration_from_file(&file)
                } else {
                    arnalisa::error::GetCalibration {
                        message: "calibration-path parameter required"
                            .to_string(),
                    }
                    .fail()
                }
            }
        }
    }
}

impl CalibrationResolver {
    fn load_calibration_from_file(
        file: &PathBuf,
    ) -> arnalisa::Result<IndexMap<arnalisa::R64, arnalisa::R64>> {
        use arnalisa::R64;
        info!("Loading calibration from file {:?}", file);
        let file = File::open(file).context(arnalisa::error::Io)?;

        let curve: Vec<(f64, f64)> = serde_json::from_reader(file)
            .context(arnalisa::error::SerdeJson)?;
        Ok(curve
            .into_iter()
            .map(|(x, y)| (R64::from(x), R64::from(y)))
            .collect::<IndexMap<_, _>>())
    }
}

struct UnitCalibration {}

impl arnalisa::GetCalibration for UnitCalibration {
    fn calibration(
        &mut self,
        _mode: &arnalisa::CalibrationSource,
    ) -> arnalisa::Result<IndexMap<arnalisa::R64, arnalisa::R64>> {
        let calibration = vec![(0f64, 0f64), (1f64, 1f64)]
            .into_iter()
            .map(|(x, y)| (arnalisa::R64::from(x), arnalisa::R64::from(y)))
            .collect();
        Ok(calibration)
    }
}

fn load_description(
    path: &str,
) -> Result<arnalisa::bins::SourceSinkDescription> {
    let d: arnalisa::bins::SourceSinkDescription =
        serde_json::from_reader(File::open(path.to_string()).context(Io)?)
            .context(SerdeJson)?;
    Ok(d)
}

fn load_and_check_description(
    path: &str,
    get_calibration: &mut arnalisa::GetCalibration,
) -> Result<arnalisa::bins::SourceSinkDescription> {
    let f = load_description(path)?;
    info!("Checking file: {:?}", path);
    f.check_validity(
        &arnalisa::Scope::File(path.to_string()),
        get_calibration,
    )
    .context(Arnalisa)?;
    info!("File {} OK.", path);
    Ok(f)
}

type Result<T, E = Error> = std::result::Result<T, E>;

#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub enum Error {
    #[snafu(display("calibration-path parameter required"))]
    CalibrationPathRequired { backtrace: Backtrace },

    #[snafu(display("invalid parent directory for file {:?}", path))]
    InvalidParentDirectory { path: String, backtrace: Backtrace },

    #[snafu(display("{}", source))]
    #[snafu(backtrace(delegate))]
    Arnalisa { source: arnalisa::Error },

    #[snafu(display("serde json error: {:?}", source))]
    SerdeJson {
        source: serde_json::Error,
        backtrace: Backtrace,
    },

    #[snafu(display("i/o error: {:?}", source))]
    Io {
        source: std::io::Error,
        backtrace: Backtrace,
    },
}
