///|
/// Per-view callbacks and observable lifecycle. The native backend owns
/// command queuing before Ready; this registry only routes callbacks.
priv struct Registration {
  lifecycle : WebViewLifecycle
  navigation_state : NavigationState
  on_event : (WebViewEvent) -> Unit
  on_navigation : (String) -> NavigationDecision
  on_new_window : (String) -> NewWindowDecision
  on_media_permission : (MediaPermissionRequest) -> MediaPermissionDecision
}

///|
let registrations : Map[UInt64, Registration] = Map([])

///|
let callback_installation : Map[Int, Unit] = Map([])

///|
let custom_schemes_locked : Map[Int, Unit] = Map([])

///|
fn ensure_native_callbacks() -> Unit {
  if !callback_installation.contains(0) {
    install_native_callbacks(
      dispatch_native_event, decide_navigation, decide_new_window, dispatch_protocol_request,
      decide_media_permission,
    )
    callback_installation.set(0, ())
  }
}

///|
fn register_view(handle : UInt64, options : WebViewOptions) -> Unit {
  registrations.set(handle, {
    lifecycle: WebViewLifecycle::Creating,
    navigation_state: NavigationState::new(),
    on_event: options.on_event,
    on_navigation: options.on_navigation,
    on_new_window: options.on_new_window,
    on_media_permission: options.on_media_permission,
  })
}

///|
fn register_ohos_view(handle : UInt64, options : OhosAttachOptions) -> Unit {
  registrations.set(handle, {
    lifecycle: WebViewLifecycle::Creating,
    navigation_state: NavigationState::new(),
    on_event: options.on_event,
    on_navigation: allow_navigation,
    on_new_window: deny_new_window,
    on_media_permission: deny_media_permission,
  })
}

///|
fn unregister_view(handle : UInt64) -> Unit {
  ignore(registrations.remove(handle))
}

///|
fn lifecycle_of(handle : UInt64) -> WebViewLifecycle {
  match registrations.get(handle) {
    Some(registration) => registration.lifecycle
    None => WebViewLifecycle::Destroyed
  }
}

///|
fn navigation_state_of(handle : UInt64) -> NavigationState {
  match registrations.get(handle) {
    Some(registration) => registration.navigation_state
    None => NavigationState::new()
  }
}

///|
fn update_lifecycle(handle : UInt64, lifecycle : WebViewLifecycle) -> Unit {
  match registrations.get(handle) {
    Some(registration) =>
      registrations.set(handle, {
        lifecycle,
        navigation_state: registration.navigation_state,
        on_event: registration.on_event,
        on_navigation: registration.on_navigation,
        on_new_window: registration.on_new_window,
        on_media_permission: registration.on_media_permission,
      })
    None => ()
  }
}

///|
fn dispatch_native_event(
  handle : UInt64,
  kind : Int,
  value : Bytes,
  detail : Bytes,
  code : Int,
) -> Unit {
  match registrations.get(handle) {
    Some(registration) => {
      let event = decode_native_event(kind, value, detail, code)
      match event {
        WebViewEvent::Ready => update_lifecycle(handle, WebViewLifecycle::Ready)
        WebViewEvent::CreationFailed(error) =>
          update_lifecycle(handle, WebViewLifecycle::Failed(error))
        WebViewEvent::ProcessFailed(error) =>
          update_lifecycle(handle, WebViewLifecycle::Failed(error))
        _ => ()
      }
      update_navigation_state(handle, event)
      (registration.on_event)(event)
    }
    None => ()
  }
}

///|
fn update_navigation_state(handle : UInt64, event : WebViewEvent) -> Unit {
  match registrations.get(handle) {
    Some(registration) => {
      let navigation_state = match event {
        WebViewEvent::SourceChanged(url) =>
          {
            url,
            can_go_back: registration.navigation_state.can_go_back,
            can_go_forward: registration.navigation_state.can_go_forward,
          }
        WebViewEvent::HistoryChanged(can_go_back, can_go_forward) =>
          {
            url: registration.navigation_state.url,
            can_go_back,
            can_go_forward,
          }
        _ => return
      }
      registrations.set(handle, {
        lifecycle: registration.lifecycle,
        navigation_state,
        on_event: registration.on_event,
        on_navigation: registration.on_navigation,
        on_new_window: registration.on_new_window,
        on_media_permission: registration.on_media_permission,
      })
    }
    None => ()
  }
}

///|
fn decide_navigation(handle : UInt64, url : Bytes) -> Int {
  match registrations.get(handle) {
    Some(registration) =>
      match (registration.on_navigation)(decode_utf8(url)) {
        NavigationDecision::Allow => 1
        NavigationDecision::Deny => 0
      }
    None => 0
  }
}

///|
fn decide_new_window(handle : UInt64, url : Bytes) -> Int {
  match registrations.get(handle) {
    Some(registration) =>
      match (registration.on_new_window)(decode_utf8(url)) {
        NewWindowDecision::Deny => 0
        NewWindowDecision::NavigateCurrent => 1
      }
    None => 0
  }
}

///|
fn dispatch_protocol_request(
  handle : UInt64,
  request_id : Bytes,
  scheme : Bytes,
  http_method : Bytes,
  uri : Bytes,
  headers : Bytes,
  body : Bytes,
) -> Unit {
  match registrations.get(handle) {
    Some(registration) =>
      (registration.on_event)(
        WebViewEvent::ProtocolRequest({
          id: decode_utf8(request_id),
          scheme: decode_utf8(scheme),
          http_method: decode_utf8(http_method),
          uri: decode_utf8(uri),
          headers: decode_http_headers(headers),
          body,
        }),
      )
    None => ()
  }
}

