///|
#cfg(target="native")
pub suberror Http2Error {
  Http2BadFrame
  Http2ConnectionError(String)
  Http2StreamReset(Int)
  Http2MissingStatus
} derive(Debug, ToJson)

///|
#cfg(target="native")
let h2_frame_data : Int = 0

///|
#cfg(target="native")
let h2_frame_headers : Int = 1

///|
#cfg(target="native")
let h2_frame_rst_stream : Int = 3

///|
#cfg(target="native")
let h2_frame_settings : Int = 4

///|
#cfg(target="native")
let h2_frame_ping : Int = 6

///|
#cfg(target="native")
let h2_frame_goaway : Int = 7

///|
#cfg(target="native")
let h2_frame_window_update : Int = 8

///|
#cfg(target="native")
let h2_frame_continuation : Int = 9

///|
#cfg(target="native")
let h2_default_max_frame_size : Int = 16384

///|
#cfg(target="native")
let h2_flag_end_stream : Int = 0x1

///|
#cfg(target="native")
let h2_flag_ack : Int = 0x1

///|
#cfg(target="native")
let h2_flag_end_headers : Int = 0x4

///|
#cfg(target="native")
let h2_preface : Bytes = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"

///|
#cfg(target="native")
priv struct H2Frame {
  frame_type : Int
  flags : Int
  stream_id : Int
  payload : Bytes
}

///|
#cfg(target="native")
fn h2_write_u24(out : @buffer.Buffer, value : Int) -> Unit {
  out.write_byte(((value >> 16) & 0xff).to_byte())
  out.write_byte(((value >> 8) & 0xff).to_byte())
  out.write_byte((value & 0xff).to_byte())
}

///|
#cfg(target="native")
fn h2_write_u31(out : @buffer.Buffer, value : Int) -> Unit {
  out.write_byte(((value >> 24) & 0x7f).to_byte())
  out.write_byte(((value >> 16) & 0xff).to_byte())
  out.write_byte(((value >> 8) & 0xff).to_byte())
  out.write_byte((value & 0xff).to_byte())
}

///|
#cfg(target="native")
async fn[W : @io.Writer] h2_write_frame(
  writer : W,
  frame_type : Int,
  flags : Int,
  stream_id : Int,
  payload : Bytes,
) -> Unit {
  let out = @buffer.new()
  h2_write_u24(out, payload.length())
  out.write_byte(frame_type.to_byte())
  out.write_byte(flags.to_byte())
  h2_write_u31(out, stream_id)
  out.write_bytes(payload)
  writer.write(out.contents())
}

///|
#cfg(target="native")
async fn[W : @io.Writer] h2_write_window_update(
  writer : W,
  stream_id : Int,
  increment : Int,
) -> Unit {
  guard increment > 0 else { return }
  let payload = @buffer.new()
  h2_write_u31(payload, increment)
  h2_write_frame(
    writer,
    h2_frame_window_update,
    0,
    stream_id,
    payload.contents(),
  )
}

///|
#cfg(target="native")
async fn[W : @io.Writer] h2_write_data_frames(
  writer : W,
  stream_id : Int,
  data : Bytes,
) -> Unit {
  if data.length() == 0 {
    return
  }
  for offset = 0; offset < data.length(); {
    let len = @cmp.minimum(h2_default_max_frame_size, data.length() - offset)
    let end = offset + len
    let flags = if end == data.length() { h2_flag_end_stream } else { 0 }
    let payload = @buffer.new()
    payload.write_bytes(data[offset:end])
    h2_write_frame(writer, h2_frame_data, flags, stream_id, payload.contents())
    continue end
  }
}

///|
#cfg(target="native")
async fn[R : @io.Reader] h2_read_frame(reader : R) -> H2Frame {
  let header = reader.read_exactly(9)
  let length = (header[0].to_int() << 16) |
    (header[1].to_int() << 8) |
    header[2].to_int()
  let frame_type = header[3].to_int()
  let flags = header[4].to_int()
  let stream_id = ((header[5].to_int() & 0x7f) << 24) |
    (header[6].to_int() << 16) |
    (header[7].to_int() << 8) |
    header[8].to_int()
  let payload = if length == 0 { b"" } else { reader.read_exactly(length) }
  { frame_type, flags, stream_id, payload }
}

