///|
fn array_key(keys : Array[Expr], st : State) -> String raise {
  let values = []
  for key in keys {
    values.push(st.text(evaluate(key, st)))
  }
  values.join(st.text(st.variable("SUBSEP")))
}

///|
fn slot(expr : Expr, st : State) -> Slot raise {
  match expr {
    Variable(name) => VariableSlot(name)
    Field(index) => FieldSlot(field_index(evaluate(index, st).number()))
    Element(name, keys) => ElementSlot(name, array_key(keys, st))
    _ => raise Invalid("assignment target")
  }
}

///|
fn calculate(
  op : String,
  a : Value,
  b : Value,
  st : State,
) -> Value raise ParseError {
  if op == "concat" {
    let text = st.text(a) + st.text(b)
    if text.length() > 1000000 {
      raise Invalid("string length budget")
    }
    return Text(text)
  }
  if ["==", "!=", "<", ">", "<=", ">="].contains(op) {
    if !(a is Text(_)) &&
      !(b is Text(_)) &&
      (a.number().is_nan() || b.number().is_nan()) {
      return boolean(op == "!=")
    }
    let cmp = if a is Text(_) || b is Text(_) {
      st.text(a).lexical_compare(st.text(b))
    } else {
      a.number().compare(b.number())
    }
    return boolean(
      match op {
        "==" => cmp == 0
        "!=" => cmp != 0
        "<" => cmp < 0
        ">" => cmp > 0
        "<=" => cmp <= 0
        _ => cmp >= 0
      },
    )
  }
  let left = a.number()
  let right = b.number()
  finite(
    match op {
      "+" | "+=" => left + right
      "-" | "-=" => left - right
      "*" | "*=" => left * right
      "/" | "/=" => {
        if right == 0.0 {
          raise Invalid("division by zero")
        }
        left / right
      }
      "%" | "%=" => {
        if right == 0.0 {
          raise Invalid("division by zero")
        }
        left % right
      }
      "^" | "^=" => @math.pow(left, right)
      _ => raise Invalid("unsupported operator " + op)
    },
  )
}

///|
fn evaluate(expr : Expr, st : State) -> Value raise {
  st.tick()
  st.eval_depth += 1
  if st.eval_depth > 128 {
    raise Invalid("evaluation depth budget")
  }
  defer {
    st.eval_depth -= 1
  }
  let result = match expr {
    Getline(target, source) => {
      let location = match target {
        // GoAWK 1.32 evaluates this address but its GetlineField instruction
        // replaces $0 for every field number. Keep that pinned profile explicit.
        Some(Field(index)) => {
          ignore(evaluate(index, st))
          Some(FieldSlot(0))
        }
        Some(e) => Some(slot(e, st))
        None => None
      }
      let source = source.map(pair => {
        let (pipe, expr) = pair
        (pipe, st.text(evaluate(expr, st)))
      })
      try {
        let record = match source {
          Some((pipe, name)) => st.read_redirect(name, pipe)
          None => st.take_input()
        }
        match record {
          None => Number(0.0)
          Some(text) => {
            match location {
              Some(FieldSlot(_)) => st.set_record(text, true_string=true)
              None => st.set_record(text)
              Some(location) => st.write(location, input_value(text))
            }
            Number(1.0)
          }
        }
      } catch {
        InputFailure(_) => Number(-1.0)
        e => raise e
      }
    }
    Literal(v) => v
    RegexLiteral(pattern) =>
      boolean(
        regex_match(st.regex(pattern), st.record.to_array(), 0, st) != None,
      )
    NamedField(name) => st.named_field(st.text(evaluate(name, st)))
    Variable(_) | Field(_) | Element(_, _) => st.read(slot(expr, st))
    Tuple(_) => raise Invalid("tuple is only supported with in")
    Unary(op, child) => {
      let v = evaluate(child, st)
      if op == "!" {
        boolean(!v.truth())
      } else {
        finite((if op == "-" { -1.0 } else { 1.0 }) * v.number())
      }
    }
    Assign(op, target, expr) => {
      let location = slot(target, st)
      let right = evaluate(expr, st)
      let value = if op == "=" {
        right
      } else {
        calculate(op, st.read(location), right, st)
      }
      st.write(location, value)
      value
    }
    Increment(target, delta, prefix) => {
      let location = slot(target, st)
      let before = finite(st.read(location).number())
      let after = calculate("+", before, Number(delta.to_double()), st)
      st.write(location, after)
      if prefix {
        after
      } else {
        before
      }
    }
    Conditional(condition, yes, no) =>
      evaluate(if evaluate(condition, st).truth() { yes } else { no }, st)
    Binary("&&", left, right) =>
      boolean(evaluate(left, st).truth() && evaluate(right, st).truth())
    Binary("||", left, right) =>
      boolean(evaluate(left, st).truth() || evaluate(right, st).truth())
    Binary("in", left, Variable(name)) => {
      let key = match left {
        Tuple(keys) => array_key(keys, st)
        _ => st.text(evaluate(left, st))
      }
      boolean(st.array(name).contains(key))
    }
    Binary("in", _, _) => raise Invalid("in requires an array name")
    Binary("~" | "!~" as op, left, right) => {
      let text = st.text(evaluate(left, st))
      let pattern = regex_pattern(right, st)
      boolean(
        (regex_match(st.regex(pattern), text.to_array(), 0, st) != None) ==
        (op == "~"),
      )
    }
    Binary(op, left, right) =>
      calculate(op, evaluate(left, st), evaluate(right, st), st)
    Call(name, args) =>
      if st.functions.contains(name) {
        call_function(name, args, st)
      } else {
        builtin(name, args, st)
      }
  }
  result
}