///|
fn decide_media_permission(handle : UInt64, kind : Int, origin : Bytes) -> Int {
  match media_permission_kind_from_native(kind) {
    MediaPermissionKind::Unknown(_) => 0
    kind =>
      match registrations.get(handle) {
        Some(registration) =>
          match
            (registration.on_media_permission)({
              kind,
              origin: decode_utf8(origin),
            }) {
            MediaPermissionDecision::Allow => 1
            MediaPermissionDecision::Deny => 0
          }
        None => 0
      }
  }
}

///|
fn decode_native_event(
  kind : Int,
  value : Bytes,
  detail : Bytes,
  code : Int,
) -> WebViewEvent {
  let value = decode_utf8(value)
  let detail = decode_utf8(detail)
  match kind {
    1 => WebViewEvent::Ready
    2 => WebViewEvent::CreationFailed(WebViewError::NativeFailure(code, detail))
    3 => WebViewEvent::PageMessage(value)
    4 => WebViewEvent::NavigationStarting(value)
    5 => WebViewEvent::SourceChanged(value)
    6 =>
      if code == 0 {
        WebViewEvent::NavigationCompleted(value)
      } else {
        WebViewEvent::NavigationFailed(
          value,
          WebViewError::NativeFailure(code, detail),
        )
      }
    7 => WebViewEvent::TitleChanged(value)
    8 =>
      WebViewEvent::HistoryChanged(
        contains_text(detail, "back=1"),
        contains_text(detail, "forward=1"),
      )
    9 =>
      if code == 0 {
        WebViewEvent::ScriptResult(detail, value)
      } else {
        WebViewEvent::ScriptFailed(
          detail,
          WebViewError::NativeFailure(code, value),
        )
      }
    10 => WebViewEvent::ProtocolCancelled(value)
    11 => WebViewEvent::ProcessFailed(WebViewError::NativeFailure(code, detail))
    _ => WebViewEvent::CreationFailed(WebViewError::NativeFailure(code, detail))
  }
}

///|
fn decode_utf8(bytes : BytesView) -> String {
  @utf8.decode_lossy(bytes[:], ignore_bom=true)
}

///|
fn encode_utf8(text : String) -> Bytes {
  @utf8.encode(text[:], bom=false)
}

///|
/// Header wire format: `u32be count`, followed by each UTF-8 name and value
/// prefixed by its `u32be` byte length. The format is shared by every backend.
fn encode_http_headers(headers : Array[HttpHeader]) -> Bytes {
  let output : Array[Byte] = []
  append_u32be(output, headers.length())
  for header in headers {
    let name = encode_utf8(header.name)
    let value = encode_utf8(header.value)
    append_u32be(output, name.length())
    append_bytes(output, name)
    append_u32be(output, value.length())
    append_bytes(output, value)
  }
  Bytes::from_array(output)
}

///|
fn decode_http_headers(bytes : Bytes) -> Array[HttpHeader] {
  let mut offset = 0
  let count = match read_u32be(bytes, offset) {
    Some(value) => value
    None => return []
  }
  offset += 4
  if count < 0 || count > bytes.length() / 8 {
    return []
  }
  let headers : Array[HttpHeader] = []
  for _ in 0.. value
      None => return []
    }
    offset += 4
    if name_length < 0 || name_length > bytes.length() - offset {
      return []
    }
    let name = decode_utf8(bytes[offset:offset + name_length])
    offset += name_length
    let value_length = match read_u32be(bytes, offset) {
      Some(value) => value
      None => return []
    }
    offset += 4
    if value_length < 0 || value_length > bytes.length() - offset {
      return []
    }
    let value = decode_utf8(bytes[offset:offset + value_length])
    offset += value_length
    headers.push({ name, value })
  }
  if offset == bytes.length() {
    headers
  } else {
    []
  }
}

///|
fn append_u32be(output : Array[Byte], value : Int) -> Unit {
  output.push(((value >> 24) & 0xff).to_byte())
  output.push(((value >> 16) & 0xff).to_byte())
  output.push(((value >> 8) & 0xff).to_byte())
  output.push((value & 0xff).to_byte())
}

///|
fn append_bytes(output : Array[Byte], value : Bytes) -> Unit {
  for byte in value {
    output.push(byte)
  }
}

///|
fn read_u32be(bytes : Bytes, offset : Int) -> Int? {
  if offset < 0 || offset + 4 > bytes.length() {
    None
  } else {
    Some(
      (bytes[offset].to_int() << 24) |
      (bytes[offset + 1].to_int() << 16) |
      (bytes[offset + 2].to_int() << 8) |
      bytes[offset + 3].to_int(),
    )
  }
}

///|
fn media_permission_kind_from_native(kind : Int) -> MediaPermissionKind {
  match kind {
    1 => MediaPermissionKind::Camera
    2 => MediaPermissionKind::Microphone
    3 => MediaPermissionKind::CameraAndMicrophone
    _ => MediaPermissionKind::Unknown(kind)
  }
}

///|
fn contains_text(text : String, needle : String) -> Bool {
  let text = text.to_array()
  let needle = needle.to_array()
  if needle.length() == 0 {
    true
  } else if needle.length() > text.length() {
    false
  } else {
    for start in 0..<=(text.length() - needle.length()) {
      let mut matched = true
      for offset in 0..