///|
using @symcore {type Expr, int}

///|
fn unary_application_arg(expr : Expr, name : String) -> Expr? {
  match @symcore.application_args(expr) {
    Some([arg]) if @symcore.application_has_name(expr, name, arity=1) =>
      Some(arg)
    _ => None
  }
}

///|
fn named_unary_application(expr : Expr) -> (String, Expr)? {
  match (@symcore.application_name(expr), @symcore.application_args(expr)) {
    (Some(name), Some([arg])) => Some((name, arg))
    _ => None
  }
}

///|
fn named_binary_application(expr : Expr) -> (String, Expr, Expr)? {
  match (@symcore.application_name(expr), @symcore.application_args(expr)) {
    (Some(name), Some([lhs, rhs])) => Some((name, lhs, rhs))
    _ => None
  }
}

///|
fn pow_named_unary_application(expr : Expr) -> (String, Expr, Expr)? {
  match expr {
    Expr::Pow(base, exp) =>
      match named_unary_application(base) {
        Some((name, arg)) => Some((name, arg, exp))
        None => None
      }
    _ => None
  }
}

///|
fn exact_numeric_value(expr : Expr) -> @symnum.BigRational? {
  match expr {
    Expr::Number(n) => Some(n)
    Expr::Float(f) =>
      if !f.is_finite() {
        None
      } else {
        let pair : Result[(BigInt, BigInt), @symnum.MpfError] = try? f.to_rational()
        match pair {
          Ok((num, den)) =>
            match
              (
                try? @symnum.BigRational::new(num, den) :
                Result[@symnum.BigRational, @symnum.RationalError]) {
              Ok(value) => Some(value)
              Err(_) => None
            }
          Err(_) => None
        }
      }
    Expr::ComplexFloat(z) =>
      if !z.is_finite() || !@symnum.is_zero(z.to_mpc().imag) {
        None
      } else {
        let pair : Result[(BigInt, BigInt), @symnum.MpfError] = try? z
          .real_part()
          .to_rational()
        match pair {
          Ok((num, den)) =>
            match
              (
                try? @symnum.BigRational::new(num, den) :
                Result[@symnum.BigRational, @symnum.RationalError]) {
              Ok(value) => Some(value)
              Err(_) => None
            }
          Err(_) => None
        }
      }
    _ => None
  }
}

///|
fn exact_integer_value(expr : Expr) -> Int? {
  match exact_numeric_value(expr) {
    Some(value) if value.is_integral() => {
      let num = value.numerator()
      if num.bit_length() > 30 {
        None
      } else {
        Some(num.to_int())
      }
    }
    _ => None
  }
}

///|
fn float_source_precision(expr : Expr) -> Int? {
  match expr {
    Expr::Float(f) => Some(f.precision())
    Expr::ComplexFloat(z) => Some(z.precision())
    Expr::Add(args) | Expr::Mul(args) => {
      let mut best : Int? = None
      for arg in args {
        match float_source_precision(arg) {
          Some(prec) =>
            best = Some(
              match best {
                Some(current) if current > prec => current
                _ => prec
              },
            )
          None => ()
        }
      }
      best
    }
    _ if @symcore.application_parts(expr) is Some(_) => {
      let mut best : Int? = None
      for arg in @symcore.application_args(expr).unwrap_or([]) {
        match float_source_precision(arg) {
          Some(prec) =>
            best = Some(
              match best {
                Some(current) if current > prec => current
                _ => prec
              },
            )
          None => ()
        }
      }
      best
    }
    Expr::Pow(base, exp) =>
      match (float_source_precision(base), float_source_precision(exp)) {
        (Some(lhs), Some(rhs)) => Some(if lhs > rhs { lhs } else { rhs })
        (Some(lhs), None) => Some(lhs)
        (None, Some(rhs)) => Some(rhs)
        (None, None) => None
      }
    _ => None
  }
}

///|
fn numeric_result_like(source : Expr, exact_result : Expr) -> Expr {
  match float_source_precision(source) {
    Some(prec) => @symcore.evalf(exact_result, prec~)
    None => exact_result
  }
}

///|
fn numeric_zero_pred(expr : Expr) -> Bool {
  match exact_numeric_value(expr) {
    Some(value) => value.is_zero()
    None => false
  }
}

///|
fn numeric_one_pred(expr : Expr) -> Bool {
  match exact_numeric_value(expr) {
    Some(value) => value.is_one()
    None => false
  }
}