///|
pub type MatchEnv = Map[Expr, Expr]

///|
pub type WildPropertyResolver = (WildProperty, Expr) -> Bool?

///|
let wild_property_resolver_ref : Ref[WildPropertyResolver?] = { val: None }

///|
pub fn set_wild_property_resolver(resolver : WildPropertyResolver) -> Unit {
  wild_property_resolver_ref.val = Some(resolver)
}

///|
pub fn clear_wild_property_resolver() -> Unit {
  wild_property_resolver_ref.val = None
}

///|
fn clone_match_env(env : MatchEnv) -> MatchEnv {
  let out : MatchEnv = {}
  for key, value in env {
    out.set(key, value)
  }
  out
}

///|
fn match_env_rules(env : MatchEnv) -> Array[(Expr, Expr)] {
  let out : Array[(Expr, Expr)] = []
  for key, value in env {
    out.push((key, value))
  }
  out
}

///|
fn expr_contains_wild(expr : Expr) -> Bool {
  let expr = normalize_legacy_expr(expr)
  match expr {
    Expr::Wild(_, _, _) | Expr::WildFunction(_, _) => true
    _ => {
      for child in args(expr) {
        if expr_contains_wild(child) {
          return true
        }
      }
      false
    }
  }
}

///|
fn combine_commutative_items(items : Array[Expr], is_add : Bool) -> Expr {
  match items.length() {
    0 => if is_add { int(0) } else { int(1) }
    1 => items[0]
    _ => if is_add { add(items) } else { mul(items) }
  }
}

///|
fn match_count_ops(expr : Expr) -> Int {
  match normalize_legacy_expr(expr) {
    Expr::Add(items) => {
      let mut count = if items.is_empty() { 0 } else { items.length() - 1 }
      for item in items {
        count += match_count_ops(item)
      }
      count
    }
    Expr::Mul(items) =>
      match split_fraction_for_match(Expr::Mul(items)) {
        Some((numerator, denominator)) => {
          let numerator_is_exact_integer = match numerator {
            Expr::Number(value) => value.is_integral()
            _ => false
          }
          if is_one(denominator) {
            let mut count = if items.is_empty() {
              0
            } else {
              items.length() - 1
            }
            for item in items {
              count += match_count_ops(item)
            }
            count
          } else if numerator_is_exact_integer {
            1 + match_count_ops(denominator)
          } else {
            1 + match_count_ops(numerator) + match_count_ops(denominator)
          }
        }
        None => {
          let mut count = if items.is_empty() { 0 } else { items.length() - 1 }
          for item in items {
            count += match_count_ops(item)
          }
          count
        }
      }
    Expr::Pow(base, exp) =>
      match exact_integer_value(exp) {
        Some(value) if value.compare(-1N) == 0 => 1 + match_count_ops(base)
        _ => 1 + match_count_ops(base) + match_count_ops(exp)
      }
    Expr::Mod(lhs, rhs) => 1 + match_count_ops(lhs) + match_count_ops(rhs)
    Expr::Apply(head, args) => {
      let mut count = 1 + match_count_ops(head)
      for arg in args {
        count += match_count_ops(arg)
      }
      count
    }
    Expr::Tuple(items) => {
      let mut count = 0
      for item in items {
        count += match_count_ops(item)
      }
      count
    }
    Expr::Dict(items) => {
      let mut count = 0
      for item in items {
        let (key, value) = item
        count += match_count_ops(key) + match_count_ops(value)
      }
      count
    }
    Expr::Relational(_, lhs, rhs) =>
      1 + match_count_ops(lhs) + match_count_ops(rhs)
    Expr::Derivative(inner, deriv_args) => {
      let mut count = 1 + match_count_ops(inner)
      for arg in deriv_args {
        count += match_count_ops(arg)
      }
      count
    }
    Expr::Subs(inner, variable, value) =>
      1 +
      match_count_ops(inner) +
      match_count_ops(variable) +
      match_count_ops(value)
    Expr::Lambda(vars, body) =>
      1 + match_count_ops(vars) + match_count_ops(body)
    _ => 0
  }
}

