///|
fn build_offline_network_conditions_json() -> Json {
  make_object({ "type": Json::string("offline") })
}

///|
fn compact_network_conditions_function_source(
  function_declaration : String,
) -> String {
  let chars = function_declaration.to_array()
  let mut compact = ""
  for ch in chars {
    if ch != ' ' && ch != '\n' && ch != '\r' && ch != '\t' {
      compact = compact + ch.to_string()
    }
  }
  compact
}

///|
fn is_network_conditions_listener_registration(
  function_declaration : String,
) -> Bool {
  let compact = compact_network_conditions_function_source(function_declaration)
  compact.contains(
    "window.addEventListener('offline',(e)=>{channel('offline,isTrusted:'+e.isTrusted);});",
  ) &&
  compact.contains(
    "window.addEventListener('online',(e)=>{channel('online,isTrusted:'+e.isTrusted);});",
  )
}

///|
fn get_first_string_argument(params : Json?) -> String? {
  match get_param_raw(params, "arguments") {
    Some(Array(arguments)) =>
      if arguments.length() > 0 {
        remote_value_as_string(arguments[0])
      } else {
        None
      }
    _ => None
  }
}

///|
fn BidiProtocol::remember_network_state_channel_if_applicable(
  self : BidiProtocol,
  ctx_id : String,
  function_declaration : String,
  params : Json?,
) -> Unit {
  if !is_network_conditions_listener_registration(function_declaration) {
    return
  }
  match @browser_domain.extract_script_channel_name(params) {
    Some(channel_name) =>
      if channel_name != "" {
        self.network_state.network_state_channel_by_context[ctx_id] = channel_name
      }
    None => ()
  }
}

///|
fn BidiProtocol::emit_network_state_channel_message(
  self : BidiProtocol,
  ctx_id : String,
) -> Unit {
  match self.network_state.network_state_channel_by_context.get(ctx_id) {
    Some(channel_name) =>
      if channel_name != "" {
        let realm_id = self.context_realm
          .get(ctx_id)
          .unwrap_or("realm-" + ctx_id)
        let text = if self.resolve_effective_network_offline(ctx_id) {
          "offline, isTrusted: true"
        } else {
          "online, isTrusted: true"
        }
        self.emit_script_message(
          ctx_id,
          realm_id,
          channel_name,
          make_object({
            "type": Json::string("string"),
            "value": Json::string(text),
          }),
        )
      }
    None => ()
  }
}

///|
fn BidiProtocol::emit_network_condition_messages_for_contexts(
  self : BidiProtocol,
  target_contexts : Array[String],
  target_user_contexts : Array[String],
) -> Unit {
  let affected_top_level_contexts : Array[String] = []
  if target_contexts.length() > 0 {
    for ctx_id in target_contexts {
      let top_level_ctx_id = self.resolve_top_level_context(ctx_id)
      if !array_contains(affected_top_level_contexts, top_level_ctx_id) {
        affected_top_level_contexts.push(top_level_ctx_id)
      }
    }
  } else {
    for ctx_id in self.manager.list_sessions() {
      let top_level_ctx_id = self.resolve_top_level_context(ctx_id)
      if array_contains(affected_top_level_contexts, top_level_ctx_id) {
        continue
      }
      if target_user_contexts.length() > 0 {
        let user_context_id = self.context_user_context
          .get(top_level_ctx_id)
          .unwrap_or("default")
        if !array_contains(target_user_contexts, user_context_id) {
          continue
        }
      }
      affected_top_level_contexts.push(top_level_ctx_id)
    }
  }
  for ctx_id in affected_top_level_contexts {
    let realm_id = self.context_realm.get(ctx_id).unwrap_or("realm-" + ctx_id)
    self.process_channel_messages(ctx_id, realm_id)
    self.emit_network_state_channel_message(ctx_id)
  }
}

///|
fn BidiProtocol::resolve_effective_network_conditions(
  self : BidiProtocol,
  ctx_id : String,
) -> Json? {
  self.resolve_effective_scoped_override(
    ctx_id,
    self.emulation_state.emulation_network_conditions_by_context,
    self.emulation_state.emulation_network_conditions_by_user_context,
    self.emulation_state.emulation_network_conditions_global,
  )
}

///|
fn BidiProtocol::resolve_effective_network_offline(
  self : BidiProtocol,
  ctx_id : String,
) -> Bool {
  match self.resolve_effective_network_conditions(ctx_id) {
    Some(Object(conditions_map)) =>
      match conditions_map.get("type") {
        Some(String("offline")) => true
        _ => false
      }
    _ => false
  }
}

///|
fn BidiProtocol::refresh_effective_network_conditions_for_all_contexts(
  self : BidiProtocol,
) -> Unit {
  for ctx_id in self.manager.list_sessions() {
    self.apply_effective_viewport_to_runtime_context(ctx_id, ctx_id)
  }
}

///|
fn is_network_conditions_requires_network_transport(url : String) -> Bool {
  url.has_prefix("http://") || url.has_prefix("https://")
}

///|
fn BidiProtocol::navigation_blocked_by_network_conditions(
  self : BidiProtocol,
  ctx_id : String,
  resolved_url : String,
) -> Bool {
  is_network_conditions_requires_network_transport(resolved_url) &&
  self.resolve_effective_network_offline(ctx_id)
}

