///|
priv struct ExportInfo {
  size : UInt64
  flags : UInt32
  min_block : UInt32
  max_block : UInt32
}

///|
async fn send_option(
  tcp : @socket.Tcp,
  option : UInt32,
  payload : Bytes,
) -> Unit {
  let out : Array[Byte] = []
  put64(out, 0x49484156454f5054UL)
  put32(out, option)
  put32(out, payload.length().reinterpret_as_uint())
  for b in payload {
    out.push(b)
  }
  tcp.write(Bytes::from_array(out))
}

///|
async fn legacy_export(
  tcp : @socket.Tcp,
  name : Bytes,
  no_zeroes : Bool,
) -> ExportInfo {
  send_option(tcp, 1U, name)
  let data = tcp.read_exactly(if no_zeroes { 10 } else { 134 })
  let flags = get16(data, 8)
  if (flags & 1U) == 0U {
    raise Protocol("server did not set HAS_FLAGS")
  }
  { size: get64(data, 0), flags, min_block: 1U, max_block: 16777216U, }
}

///|
async fn negotiate_go(
  tcp : @socket.Tcp,
  name : Bytes,
  no_zeroes : Bool,
) -> ExportInfo {
  let body : Array[Byte] = []
  put32(body, name.length().reinterpret_as_uint())
  for b in name {
    body.push(b)
  }
  put16(body, 1U)
  put16(body, 3U)
  send_option(tcp, 7U, Bytes::from_array(body))
  let mut size = 0UL
  let mut flags = 0U
  let mut minimum = 1U
  let mut maximum = 16777216U
  let mut got_export = false
  let mut got_block = false
  for _ in 0..<256 {
    let header = tcp.read_exactly(20)
    if get64(header, 0) != 0x0003e889045565a9UL || get32(header, 8) != 7U {
      raise Protocol("invalid option reply header")
    }
    let kind = get32(header, 12)
    let length = get32(header, 16)
    if length > 65536U {
      raise Protocol("option reply exceeds limit")
    }
    let data = tcp.read_exactly(length.reinterpret_as_int())
    if kind == 0x80000001U {
      if got_export || got_block {
        raise Protocol("unsupported reply after export info")
      }
      return legacy_export(tcp, name, no_zeroes)
    }
    if (kind & 0x80000000U) != 0U {
      raise Server(kind)
    }
    if kind == 1U {
      if length != 0U || !got_export {
        raise Protocol("incomplete GO reply")
      }
      if size % minimum.to_uint64() != 0UL {
        raise Protocol("export size violates negotiated minimum block size")
      }
      return { size, flags, min_block: minimum, max_block: maximum, }
    }
    if kind != 3U || length < 2U {
      raise Protocol("unexpected GO response")
    }
    match get16(data, 0) {
      0U => {
        if length != 12U || got_export {
          raise Protocol("invalid export info")
        }
        size = get64(data, 2)
        flags = get16(data, 10)
        if (flags & 1U) == 0U {
          raise Protocol("server did not set HAS_FLAGS")
        }
        got_export = true
      }
      3U => {
        if length != 14U || got_block {
          raise Protocol("invalid block sizes")
        }
        let min = get32(data, 2)
        let preferred = get32(data, 6)
        let max = get32(data, 10)
        if min == 0U ||
          (min & (min - 1U)) != 0U ||
          preferred < min ||
          preferred < 512U ||
          (preferred & (preferred - 1U)) != 0U ||
          max < preferred ||
          min > 65536U {
          raise Protocol("invalid server block constraints")
        }
        minimum = min
        let capped = if max < 16777216U { max } else { 16777216U }
        maximum = capped - capped % min
        got_block = true
      }
      _ => ()
    }
  }
  raise Protocol("too many option replies")
}