///|
#cfg(target="native")
fn h2_validate_settings(frame : H2Frame) -> Unit raise {
  guard frame.stream_id == 0 else { raise Http2BadFrame }
  if (frame.flags & h2_flag_ack) != 0 {
    guard frame.payload.length() == 0 else { raise Http2BadFrame }
  } else {
    guard frame.payload.length() % 6 == 0 else { raise Http2BadFrame }
  }
}

///|
#cfg(target="native")
fn RequestMethod::h2_text(self : RequestMethod) -> String {
  match self {
    Get => "GET"
    Head => "HEAD"
    Post => "POST"
    Put => "PUT"
    Delete => "DELETE"
    Connect => "CONNECT"
    Options => "OPTIONS"
    Trace => "TRACE"
    Patch => "PATCH"
  }
}

///|
#cfg(target="native")
fn h2_request_headers(
  protocol : Protocol,
  host : String,
  path : String,
  meth : RequestMethod,
  headers : Map[String, String],
  body_len : Int,
) -> Bytes {
  let header_list = []
  header_list.push((":method", meth.h2_text()))
  header_list.push(
    (
      ":scheme",
      match protocol {
        Http => "http"
        Https => "https"
      },
    ),
  )
  header_list.push((":authority", host))
  header_list.push((":path", path))
  if body_len > 0 {
    header_list.push(("content-length", body_len.to_string()))
  }
  let mut has_accept_encoding = false
  for item in headers {
    let (key, value) = item
    let key = key.to_lower()
    match key {
      "connection"
      | "host"
      | "keep-alive"
      | "proxy-connection"
      | "transfer-encoding"
      | "upgrade" => ()
      "accept-encoding" => {
        has_accept_encoding = true
        header_list.push((key, value))
      }
      _ => header_list.push((key, value))
    }
  }
  if !has_accept_encoding {
    header_list.push(("accept-encoding", "gzip, deflate, br, zstd"))
  }
  hpack_encode_headers(header_list)
}

///|
#cfg(target="native")
fn h2_response_from_headers(
  headers : Array[(String, String)],
) -> Response raise {
  let response_headers = {}
  let cookies = []
  let mut code = -1
  for item in headers {
    let (key, value) = item
    if key == ":status" {
      code = @string.parse_int(value, base=10) catch { _ => -1 }
    } else if key == "set-cookie" {
      cookies.push(Cookie::parse(value)) catch {
        _ => ()
      }
    } else if key.has_prefix(":") {
      ()
    } else {
      match response_headers.get(key) {
        None => response_headers[key] = value
        Some(value0) => response_headers[key] = "\{value0},\{value}"
      }
    }
  }
  guard code >= 0 else { raise Http2MissingStatus }
  { code, reason: "", headers: response_headers, cookies }
}

///|
#cfg(target="native")
async fn h2_decode_body(response : Response, body : Bytes) -> Bytes {
  match response.headers.get("content-encoding") {
    Some("gzip") => Gzip.decode(body)
    Some("deflate") => Deflate.decode(body)
    Some("br") => Brotli.decode(body)
    Some("zstd") => Zstd.decode(body)
    _ => body
  }
}