///|
fn BidiProtocol::try_handle_synthetic_network_conditions_fetch_call(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  realm_id : String,
  function_declaration : String,
  params : Json?,
  unwrap_result : Bool,
) -> Bool {
  if compact_network_conditions_function_source(function_declaration) !=
    "(url)=>fetch(url)" {
    return false
  }
  let request_url = match get_first_string_argument(params) {
    Some(request_url) => request_url
    None => return false
  }
  let resolved_url = self.resolve_navigation_target_url(ctx_id, request_url)
  if self.navigation_blocked_by_network_conditions(ctx_id, resolved_url) {
    self.send_script_exception_result(
      request_id,
      realm_id,
      make_object({
        "exceptionDetails": make_object({
          "text": Json::string("TypeError: Network is offline"),
        }),
      }),
    )
    return true
  }
  if is_network_conditions_requires_network_transport(resolved_url) {
    self.send_script_undefined_response(request_id, realm_id, unwrap_result)
    return true
  }
  false
}

///|
fn BidiProtocol::resolve_emulation_set_network_conditions(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Bool? {
  let map = match params {
    Some(Object(map)) => map
    _ => {
      self.send_error(
        request_id, "invalid argument", "params must be an object",
      )
      return None
    }
  }

  let network_conditions : Json? = match
    get_map_field_with_alias(map, "networkConditions", "network_conditions") {
    Some(Null) => None
    Some(Object(network_conditions_map)) =>
      match network_conditions_map.get("type") {
        Some(String("offline")) => Some(build_offline_network_conditions_json())
        _ => {
          self.send_error(
            request_id, "invalid argument", "networkConditions is invalid",
          )
          return None
        }
      }
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "networkConditions must be an object or null",
      )
      return None
    }
    None => {
      self.send_error(
        request_id, "invalid argument", "networkConditions must be provided",
      )
      return None
    }
  }

  let target_contexts : Array[String] = []
  match map.get("contexts") {
    Some(Array(contexts)) =>
      if contexts.length() == 0 {
        self.send_error(
          request_id, "invalid argument", "contexts must not be an empty array",
        )
        return None
      } else {
        for context in contexts {
          match context {
            String(ctx_id) =>
              if !self.manager.has_session(ctx_id) {
                self.send_error(
                  request_id,
                  "no such frame",
                  "Unknown context: " + ctx_id,
                )
                return None
              } else if self.context_parent.contains(ctx_id) {
                self.send_error(
                  request_id, "invalid argument", "contexts must reference top-level browsing contexts",
                )
                return None
              } else if !array_contains(target_contexts, ctx_id) {
                target_contexts.push(ctx_id)
              }
            _ => {
              self.send_error(
                request_id, "invalid argument", "contexts entries must be strings",
              )
              return None
            }
          }
        }
      }
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "contexts must be an array",
      )
      return None
    }
    None => ()
  }

  let target_user_contexts : Array[String] = []
  match get_map_field_with_alias(map, "userContexts", "user_contexts") {
    Some(Array(user_contexts)) =>
      if user_contexts.length() == 0 {
        self.send_error(
          request_id, "invalid argument", "userContexts must not be an empty array",
        )
        return None
      } else {
        for raw_user_context in user_contexts {
          match raw_user_context {
            String(user_context_id) =>
              if user_context_id == "" ||
                !self.user_contexts.contains(user_context_id) {
                self.send_error(
                  request_id,
                  "no such user context",
                  "Unknown user context: " + user_context_id,
                )
                return None
              } else if !array_contains(target_user_contexts, user_context_id) {
                target_user_contexts.push(user_context_id)
              }
            _ => {
              self.send_error(
                request_id, "invalid argument", "userContexts entries must be strings",
              )
              return None
            }
          }
        }
      }
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "userContexts must be an array",
      )
      return None
    }
    None => ()
  }

  if target_contexts.length() > 0 && target_user_contexts.length() > 0 {
    self.send_error(
      request_id, "invalid argument", "contexts and userContexts are mutually exclusive",
    )
    return None
  }

  if target_contexts.length() > 0 {
    for ctx_id in target_contexts {
      match network_conditions {
        Some(network_conditions) =>
          self.emulation_state.emulation_network_conditions_by_context[ctx_id] = network_conditions
        None =>
          self.emulation_state.emulation_network_conditions_by_context.remove(
            ctx_id,
          )
      }
    }
    self.refresh_effective_network_conditions_for_all_contexts()
    self.emit_network_condition_messages_for_contexts(
      target_contexts, target_user_contexts,
    )
    return Some(true)
  }

  if target_user_contexts.length() > 0 {
    for user_context_id in target_user_contexts {
      match network_conditions {
        Some(network_conditions) =>
          self.emulation_state.emulation_network_conditions_by_user_context[user_context_id] = network_conditions
        None =>
          self.emulation_state.emulation_network_conditions_by_user_context.remove(
            user_context_id,
          )
      }
    }
    self.refresh_effective_network_conditions_for_all_contexts()
    self.emit_network_condition_messages_for_contexts(
      target_contexts, target_user_contexts,
    )
    return Some(true)
  }

  self.emulation_state.emulation_network_conditions_global = network_conditions
  self.refresh_effective_network_conditions_for_all_contexts()
  self.emit_network_condition_messages_for_contexts(
    target_contexts, target_user_contexts,
  )
  Some(true)
}