use pom::parser::*;
use crate::Expr;
use crate::Expr::{AnyValueExpr, CronExpr, LastValueExpr, ListExpr, NoOp, PerExpr, RangeExpr, ValueExpr};
fn min_digit<'a>() -> Parser<'a, u8, Expr> {
  (one_of(b"12345") + one_of(b"0123456789")).map(|(e1, e2)| ValueExpr((e1 - 48) * 10 + e2 - 48))
    | (sym(b'0') * one_of(b"0123456789")).map(|e| ValueExpr(e - 48))
    | (one_of(b"0123456789")).map(|e| ValueExpr(e - 48))
}
fn hour_digit<'a>() -> Parser<'a, u8, Expr> {
  (sym(b'2') + one_of(b"0123")).map(|(e1, e2)| ValueExpr((e1 - 48) * 10 + e2 - 48))
    | (sym(b'1') + one_of(b"0123456789")).map(|(e1, e2)| ValueExpr((e1 - 48) * 10 + e2 - 48))
    | sym(b'0') * one_of(b"0123456789").map(|e| ValueExpr(e - 48))
    | one_of(b"0123456789").map(|e| ValueExpr(e - 48))
}
fn day_digit<'a>() -> Parser<'a, u8, Expr> {
  (sym(b'3') + one_of(b"01")).map(|(e1, e2)| ValueExpr((e1 - 48) * 10 + e2 - 48))
    | (one_of(b"12") + one_of(b"0123456789")).map(|(e1, e2)| ValueExpr((e1 - 48) * 10 + e2 - 48))
    | (sym(b'0') * one_of(b"123456789")).map(|e| ValueExpr(e - 48))
    | one_of(b"123456789").map(|e| ValueExpr(e - 48))
}
fn month_digit<'a>() -> Parser<'a, u8, Expr> {
  (sym(b'1') + one_of(b"012")).map(|(e1, e2)| ValueExpr((e1 - 48) * 10 + e2 - 48))
    | (sym(b'0') * one_of(b"123456789")).map(|e| ValueExpr(e - 48))
    | one_of(b"123456789").map(|e| ValueExpr(e - 48))
}
fn day_of_week_digit<'a>() -> Parser<'a, u8, Expr> {
  seq(b"SUN").map(|_| ValueExpr(1))
    | seq(b"MON").map(|_| ValueExpr(2))
    | seq(b"TUE").map(|_| ValueExpr(3))
    | seq(b"WED").map(|_| ValueExpr(4))
    | seq(b"THU").map(|_| ValueExpr(5))
    | seq(b"FRI").map(|_| ValueExpr(6))
    | seq(b"SAT").map(|_| ValueExpr(7))
    | sym(b'L').map(|_| LastValueExpr)
}
fn day_of_week_text<'a>() -> Parser<'a, u8, Expr> {
  one_of(b"1234567").map(|e| ValueExpr(e as u8 - 48))
}
fn asterisk<'a>() -> Parser<'a, u8, Expr> {
  sym(b'*').map(|_| AnyValueExpr)
}
fn per(p: Parser<u8, Expr>) -> Parser<u8, Expr> {
  sym(b'/') * p
}
fn asterisk_per(p: Parser<u8, Expr>) -> Parser<u8, Expr> {
  (asterisk() + per(p)).map(|(d, op)| PerExpr {
    digit: Box::from(d.clone()),
    option: Box::from(op.clone()),
  })
}
fn range_per(p: Parser<u8, Expr>) -> Parser<u8, Expr> {
  per(p).opt().map(|e| match e {
    None => NoOp,
    Some(s) => s,
  })
}
macro_rules! range {
  ( $x:expr ) => {
    ($x - sym(b'-') + $x + range_per($x)).map(|((e1, e2), e3)| RangeExpr {
      from: Box::from(e1),
      to: Box::from(e2),
      per_option: Box::from(e3),
    })
  };
}
fn list(p: Parser<u8, Expr>) -> Parser<u8, Expr> {
  pom::parser::list(p, sym(b',')).map(|e| match e {
    e if e.len() == 1 => e.get(0).unwrap().clone(),
    e => ListExpr(e),
  })
}
macro_rules! digit_instruction {
  ( $x:expr ) => {
    asterisk_per($x) | asterisk() | list(range!($x) | $x)
  };
}
fn instruction<'a>() -> Parser<'a, u8, Expr> {
  (digit_instruction!(min_digit()) - sym(b' ') + digit_instruction!(hour_digit()) - sym(b' ')
    + digit_instruction!(day_digit())
    - sym(b' ')
    + digit_instruction!(month_digit())
    - sym(b' ')
    + digit_instruction!(day_of_week_text() | day_of_week_digit()))
  .map(|((((mins, hours), days), months), day_of_weeks)| CronExpr {
    mins: Box::from(mins),
    hours: Box::from(hours),
    days: Box::from(days),
    months: Box::from(months),
    day_of_weeks: Box::from(day_of_weeks),
  })
}
pub struct CronParser;
impl CronParser {
  pub fn parse(source: &str) -> pom::Result<Expr> {
    (instruction() - end()).parse(source.as_bytes())
  }
}
#[cfg(test)]
mod tests {
  use super::*;
  #[test]
  fn test_instruction() {
    let result = (instruction() - end()).parse(b"* * * * *").unwrap();
    assert_eq!(
      result,
      CronExpr {
        mins: Box::from(AnyValueExpr),
        hours: Box::from(AnyValueExpr),
        days: Box::from(AnyValueExpr),
        months: Box::from(AnyValueExpr),
        day_of_weeks: Box::from(AnyValueExpr)
      }
    );
    let result = (instruction() - end()).parse(b"1 1 1 1 1").unwrap();
    assert_eq!(
      result,
      CronExpr {
        mins: Box::from(ValueExpr(1)),
        hours: Box::from(ValueExpr(1)),
        days: Box::from(ValueExpr(1)),
        months: Box::from(ValueExpr(1)),
        day_of_weeks: Box::from(ValueExpr(1))
      }
    );
  }
  #[test]
  fn test_digit_instruction() {
    let result = (digit_instruction!(min_digit()) - end()).parse(b"*").unwrap();
    assert_eq!(result, AnyValueExpr);
    let result = (digit_instruction!(min_digit()) - end()).parse(b"*/2").unwrap();
    assert_eq!(
      result,
      PerExpr {
        digit: Box::from(AnyValueExpr),
        option: Box::from(ValueExpr(2))
      }
    );
    let result = (digit_instruction!(min_digit()) - end()).parse(b"1-10/2").unwrap();
    assert_eq!(
      result,
      RangeExpr {
        from: Box::from(ValueExpr(1)),
        to: Box::from(ValueExpr(10)),
        per_option: Box::from(ValueExpr(2))
      }
    );
    let result = (digit_instruction!(min_digit()) - end()).parse(b"1,2,3").unwrap();
    assert_eq!(result, ListExpr(vec![ValueExpr(1), ValueExpr(2), ValueExpr(3)]));
    let result = (digit_instruction!(min_digit()) - end()).parse(b"1").unwrap();
    assert_eq!(result, ValueExpr(1));
  }
  #[test]
  fn test_list() {
    let s = (0..=59).map(|v| v.to_string()).collect::<Vec<_>>().join(",");
    let result = (list(min_digit()) - end()).parse(s.as_bytes()).unwrap();
    let values = (0..=59).map(|v| ValueExpr(v)).collect::<Vec<_>>();
    assert_eq!(result, ListExpr(values));
  }
  #[test]
  fn test_range() {
    for n2 in 1..=59 {
      let option = n2 / 2;
      let n1 = n2 - 1;
      let s: &str = &format!("{:<02}-{:<02}/{:<02}", n1, n2, option);
      println!("{}", s);
      let result = (range!(min_digit()) - end()).parse(s.as_bytes()).unwrap();
      assert_eq!(
        result,
        RangeExpr {
          from: Box::from(ValueExpr(n1)),
          to: Box::from(ValueExpr(n2)),
          per_option: Box::from(ValueExpr(option)),
        }
      );
    }
  }
  #[test]
  fn test_asterisk_per() {
    for n in 0..59 {
      let s: &str = &format!("*/{:<02}", n);
      let result = (asterisk_per(min_digit()) - end()).parse(s.as_bytes()).unwrap();
      assert_eq!(
        result,
        PerExpr {
          digit: Box::from(AnyValueExpr),
          option: Box::from(ValueExpr(n)),
        }
      );
    }
  }
  #[test]
  fn test_per() {
    let result = (per(min_digit()) - end()).parse(b"/2").unwrap();
    assert_eq!(result, ValueExpr(2));
  }
  #[test]
  fn test_min_digit() {
    for n in 0..59 {
      let s: &str = &format!("{:<02}", n);
      let result = (min_digit() - end()).parse(s.as_bytes()).unwrap();
      assert_eq!(result, ValueExpr(n));
    }
    let result = (min_digit() - end()).parse(b"60");
    assert_eq!(result.is_err(), true);
  }
  #[test]
  fn test_hour_digit() {
    for n in 0..=23 {
      if n < 10 {
        let s: &str = &n.to_string();
        let result: Expr = (hour_digit() - end()).parse(s.as_bytes()).unwrap();
        assert_eq!(result, ValueExpr(n));
      }
      let s: &str = &format!("{:<02}", n);
      let result: Expr = (hour_digit() - end()).parse(s.as_bytes()).unwrap();
      assert_eq!(result, ValueExpr(n));
    }
    let result = (hour_digit() - end()).parse(b"24");
    assert_eq!(result.is_err(), true);
  }
  #[test]
  fn test_day_digit() {
    for n in 1..=31 {
      if n < 10 {
        let s: &str = &n.to_string();
        let result: Expr = (day_digit() - end()).parse(s.as_bytes()).unwrap();
        assert_eq!(result, ValueExpr(n));
      }
      let s: &str = &format!("{:<02}", n);
      let result: Expr = (day_digit() - end()).parse(s.as_bytes()).unwrap();
      assert_eq!(result, ValueExpr(n));
    }
    let result = (day_digit() - end()).parse(b"32");
    assert_eq!(result.is_err(), true);
  }
  #[test]
  fn test_month_digit() {
    for n in 1..=12 {
      if n < 10 {
        let s: &str = &n.to_string();
        let result: Expr = (month_digit() - end()).parse(s.as_bytes()).unwrap();
        assert_eq!(result, ValueExpr(n));
      }
      let s: &str = &format!("{:<02}", n);
      let result: Expr = (month_digit() - end()).parse(s.as_bytes()).unwrap();
      assert_eq!(result, ValueExpr(n));
    }
    let result = (month_digit() - end()).parse(b"13");
    assert_eq!(result.is_err(), true);
  }
}