///|
/// The predefined functions of Figure 2.7.
///
/// The figure gives `len`, `range` and `sys.exit` as partial maps from
/// arguments to outcomes, and a call outside a map's domain has no
/// derivation. So `len` has an explicit "otherwise aborts TypeError" and
/// `range` does not: `range("x")` is undefined, while `len(1)` aborts. The
/// asymmetry is the figure's, not this port's.
///
/// The figure leaves `print` and the `math` functions without a definition.
/// They are implemented as Python implements them, since CPython is the
/// oracle for what a run prints.
async fn Interp::call_primitive(
  self : Interp,
  p : @value.Primitive,
  args : Array[Value],
) -> Outcome noraise {
  match p {
    Print =>
      match @value.print_text(args) {
        Some(text) => {
          self.write(text)
          Val(None)
        }
        // A closure, a module or a class has no printable form: Python prints
        // an address, which no implementation can reproduce.
        None => Stuck("printing " + unprintable(args))
      }
    Len =>
      if args.length() != 1 {
        Aborts(TypeError)
      } else {
        match @value.iter(args[0]) {
          Some(xs) => Val(Int(BigInt::from_int(xs.length())))
          None => Aborts(TypeError)
        }
      }
    Range =>
      if args.length() != 1 {
        Stuck("range with \{args.length()} arguments")
      } else {
        match args[0] {
          Int(n) => {
            // `range` is the one predefined function that builds a sequence
            // from a NUMBER, so it is the one place a three-character program
            // can ask for more memory than the machine has. Charged before it
            // builds, not after: `range(10 ** 9)` has to be refused rather
            // than survived.
            if !self.spend(n) {
              return Stuck("a run longer than \{self.max_steps} steps")
            }
            let out : Array[Value] = []
            let mut i = 0N
            while i < n {
              out.push(Int(i))
              i = i + 1N
            }
            Val(List(out))
          }
          v => Stuck("range of " + v.kind_name())
        }
      }
    Exit =>
      if args.is_empty() {
        Aborts(SystemExit(0N))
      } else if args.length() == 1 {
        match args[0] {
          Int(n) => Aborts(SystemExit(n))
          v => Stuck("sys.exit of " + v.kind_name())
        }
      } else {
        Stuck("sys.exit with \{args.length()} arguments")
      }
    // `math.floor` and `math.ceil` return an integer in Python; the rest
    // return a float.
    MathFloor | MathCeil => {
      let x = match numeric(args) {
        Some(d) => d
        None => return Stuck("a math function of the wrong shape")
      }
      let r = if p is MathFloor { x.floor() } else { @math.ceil(x) }
      match @value.exact_integer(r) {
        Some(n) => Val(Int(n))
        None => Stuck("floor or ceil of a value with no integer")
      }
    }
    Sqrt | Exp | Log | Sin | Cos | Tan => {
      let x = match numeric(args) {
        Some(d) => d
        None => return Stuck("a math function of the wrong shape")
      }
      match p {
        // `sqrt` of a negative number and `log` of a non-positive one raise
        // ValueError in Python, which is not a termination kind.
        Sqrt =>
          if x < 0.0 {
            Stuck("the square root of a negative number")
          } else {
            Val(Float(x.sqrt()))
          }
        Log =>
          if x <= 0.0 {
            Stuck("the logarithm of a number that is not positive")
          } else {
            Val(Float(@math.ln(x)))
          }
        Exp => Val(Float(@math.exp(x)))
        Sin => Val(Float(@math.sin(x)))
        Cos => Val(Float(@math.cos(x)))
        _ => Val(Float(@math.tan(x)))
      }
    }
    // The builtins a profile adds. Nothing below checks the profile: a
    // primitive is only reachable by being bound in an environment, and only
    // `Interp::predefined` under a profile that asked binds these.
    //
    // Every one of them declines to coerce. Python answers `sum([True])` with
    // 1, `any([1])` with True and `int(True)` with 1, all by treating a value
    // as something it is not; PurePy makes a bool not a number and has no
    // truthiness at all, so each of those is undefined here. That is the whole
    // difference between adding a library to PurePy and adding Python to it.
    Abs
    | Repr
    | ToStr
    | ToInt
    | ToFloat
    | ToList
    | ToTuple
    | Reversed
    | Round
    | Sorted
    | Sum
    | All
    | Any
    | Enumerate =>
      if args.length() != 1 {
        Aborts(TypeError)
      } else {
        unary_builtin(p, args[0])
      }
    Min | Max => extremum(p is Min, args)
    DivMod =>
      if args.length() != 2 {
        Aborts(TypeError)
      } else {
        match
          (
            @value.binop(FloorDiv, args[0], args[1]),
            @value.binop(Mod, args[0], args[1]),
          ) {
          (Val(q), Val(r)) => Val(Tuple([q, r]))
          (Aborts(k), _) | (_, Aborts(k)) => Aborts(k)
          (Stuck(w), _) | (_, Stuck(w)) => Stuck(w)
        }
      }
    Zip => {
      let columns : Array[Array[Value]] = []
      for a in args {
        match @value.elems(a) {
          Some(xs) => columns.push(xs)
          None => return Aborts(TypeError)
        }
      }
      // Python's `zip` stops at the shortest, and with no arguments is empty.
      let mut shortest = 0
      for i, col in columns {
        if i == 0 || col.length() < shortest {
          shortest = col.length()
        }
      }
      let out : Array[Value] = []
      for i in 0.. Stuck("calling " + name)
    // A function the host supplies. It answers by name, and what it cannot
    // answer is an operation the semantics does not cover -- the same as any
    // other undefined operation, and reported the same way.
    Foreign(name) => (self.host.call)(name, args)
  }
}

///|
/// The kind of the first argument `print` could not render, for the message.
///
/// A second pass, on the path where the run has already ended, so that the
/// join itself stays in one place. `print_text` answered `None`, so one of
/// them is here.
fn unprintable(args : Array[Value]) -> String {
  for a in args {
    if a.str() is None {
      return a.kind_name()
    }
  }
  "a value with no printable form"
}

///|
/// The single numeric argument a `math` function takes, as a double. An
/// integer is converted, as Python converts it.
fn numeric(args : Array[Value]) -> Double? {
  if args.length() != 1 {
    return None
  }
  match args[0] {
    Float(d) => Some(d)
    Int(n) => Some(@value.big_to_double(n))
    _ => None
  }
}

///|
/// The one-argument builtins a profile adds.
///
/// `enumerate`, `zip`, `reversed`, `sorted`, `list` and `tuple` answer with a
/// list or a tuple where Python answers with a lazy object. That is not a
/// shortcut: `range` already does it, because Figure 2.7 defines `range` as a
/// list, and PurePy has no iterator to be lazy with. It is observable --
/// `print(zip(a, b))` prints the pairs here and `` in
/// Python -- and it is observable for `range` today. Everything that CONSUMES
/// one agrees.
fn unary_builtin(p : @value.Primitive, v : Value) -> Outcome {
  match p {
    Abs =>
      match v {
        Int(n) => Val(Int(if n < 0N { -n } else { n }))
        Float(d) => Val(Float(if d < 0.0 { -d } else { d }))
        // `abs(True)` is 1 in Python. A bool is not a number here.
        _ => Aborts(TypeError)
      }
    Repr =>
      match v.repr() {
        Some(t) => Val(Str(t))
        None => Stuck("repr of " + v.kind_name())
      }
    ToStr =>
      match v.str() {
        Some(t) => Val(Str(t))
        None => Stuck("str of " + v.kind_name())
      }
    ToInt =>
      match v {
        Int(_) => Val(v)
        // Python truncates toward zero, which is not the floor.
        Float(d) =>
          if d.is_nan() || d.is_inf() {
            Aborts(TypeError)
          } else {
            match
              @value.exact_integer(
                if d < 0.0 {
                  @math.ceil(d)
                } else {
                  d.floor()
                },
              ) {
              Some(n) => Val(Int(n))
              None => Stuck("int of a float with no integer")
            }
          }
        Str(t) =>
          match parse_int(t) {
            Some(n) => Val(Int(n))
            // Python raises ValueError, which PurePy does not model.
            None => Stuck("int of a string that is not a number")
          }
        _ => Aborts(TypeError)
      }
    ToFloat =>
      match v {
        Float(_) => Val(v)
        Int(n) => Val(Float(@value.big_to_double(n)))
        Str(t) =>
          try @string.parse_double(t.trim(chars=" ").to_owned()) catch {
            _ => Stuck("float of a string that is not a number")
          } noraise {
            d => Val(Float(d))
          }
        _ => Aborts(TypeError)
      }
    ToList | ToTuple | Reversed | Enumerate | Sorted =>
      match @value.iter(v) {
        None => Aborts(TypeError)
        Some(xs) =>
          match p {
            ToList => Val(List(xs))
            ToTuple => Val(Tuple(xs))
            Reversed => Val(List(xs.rev()))
            Enumerate => {
              let out : Array[Value] = []
              for i, x in xs {
                out.push(Tuple([Int(BigInt::from_int(i)), x]))
              }
              Val(List(out))
            }
            // A total order over the whole list or nothing: `@value.order` is
            // partial, and a sort that fell back on the original positions
            // would be inventing one.
            _ =>
              match sorted(xs) {
                Some(out) => Val(List(out))
                None => Stuck("sorting a list this order does not cover")
              }
          }
      }
    Round =>
      match v {
        Int(_) => Val(v)
        // Python rounds halves to even, and `round(x)` answers an int.
        Float(d) =>
          match @value.exact_integer(round_half_even(d)) {
            Some(n) => Val(Int(n))
            None => Stuck("round of a value with no integer")
          }
        _ => Aborts(TypeError)
      }
    Sum =>
      match @value.elems(v) {
        None => Aborts(TypeError)
        Some(xs) => {
          let mut acc = Value::Int(0N)
          for x in xs {
            match @value.binop(Add, acc, x) {
              Val(w) => acc = w
              other => return other
            }
          }
          Val(acc)
        }
      }
    // PurePy has no truthiness, so these want real bools -- the same thing
    // `if`, `and` and an `assert` want, and for the same reason.
    All | Any =>
      match @value.elems(v) {
        None => Aborts(TypeError)
        Some(xs) => {
          let want = p is Any
          for x in xs {
            match x {
              Bool(b) => if b == want { return Val(Bool(want)) }
              other =>
                return Stuck(
                  "'" +
                  (if want { "any" } else { "all" }) +
                  "' over a " +
                  other.kind_name() +
                  ": PurePy has no truthiness",
                )
            }
          }
          Val(Bool(!want))
        }
      }
    _ => Stuck("a builtin of the wrong shape")
  }
}

///|
/// `min` and `max`: one iterable, or two or more values.
fn extremum(want_min : Bool, args : Array[Value]) -> Outcome {
  let xs = if args.length() == 1 {
    match @value.elems(args[0]) {
      Some(xs) => xs
      None => return Aborts(TypeError)
    }
  } else if args.is_empty() {
    return Aborts(TypeError)
  } else {
    args
  }
  if xs.is_empty() {
    // Python raises ValueError on an empty sequence.
    return Stuck("min or max of nothing")
  }
  let mut best = xs[0]
  for x in xs[1:] {
    match @value.order(x, best) {
      Some(c) => if (c < 0) == want_min && c != 0 { best = x }
      None =>
        return Stuck("comparing " + x.kind_name() + " with " + best.kind_name())
    }
  }
  Val(best)
}

///|
/// An insertion sort over a PARTIAL order, refusing rather than guessing.
///
/// `@value.order` answers `None` for a pair it does not cover -- an int and a
/// str, a nan and anything -- and a sort that skipped such a pair would put
/// the list in an order the language cannot justify. So one unordered pair
/// makes the whole call undefined, which is the same discipline `@value.eq`
/// follows.
fn sorted(xs : Array[Value]) -> Array[Value]? {
  let out : Array[Value] = []
  for x in xs {
    let mut at = out.length()
    for i, y in out {
      match @value.order(x, y) {
        Some(c) => if c < 0 && at == out.length() { at = i }
        None => return None
      }
    }
    out.insert(at, x)
  }
  Some(out)
}

///|
/// Python's `round`: halves go to the even neighbour, not away from zero.
fn round_half_even(d : Double) -> Double {
  let down = d.floor()
  let frac = d - down
  if frac > 0.5 {
    down + 1.0
  } else if frac < 0.5 {
    down
  } else if down % 2.0 == 0.0 {
    down
  } else {
    down + 1.0
  }
}

///|
/// `int("...")`: an optionally signed run of digits, and nothing else.
///
/// Deliberately narrower than Python's, which takes underscores and every
/// Unicode decimal digit. What it accepts, it agrees with Python about; what
/// it does not, it declines rather than guessing.
fn parse_int(t : String) -> BigInt? {
  let s = t.trim(chars=" ")
  let (sign, digits) = if s.has_prefix("-") {
    (-1N, s[1:])
  } else if s.has_prefix("+") {
    (1N, s[1:])
  } else {
    (1N, s)
  }
  if digits.is_empty() {
    return None
  }
  let mut acc = 0N
  for c in digits {
    if c < '0' || c > '9' {
      return None
    }
    acc = acc * 10N + BigInt::from_int(c.to_int() - '0'.to_int())
  }
  Some(sign * acc)
}