use chrono::{NaiveTime, Timelike};
use diesel::backend::Backend;
use diesel::deserialize::FromSql;
use diesel::serialize::{IsNull, Output, ToSql};
use diesel::sql_types::Binary;
use diesel::sqlite::Sqlite;
use diesel::{deserialize, serialize};
use serde::{Deserialize, Serialize};
use std::io::Write;
use crate::db::models::Periods;

#[derive(Debug, Serialize, Deserialize, AsExpression, FromSqlRow, PartialEq, Clone)]
#[sql_type = "Binary"]
pub struct Period {
    #[serde(with = "period_format")]
    pub start: NaiveTime,
    #[serde(with = "period_format")]
    pub end: NaiveTime,
}

mod period_format {
    use chrono::NaiveTime;
    use serde::{self, Deserialize, Deserializer, Serializer};

    const FORMAT: &'static str = "%H:%M";

    pub fn serialize<S>(time: &NaiveTime, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let s = format!("{}", time.format(FORMAT));
        serializer.serialize_str(&s)
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<NaiveTime, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        NaiveTime::parse_from_str(&s, FORMAT).map_err(serde::de::Error::custom)
    }
}

impl ToSql<Binary, Sqlite> for Periods {
    fn to_sql<W: Write>(&self, out: &mut Output<W, Sqlite>) -> serialize::Result {
        for period in self.0.iter() {
            out.write_all(&[
                period.start.hour() as u8,
                period.start.minute() as u8,
                period.end.hour() as u8,
                period.end.minute() as u8,
            ])?;
        }
        Ok(IsNull::No)
    }
}

impl FromSql<Binary, Sqlite> for Periods {
    fn from_sql(bytes: Option<&<Sqlite as Backend>::RawValue>) -> deserialize::Result<Self> {
        let blob =  bytes.unwrap().read_blob();

        let mut vec = Vec::new();
        for i in (3..blob.len()).step_by(4) {
            let start_val_h: u32 = blob[i - 3] as u32;
            let start_val_m: u32 = blob[i - 2] as u32;
            let end_val_h: u32 = blob[i - 1] as u32;
            let end_val_m: u32 = blob[i - 0] as u32;
            vec.push(Period {
                start: NaiveTime::from_hms(start_val_h, start_val_m, 0),
                end: NaiveTime::from_hms(end_val_h, end_val_m, 0),
            });
        }
        Ok(Periods(vec))
    }
}