///|
/// Type definitions of the SPOP protocol (HAProxy SPOE, doc/SPOE.txt v1.2).

///|
/// A typed value carried by the SPOP protocol (TYPED-DATA).
///
/// On the wire the type id occupies the low 4 bits of the first byte and
/// the flags the high 4 bits; for `Bool` the truth value is flag 0x10
/// (haproxy's `SPOP_DATA_FL_TRUE`).
///
/// Signed integers (`Int32`/`Int64`) are encoded by reinterpreting their
/// two's complement bit pattern as an unsigned 64-bit integer which is then
/// varint-encoded (a negative `Int64` therefore occupies a 10-byte varint),
/// matching haproxy's `spoe_encode_data`/`spoe_decode_data`. Decoding reads
/// an unsigned 64-bit varint and reinterprets/truncates it.
pub(all) enum Data {
  Null
  Bool(Bool)
  Int32(Int)
  UInt32(UInt)
  Int64(Int64)
  UInt64(UInt64)
  Ipv4(FixedArray[Byte]) // 4 bytes
  Ipv6(FixedArray[Byte]) // 16 bytes
  Str(String)
  Bin(Bytes)
} derive(Eq, Debug)

///|
/// The numeric type id of a `Data` value (high 4 bits of the type byte).
fn Data::type_id(self : Data) -> Int {
  match self {
    Null => 0
    Bool(_) => 1
    Int32(_) => 2
    UInt32(_) => 3
    Int64(_) => 4
    UInt64(_) => 5
    Ipv4(_) => 6
    Ipv6(_) => 7
    Str(_) => 8
    Bin(_) => 9
  }
}

///|
/// Scope of a variable set or unset by an `Action`.
pub(all) enum VarScope {
  Process
  Session
  Transaction
  Request
  Response
} derive(Eq, Debug)

///|
/// The numeric id of a variable scope on the wire.
pub fn VarScope::to_int(self : VarScope) -> Int {
  match self {
    Process => 0
    Session => 1
    Transaction => 2
    Request => 3
    Response => 4
  }
}

///|
/// Convert a wire scope id to a `VarScope`; `None` for unknown ids.
pub fn VarScope::from_int(id : Int) -> VarScope? {
  match id {
    0 => Some(Process)
    1 => Some(Session)
    2 => Some(Transaction)
    3 => Some(Request)
    4 => Some(Response)
    _ => None
  }
}

///|
/// An action carried by an ACK frame, asking HAProxy to set or unset a
/// variable. `SetVar` is action type 1 (3 arguments: scope, name, value),
/// `UnsetVar` is action type 2 (2 arguments: scope, name).
pub(all) enum Action {
  SetVar(VarScope, String, Data)
  UnsetVar(VarScope, String)
} derive(Eq, Debug)

///|
/// A message carried by a NOTIFY frame: a name plus an ordered list of
/// named arguments (the protocol encodes arguments in order).
pub(all) struct Message {
  name : String
  args : Array[(String, Data)]
} derive(Eq, Debug)

///|
/// Type of a SPOP frame.
pub(all) enum FrameType {
  HAProxyHello
  HAProxyDisconnect
  Notify
  AgentHello
  AgentDisconnect
  Ack
} derive(Eq, Debug)

///|
/// The numeric frame type id on the wire.
pub fn FrameType::to_int(self : FrameType) -> Int {
  match self {
    HAProxyHello => 1
    HAProxyDisconnect => 2
    Notify => 3
    AgentHello => 101
    AgentDisconnect => 102
    Ack => 103
  }
}

///|
/// Convert a wire frame type id to a `FrameType`; `None` for unknown ids.
pub fn FrameType::from_int(id : Int) -> FrameType? {
  match id {
    1 => Some(HAProxyHello)
    2 => Some(HAProxyDisconnect)
    3 => Some(Notify)
    101 => Some(AgentHello)
    102 => Some(AgentDisconnect)
    103 => Some(Ack)
    _ => None
  }
}

///|
/// Status code reported in DISCONNECT frames and carried by `SpopError`.
pub(all) enum StatusCode {
  Normal
  IoError
  Timeout
  FrameTooBig
  InvalidFrame
  VersionNotFound
  MaxFrameSizeNotFound
  CapabilitiesNotFound
  UnsupportedVersion
  InvalidMaxFrameSize
  FragmentationNotSupported
  InvalidInterlacedFrames
  FrameIdNotFound
  ResourceAllocError
  UnknownError
} derive(Eq, Debug)

