use std::fmt;
use std::fmt::{Debug, Display, Formatter};

use std::str::FromStr;

use nom::Parser;
use nom::{
  branch::alt,
  bytes::complete::tag,
  character::complete::{digit1 as digit, multispace0 as multispace},
  combinator::{map, map_res},
  multi::many,
  sequence::{delimited, preceded},
  IResult,
};

pub enum Expr {
  Value(i64),
  Add(Box<Expr>, Box<Expr>),
  Sub(Box<Expr>, Box<Expr>),
  Mul(Box<Expr>, Box<Expr>),
  Div(Box<Expr>, Box<Expr>),
  Paren(Box<Expr>),
}

#[derive(Debug)]
pub enum Oper {
  Add,
  Sub,
  Mul,
  Div,
}

impl Display for Expr {
  fn fmt(&self, format: &mut Formatter<'_>) -> fmt::Result {
    use self::Expr::*;
    match *self {
      Value(val) => write!(format, "{}", val),
      Add(ref left, ref right) => write!(format, "{} + {}", left, right),
      Sub(ref left, ref right) => write!(format, "{} - {}", left, right),
      Mul(ref left, ref right) => write!(format, "{} * {}", left, right),
      Div(ref left, ref right) => write!(format, "{} / {}", left, right),
      Paren(ref expr) => write!(format, "({})", expr),
    }
  }
}

impl Debug for Expr {
  fn fmt(&self, format: &mut Formatter<'_>) -> fmt::Result {
    use self::Expr::*;
    match *self {
      Value(val) => write!(format, "{}", val),
      Add(ref left, ref right) => write!(format, "({:?} + {:?})", left, right),
      Sub(ref left, ref right) => write!(format, "({:?} - {:?})", left, right),
      Mul(ref left, ref right) => write!(format, "({:?} * {:?})", left, right),
      Div(ref left, ref right) => write!(format, "({:?} / {:?})", left, right),
      Paren(ref expr) => write!(format, "[{:?}]", expr),
    }
  }
}

fn parens(i: &str) -> IResult<&str, Expr> {
  delimited(
    multispace,
    delimited(tag("("), map(expr, |e| Expr::Paren(Box::new(e))), tag(")")),
    multispace,
  )
  .parse(i)
}

fn factor(i: &str) -> IResult<&str, Expr> {
  alt((
    map(
      map_res(delimited(multispace, digit, multispace), FromStr::from_str),
      Expr::Value,
    ),
    parens,
  ))
  .parse(i)
}

fn fold_exprs(initial: Expr, remainder: Vec<(Oper, Expr)>) -> Expr {
  remainder.into_iter().fold(initial, |acc, pair| {
    let (oper, expr) = pair;
    match oper {
      Oper::Add => Expr::Add(Box::new(acc), Box::new(expr)),
      Oper::Sub => Expr::Sub(Box::new(acc), Box::new(expr)),
      Oper::Mul => Expr::Mul(Box::new(acc), Box::new(expr)),
      Oper::Div => Expr::Div(Box::new(acc), Box::new(expr)),
    }
  })
}

fn term(i: &str) -> IResult<&str, Expr> {
  let (i, initial) = factor(i)?;
  let (i, remainder) = many(
    0..,
    alt((
      |i| {
        let (i, mul) = preceded(tag("*"), factor).parse(i)?;
        Ok((i, (Oper::Mul, mul)))
      },
      |i| {
        let (i, div) = preceded(tag("/"), factor).parse(i)?;
        Ok((i, (Oper::Div, div)))
      },
    )),
  )
  .parse(i)?;

  Ok((i, fold_exprs(initial, remainder)))
}

fn expr(i: &str) -> IResult<&str, Expr> {
  let (i, initial) = term(i)?;
  let (i, remainder) = many(
    0..,
    alt((
      |i| {
        let (i, add) = preceded(tag("+"), term).parse(i)?;
        Ok((i, (Oper::Add, add)))
      },
      |i| {
        let (i, sub) = preceded(tag("-"), term).parse(i)?;
        Ok((i, (Oper::Sub, sub)))
      },
    )),
  )
  .parse(i)?;

  Ok((i, fold_exprs(initial, remainder)))
}

#[test]
fn factor_test() {
  assert_eq!(
    factor("  3  ").map(|(i, x)| (i, format!("{:?}", x))),
    Ok(("", String::from("3")))
  );
}

#[test]
fn term_test() {
  assert_eq!(
    term(" 3 *  5   ").map(|(i, x)| (i, format!("{:?}", x))),
    Ok(("", String::from("(3 * 5)")))
  );
}

#[test]
fn expr_test() {
  assert_eq!(
    expr(" 1 + 2 *  3 ").map(|(i, x)| (i, format!("{:?}", x))),
    Ok(("", String::from("(1 + (2 * 3))")))
  );
  assert_eq!(
    expr(" 1 + 2 *  3 / 4 - 5 ").map(|(i, x)| (i, format!("{:?}", x))),
    Ok(("", String::from("((1 + ((2 * 3) / 4)) - 5)")))
  );
  assert_eq!(
    expr(" 72 / 2 / 3 ").map(|(i, x)| (i, format!("{:?}", x))),
    Ok(("", String::from("((72 / 2) / 3)")))
  );
}

#[test]
fn parens_test() {
  assert_eq!(
    expr(" ( 1 + 2 ) *  3 ").map(|(i, x)| (i, format!("{:?}", x))),
    Ok(("", String::from("([(1 + 2)] * 3)")))
  );
}
