///|
pub using @runtime {type ConsoleOutputKind}

///|
/// Realm-independent Console Output delivered to an application-owned Output
/// Sink. Runtime Values never cross this boundary.
pub struct ConsoleOutput {
  priv kind_ : ConsoleOutputKind
  priv text_ : String
}

///|
fn ConsoleOutput::ConsoleOutput(
  kind~ : ConsoleOutputKind,
  text~ : String,
) -> ConsoleOutput {
  { kind_: kind, text_: text, }
}

///|
pub fn ConsoleOutput::kind(self : ConsoleOutput) -> ConsoleOutputKind {
  self.kind_
}

///|
pub fn ConsoleOutput::text(self : ConsoleOutput) -> String {
  self.text_
}

///|
/// Application-defined request for a Script Resource. The request and
/// referrer are opaque names; the engine does not apply path or URL rules.
pub struct ScriptResourceRequest {
  priv request_ : String
  priv referrer_ : String?
}

///|
fn ScriptResourceRequest::ScriptResourceRequest(
  request~ : String,
  referrer~ : String?,
) -> ScriptResourceRequest {
  { request_: request, referrer_: referrer, }
}

///|
pub fn ScriptResourceRequest::request(self : ScriptResourceRequest) -> String {
  self.request_
}

///|
pub fn ScriptResourceRequest::referrer(self : ScriptResourceRequest) -> String? {
  self.referrer_
}

///|
/// JavaScript source resolved by an application under an opaque identity.
pub struct ScriptResource {
  priv identity_ : String
  priv source_ : String
}

///|
pub fn ScriptResource::ScriptResource(
  identity~ : String,
  source~ : String,
) -> ScriptResource {
  { identity_: identity, source_: source, }
}

///|
pub fn ScriptResource::identity(self : ScriptResource) -> String {
  self.identity_
}

///|
pub fn ScriptResource::source(self : ScriptResource) -> String {
  self.source_
}

///|
/// Opaque permission to resume one Session-owned timer callback once.
pub struct ScheduledTurn {
  priv host_binding : SessionHostBinding
  priv id : Int
}

///|
fn ScheduledTurn::ScheduledTurn(
  host_binding~ : SessionHostBinding,
  id~ : Int,
) -> ScheduledTurn {
  { host_binding, id, }
}

///|
/// A timer request delivered to an application-owned scheduler.
pub struct TimerSchedule {
  priv delay_milliseconds_ : Int
  priv turn_ : ScheduledTurn
}

///|
fn TimerSchedule::TimerSchedule(
  delay_milliseconds~ : Int,
  turn~ : ScheduledTurn,
) -> TimerSchedule {
  { delay_milliseconds_: delay_milliseconds, turn_: turn, }
}

///|
pub fn TimerSchedule::delay_milliseconds(self : TimerSchedule) -> Int {
  self.delay_milliseconds_
}

///|
pub fn TimerSchedule::turn(self : TimerSchedule) -> ScheduledTurn {
  self.turn_
}

///|
const MAX_SCHEDULED_TURN_ID : Int = 0x7FFFFFFF

///|
/// Reusable application-owned configuration for creating independent
/// Execution Sessions.
pub struct HostEnvironment {
  priv annex_b : Bool
  priv console : Bool
  priv script_resources : Bool
  priv timers : Bool
}

///|
pub fn HostEnvironment::HostEnvironment(
  annex_b? : Bool = false,
  console? : Bool = false,
  script_resources? : Bool = false,
  timers? : Bool = false,
) -> HostEnvironment {
  { annex_b, console, script_resources, timers, }
}

///|
/// Application services bound while creating one Execution Session.
pub struct SessionBindings {
  priv console_output_sink : ((ConsoleOutput) -> Unit raise Error)?
  priv script_resource_resolver : ((ScriptResourceRequest) -> ScriptResource? raise Error)?
  priv timer_scheduler : ((TimerSchedule) -> Unit raise Error)?
}