///|
#cfg(target="native")
async fn[Conn : @io.Reader + @io.Writer] h2_request_on_conn(
  conn : Conn,
  protocol : Protocol,
  host : String,
  path : String,
  meth : RequestMethod,
  headers : Map[String, String],
  body : Bytes,
) -> ResponseBody {
  conn.write(h2_preface)
  h2_write_frame(conn, h2_frame_settings, 0, 0, b"")
  let header_block = h2_request_headers(
    protocol,
    host,
    path,
    meth,
    headers,
    body.length(),
  )
  let mut flags = h2_flag_end_headers
  if body.length() == 0 {
    flags = flags | h2_flag_end_stream
  }
  h2_write_frame(conn, h2_frame_headers, flags, 1, header_block)
  h2_write_data_frames(conn, 1, body)
  let body_out = @buffer.new()
  let header_out = @buffer.new()
  let hpack_decoder = HpackDecoder::new()
  let mut response : Response? = None
  let mut end_stream = false
  for ; !end_stream; {
    let frame = h2_read_frame(conn)
    match frame.frame_type {
      t if t == h2_frame_settings => {
        h2_validate_settings(frame)
        if (frame.flags & h2_flag_ack) == 0 {
          h2_write_frame(conn, h2_frame_settings, h2_flag_ack, 0, b"")
        }
      }
      t if t == h2_frame_headers && frame.stream_id == 1 => {
        header_out.write_bytes(frame.payload)
        if (frame.flags & h2_flag_end_headers) == 0 {
          for ;; {
            let cont = h2_read_frame(conn)
            guard cont.frame_type == h2_frame_continuation &&
              cont.stream_id == 1 else {
              raise Http2BadFrame
            }
            header_out.write_bytes(cont.payload)
            if (cont.flags & h2_flag_end_headers) != 0 {
              break
            }
          }
        }
        if response is None {
          response = Some(
            h2_response_from_headers(
              hpack_decoder.decode(header_out.contents()),
            ),
          )
        } else {
          hpack_decoder.decode(header_out.contents()) |> ignore
        }
        header_out.reset()
        if (frame.flags & h2_flag_end_stream) != 0 {
          end_stream = true
        }
      }
      t if t == h2_frame_data && frame.stream_id == 1 => {
        body_out.write_bytes(frame.payload)
        h2_write_window_update(conn, 0, frame.payload.length())
        h2_write_window_update(conn, 1, frame.payload.length())
        if (frame.flags & h2_flag_end_stream) != 0 {
          end_stream = true
        }
      }
      t if t == h2_frame_rst_stream && frame.stream_id == 1 => {
        let code = if frame.payload.length() >= 4 {
          (frame.payload[0].to_int() << 24) |
          (frame.payload[1].to_int() << 16) |
          (frame.payload[2].to_int() << 8) |
          frame.payload[3].to_int()
        } else {
          -1
        }
        raise Http2StreamReset(code)
      }
      t if t == h2_frame_ping =>
        if (frame.flags & h2_flag_ack) == 0 {
          h2_write_frame(conn, h2_frame_ping, h2_flag_ack, 0, frame.payload)
        }
      t if t == h2_frame_goaway => raise Http2ConnectionError("GOAWAY")
      t if t == h2_frame_window_update => ()
      _ => ()
    }
  }
  guard response is Some(response) else { raise Http2MissingStatus }
  { response, body: h2_decode_body(response, body_out.contents()) }
}

///|
#cfg(target="native")
async fn h2_perform_request(
  uri : String,
  meth : RequestMethod,
  headers : Map[String, String],
  body : &@io.Data,
  proxy? : Client,
  verify? : Bool = true,
) -> ResponseBody {
  let (protocol, port, host, path) = resolve_url(uri)
  guard proxy is None else {
    raise Http2ConnectionError("HTTP/2 proxy transport is not implemented")
  }
  let request_body = body.binary()
  match protocol {
    Http => {
      let conn = @socket.Tcp::connect_to_host(host, port~)
      defer conn.close()
      h2_request_on_conn(
        conn, protocol, host, path, meth, headers, request_body,
      )
    }
    Https => {
      let tcp = @socket.Tcp::connect_to_host(host, port~)
      let tls = @tls.Tls::client(tcp, host~, verify~) catch {
        err => {
          tcp.close()
          raise err
        }
      }
      defer {
        tls.close()
        tcp.close()
      }
      h2_request_on_conn(tls, protocol, host, path, meth, headers, request_body)
    }
  }
}