///|
fn split_fraction_for_match(expr : Expr) -> (Expr, Expr)? {
  let expr = normalize_legacy_expr(expr)
  let numer_items : Array[Expr] = []
  let denom_items : Array[Expr] = []
  let mut coeff = @symnum.BigRational::one()
  let push_power = fn(items : Array[Expr], base : Expr, exponent : BigInt) {
    if exponent.compare(1N) == 0 {
      items.push(base)
    } else {
      items.push(
        pow(base, Expr::Number(@symnum.BigRational::from_bigint(exponent))),
      )
    }
  }
  let push_term = fn(term : Expr, sign : Int) -> Bool {
    match normalize_legacy_expr(term) {
      Expr::Number(value) => {
        coeff = if sign >= 0 {
          coeff.mul_r(value)
        } else {
          coeff.div_r(value) catch {
            _ => return false
          }
        }
        true
      }
      Expr::Pow(base, exp) =>
        match exact_integer_value(exp) {
          Some(value) if value.compare(0N) < 0 => {
            if sign >= 0 {
              push_power(denom_items, base, value.neg())
            } else {
              push_power(numer_items, base, value.neg())
            }
            true
          }
          Some(value) => {
            if sign >= 0 {
              push_power(numer_items, base, value)
            } else {
              push_power(denom_items, base, value)
            }
            true
          }
          None => {
            if sign >= 0 {
              numer_items.push(term)
            } else {
              denom_items.push(term)
            }
            true
          }
        }
      other => {
        if sign >= 0 {
          numer_items.push(other)
        } else {
          denom_items.push(other)
        }
        true
      }
    }
  }
  match expr {
    Expr::Mul(items) =>
      for item in items {
        if !push_term(item, 1) {
          return None
        }
      }
    Expr::Pow(_, _) | Expr::Number(_) => if !push_term(expr, 1) { return None }
    _ => return None
  }
  if coeff.denominator().compare(1N) != 0 {
    denom_items.push(
      Expr::Number(@symnum.BigRational::from_bigint(coeff.denominator())),
    )
  }
  if coeff.numerator().compare(1N) != 0 || numer_items.is_empty() {
    numer_items.insert(
      0,
      Expr::Number(@symnum.BigRational::from_bigint(coeff.numerator())),
    )
  }
  Some(
    (
      combine_commutative_items(numer_items, false),
      combine_commutative_items(denom_items, false),
    ),
  )
}

///|
fn common_add_wild_factor(items : Array[Expr]) -> (Expr, Array[Expr])? {
  let mut common : Expr? = None
  let remainders : Array[Expr] = []
  for item in items {
    match item {
      Expr::Mul(factors) => {
        let wild_factors : Array[Expr] = []
        let plain_factors : Array[Expr] = []
        for factor in factors {
          if expr_contains_wild(factor) {
            wild_factors.push(factor)
          } else {
            plain_factors.push(factor)
          }
        }
        guard wild_factors.length() == 1 else { return None }
        let factor = wild_factors[0]
        match common {
          Some(existing) if compare_expr(existing, factor) != 0 => return None
          Some(_) => ()
          None => common = Some(factor)
        }
        remainders.push(combine_commutative_items(plain_factors, false))
      }
      _ if expr_contains_wild(item) => {
        match common {
          Some(existing) if compare_expr(existing, item) != 0 => return None
          Some(_) => ()
          None => common = Some(item)
        }
        remainders.push(int(1))
      }
      _ => return None
    }
  }
  common.map(factor => (factor, remainders))
}