///|
pub fn SessionBindings::SessionBindings(
  console_output_sink? : ((ConsoleOutput) -> Unit raise Error)? = None,
  script_resource_resolver? : ((ScriptResourceRequest) -> ScriptResource? raise Error)? = None,
  timer_scheduler? : ((TimerSchedule) -> Unit raise Error)? = None,
) -> SessionBindings {
  { console_output_sink, script_resource_resolver, timer_scheduler, }
}

///|
/// Invalid combinations of a fixed Host Capability Set and Session Bindings.
pub(all) suberror SessionCreationError {
  BindingForAbsentCapability(String)
  MissingBindingForCapability(String)
} derive(Debug)

///|
pub extend SessionCreationError with @debug.Debug::{to_repr}

///|
pub impl Show for SessionCreationError with fn output(self, logger) {
  match self {
    BindingForAbsentCapability(capability) => {
      logger.write_string("BindingForAbsentCapability: ")
      logger.write_string(capability)
    }
    MissingBindingForCapability(capability) => {
      logger.write_string("MissingBindingForCapability: ")
      logger.write_string(capability)
    }
  }
}

///|
pub extend SessionCreationError with Show::{output, to_string}

///|
/// Errors reported by an Execution Session at its application boundary.
pub(all) suberror ExecutionSessionError {
  ExecutionFailure(EngineDiagnostic)
  HostFailure(capability~ : String, detail~ : String)
  SessionBusy
  SessionClosed
  SessionFaulted
  ScheduledTurnUnavailable
}

///|
pub impl Show for ExecutionSessionError with fn output(self, logger) {
  match self {
    ExecutionFailure(diagnostic) => {
      logger.write_string(diagnostic.failure_kind_code())
      logger.write_string(": ")
      logger.write_string(diagnostic.message())
    }
    HostFailure(capability~, detail~) => {
      logger.write_string("HostFailure(")
      logger.write_string(capability)
      logger.write_string("): ")
      logger.write_string(detail)
    }
    SessionBusy => logger.write_string("SessionBusy")
    SessionClosed => logger.write_string("SessionClosed")
    SessionFaulted => logger.write_string("SessionFaulted")
    ScheduledTurnUnavailable => logger.write_string("ScheduledTurnUnavailable")
  }
}

///|
pub extend ExecutionSessionError with Show::{output, to_string}

///|
priv struct SessionHostFailure {
  capability : String
  detail : String
}

///|
priv struct ScheduledCallback {
  callback : @runtime.Value
  arguments : Array[@runtime.Value]
}

///|
priv suberror ScheduledTurnAllocationFailure {
  ScheduledTurnIdSpaceExhausted
}

///|
impl Show for ScheduledTurnAllocationFailure with fn output(self, logger) {
  match self {
    ScheduledTurnIdSpaceExhausted =>
      logger.write_string("Scheduled Turn ID space is exhausted")
  }
}

///|
priv struct SessionHostBinding {
  console_output_sink : ((ConsoleOutput) -> Unit raise Error)?
  script_resource_resolver : ((ScriptResourceRequest) -> ScriptResource? raise Error)?
  timer_scheduler : ((TimerSchedule) -> Unit raise Error)?
  load_stack : Array[String]
  scheduled_callbacks : Map[Int, ScheduledCallback]
  mut next_scheduled_turn_id : Int
  mut failure : SessionHostFailure?
}

///|
fn SessionHostBinding::SessionHostBinding(
  bindings : SessionBindings,
) -> SessionHostBinding {
  {
    console_output_sink: bindings.console_output_sink,
    script_resource_resolver: bindings.script_resource_resolver,
    timer_scheduler: bindings.timer_scheduler,
    load_stack: [],
    scheduled_callbacks: Map([]),
    next_scheduled_turn_id: 1,
    failure: None,
  }
}

///|
fn SessionHostBinding::record_failure(
  self : SessionHostBinding,
  capability : String,
  error : Error,
) -> Unit {
  self.failure = Some({ capability, detail: error.to_string(), })
}