///|
/// The numeric status code on the wire.
pub fn StatusCode::to_uint(self : StatusCode) -> UInt {
  match self {
    Normal => 0
    IoError => 1
    Timeout => 2
    FrameTooBig => 3
    InvalidFrame => 4
    VersionNotFound => 5
    MaxFrameSizeNotFound => 6
    CapabilitiesNotFound => 7
    UnsupportedVersion => 8
    InvalidMaxFrameSize => 9
    FragmentationNotSupported => 10
    InvalidInterlacedFrames => 11
    FrameIdNotFound => 12
    ResourceAllocError => 13
    UnknownError => 99
  }
}

///|
/// Convert a wire status code to a `StatusCode`; `None` for unknown codes.
pub fn StatusCode::from_uint(code : UInt) -> StatusCode? {
  match code {
    0 => Some(Normal)
    1 => Some(IoError)
    2 => Some(Timeout)
    3 => Some(FrameTooBig)
    4 => Some(InvalidFrame)
    5 => Some(VersionNotFound)
    6 => Some(MaxFrameSizeNotFound)
    7 => Some(CapabilitiesNotFound)
    8 => Some(UnsupportedVersion)
    9 => Some(InvalidMaxFrameSize)
    10 => Some(FragmentationNotSupported)
    11 => Some(InvalidInterlacedFrames)
    12 => Some(FrameIdNotFound)
    13 => Some(ResourceAllocError)
    99 => Some(UnknownError)
    _ => None
  }
}

///|
/// The payload of a SPOP frame, whose shape depends on the frame type:
/// HELLO and DISCONNECT frames carry a key-value list, NOTIFY frames carry
/// a list of messages, and ACK frames carry a list of actions.
pub(all) enum FramePayload {
  KeyValues(Array[(String, Data)])
  Messages(Array[Message])
  Actions(Array[Action])
} derive(Eq, Debug)

///|
/// A SPOP frame: ``.
///
/// `fin` is flag bit 0 and must be set on every frame (fragmentation is not
/// supported); `abort` is flag bit 1. `stream_id` and `frame_id` are
/// varint-encoded on the wire.
pub(all) struct Frame {
  frame_type : FrameType
  fin : Bool
  abort : Bool
  stream_id : UInt64
  frame_id : UInt64
  payload : FramePayload
} derive(Eq, Debug)

///|
/// Build a HAPROXY-HELLO or AGENT-HELLO frame carrying `args`.
pub fn Frame::hello(
  frame_type : FrameType,
  args : Array[(String, Data)],
) -> Frame {
  {
    frame_type,
    fin: true,
    abort: false,
    stream_id: 0,
    frame_id: 0,
    payload: KeyValues(args),
  }
}

///|
/// Build a NOTIFY frame carrying `messages`.
pub fn Frame::notify(
  stream_id : UInt64,
  frame_id : UInt64,
  messages : Array[Message],
) -> Frame {
  {
    frame_type: Notify,
    fin: true,
    abort: false,
    stream_id,
    frame_id,
    payload: Messages(messages),
  }
}

///|
/// Build an ACK frame carrying `actions`, echoing the stream and frame ids
/// of the NOTIFY it answers.
pub fn Frame::ack(
  stream_id : UInt64,
  frame_id : UInt64,
  actions : Array[Action],
) -> Frame {
  {
    frame_type: Ack,
    fin: true,
    abort: false,
    stream_id,
    frame_id,
    payload: Actions(actions),
  }
}

///|
/// Build a HAPROXY-DISCONNECT or AGENT-DISCONNECT frame reporting
/// `status_code` and `message`.
pub fn Frame::disconnect(
  frame_type : FrameType,
  status_code : StatusCode,
  message : String,
) -> Frame {
  {
    frame_type,
    fin: true,
    abort: false,
    stream_id: 0,
    frame_id: 0,
    payload: KeyValues(disconnect_args(status_code, message)),
  }
}

///|
/// The key-value list carried by a DISCONNECT frame for `status_code` and
/// `message`: `status-code` (UINT32) and `message` (STRING).
pub fn disconnect_args(
  status_code : StatusCode,
  message : String,
) -> Array[(String, Data)] {
  [("status-code", UInt32(status_code.to_uint())), ("message", Str(message))]
}

///|
/// Extract the status code and message from the key-value list of a
/// DISCONNECT frame. Missing keys default to `StatusCode::UnknownError`
/// and an empty message; entries of the wrong type raise
/// `StatusCode::InvalidFrame`.
pub fn parse_disconnect_args(
  args : Array[(String, Data)],
) -> (StatusCode, String) raise SpopError {
  let mut code = UnknownError
  let mut message = ""
  for kv in args {
    match kv {
      ("status-code", UInt32(v)) =>
        code = StatusCode::from_uint(v).unwrap_or(UnknownError)
      ("status-code", _) =>
        raise SpopError::new(InvalidFrame, "status-code must be a UINT32")
      ("message", Str(s)) => message = s
      ("message", _) =>
        raise SpopError::new(InvalidFrame, "message must be a STRING")
      _ => ()
    }
  }
  (code, message)
}