Code to specify how to present time-series and Phillip's diagram graphics from FRED data.
use std::convert::TryInto;

use fred_api::Fred;
use keytree::{ KeyTreeRef };
use time_series::{ DatePoint, RegularTimeSeries, TimeSeries };

use crate::SeriesSpec;
use crate::error::*;

// --- Specifications -----------------------------------------------------------------------------

#[derive(Clone, Copy, Debug)]
pub enum ParserSpec {
    FredSpec(FredSpec),
    FredDailySpec(FredDailySpec),
}

#[derive(Clone, Copy, Debug)]
pub struct FredSpec {
    pub drop_first: usize
}

impl FredSpec {
    pub fn empty() -> Self {
        FredSpec { drop_first: 0 }
    }
}

#[derive(Clone, Copy, Debug)]
pub struct FredDailySpec;

impl FredDailySpec {
    pub fn empty() -> Self {
        FredDailySpec
    }
}

impl<'a> TryInto<FredSpec> for KeyTreeRef<'a> {
    type Error = keytree::Error;

    fn try_into(self) -> Result<FredSpec, Self::Error> {

        let drop_first: usize = self.opt_value("parser::drop_first")?
            .unwrap_or(0);

        Ok(FredSpec { drop_first })
    }
}

pub struct FredLineParser;

impl FredLineParser {
    pub fn default() -> Self {
        FredLineParser
    }
}

pub enum FredLine {
    Ok(DatePoint::<1>),
    Dot(time_series::MonthlyDate),
}

impl FredLine {

    fn to_datepoint(&self) -> Option<DatePoint::<1>> {
        match self {
            FredLine::Ok(dp) => Some(*dp),
            FredLine::Dot(_) => None,
        }
    }

    fn new(obs: &fred_api::Observation, line: usize) -> Result<FredLine, Error> {
        let year = obs.date[..4].parse()
            .map_err(|_| {
                err(
                    file!(), line!(),
                    &format!( "Failed to parse {}, {} at line {}.", obs.date, obs.value, &line),
                )
            })?;

        let month = obs.date[5..7].parse()
            .map_err(|_| {
                err(file!(), line!(),
                    &format!("Failed to parse {}, {} at line {}.",
                        obs.date,
                        obs.value,
                        &line.to_string(),
                    ),
                )
            })?;

        let date = time_series::MonthlyDate::ym(year, month);

        match obs.value.as_str() {
            "." => Ok(FredLine::Dot(date)),
            _ => {
                let value = obs.value[12..].parse::<f32>()
                    .map_err(|_| {
                        err(
                            file!(), line!(),
                            &format!("Failed to parse {}, {} at line {}.",
                                obs.date,
                                obs.value,
                                &line,
                            ),
                        )
                    })?;
                Ok(FredLine::Ok(
                    DatePoint::new(date, [value])
                )) 
            },
        }
    }
}

// I'm not sure if its necessary to separate out FredParser from its specification, but it 'feels
// right' at this point. Maybe fix in future?
pub struct FredParser(pub FredSpec);

impl FredParser {

    pub (crate) fn drop_first(&self) -> usize {
        self.0.drop_first
    }

    /// Given the series specification and the parser type, fetch the data and build it into a 
    /// `RegularTimeSeries`. 
    pub (crate) fn to_data(&self, spec: &SeriesSpec) -> Result<RegularTimeSeries::<1>, Error> {

        // First fetch the data

        let observations = match Fred::series_observations(&spec.series_id.to_string()) {
            Ok(series_obs) => series_obs.observations,
            Err(e) => { 
                return Err(err(file!(), line!(), &e.to_string()))
            },
        };

        // Iterate through observations to build time_series.

        let mut ts = TimeSeries::<1>::new(Vec::new());
        for (line_num, obs) in observations.iter()
            .enumerate()
            .skip(self.0.drop_first)
        {

            let dp = FredLine::new(obs, line_num + 1)?.to_datepoint().ok_or_else(|| {
                err(file!(), line!(),
                    &format!("Expected datapoint found dot from obs: {}, {} at line {}.",
                        obs.date,
                        obs.value,
                        &(line_num + 1).to_string(),
                    ),
                )
            })?;
            ts.push(dp);
        }

        // Convert time-series to regular time-series.        

        ts.try_into()
            .map_err(|e: time_series::error::Error| err(file!(), line!(), &e.to_string()))
    }

    // /// Parse observations into string for debugging and exit.
    // pub fn soft_parse(&self, observations: fred_api::Observations, err: Error) {
    //     for (i, obs) in observations.iter().enumerate() {
    //         eprintln!(
    //             "{} {}, {}",
    //             i + 1,
    //             obs.date,
    //             obs.value,
    //         )
    //     }
    //     eprintln!("{}", err);
    //     std::process::exit(1);
    // }
}

// --- Parsers ------------------------------------------------------------------------------------

pub enum Parser {
    FredParser,
    FredDailyParser,
}

pub struct FredDailyParser(pub FredDailySpec);

impl FredDailyParser {

    pub (crate) fn to_data(&self, series_spec: &SeriesSpec) -> Result<RegularTimeSeries::<1>, Error> {

        let observations = match Fred::series_observations(&series_spec.series_id.to_string()) {
            Ok(series_obs) => series_obs.observations,
            Err(e) => { return Err(err(file!(), line!(), &e.to_string())) },
        };

        let mut ts = TimeSeries::<1>::new(Vec::new());
        let mut month_data = Vec::new();
        let mut current_date = time_series::MonthlyDate::ym(0,0);

        for (i, obs) in observations.iter().enumerate() {

            // Get the Datapoint or else continue.

            match FredLine::new(obs, i + 1)?.to_datepoint() {
                Some(dp) => {

                    if dp.date() != current_date {

                        let value = month_data.iter().sum::<f32>() / month_data.len() as f32;

                        ts.push(DatePoint::<1>::new(current_date, [value]));

                        current_date = dp.date();

                    } else { month_data.push(dp.value(0)); }
                },
                None => {}, // Ignore dot line.
            }
        }
        ts.try_into()
            .map_err(|_| {
                err(file!(), line!(), &format!("Expected regular time-series ({} {} {})",
                    &series_spec.country(),
                    &series_spec.data_type,
                    &series_spec.series_id,
                ))
            })
    }
}