///|
/// Notify this Session's Console destination. A raised application error
/// poisons this binding so the enclosing Hosted Turn can fault its Session.
fn SessionHostBinding::emit_console_output(
  self : SessionHostBinding,
  output : ConsoleOutput,
) -> Unit raise Error {
  match self.console_output_sink {
    Some(sink) => {
      let result : Result[Unit, Error] = Ok(sink(output)) catch {
        error => Err(error)
      }
      match result {
        Ok(_) => ()
        Err(error) => {
          self.record_failure("Console Output Sink", error)
          raise error
        }
      }
    }
    None => ()
  }
}

///|
fn SessionHostBinding::take_failure(
  self : SessionHostBinding,
) -> SessionHostFailure? {
  let failure = self.failure
  self.failure = None
  failure
}

///|
fn SessionHostBinding::load_referrer(self : SessionHostBinding) -> String? {
  guard self.load_stack.length() > 0 else { return None }
  Some(self.load_stack[self.load_stack.length() - 1])
}

///|
fn SessionHostBinding::resolve_script_resource(
  self : SessionHostBinding,
  request : String,
) -> ScriptResource? raise Error {
  guard self.script_resource_resolver is Some(resolve) else {
    fail("selected Script Resources capability has no Session binding")
  }
  let result : Result[ScriptResource?, Error] = Ok(
    resolve(ScriptResourceRequest(request~, referrer=self.load_referrer())),
  ) catch {
    error => Err(error)
  }
  match result {
    Ok(resource) => resource
    Err(error) => {
      self.record_failure("Script Resource Resolver", error)
      raise error
    }
  }
}

///|
fn SessionHostBinding::schedule_timer(
  self : SessionHostBinding,
  callback : @runtime.Value,
  arguments : Array[@runtime.Value],
  delay_milliseconds : Int,
) -> Int raise Error {
  guard self.timer_scheduler is Some(schedule) else {
    fail("selected Timers capability has no Session binding")
  }
  guard self.next_scheduled_turn_id >= 1 &&
    self.next_scheduled_turn_id < MAX_SCHEDULED_TURN_ID else {
    let error : Error = ScheduledTurnIdSpaceExhausted
    self.record_failure("Timer Scheduler", error)
    raise error
  }
  let id = self.next_scheduled_turn_id
  self.next_scheduled_turn_id += 1
  self.scheduled_callbacks[id] = { callback, arguments, }
  let result : Result[Unit, Error] = Ok(
    schedule(
      TimerSchedule(
        delay_milliseconds~,
        turn=ScheduledTurn(host_binding=self, id~),
      ),
    ),
  ) catch {
    error => Err(error)
  }
  match result {
    Ok(_) => id
    Err(error) => {
      ignore(self.scheduled_callbacks.remove(id))
      self.record_failure("Timer Scheduler", error)
      raise error
    }
  }
}

///|
fn SessionHostBinding::cancel_scheduled_turn(
  self : SessionHostBinding,
  id : Int,
) -> Unit {
  ignore(self.scheduled_callbacks.remove(id))
}

///|
fn SessionHostBinding::take_scheduled_callback(
  self : SessionHostBinding,
  turn : ScheduledTurn,
) -> ScheduledCallback? {
  guard physical_equal(self, turn.host_binding) else { return None }
  let callback = self.scheduled_callbacks.get(turn.id)
  ignore(self.scheduled_callbacks.remove(turn.id))
  callback
}

///|
fn SessionHostBinding::invalidate_scheduled_turns(
  self : SessionHostBinding,
) -> Unit {
  self.scheduled_callbacks.clear()
}

///|
priv suberror ScriptResourceUnavailable {
  ScriptResourceUnavailable(String)
}

///|
impl Show for ScriptResourceUnavailable with fn output(self, logger) {
  let ScriptResourceUnavailable(request) = self
  logger.write_string("Script resource is unavailable: ")
  logger.write_string(request)
}

