/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2024. All rights reserved.
 */

/**
 * @file The file manage ExchangeCodec.
 */

package httpclient4cj

/**
 * The class is Http2ExchangeCodec
 * @author luoyukai4
 * @since 0.34.3
 */
public class Http2ExchangeCodec <: ExchangeCodec {
    private static let CONNECTION = "connection"
    private static let HOST = "host"
    private static let KEEP_ALIVE = "keep-alive"
    private static let PROXY_CONNECTION = "proxy-connection"
    private static let TRANSFER_ENCODING = "transfer-encoding"
    private static let TE = "te"
    private static let ENCODING = "encoding"
    private static let UPGRADE = "upgrade"
    public static let RESPONSE_STATUS = ":status"
    public static let TARGET_METHOD = ":method"
    public static let TARGET_PATH = ":path"
    public static let TARGET_SCHEME = ":scheme"
    public static let TARGET_AUTHORITY = ":authority"

    private static let HTTP_2_SKIPPED_REQUEST_HEADERS: Array<String> = [
        CONNECTION,
        HOST,
        KEEP_ALIVE,
        PROXY_CONNECTION,
        TE,
        TRANSFER_ENCODING,
        ENCODING,
        UPGRADE,
        TARGET_METHOD,
        TARGET_PATH,
        TARGET_SCHEME,
        TARGET_AUTHORITY
    ]

    private static let HTTP_2_SKIPPED_RESPONSE_HEADERS: Array<String> = [
        CONNECTION,
        HOST,
        KEEP_ALIVE,
        PROXY_CONNECTION,
        TE,
        TRANSFER_ENCODING,
        ENCODING,
        UPGRADE
    ]

    private let realConnection: Connection
    private var canceled = false
    private var stream: Option<H2Stream> = Option<H2Stream>.None
    private let http2Connection: H2Connection
    private var client: HttpClient
    private let protocol: Protocol

    public init(client: HttpClient, realConnection: Connection, http2Connection: H2Connection) {
        this.client = client
        this.realConnection = realConnection
        this.http2Connection = http2Connection
        if (client.getProtocols().contains("h2_prior_knowledge")) {
            protocol = H2_PRIOR_KNOWLEDGE
        } else {
            protocol = HTTP_2
        }
    }

    public func readResponseHeaders(expectContinue: Bool): Option<ResponseBuilder> {
        let headers = stream.getOrThrow().takeHeaders()
        let res: ResponseBuilder = readHttp2HeadersList(headers, protocol)

        if (expectContinue && res.codeBuild == HttpStatusCode.STATUS_CONTINUE) {
            return Option<ResponseBuilder>.None
        }
        return res
    }

    public static func readHttp2HeadersList(headerBlock: Header, protocol: Protocol) {
        let res = Response.builder().protocol(protocol)
        let header: Header = Header()
        var hasStatus: Bool = false
        for ((k, v) in headerBlock) {
            if (k == RESPONSE_STATUS) {
                let statusLine = StatusLine.parse("HTTP/1.1 " + v[0])
                res.code(statusLine.code).message(statusLine.message)
                hasStatus = true
            } else if (!HTTP_2_SKIPPED_RESPONSE_HEADERS.contains(k)) {
                header.add(k, v[0])
            }
        }
        if (!hasStatus) {
            throw ProtocolException("Expected ':status' header not present")
        }
        res.header(header)
        return res
    }

    public func getTrailers(): Header {
        return stream.getOrThrow().getTrailers()
    }

    public func reportedContentLength(response: Response): Int64 {
        return contentLength(response)
    }

    public func openResponseBodySource(_: Response): Source {
        return stream.getOrThrow().getSource()
    }

    public func getConnection(): Connection {
        return realConnection
    }

    public func cancel(): Unit {
        canceled = true
        if (has(stream)) {
            stream.getOrThrow().closeLater(ErrorCode.CANCEL)
        }
    }

    public func createRequestBody(_: Request): Sink {
        return stream.getOrThrow().getSink()
    }

    public func writeRequestHeaders(request: Request): Unit {
        if (has(stream)) {
            return
        }

        stream = http2Connection.newStream(request)

        if (canceled) {
            stream.getOrThrow().closeLater(ErrorCode.CANCEL)
            throw HttpException("Canceled")
        }
    }

    static func http2HeadersList(request: Request): ArrayList<HeaderField> {
        let result: ArrayList<HeaderField> = ArrayList<HeaderField>()
        result.add(HeaderField(TARGET_METHOD, request.getMethod()))
        let u = request.getUrl()
        let rawPath = if (u.rawPath.size > 0) {
            u.rawPath
        } else {
            "/"
        }
        let rawQuery = match (u.rawQuery) {
            case Some(v) => "?${v}"
            case _ => ""
        }
        result.add(HeaderField(TARGET_PATH, "${rawPath}${rawQuery}"))
        let host = request.getHeader("Host", "")

        if (host != "") {
            result.add(HeaderField(TARGET_AUTHORITY, host))
        }
        result.add(HeaderField(TARGET_SCHEME, request.getUrl().scheme))
        for ((k, v) in request.getHeaders()) {
            let name = k.toLower()
            if (!HTTP_2_SKIPPED_REQUEST_HEADERS.contains(name) || (k == TE && v[0] == "trailers")) {
                result.add(HeaderField(name, v[0]))
            }
        }
        return result
    }

    public func flushRequest() {
        http2Connection.flush()
    }

    public func finishRequest(): Unit {
        stream.getOrThrow().getSink().close()
    }
}