///|
/// Emit log.entryAdded event
fn BidiProtocol::emit_log_entry_with_details(
  self : BidiProtocol,
  ctx_id : String,
  realm_id : String,
  entry_type : String,
  level : String,
  text : String,
  console_method : String?,
  args : Array[Json],
  timestamp : Double,
  stack_trace : Json?,
) -> Unit {
  let params_map : Map[String, Json] = {
    "level": Json::string(level),
    "source": make_object({
      "realm": Json::string(realm_id),
      "context": Json::string(ctx_id),
    }),
    "text": Json::string(text),
    "timestamp": Json::number(timestamp),
    "type": Json::string(entry_type),
  }
  if entry_type == "console" {
    params_map["method"] = Json::string(console_method.unwrap_or("log"))
    params_map["args"] = Json::array(args)
  }
  match stack_trace {
    Some(value) => params_map["stackTrace"] = value
    None => ()
  }
  let params = make_object(params_map)
  if !self.is_subscribed_for_context("log.entryAdded", ctx_id) {
    self.subscription_state.buffer_log_entry(ctx_id, params)
    return
  }
  self.outbox.push(Event({ event_method: "log.entryAdded", params }))
}

///|
/// Emit log.entryAdded event for console entries.
fn BidiProtocol::emit_log_entry(
  self : BidiProtocol,
  ctx_id : String,
  level : String,
  text : String,
  console_method : String,
  args : Array[Json],
  timestamp : Double,
) -> Unit {
  let realm_id = self.context_realm.get(ctx_id).unwrap_or("realm-" + ctx_id)
  self.emit_log_entry_with_details(
    ctx_id,
    realm_id,
    "console",
    level,
    text,
    Some(console_method),
    args,
    timestamp,
    None,
  )
}

///|
/// Emit log.entryAdded event for javascript errors.
fn BidiProtocol::emit_javascript_log_entry(
  self : BidiProtocol,
  ctx_id : String,
  realm_id : String,
  level : String,
  text : String,
  timestamp : Double,
  stack_trace : Json?,
) -> Unit {
  self.emit_log_entry_with_details(
    ctx_id,
    realm_id,
    "javascript",
    level,
    text,
    None,
    [],
    timestamp,
    stack_trace,
  )
}

///|
/// Process console entries from JS evaluation result and emit log events
fn BidiProtocol::process_console_entries(
  self : BidiProtocol,
  eval_result : Json,
  ctx_id : String,
  realm_id : String,
) -> Unit {
  match get_field(eval_result, "consoleEntries") {
    Some(Array(entries)) =>
      for entry in entries {
        match entry {
          Object(_) => {
            let entry_type = get_string_field(entry, "type").unwrap_or(
              "console",
            )
            let level = get_string_field(entry, "level").unwrap_or("info")
            let console_method = get_string_field(entry, "method").unwrap_or(
              "log",
            )
            let text = get_string_field(entry, "text").unwrap_or("")
            let entry_realm_raw = get_string_field(entry, "realm").unwrap_or(
              realm_id,
            )
            let entry_realm = if entry_realm_raw == "" {
              realm_id
            } else {
              entry_realm_raw
            }
            let args = match get_field(entry, "args") {
              Some(Array(values)) => values
              _ => []
            }
            let timestamp = match get_field(entry, "timestamp") {
              Some(Number(n, ..)) => n
              _ => 0.0
            }
            let stack_trace = match get_field(entry, "stackTrace") {
              Some(Object(_)) => get_field(entry, "stackTrace")
              _ => None
            }
            if entry_type == "javascript" {
              self.emit_javascript_log_entry(
                ctx_id, entry_realm, level, text, timestamp, stack_trace,
              )
            } else {
              self.emit_log_entry_with_details(
                ctx_id,
                entry_realm,
                "console",
                level,
                text,
                Some(console_method),
                args,
                timestamp,
                stack_trace,
              )
            }
          }
          _ => ()
        }
      }
    _ => ()
  }
}

///|
/// Emit script.message event.
fn BidiProtocol::emit_script_message(
  self : BidiProtocol,
  ctx_id : String,
  realm_id : String,
  channel : String,
  data : Json,
) -> Unit {
  let event_name = "script.message"
  if !self.is_subscribed_for_context(event_name, ctx_id) {
    return
  }
  let params = make_object({
    "channel": Json::string(channel),
    "data": data,
    "source": make_object({
      "realm": Json::string(realm_id),
      "context": Json::string(ctx_id),
    }),
  })
  self.outbox.push(Event({ event_method: event_name, params }))
}

///|
/// Drain pending channel callback messages from JS runtime and emit script.message.
fn BidiProtocol::process_channel_messages(
  self : BidiProtocol,
  ctx_id : String,
  realm_id : String,
) -> Unit {
  let channel_messages = @json.parse(take_channel_messages_json()) catch {
    _ => Json::array([])
  }
  match channel_messages {
    Array(entries) =>
      for entry in entries {
        match entry {
          Object(msg_map) => {
            let channel = match msg_map.get("channel") {
              Some(String(s)) => s
              _ => ""
            }
            if channel != "" {
              let data = msg_map
                .get("data")
                .unwrap_or(make_object({ "type": Json::string("undefined") }))
              self.emit_script_message(ctx_id, realm_id, channel, data)
            }
          }
          _ => ()
        }
      }
    _ => ()
  }
}