///|
fn Engine::eval_host_loaded_script(
  self : Engine,
  source : String,
  identity : String,
) -> Unit raise Error {
  let program = @parser.parse(source) catch {
    error => raise @errors.SyntaxError(message=error.to_string())
  }
  ignore(
    @runtime.with_source_identity(self.interp.realm_state, Some(identity), fn() raise {
      if @engine_candidate_mode.candidate_mode_enabled() {
        @compiler.run_candidate_program(self.interp, program.stmts)
      } else {
        self.interp.run(program.stmts)
      }
    }),
  )
}

///|
fn Engine::install_host_global(
  self : Engine,
  name : String,
  value : @runtime.Value,
) -> Unit {
  match self.interp.global_this {
    @runtime.Object(data) => {
      data.bag.properties[name] = value
      data.bag.descriptors[name] = {
        writable: true,
        enumerable: false,
        configurable: true,
        getter: None,
        setter: None,
        is_accessor: false,
      }
    }
    _ => ()
  }
}

///|
fn Engine::install_script_resource_loader(
  self : Engine,
  host_binding : SessionHostBinding,
) -> Unit {
  let load = @runtime.make_native_func(
    name="load",
    length=1,
    realm_state=Some(self.interp.realm_state),
    fn(arguments) raise {
      guard arguments.length() > 0 else {
        raise @errors.TypeError(message="load() requires a resource request")
      }
      let request = self.interp.to_js_string(arguments[0])
      let resource = match host_binding.resolve_script_resource(request) {
        Some(resource) => resource
        None => {
          let error : Error = ScriptResourceUnavailable(request)
          @runtime.raise_js_exception(
            @runtime.js_error_to_value_with_env(error, Some(self.interp.global)),
          )
          return @runtime.Undefined
        }
      }
      host_binding.load_stack.push(resource.identity())
      let evaluated : Result[Unit, Error] = Ok(
        self.eval_host_loaded_script(resource.source(), resource.identity()),
      ) catch {
        error => Err(error)
      }
      ignore(host_binding.load_stack.pop())
      match evaluated {
        Ok(_) => @runtime.Undefined
        Err(error) => raise error
      }
    },
  )
  self.install_host_global("load", load)
}

///|
fn Engine::install_timer_scheduler(
  self : Engine,
  host_binding : SessionHostBinding,
) -> Unit {
  let set_timeout = @runtime.make_native_func(
    name="setTimeout",
    length=1,
    realm_state=Some(self.interp.realm_state),
    fn(arguments) raise {
      let callback = if arguments.length() > 0 {
        arguments[0]
      } else {
        @runtime.Undefined
      }
      guard @runtime.is_callable(callback) else {
        raise @errors.TypeError(
          message="setTimeout requires a function as first argument",
        )
      }
      let delay = if arguments.length() > 1 {
        match arguments[1] {
          @runtime.Number(value) => {
            let milliseconds = value.to_int()
            if milliseconds < 0 {
              0
            } else {
              milliseconds
            }
          }
          _ => 0
        }
      } else {
        0
      }
      let callback_arguments : Array[@runtime.Value] = []
      for i = 2; i < arguments.length(); i = i + 1 {
        callback_arguments.push(arguments[i])
      }
      let id = host_binding.schedule_timer(callback, callback_arguments, delay)
      @runtime.Number(id.to_double())
    },
  )
  let clear_timeout = @runtime.make_native_func(
    name="clearTimeout",
    length=1,
    realm_state=Some(self.interp.realm_state),
    fn(arguments) {
      if arguments.length() > 0 {
        match arguments[0] {
          @runtime.Number(value) =>
            host_binding.cancel_scheduled_turn(value.to_int())
          _ => ()
        }
      }
      @runtime.Undefined
    },
  )
  self.install_host_global("setTimeout", set_timeout)
  self.install_host_global("clearTimeout", clear_timeout)
}

