// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// An endpoint's settings (RFC 9113 §6.5.2, RFC 8441 §3).
///
/// `None` is the RFC's "unlimited". The preset is [`settings`]; [`initial`] is what
/// both ends assume before any SETTINGS frame, and so what the peer's settings start
/// from. Build others with [`Settings::new`] or `{ ..settings, max_frame_size: n }`.
pub(all) struct Settings {
  header_table_size : Int32
  enable_push : Bool
  max_concurrent_streams : Int32?
  initial_window_size : Int32
  max_frame_size : Int32
  max_header_list_size : Int32?
  enable_connect_protocol : Bool
} derive(Eq, Debug)

///|
pub extend Settings with Eq::{not_equal, equal}

///|
pub extend Settings with Debug::{to_repr}

///|
/// The value RFC 9113 §6.5.2 gives every setting before an endpoint has said
/// otherwise.
pub let initial : Settings = {
  header_table_size: 4096,
  enable_push: true,
  max_concurrent_streams: None,
  initial_window_size: 65535,
  max_frame_size: 16384,
  max_header_list_size: None,
  enable_connect_protocol: false,
}

///|
/// What a connection advertises unless told otherwise: the RFC's values, with a
/// hundred concurrent streams and 64 KiB of header list.
///
/// Those two are python-hyper h2's own defaults, and a hundred streams is also where
/// nghttp2 and gRPC settle. Left unlimited, every stream a peer opens and never
/// finishes holds memory, and a few octets of HPACK can name one large table entry
/// thousands of times.
pub let settings : Settings = {
  ..initial,
  max_concurrent_streams: Some(100),
  max_header_list_size: Some(65536),
}

///|
/// Settings by name, every one with the preset's value.
pub fn Settings::new(
  header_table_size~ : Int32 = settings.header_table_size,
  enable_push~ : Bool = settings.enable_push,
  max_concurrent_streams~ : Int32? = settings.max_concurrent_streams,
  initial_window_size~ : Int32 = settings.initial_window_size,
  max_frame_size~ : Int32 = settings.max_frame_size,
  max_header_list_size~ : Int32? = settings.max_header_list_size,
  enable_connect_protocol~ : Bool = settings.enable_connect_protocol,
) -> Settings {
  {
    header_table_size,
    enable_push,
    max_concurrent_streams,
    initial_window_size,
    max_frame_size,
    max_header_list_size,
    enable_connect_protocol,
  }
}

///|
/// Why these settings cannot be sent, or `None` (RFC 9113 §6.5.2).
fn Settings::fault(self : Settings) -> String? {
  if self.header_table_size < 0 {
    Some("a negative header_table_size")
  } else if self.initial_window_size < 0 {
    Some("an initial_window_size past 2^31 - 1")
  } else if self.max_frame_size < 16384 || self.max_frame_size > 16777215 {
    Some("a max_frame_size outside 16384..16777215")
  } else if self.max_concurrent_streams is Some(n) && n < 0 {
    Some("a negative max_concurrent_streams")
  } else if self.max_header_list_size is Some(n) && n < 0 {
    Some("a negative max_header_list_size")
  } else {
    None
  }
}

///|
/// The parameters of a SETTINGS frame that moves the peer from `base` to `self`.
///
/// A server only ever says `0` for push (§6.5.2), so a server's `enable_push` is
/// sent when it is off and never when it is on. "Unlimited" has no encoding of its
/// own, so going back to it is sent as the largest value the field holds.
fn Settings::pairs(
  self : Settings,
  base : Settings,
  role : Role,
) -> Array[(Int32, Int32)] {
  let out = []
  let unset = (v : Int32?) => v.unwrap_or(-1)
  if self.header_table_size != base.header_table_size {
    out.push((settings_header_table_size, self.header_table_size))
  }
  if self.enable_push != base.enable_push &&
    (role is Client || !self.enable_push) {
    out.push((settings_enable_push, if self.enable_push { 1 } else { 0 }))
  }
  if self.max_concurrent_streams != base.max_concurrent_streams {
    out.push(
      (settings_max_concurrent_streams, unset(self.max_concurrent_streams)),
    )
  }
  if self.initial_window_size != base.initial_window_size {
    out.push((settings_initial_window_size, self.initial_window_size))
  }
  if self.max_frame_size != base.max_frame_size {
    out.push((settings_max_frame_size, self.max_frame_size))
  }
  if self.max_header_list_size != base.max_header_list_size {
    out.push((settings_max_header_list_size, unset(self.max_header_list_size)))
  }
  if self.enable_connect_protocol != base.enable_connect_protocol {
    out.push(
      (
        settings_enable_connect_protocol,
        if self.enable_connect_protocol {
          1
        } else {
          0
        },
      ),
    )
  }
  out
}

///|
/// `self` with the parameters of a received SETTINGS frame applied, or why they
/// break the connection, with the error code to say so in (RFC 9113 §6.5.2).
///
/// Values arrive as 32 bits; one past 2^31 - 1 reads negative here, which for a
/// limit means as good as unlimited. Unknown identifiers are ignored (§6.5.2).
fn Settings::merge(
  self : Settings,
  params : Array[(Int32, Int32)],
  role : Role,
) -> Result[Settings, (Int32, String)] {
  let mut s = self
  let limit = (v : Int32) => if v < 0 { None } else { Some(v) }
  for p in params {
    let (id, v) = p
    if id == settings_header_table_size {
      s = { ..s, header_table_size: if v < 0 { 0x7FFFFFFF } else { v }, }
    } else if id == settings_enable_push {
      if v != 0 && v != 1 {
        return Err((error_protocol_error, "SETTINGS_ENABLE_PUSH is 0 or 1"))
      }
      if v == 1 && role is Client {
        return Err((error_protocol_error, "a server may not enable push"))
      }
      s = { ..s, enable_push: v == 1, }
    } else if id == settings_max_concurrent_streams {
      s = { ..s, max_concurrent_streams: limit(v), }
    } else if id == settings_initial_window_size {
      if v < 0 {
        return Err(
          (
            error_flow_control_error, "SETTINGS_INITIAL_WINDOW_SIZE past 2^31 - 1",
          ),
        )
      }
      s = { ..s, initial_window_size: v, }
    } else if id == settings_max_frame_size {
      if v < 16384 || v > 16777215 {
        return Err(
          (
            error_protocol_error, "SETTINGS_MAX_FRAME_SIZE outside 16384..16777215",
          ),
        )
      }
      s = { ..s, max_frame_size: v, }
    } else if id == settings_max_header_list_size {
      s = { ..s, max_header_list_size: limit(v), }
    } else if id == settings_enable_connect_protocol {
      if v != 0 && v != 1 {
        return Err(
          (error_protocol_error, "SETTINGS_ENABLE_CONNECT_PROTOCOL is 0 or 1"),
        )
      }
      // RFC 8441 §3: once on, it stays on.
      if v == 0 && s.enable_connect_protocol {
        return Err(
          (error_protocol_error, "SETTINGS_ENABLE_CONNECT_PROTOCOL turned off"),
        )
      }
      s = { ..s, enable_connect_protocol: v == 1, }
    }
  }
  Ok(s)
}