///|
fn remove_exact_commutative_items(
  exact_items : Array[Expr],
  expr_items : Array[Expr],
) -> Array[Expr]? {
  let remaining = expr_items.copy()
  for exact in exact_items {
    let mut found = false
    for i in 0.. (Array[Expr], Array[Expr]) {
  let selected : Array[Expr] = []
  let remaining : Array[Expr] = []
  for i in 0.. Bool {
  match normalize_legacy_expr(expr) {
    Expr::Wild(_, _, _) | Expr::WildFunction(_, _) => true
    _ => false
  }
}

///|
fn expr_shape_rank(expr : Expr) -> Int {
  match normalize_legacy_expr(expr) {
    Expr::Add(_) => 0
    Expr::Mul(_) => 1
    Expr::Pow(_, _) => 2
    Expr::Mod(_, _) => 3
    Expr::Apply(_, _) => 4
    Expr::Tuple(_) => 5
    Expr::Dict(_) => 6
    Expr::Relational(_, _, _) => 7
    Expr::Derivative(_, _) => 8
    Expr::Subs(_, _, _) => 9
    Expr::Lambda(_, _) => 10
    Expr::Number(_) | Expr::Float(_) | Expr::ComplexFloat(_) => 11
    Expr::NumberSymbol(_) => 12
    Expr::Symbol(_) | Expr::Dummy(_, _) => 13
    Expr::Wild(_, _, _) | Expr::WildFunction(_, _) => 14
    Expr::Boolean(_) => 15
    Expr::FunctionHead(_)
    | Expr::UndefinedFunction(_)
    | Expr::IdentityFunction => 16
    Expr::Function(_, _) => abort("legacy function should be normalized")
  }
}

///|
fn mask_selected_count(mask : Int, length : Int) -> Int {
  let mut out = 0
  for i in 0.. Int {
  let selected_count = mask_selected_count(mask, remaining_items.length())
  if !is_add &&
    is_plain_pattern_wild(pattern) &&
    remaining_items.length() < remaining_patterns_len {
    return if selected_count == 0 { 0 } else { 1000 + selected_count }
  }
  let (selected, _) = split_items_by_mask(remaining_items, mask)
  let candidate = combine_commutative_items(selected, is_add)
  let pattern_rank = expr_shape_rank(pattern)
  let candidate_rank = expr_shape_rank(candidate)
  let shape_penalty = if pattern_rank == candidate_rank { 0 } else { 100 }
  let empty_penalty = if selected_count == 0 { 10000 } else { 0 }
  shape_penalty + empty_penalty + selected_count
}

///|
fn match_commutative_items(
  pattern_items : Array[Expr],
  expr_items : Array[Expr],
  is_add : Bool,
  repl_dict : MatchEnv,
) -> MatchEnv? {
  let exact_items : Array[Expr] = []
  let wild_items : Array[Expr] = []
  let wild_priority = fn(expr : Expr) -> Int {
    match normalize_legacy_expr(expr) {
      Expr::Apply(Expr::WildFunction(_, _), _) => 0
      Expr::WildFunction(_, _) => 1
      Expr::Wild(_, _, _) => 3
      _ => 2
    }
  }
  for item in pattern_items {
    if expr_contains_wild(item) {
      wild_items.push(item)
    } else {
      exact_items.push(item)
    }
  }
  wild_items.sort_by((lhs, rhs) => {
    cmp_int(wild_priority(lhs), wild_priority(rhs))
  })
  if is_add && wild_items.length() == 1 {
    let expr_sum = combine_commutative_items(expr_items, true)
    let remainder = match
      remove_exact_commutative_items(exact_items, expr_items) {
      Some(items) => combine_commutative_items(items, true)
      None =>
        match exact_items.length() {
          0 => expr_sum
          _ =>
            add([
              expr_sum,
              mul([int(-1), combine_commutative_items(exact_items, true)]),
            ])
        }
    }
    if exact_items.length() > 0 &&
      match_count_ops(remainder) > match_count_ops(expr_sum) {
      return None
    }
    return expr_match(wild_items[0], remainder, repl_dict~)
  }
  let remaining_expr = match
    remove_exact_commutative_items(exact_items, expr_items) {
    Some(items) => items
    None => return None
  }
  if wild_items.is_empty() {
    return if remaining_expr.is_empty() {
      Some(clone_match_env(repl_dict))
    } else {
      None
    }
  }
  letrec go = (
    remaining_patterns : Array[Expr],
    remaining_items : Array[Expr],
    env : MatchEnv,
  ) => {
    match remaining_patterns.length() {
      0 => if remaining_items.is_empty() { Some(env) } else { None }
      1 =>
        expr_match(
          remaining_patterns[0],
          combine_commutative_items(remaining_items, is_add),
          repl_dict=env,
        )
      _ => {
        let current = remaining_patterns[0]
        let rest = remaining_patterns[1:].to_owned()
        let masks : Array[Int] = []
        let max_mask = 1 << remaining_items.length()
        for mask in 0.. {
          let lhs_score = commutative_mask_score(
            current,
            remaining_patterns.length(),
            remaining_items,
            is_add,
            lhs,
          )
          let rhs_score = commutative_mask_score(
            current,
            remaining_patterns.length(),
            remaining_items,
            is_add,
            rhs,
          )
          cmp_int(lhs_score, rhs_score)
        })
        for mask in masks {
          let (selected, leftover) = split_items_by_mask(remaining_items, mask)
          let candidate = combine_commutative_items(selected, is_add)
          match expr_match(current, candidate, repl_dict=env) {
            Some(next_env) =>
              match go(rest, leftover, next_env) {
                Some(done) => return Some(done)
                None => ()
              }
            None => ()
          }
        }
        None
      }
    }
  }
  go(wild_items, remaining_expr, clone_match_env(repl_dict))
}

///|
fn exact_number_sign(value : @symnum.BigRational) -> Int {
  value.numerator().compare(0N)
}

///|
fn insert_monomial_exponent(
  parts : Array[(Expr, BigInt)],
  base : Expr,
  exponent : BigInt,
) -> Unit {
  if exponent.compare(0N) == 0 {
    return
  }
  for i in 0.. @symnum.BigRational? {
  if exponent.compare(0N) == 0 {
    return Some(@symnum.BigRational::one())
  }
  let mut power = exponent
  let mut base_value = base
  if power.compare(0N) < 0 {
    power = power.neg()
    base_value = base.reciprocal() catch { _ => return None }
  }
  let mut acc = @symnum.BigRational::one()
  while power.compare(0N) > 0 {
    if power.mod(2N).compare(0N) != 0 {
      acc = acc.mul_r(base_value)
    }
    power = power.div(2N)
    if power.compare(0N) > 0 {
      base_value = base_value.mul_r(base_value)
    }
  }
  Some(acc)
}

///|
fn monomial_parts(expr : Expr) -> (@symnum.BigRational, Array[(Expr, BigInt)])? {
  let mut coeff = @symnum.BigRational::one()
  let factors : Array[(Expr, BigInt)] = []
  let merge = fn(term : Expr, exponent : BigInt) -> Bool {
    match exact_numeric_expr_to_rational(term) {
      Some(value) =>
        match exact_rational_pow(value, exponent) {
          Some(powered) => {
            coeff = coeff.mul_r(powered)
            true
          }
          None => false
        }
      None => {
        insert_monomial_exponent(factors, term, exponent)
        true
      }
    }
  }
  letrec go = (term : Expr) => {
    match normalize_legacy_expr(term) {
      Expr::Mul(items) => {
        for item in items {
          if !go(item) {
            return false
          }
        }
        true
      }
      Expr::Pow(base, exp) =>
        match exact_numeric_expr_to_rational(exp) {
          Some(value) if value.is_integral() => merge(base, value.numerator())
          _ => false
        }
      other => merge(other, 1N)
    }
  }
  if !go(normalize_legacy_expr(expr)) {
    return None
  }
  Some((coeff, factors))
}

///|
fn monomial_expr(
  coeff : @symnum.BigRational,
  factors : Array[(Expr, BigInt)],
) -> Expr {
  let items : Array[Expr] = []
  if coeff.compare(@symnum.BigRational::one()) != 0 || factors.is_empty() {
    items.push(Expr::Number(coeff))
  }
  let ordered = factors.copy()
  ordered.sort_by((lhs, rhs) => compare_expr(lhs.0, rhs.0))
  for factor in ordered {
    let (base, exponent) = factor
    if exponent.compare(1N) == 0 {
      items.push(base)
    } else {
      items.push(
        pow(base, Expr::Number(@symnum.BigRational::from_bigint(exponent))),
      )
    }
  }
  combine_commutative_items(items, false)
}

///|
fn divide_monomial_expr(expr : Expr, factor : Expr) -> Expr? {
  match (monomial_parts(expr), monomial_parts(factor)) {
    (Some((expr_coeff, expr_factors)), Some((factor_coeff, factor_factors))) =>
      if factor_coeff.is_zero() {
        None
      } else {
        let quotient_coeff = expr_coeff.div_r(factor_coeff) catch {
          _ => return None
        }
        let quotient_factors = expr_factors.copy()
        for factor_item in factor_factors {
          let (base, exponent) = factor_item
          insert_monomial_exponent(quotient_factors, base, exponent.neg())
        }
        Some(monomial_expr(quotient_coeff, quotient_factors))
      }
    _ => None
  }
}

///|
fn divide_add_by_exact_template(
  expr_items : Array[Expr],
  factor_items : Array[Expr],
) -> Expr? {
  if expr_items.length() != factor_items.length() {
    return None
  }
  letrec go = (
    remaining_expr : Array[Expr],
    remaining_factor : Array[Expr],
    quotient : Expr?,
  ) => {
    if remaining_factor.is_empty() {
      return quotient
    }
    let factor_term = remaining_factor[0]
    let rest_factor = remaining_factor[1:].to_owned()
    for i in 0.. {
          if quotient is Some(existing) &&
            compare_expr(existing, candidate) != 0 {
            continue
          }
          let leftover = remaining_expr.copy()
          ignore(leftover.remove(i))
          match go(leftover, rest_factor, Some(candidate)) {
            Some(done) => return Some(done)
            None => ()
          }
        }
        None => ()
      }
    }
    None
  }
  go(expr_items, factor_items, None)
}

///|
fn divide_expr_by_exact_factor(expr : Expr, factor : Expr) -> Expr? {
  let expr = normalize_legacy_expr(expr)
  let factor = normalize_legacy_expr(factor)
  if is_one(factor) {
    return Some(expr)
  }
  if compare_expr(expr, factor) == 0 {
    return Some(int(1))
  }
  match (expr, factor) {
    (Expr::Add(expr_items), Expr::Add(factor_items)) =>
      divide_add_by_exact_template(expr_items, factor_items)
    _ => divide_monomial_expr(expr, factor)
  }
}

///|
fn resolve_wild_property(property : WildProperty, expr : Expr) -> Bool? {
  match wild_property_resolver_ref.val {
    Some(resolver) => resolver(property, expr)
    None => None
  }
}

///|
fn wild_property_matches(property : WildProperty, expr : Expr) -> Bool {
  let expr = normalize_legacy_expr(expr)
  match resolve_wild_property(property, expr) {
    Some(value) => return value
    None => ()
  }
  match property {
    WildProperty::Symbol =>
      match expr_form(expr) {
        ExprForm::Symbol(_) | ExprForm::Dummy(_, _) | ExprForm::Wild(_, _, _) =>
          true
        _ => false
      }
    WildProperty::Integer =>
      match expr_form(expr) {
        ExprForm::Number(value) => value.is_integral()
        _ => false
      }
    WildProperty::Rational =>
      match expr_form(expr) {
        ExprForm::Number(_) => true
        _ => false
      }
    WildProperty::Real => is_real_number_atom(expr)
    WildProperty::Positive =>
      match expr_form(expr) {
        ExprForm::Number(value) => exact_number_sign(value) > 0
        ExprForm::NumberSymbol(kind) =>
          match kind {
            NumberSymbolKind::Pi
            | NumberSymbolKind::Exp1
            | NumberSymbolKind::EulerGamma
            | NumberSymbolKind::GoldenRatio
            | NumberSymbolKind::Catalan
            | NumberSymbolKind::Infinity => true
            _ => false
          }
        _ => false
      }
    WildProperty::Negative =>
      match expr_form(expr) {
        ExprForm::Number(value) => exact_number_sign(value) < 0
        ExprForm::NumberSymbol(NumberSymbolKind::NegativeInfinity) => true
        _ => false
      }
    WildProperty::Finite => is_finite_number_atom(expr)
    WildProperty::Nonzero =>
      match expr_form(expr) {
        ExprForm::Number(value) => !value.is_zero()
        ExprForm::NumberSymbol(kind) =>
          match kind {
            NumberSymbolKind::Pi
            | NumberSymbolKind::Exp1
            | NumberSymbolKind::EulerGamma
            | NumberSymbolKind::GoldenRatio
            | NumberSymbolKind::Catalan
            | NumberSymbolKind::Infinity
            | NumberSymbolKind::NegativeInfinity
            | NumberSymbolKind::ComplexInfinity => true
            _ => false
          }
        _ => false
      }
  }
}

///|
fn is_exact_negative_one(expr : Expr) -> Bool {
  match expr_form(expr) {
    ExprForm::Number(value) =>
      value.compare(@symnum.BigRational::from_int(-1)) == 0
    _ => false
  }
}

///|
fn exact_integer_value(expr : Expr) -> BigInt? {
  match expr_form(expr) {
    ExprForm::Number(value) if value.is_integral() => Some(value.numerator())
    _ => None
  }
}

///|
fn reciprocal_for_match(expr : Expr) -> Expr {
  let expr = normalize_legacy_expr(expr)
  match expr {
    Expr::Pow(base, exp) if is_exact_negative_one(exp) => base
    Expr::Number(value) =>
      if value.is_zero() {
        pow(expr, int(-1))
      } else {
        Expr::Number(
          @symnum.BigRational::new(value.denominator(), value.numerator()) catch {
            _ => return pow(expr, int(-1))
          },
        )
      }
    Expr::NumberSymbol(NumberSymbolKind::Infinity)
    | Expr::NumberSymbol(NumberSymbolKind::NegativeInfinity)
    | Expr::NumberSymbol(NumberSymbolKind::ComplexInfinity) => int(0)
    Expr::NumberSymbol(NumberSymbolKind::NaN) => expr
    _ => pow(expr, int(-1))
  }
}

///|
fn reciprocal_root_for_match(expr : Expr, degree : BigInt) -> Expr {
  if degree.compare(1N) == 0 {
    reciprocal_for_match(expr)
  } else {
    let exponent = Expr::Number(
      @symnum.BigRational::new(-1N, degree) catch {
        _ => return reciprocal_for_match(expr)
      },
    )
    pow(normalize_legacy_expr(expr), exponent)
  }
}

///|
fn base_exp_for_match(expr : Expr) -> (Expr, Expr) {
  match normalize_legacy_expr(expr) {
    Expr::Pow(base, exp) => (base, exp)
    other => (other, int(1))
  }
}

///|
fn positive_root_for_match(expr : Expr, degree : BigInt) -> Expr {
  let expr = normalize_legacy_expr(expr)
  if degree.compare(1N) == 0 {
    return expr
  }
  let one_over_degree_result : Result[
    @symnum.BigRational,
    @symnum.RationalError,
  ] = try? @symnum.BigRational::new(1N, degree)
  let one_over_degree = match one_over_degree_result {
    Ok(value) => Expr::Number(value)
    Err(_) => return pow(expr, int(1))
  }
  match expr {
    Expr::Number(value) =>
      if exact_number_sign(value) < 0 && degree.mod(2N).compare(0N) == 0 {
        let magnitude_result : Result[
          @symnum.BigRational,
          @symnum.RationalError,
        ] = try? @symnum.BigRational::new(
          value.numerator().neg(),
          value.denominator(),
        )
        let magnitude = match magnitude_result {
          Ok(ratio) => Expr::Number(ratio)
          Err(_) => return pow(expr, one_over_degree)
        }
        mul([
          Expr::NumberSymbol(NumberSymbolKind::ImaginaryUnit),
          pow(magnitude, one_over_degree),
        ])
      } else {
        pow(expr, one_over_degree)
      }
    Expr::Mul(items) =>
      if degree.mod(2N).compare(0N) == 0 && !items.is_empty() {
        let mut minus_idx = -1
        for i in 0..= 0 {
          let remainder : Array[Expr] = []
          for i in 0.. pow(base, mul([exp, one_over_degree]))
    _ => pow(expr, one_over_degree)
  }
}

///|
fn expand_multiplicative_coeff_for_add_match(expr : Expr) -> Expr? {
  match monomial_parts(expr) {
    Some((coeff, factors)) => {
      let one = @symnum.BigRational::one()
      let neg_one = @symnum.BigRational::from_int(-1)
      if coeff.compare(one) > 0 {
        Some(
          raw_add([
            monomial_expr(one, factors),
            monomial_expr(coeff.add_r(neg_one), factors),
          ]),
        )
      } else if coeff.compare(neg_one) < 0 {
        Some(
          raw_add([
            monomial_expr(neg_one, factors),
            monomial_expr(coeff.add_r(one), factors),
          ]),
        )
      } else {
        None
      }
    }
    None => None
  }
}

///|
fn expand_positive_power_for_mul_match(expr : Expr) -> Expr? {
  match normalize_legacy_expr(expr) {
    Expr::Pow(base, exp) =>
      match exact_integer_value(exp) {
        Some(n) if n.compare(1N) > 0 && n.compare(32N) <= 0 => {
          let count = n.to_int()
          let factors : Array[Expr] = []
          for _ in 0.. None
      }
    _ => None
  }
}

///|
pub fn wild_matches(
  pattern : Expr,
  expr : Expr,
  repl_dict? : MatchEnv = {},
) -> MatchEnv? {
  let pattern = normalize_legacy_expr(pattern)
  let expr = normalize_legacy_expr(expr)
  match pattern {
    Expr::Wild(_, exclude, properties) => {
      for item in exclude {
        if has(expr, item) {
          return None
        }
      }
      for property in properties {
        if !wild_property_matches(property, expr) {
          return None
        }
      }
      let out = clone_match_env(repl_dict)
      out.set(pattern, expr)
      Some(out)
    }
    _ => None
  }
}

///|
fn wild_function_matches_arity(nargs : Array[Int], arity : Int) -> Bool {
  nargs.is_empty() || nargs.contains(arity)
}

///|
pub fn wild_function_matches(
  pattern : Expr,
  expr : Expr,
  repl_dict? : MatchEnv = {},
) -> MatchEnv? {
  let pattern = normalize_legacy_expr(pattern)
  let expr = normalize_legacy_expr(expr)
  match pattern {
    Expr::WildFunction(_, nargs) =>
      match expr {
        Expr::WildFunction(_, _) => {
          let out = clone_match_env(repl_dict)
          out.set(pattern, expr)
          Some(out)
        }
        Expr::Apply(_, args) if wild_function_matches_arity(
            nargs,
            args.length(),
          ) => {
          let out = clone_match_env(repl_dict)
          out.set(pattern, expr)
          Some(out)
        }
        _ => None
      }
    _ => None
  }
}

///|
pub fn expr_match(
  pattern : Expr,
  expr : Expr,
  repl_dict? : MatchEnv = {},
) -> MatchEnv? {
  let pattern = normalize_legacy_expr(pattern)
  let expr = normalize_legacy_expr(expr)
  match wild_matches(pattern, expr, repl_dict~) {
    Some(result) => return Some(result)
    None => ()
  }
  match wild_function_matches(pattern, expr, repl_dict~) {
    Some(result) => return Some(result)
    None => ()
  }
  if pattern == expr {
    return Some(clone_match_env(repl_dict))
  }
  match pattern {
    Expr::Apply(Expr::WildFunction(name, nargs), pattern_args) =>
      match expr {
        Expr::Apply(expr_head, expr_args) =>
          if wild_function_matches_arity(nargs, expr_args.length()) &&
            pattern_args.length() == expr_args.length() {
            let wild_fn = Expr::WildFunction(name, nargs)
            let mut current = clone_match_env(repl_dict)
            match current.get(wild_fn) {
              Some(existing) =>
                if compare_expr(existing, expr_head) != 0 {
                  return None
                }
              None => current.set(wild_fn, expr_head)
            }
            for i in 0.. current = value
                None => return None
              }
            }
            return Some(current)
          }
        _ => ()
      }
    _ => ()
  }
  match pattern {
    Expr::Add(pattern_items) => {
      match common_add_wild_factor(pattern_items) {
        Some((factor, remainders)) =>
          match
            expr_match(
              mul([factor, combine_commutative_items(remainders, true)]),
              expr,
              repl_dict~,
            ) {
            Some(result) => return Some(result)
            None => ()
          }
        None => ()
      }
      match expr {
        Expr::Add(_) => ()
        _ =>
          match expand_multiplicative_coeff_for_add_match(expr) {
            Some(expanded) => return expr_match(pattern, expanded, repl_dict~)
            None => return expr_match(pattern, Expr::Add([expr]), repl_dict~)
          }
      }
    }
    Expr::Mul(pattern_items) if is_zero(expr) => {
      let wild_items : Array[Expr] = []
      for item in pattern_items {
        if expr_contains_wild(item) {
          wild_items.push(item)
        }
      }
      if !wild_items.is_empty() {
        return expr_match(
          combine_commutative_items(wild_items, false),
          int(0),
          repl_dict~,
        )
      }
    }
    Expr::Mul(pattern_items) => {
      let exact_items : Array[Expr] = []
      let wild_items : Array[Expr] = []
      for item in pattern_items {
        if expr_contains_wild(item) {
          wild_items.push(item)
        } else {
          exact_items.push(item)
        }
      }
      if wild_items.length() == 1 && !exact_items.is_empty() {
        match
          divide_expr_by_exact_factor(
            expr,
            combine_commutative_items(exact_items, false),
          ) {
          Some(quotient) =>
            if match_count_ops(quotient) > match_count_ops(expr) {
              ()
            } else {
              match expr_match(wild_items[0], quotient, repl_dict~) {
                Some(result) => return Some(result)
                None => ()
              }
            }
          None => ()
        }
      }
      match expand_positive_power_for_mul_match(expr) {
        Some(expanded) => return expr_match(pattern, expanded, repl_dict~)
        None => ()
      }
    }
    Expr::Pow(base, exp) if is_one(expr) && !expr_contains_wild(base) =>
      return expr_match(exp, int(0), repl_dict~)
    Expr::Pow(base, exp) => {
      match exact_integer_value(exp) {
        Some(n) if n.compare(0N) > 0 =>
          match expr_match(base, positive_root_for_match(expr, n), repl_dict~) {
            Some(result) => return Some(result)
            None => ()
          }
        Some(n) if n.compare(0N) < 0 =>
          if is_zero(expr) {
            return None
          } else {
            match
              expr_match(base, reciprocal_root_for_match(expr, -n), repl_dict~) {
              Some(result) => return Some(result)
              None => ()
            }
          }
        _ => ()
      }
      let (expr_base, expr_exp) = base_exp_for_match(expr)
      match expr_match(base, expr_base, repl_dict~) {
        Some(next_env) =>
          match
            expr_match(
              xreplace(exp, match_env_rules(next_env)),
              expr_exp,
              repl_dict=next_env,
            ) {
            Some(result) => return Some(result)
            None => ()
          }
        None => ()
      }
    }
    _ => ()
  }
  match pattern {
    Expr::Mul(_) =>
      match expr {
        Expr::Mul(_) => ()
        _ => return expr_match(pattern, Expr::Mul([int(1), expr]), repl_dict~)
      }
    _ => ()
  }
  match (pattern, expr) {
    (Expr::Add(pattern_items), Expr::Add(expr_items)) =>
      return match_commutative_items(pattern_items, expr_items, true, repl_dict)
    (Expr::Mul(pattern_items), Expr::Mul(expr_items)) =>
      return match_commutative_items(
        pattern_items, expr_items, false, repl_dict,
      )
    _ => ()
  }
  if is_atomic(pattern) || is_atomic(expr) {
    return None
  }
  let pattern_args = args(pattern)
  let expr_args = args(expr)
  if pattern_args.length() != expr_args.length() {
    return None
  }
  let pattern_func = match func(pattern) {
    Some(value) => value
    None => return None
  }
  let expr_func = match func(expr) {
    Some(value) => value
    None => return None
  }
  let mut current = match expr_match(pattern_func, expr_func, repl_dict~) {
    Some(value) => value
    None => return None
  }
  for i in 0.. current = value
      None => return None
    }
  }
  Some(current)
}