///|
/// What the host supplies: everything about a PurePy run that is not pure.
///
/// PurePy is a pure language, but a RUN is not a pure thing: it prints, it
/// reads its arguments, it loads modules, and -- when it is embedded -- it
/// calls out to whatever the application wants to expose. Those four are the
/// whole of it, and they are all here, so an embedder can see the surface at
/// once instead of discovering it a call at a time.
///
///   * **Output.** `write` receives what `print` produced, one call per
///     `print`, while the run is still going. A host that streams to a
///     terminal writes it out; a host that wants a transcript accumulates.
///   * **Arguments.** `argv` is what `sys.argv` reads. `argv[0]` is the
///     program's own name, as in Python.
///   * **Modules.** `modules` are importable modules the host defines, beyond
///     the five the specification predefines. Their members are values, and a
///     member may be a function the host implements.
///   * **Foreign calls.** `call` answers a call to one of those functions. It
///     is given the name the host registered and the arguments the guest
///     passed, and returns an `Outcome`: a value, an abort, or -- for
///     anything it does not recognise or cannot do -- an operation the
///     semantics does not cover.
///
///     It is `async`, so it may also answer LATER: a host that has to read a
///     socket, await a promise or ask a person suspends, and the whole run
///     parks where it stood -- mid-expression, inside a call, anywhere -- and
///     resumes on the value the host eventually supplies. Nothing about the
///     guest changes: PurePy has no `await`, and a program cannot tell a call
///     that answered late from one that answered at once. See `run_with` for
///     how a synchronous embedder drives a run that can park.
///
///   * **Redefinitions.** `redefined` replaces members of the modules the
///     SPECIFICATION predefines -- `builtins.print`, `sys.exit`, `math.sqrt`
///     -- with values of the host's own. A redefined `print` is an ordinary
///     host function: it is handed the arguments the guest passed, as VALUES
///     rather than as the text `write` would have received, and it answers
///     like any other.
///
///     It is the one part of a host that changes what a program MEANS, and so
///     it is the one part that costs something: a run that redefines a
///     builtin is not a run CPython is the oracle for. Everything else here
///     leaves the language exactly as the specification has it.
///
/// Everything else about a run is already a value: the program is a
/// `SourceTree`, so a host that keeps its guest code in a database, a zip file
/// or a string never touches the filesystem.
///
/// Note what is NOT here. There is no way for the host to mutate a guest
/// value, because there is no mutation; no way to install a callback the guest
/// invokes implicitly, because there are no hooks -- a redefinition is not
/// one, any more than `store.get` is, since the guest calls a NAME and gets
/// whatever the host bound to it; and no ambient authority at all -- a guest
/// can reach exactly the modules the host handed it. A host that gives no
/// modules gives a program that can only compute and print.
///
/// Suspension adds nothing to that list. A parked run is not a concurrent
/// one: there is one guest, it is at exactly one point, and the host holds
/// the only continuation. Resuming it twice is the host resuming it twice,
/// which is the host's bug and not an escape from the semantics.
pub struct Host {
  write : (String) -> Unit
  argv : Array[String]
  modules : Map[String, @value.Env]
  call : async (String, Array[@value.Value]) -> @value.Outcome noraise
  /// Members of the predefined modules the host has replaced, by the dotted
  /// name the guest reaches them under.
  redefined : Map[String, @value.Value]
}

///|
/// A module the host defines: a name the guest can import, and its members.
///
/// A member is any `Value`. `@value.host_fn("name")` makes one the guest can
/// call and the host answers by name.
pub(all) struct HostModule {
  name : String
  members : Array[(String, @value.Value)]
}

///|
/// A host.
///
/// Every part has a default that does nothing observable: output is dropped,
/// `sys.argv` is empty, no modules are defined, nothing is redefined, and a
/// foreign call is an operation the semantics does not cover. A host supplies
/// the parts it wants.
///
/// `redefined` is given as `("builtins.print", value)` pairs: a predefined
/// module's name, a dot, and the member within it. A key naming nothing is
/// silently no redefinition at all, which is what `unknown_redefinitions` is
/// for.
///
/// A `call` that answers on the spot is written exactly as it was before it
/// could do otherwise: a plain function is a valid `async` one.
pub fn Host::new(
  write? : (String) -> Unit = fn(_) {  },
  argv? : Array[String] = [],
  modules? : Array[HostModule] = [],
  call? : async (String, Array[@value.Value]) -> @value.Outcome noraise = fn(
    name,
    _,
  ) {
    Stuck(
      "a call to the host function '" + name + "', which it does not answer",
    )
  },
  redefined? : Array[(String, @value.Value)] = [],
) -> Host {
  let table : Map[String, @value.Env] = Map([])
  for m in modules {
    // Every module has a `__name__`, as the predefined ones do.
    let entries : Array[(String, @value.Value)] = [("__name__", Str(m.name))]
    for e in m.members {
      entries.push(e)
    }
    table[m.name] = @value.env_of(entries)
  }
  { write, argv, modules: table, call, redefined: Map::from_array(redefined), }
}

///|
/// The names of each host module's members.
///
/// The CHECKER needs these and not the values: a `from hostmod import f` has
/// to type-check before it can run, and the checker has no business knowing
/// what `f` is. This is the one place the two sides of a host module meet.
pub fn Host::member_names(self : Host) -> Map[String, Array[String]] {
  let out : Map[String, Array[String]] = Map([])
  for name, env in self.modules {
    let members : Array[String] = []
    env.each(fn(k, _) { if k != "__name__" { members.push(k) } })
    @basic.sort_names(members)
    out[name] = members
  }
  out
}

///|
/// The modules a program may import that have no source: the five the
/// specification predefines, and the host's own.
pub fn Host::module_names(self : Host) -> Array[String] {
  let out : Array[String] = []
  for p in @context.predefined_modules {
    out.push(p)
  }
  for name, _ in self.modules {
    if !out.contains(name) {
      out.push(name)
    }
  }
  @basic.sort_names(out)
  out
}

///|
/// The redefinitions that name nothing.
///
/// A redefinition is matched by NAME, so a key nobody has -- `builtins.pirnt`,
/// or a module that is the host's own rather than the specification's -- lands
/// in no environment and the run proceeds as though the host had said nothing
/// at all. That is a silence, and this is how a host breaks it: assert this is
/// empty, once, beside the run.
///
/// The profile has to be the one the run will use, because a profile decides
/// which names `builtins` has -- `builtins.sorted` is nothing under
/// `@profile.core` and a real member under a profile that asked for it.
pub fn Host::unknown_redefinitions(
  self : Host,
  profile? : @profile.Profile = @profile.core,
) -> Array[String] {
  let out : Array[String] = []
  for name, _ in self.redefined {
    let known = match name.split_once(".") {
      Some((q, within)) =>
        match @context.predefined_members(q.to_owned(), profile~) {
          Some(names) => names.contains(within.to_owned())
          None => false
        }
      None => false
    }
    if !known {
      out.push(name)
    }
  }
  @basic.sort_names(out)
  out
}

// ---------------------------------------------------------------------------

///|
/// Somewhere to put output when the caller just wants the transcript.
///
/// `run_program` uses one of these; `run_with` does not, because a host that
/// supplies its own `write` has somewhere better to put it.
pub struct Sink {
  buf : StringBuilder
}

///|
pub fn Sink::new() -> Sink {
  { buf: StringBuilder(), }
}

///|
pub fn Sink::write(self : Sink, text : String) -> Unit {
  self.buf.write_string(text)
}

///|
pub fn Sink::text(self : Sink) -> String {
  self.buf.to_string()
}