///|
fn Engine::for_host_environment(
  annex_b : Bool,
  console_enabled : Bool,
  console_output_sink : ((ConsoleOutputKind, String) -> Unit raise Error)?,
) -> Engine {
  let engine : Engine = {
    interp: @interpreter.new_interpreter(
      annex_b~,
      console_enabled~,
      console_output_sink~,
      setup_test262_harness=false,
    ),
  }
  for name in ["setTimeout", "clearTimeout", "setInterval", "clearInterval"] {
    ignore(engine.interp.global.bindings.remove(name))
  }
  engine
}

///|
fn Engine::run_scheduled_callback_diagnostic(
  self : Engine,
  scheduled : ScheduledCallback,
) -> Result[Unit, EngineDiagnostic] {
  let observed = @runtime.observe_source_failure(self.interp.realm_state, fn() raise {
    ignore(
      self.interp.call_value(
        scheduled.callback,
        @runtime.Undefined,
        scheduled.arguments,
        @token.Loc::default(),
      ),
    )
  })
  match observed {
    Ok(_) => Ok(())
    Err(failure) =>
      Err(
        runtime_engine_diagnostic(
          failure.cause(),
          operation="scheduled-turn",
          phase="callback",
          source_identity=failure.source_identity(),
          javascript_integrity=EngineIntegrity::Reusable,
          javascript_effects=RetainedEffects::MayRemain,
          javascript_jobs=self.pending_jobs_snapshot(),
          internal_integrity=EngineIntegrity::Discard,
        ),
      )
  }
}

///|
/// Create a new Execution Session with an independent Hosted Realm.
pub fn HostEnvironment::create_session(
  self : HostEnvironment,
  bindings? : SessionBindings = SessionBindings(),
) -> ExecutionSession raise SessionCreationError {
  guard self.console || bindings.console_output_sink is None else {
    raise BindingForAbsentCapability("Console")
  }
  guard self.script_resources || bindings.script_resource_resolver is None else {
    raise BindingForAbsentCapability("Script Resources")
  }
  guard !self.script_resources || bindings.script_resource_resolver is Some(_) else {
    raise MissingBindingForCapability("Script Resources")
  }
  guard self.timers || bindings.timer_scheduler is None else {
    raise BindingForAbsentCapability("Timers")
  }
  guard !self.timers || bindings.timer_scheduler is Some(_) else {
    raise MissingBindingForCapability("Timers")
  }
  let host_binding = SessionHostBinding(bindings)
  let console_output_sink = if self.console {
    Some(fn(kind, text) raise {
      host_binding.emit_console_output(ConsoleOutput(kind~, text~))
    })
  } else {
    None
  }
  let engine = Engine::for_host_environment(
    self.annex_b,
    self.console,
    console_output_sink,
  )
  if self.script_resources {
    engine.install_script_resource_loader(host_binding)
  }
  if self.timers {
    engine.install_timer_scheduler(host_binding)
  }
  ExecutionSession(engine, host_binding)
}

///|
priv enum ExecutionSessionState {
  Available
  Running
  Closed
  Faulted
}

///|
fn execution_session_state_after_integrity(
  integrity : EngineIntegrity,
) -> ExecutionSessionState {
  match integrity {
    EngineIntegrity::Reusable => Available
    EngineIntegrity::Discard
    | EngineIntegrity::Unknown
    | EngineIntegrity::NotApplicable => Faulted
  }
}

///|
fn[T] execution_session_should_checkpoint(
  result : Result[T, EngineDiagnostic],
) -> Bool {
  match result {
    Ok(_) => true
    Err(diagnostic) =>
      diagnostic.engine_integrity() is EngineIntegrity::Reusable
  }
}

///|
/// One mutable JavaScript execution state created from a Host Environment.
pub struct ExecutionSession {
  priv engine : Engine
  priv host_binding : SessionHostBinding
  priv mut state : ExecutionSessionState
}