///|
fn builtin(name : String, args : Array[Expr], st : State) -> Value raise {
  if name == "close" {
    return Number(st.close_stream(st.text(evaluate(args[0], st))).to_double())
  }
  if name == "fflush" {
    let stream = if args.is_empty() {
      ""
    } else {
      st.text(evaluate(args[0], st))
    }
    return Number(st.flush_stream(stream).to_double())
  }
  if name == "system" {
    let command = st.text(evaluate(args[0], st))
    return match st.io_call(Execute(command)) {
      IOStatus(code) => Number(code.to_double())
      IOFailed(_) => Number(-1.0)
      _ => raise Invalid("invalid system reply")
    }
  }
  if name == "rand" {
    st.random_state = st.random_state * 48271L % 2147483647L
    return Number(st.random_state.to_double() / 2147483647.0)
  }
  if name == "srand" {
    let old = st.random_seed
    let seed = if args.is_empty() {
      st.clock_seconds.trunc()
    } else {
      evaluate(args[0], st).number()
    }
    st.random_seed = seed
    let normalized = (seed.trunc() % 2147483646.0).abs()
    st.random_state = if normalized.is_nan() || normalized.is_inf() {
      1L
    } else {
      normalized.to_int64() + 1L
    }
    return Number(old)
  }
  if name == "match" {
    if args.length() != 2 {
      raise Invalid("match arguments")
    }
    let text = st.text(evaluate(args[0], st))
    let pattern = regex_pattern(args[1], st)
    let (start, length) = match
      regex_match(st.regex(pattern), text.to_array(), 0, st) {
      None => (0, -1)
      Some((a, b)) => (a + 1, b - a)
    }
    st.vars["RSTART"] = Number(start.to_double())
    st.vars["RLENGTH"] = Number(length.to_double())
    return Number(start.to_double())
  }
  if name == "sub" || name == "gsub" {
    return substitute(args, st, name == "gsub")
  }
  if name == "split" {
    if args.length() < 2 || args.length() > 3 {
      raise Invalid("split arguments")
    }
    let text = st.text(evaluate(args[0], st))
    let array = match args[1] {
      Variable(name) => name
      _ => raise Invalid("split requires array name")
    }
    let sep = if args.length() == 3 {
      regex_pattern(args[2], st)
    } else {
      st.text(st.variable("FS"))
    }
    let fields = if args.length() == 2 && st.input_mode is Some(mode) {
      csv_fields(text, mode, st)
    } else {
      split_fields(
        text,
        sep,
        st,
        is_regex=args.length() == 3 && args[2] is RegexLiteral(_),
      )
    }
    let dest = st.array(array)
    st.entries -= dest.length()
    dest.clear()
    for i, field in fields {
      st.put_element(array, (i + 1).to_string(), field)
    }
    return Number(fields.length().to_double())
  }
  if name == "length" &&
    args.length() == 1 &&
    args[0] is Variable(array) &&
    st.has_array(array) {
    return Number(st.array(array).length().to_double())
  }
  if name == "length" {
    if args.length() > 1 {
      raise Invalid("length arguments")
    }
    let text = if args.is_empty() {
      st.record
    } else {
      st.text(evaluate(args[0], st))
    }
    return Number(text.to_array().length().to_double())
  }
  let values = []
  for arg in args {
    values.push(evaluate(arg, st))
  }
  if name == "sprintf" {
    if values.is_empty() {
      raise Invalid("sprintf requires a format")
    }
    return Text(format_values(st.text(values[0]), values[1:].to_owned(), st))
  }
  if name == "substr" {
    if values.length() < 2 || values.length() > 3 {
      raise Invalid("substr arguments")
    }
    let cs = st.text(values[0]).to_array()
    let start_number = values[1].number().trunc()
    let start = if start_number < 1.0 {
      0
    } else if start_number > cs.length().to_double() {
      cs.length()
    } else {
      start_number.to_int() - 1
    }
    let available = cs.length() - start
    let requested = if values.length() == 3 {
      values[2].number().trunc()
    } else {
      available.to_double()
    }
    let length = if requested < 0.0 {
      0
    } else if requested > available.to_double() {
      available
    } else {
      requested.to_int()
    }
    return Text(
      cs[start:start + length].to_owned().map(c => c.to_string()).join(""),
    )
  }
  if name == "index" {
    if values.length() != 2 {
      raise Invalid("index arguments")
    }
    let text = st.text(values[0])
    return Number(
      match text.find(st.text(values[1])) {
        Some(i) => (text[0:i].to_owned().to_array().length() + 1).to_double()
        None => 0.0
      },
    )
  }
  if name == "atan2" {
    return finite(@math.atan2(values[0].number(), values[1].number()))
  }
  if values.length() != 1 {
    raise Invalid(name + " requires one argument")
  }
  match name {
    "int" => finite(values[0].number().trunc())
    "sqrt" => finite(values[0].number().sqrt())
    "sin" => finite(@math.sin(values[0].number()))
    "cos" => finite(@math.cos(values[0].number()))
    "exp" => finite(@math.exp(values[0].number()))
    "log" => finite(@math.ln(values[0].number()))
    "tolower" => Text(unicode_string_case(st.text(values[0]), false))
    "toupper" => Text(unicode_string_case(st.text(values[0]), true))
    _ => raise Invalid("unsupported function " + name)
  }
}