///|
fn ExecutionSession::ExecutionSession(
  engine : Engine,
  host_binding : SessionHostBinding,
) -> ExecutionSession {
  { engine, host_binding, state: Available, }
}

///|
fn ExecutionSession::require_available(
  self : ExecutionSession,
) -> Unit raise ExecutionSessionError {
  match self.state {
    Available => self.state = Running
    Running => raise SessionBusy
    Closed => raise SessionClosed
    Faulted => raise SessionFaulted
  }
}

///|
/// Permanently make this Execution Session unavailable for new Hosted Turns.
/// Closing a running Session is rejected and does not interrupt execution.
pub fn ExecutionSession::close(
  self : ExecutionSession,
) -> Unit raise ExecutionSessionError {
  match self.state {
    Available | Faulted => {
      self.host_binding.invalidate_scheduled_turns()
      self.state = Closed
    }
    Running => raise SessionBusy
    Closed => raise SessionClosed
  }
}

///|
fn[T] ExecutionSession::finish_turn(
  self : ExecutionSession,
  result : Result[T, EngineDiagnostic],
) -> T raise ExecutionSessionError {
  match self.host_binding.take_failure() {
    Some(failure) => {
      self.state = Faulted
      self.host_binding.invalidate_scheduled_turns()
      raise HostFailure(capability=failure.capability, detail=failure.detail)
    }
    None =>
      match result {
        Ok(value) => {
          self.state = Available
          value
        }
        Err(diagnostic) => {
          self.state = execution_session_state_after_integrity(
            diagnostic.engine_integrity(),
          )
          if self.state is Faulted {
            self.host_binding.invalidate_scheduled_turns()
          }
          raise ExecutionFailure(diagnostic)
        }
      }
  }
}

///|
fn[T] ExecutionSession::complete_running_turn(
  self : ExecutionSession,
  result : Result[T, EngineDiagnostic],
) -> T raise ExecutionSessionError {
  match self.host_binding.take_failure() {
    Some(failure) => {
      self.state = Faulted
      self.host_binding.invalidate_scheduled_turns()
      raise HostFailure(capability=failure.capability, detail=failure.detail)
    }
    None => ()
  }
  let should_checkpoint = execution_session_should_checkpoint(result)
  guard should_checkpoint else { return self.finish_turn(result) }
  let completed = match self.engine.run_microtask_checkpoint_diagnostic() {
    Ok(_) => result
    Err(diagnostic) => Err(diagnostic)
  }
  self.finish_turn(completed)
}

///|
fn[T] ExecutionSession::run_hosted_turn(
  self : ExecutionSession,
  action : () -> Result[T, EngineDiagnostic],
) -> T raise ExecutionSessionError {
  self.require_available()
  self.complete_running_turn(action())
}

///|
/// Resume one application-scheduled callback as a new Hosted Turn.
pub fn ExecutionSession::resume_scheduled_turn(
  self : ExecutionSession,
  turn : ScheduledTurn,
) -> Unit raise ExecutionSessionError {
  self.require_available()
  guard self.host_binding.take_scheduled_callback(turn) is Some(scheduled) else {
    self.state = Available
    raise ScheduledTurnUnavailable
  }
  self.complete_running_turn(
    self.engine.run_scheduled_callback_diagnostic(scheduled),
  )
}

///|
/// Evaluate source in this Execution Session's persistent Realm.
pub fn ExecutionSession::evaluate(
  self : ExecutionSession,
  source : String,
) -> Unit raise ExecutionSessionError {
  self.run_hosted_turn(() => self.engine.eval_diagnostic(source))
}

///|
/// Call a global JavaScript function through the explicit JSON Data Copy
/// boundary.
pub fn ExecutionSession::call_json(
  self : ExecutionSession,
  name : String,
  arguments : Array[Json],
) -> Json raise ExecutionSessionError {
  self.run_hosted_turn(() => self.engine.call_json_diagnostic(name, arguments))
}