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

/**
 * @file The file declars the xxxx class.
 */

package httpclient4cj

let PREFACE: String = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"

/**
 * The class is H2Connection
 *
 * A connection hosts streams which can send and receive
 * data.
 *
 * @author luoyukai4
 * @since 0.34.3
 */
public class H2Connection {
    protected var socket: SocketClient
    protected var nextStreamID: UInt32 = 1
    protected let streams = ConcurrentHashMap<UInt32, H2Stream>()
    let muLock: Monitor = Monitor()

    let hpackEncode: Encoder = Encoder.newEncoder()

    public prop streamMap: ConcurrentHashMap<UInt32, H2Stream> {
        get() {
            return streams
        }
    }

    static let HTTP_CLIENT_WINDOW_SIZE = 16 * 1024 * 1024
    var httpSettings: Settings = Settings()
    let peerSettings: Settings = Settings()
    let writer: Http2Writer
    let reader: Http2Reader
    var bytesLeftInWriteWindow: Int64
    private var intervalPingsSent = 0
    private var intervalPongsReceived = 0
    private var degradedPingsSent = 0
    private var degradedPongsReceived = 0
    private var awaitPingsSent = 0
    private var awaitPongsReceived = 0
    static let INTERVAL_PING = 1
    static let DEGRADED_PING = 2
    static let AWAIT_PING = 3
    private var shutdown: Bool = false
    let listener: Option<Connection>
    var degradedPongDeadline: Int64 = DateTime.now().toUnixTimeStamp().toMilliseconds()
    var unacknowledgedBytesRead = 0

    public init(
        socket: SocketClient,
        sink: BufferedOutputStream<OutputStream>,
        source: OkBuffer,
        listener!: Option<Connection> = Option<Connection>.None
    ) {
        this.socket = socket
        this.listener = listener
        httpSettings.setting(Settings.INITIAL_WINDOW_SIZE, HTTP_CLIENT_WINDOW_SIZE)
        peerSettings.setting(Settings.INITIAL_WINDOW_SIZE, Settings.DEFAULT_INITIAL_WINDOW_SIZE)
        peerSettings.setting(Settings.MAX_FRAME_SIZE, INITIAL_MAX_FRAME_SIZE)
        bytesLeftInWriteWindow = peerSettings.getInitialWindowSize()
        nextStreamID += 2
        reader = Http2Reader(source)
        writer = Http2Writer(sink, hpackEncode)
    }

    /**
     * The Function is start
     *
     * Sends any initial frames and starts reading frames from the remote peer. This should be called
     * after init for all new connections.
     *
     * @return Type of Unit
     * @since 0.34.3
     */
    public func start(): Unit {
        writer.connectionPreface()
        writer.settings(httpSettings)
        let windowSize = httpSettings.getInitialWindowSize()
        if (httpSettings.getInitialWindowSize() != Settings.DEFAULT_INITIAL_WINDOW_SIZE) {
            writer.windowUpdate(0, windowSize - Settings.DEFAULT_INITIAL_WINDOW_SIZE)
        }
        spawn {
            var connectionErrorCode: ErrorCode = INTERNAL_ERROR
            var streamErrorCode: ErrorCode = INTERNAL_ERROR
            var errorException: String = ""
            try {
                reader.readConnectionPreface(this)
                while (reader.nextFrame(false, this)) {}
                connectionErrorCode = ErrorCode.NO_ERROR
                streamErrorCode = ErrorCode.CANCEL
            } catch (e: HttpException) {
                errorException = e.message
                connectionErrorCode = ErrorCode.PROTOCOL_ERROR
                streamErrorCode = ErrorCode.PROTOCOL_ERROR
            } finally {
                close(connectionErrorCode, streamErrorCode, errorException)
                reader.source.close()
            }
        }
    }

    /**
     * The Function is openStreamCount
     *
     * Returns the number of open streams on this connection
     *
     * @return Type of Int64
     * @since 0.34.3
     */
    public func openStreamCount(): Int64 {
        synchronized(muLock) {
            return streams.size
        }
    }

    /**
     * Closes this connection. This cancels all open streams
     *
     * @since 0.34.3
     */
    public func close() {
        close(ErrorCode.NO_ERROR, ErrorCode.CANCEL, "")
    }

    func close(connectionCode: ErrorCode, streamCode: ErrorCode, cause: String) {
        try {
            shutdownConn(connectionCode)
        } catch (ignored: Exception) {}

        synchronized(muLock) {
            let arr = streamMap |> collectArray
            for (item in arr) {
                let stream = item[1]
                try {
                    stream.close(streamCode, cause)
                } catch (ignored: SocketException) {}
            }
        }

        try {
            writer.close()
        } catch (ignored: SocketException) {}

        try {
            socket.close()
        } catch (ignored: SocketException) {}
    }

    func removeStream(streamId: UInt32): Option<H2Stream> {
        let stream = streamMap.remove(streamId)
        muLock.lock()
        muLock.notifyAll()
        muLock.unlock()
        return stream
    }

    func writeHeaders(streamId: UInt32, outFinished: Bool, alternating: ArrayList<HeaderField>) {
        writer.headers(outFinished, streamId, alternating)
    }

    /**
     * The Function is writeData
     *
     * Write Data frame
     * Writes are subject to the write window of the stream and the connection. Until there is a
     * window sufficient to send
     *
     * @param streamId of UInt32
     * @param outFinished of Bool
     * @param buffer of ByteBuffer
     * @param length of Int64
     * @since 0.34.3
     */
    public func writeData(streamId: UInt32, outFinished: Bool, buffer: ByteBuffer, length: Int64) {
        var byteCount = length
        if (byteCount == 0) {
            // Empty data frames are not flow-controlled.
            writer.data(outFinished, streamId, buffer, 0)
            return
        }

        while (byteCount > 0) {
            var toWrite: Int64
            synchronized(muLock) {
                while (bytesLeftInWriteWindow <= 0) {
                    if (!streamMap.contains(streamId)) {
                        throw HttpException("stream closed")
                    }
                    muLock.wait()
                }

                toWrite = min(byteCount, bytesLeftInWriteWindow)
                toWrite = min(toWrite, writer.maxDataLength())
                bytesLeftInWriteWindow -= toWrite
            }
            byteCount -= toWrite
            writer.data(outFinished && byteCount == 0, streamId, buffer, toWrite)
        }
    }

    func headers(inFinished: Bool, streamId: UInt32, _: Int64, headerBlock: Array<HeaderField>) {
        synchronized(muLock) {
            let stream = streamMap[streamId]
            stream.receiveHeaders(toHeaders(headerBlock), inFinished)
        }
    }

    public func setSettings(settings: Settings) {
        synchronized(writer.mutex) {
            synchronized(muLock) {
                if (shutdown) {
                    throw ConnectionShutdownException("connection shutdown")
                }
                httpSettings.merge(settings)
            }
            writer.settings(settings)
        }
    }

    func data(inFinished: Bool, streamId: UInt32, source: OkBuffer, length: Int64) {
        let dataStream = streamMap[streamId]
        dataStream.receiveData(source, length)
        if (inFinished) {
            dataStream.receiveHeaders(Header(), true)
        }
    }

    func toHeaders(headerBlock: Array<HeaderField>): Header {
        let header: Header = Header()
        for (i in 0..headerBlock.size) {
            header.add(headerBlock[i].name, headerBlock[i].value.trim())
        }
        return header
    }

    func writeSynReset(streamId: UInt32, statusCode: ErrorCode) {
        writer.rstStream(streamId, statusCode)
    }

    func writeSynResetLater(streamId: UInt32, statusCode: ErrorCode) {
        spawn {
            try {
                writer.rstStream(streamId, statusCode)
            } catch (e: HttpException | SocketException) {
                close(ErrorCode.PROTOCOL_ERROR, ErrorCode.PROTOCOL_ERROR, e.message)
            }
        }
    }

    func updateConnectionFlowControl(read: Int64) {
        unacknowledgedBytesRead += read

        if (unacknowledgedBytesRead >= httpSettings.getInitialWindowSize() / 2) {
            writeWindowUpdateLater(0, unacknowledgedBytesRead)
            unacknowledgedBytesRead = 0
        }
    }

    func writeWindowUpdateLater(streamId: UInt32, unacknowledgedBytesRead: Int64) {
        spawn {
            try {
                writer.windowUpdate(streamId, unacknowledgedBytesRead)
            } catch (e: HttpException | SocketException) {
                close(ErrorCode.PROTOCOL_ERROR, ErrorCode.PROTOCOL_ERROR, e.message)
            }
        }
    }

    public func isHealthy() {
        if (shutdown) {
            return false
        }

        if (degradedPongsReceived < degradedPingsSent && DateTime.now().toUnixTimeStamp().toMilliseconds() <=
            degradedPongDeadline) {
            return false
        }

        return true
    }

    /**
     * The Function is shutdownConn
     *
     * @param statusCode of ErrorCode
     * @since 0.34.3
     */
    public func shutdownConn(statusCode: ErrorCode) {
        synchronized(writer.mutex) {
            synchronized(muLock) {
                if (shutdown) {
                    return
                }
            }
            shutdown = true
        }
        writer.goAway(0, statusCode, Array<Byte>(1, repeat: 0))
    }

    func windowUpdate(streamId: UInt32, windowSizeIncrement: Int64) {
        if (streamId == 0) {
            synchronized(muLock) {
                bytesLeftInWriteWindow += windowSizeIncrement
                muLock.notifyAll()
            }
        } else {
            let stream = streamMap[streamId]

            synchronized(stream.mutex) {
                stream.addBytesToWriteWindow(windowSizeIncrement)
            }
        }
    }

    func goAway(lastGoodStreamId: Int64, _: ErrorCode) {
        synchronized(muLock) {
            shutdown = true
            let arr = streamMap |> collectArray
            for (item in arr) {
                let stream = item[1]
                if (stream.id > UInt32(lastGoodStreamId)) {
                    stream.receiveRstStream(REFUSED_STREAM)
                    removeStream(stream.id)
                    muLock.notifyAll()
                    streamMap.remove(item[0])
                }
            }
        }
    }

    func rstStream(streamId: UInt32, errorCode: ErrorCode) {
        match (removeStream(streamId)) {
            case Some(stream) => stream.receiveRstStream(errorCode)
            case None => ()
        }
    }

    func ping(reply: Bool, payload: Int64, _: Int64) {
        if (reply) {
            degradedPongDeadline = (DateTime.now() + Duration.second).toUnixTimeStamp().toMilliseconds()
            if (payload == DEGRADED_PING) {
                degradedPongsReceived++
            } else if (payload == INTERVAL_PING) {
                intervalPongsReceived++
            } else if (payload == AWAIT_PING) {
                awaitPongsReceived++
                muLock.notifyAll()
            }
        }
    }

    func writePing(reply: Bool, payload1: Int64, payload2: Int64) {
        writer.ping(reply, payload1, payload2)
    }

    func settings(settings: Settings) {
        spawn {
            applyAndAckSettings(settings)
        }
    }

    func applyAndAckSettings(settings: Settings) {
        var delta = 0
        synchronized(writer.mutex) {
            synchronized(muLock) {
                let priorWriteWindowSize = peerSettings.getInitialWindowSize()
                peerSettings.merge(settings)
                let peerInitialWindowSize = peerSettings.getInitialWindowSize()
                if (peerInitialWindowSize != -1 && peerInitialWindowSize != priorWriteWindowSize) {
                    delta = peerInitialWindowSize - priorWriteWindowSize
                }
            }

            try {
                writer.applyAndAckSettings(peerSettings)
            } catch (e: HttpException | SocketException) {
                close(ErrorCode.PROTOCOL_ERROR, ErrorCode.PROTOCOL_ERROR, e.message)
            }
        }
        synchronized(writer.mutex) {
            for ((_, v) in streamMap) {
                synchronized(v.mutex) {
                    v.addBytesToWriteWindow(delta)
                }
            }
        }

        spawn {
            match (listener) {
                case Some(conn) => conn.onSettings(this)
                case None => ()
            }
        }
    }

    /**
     * The Function is newStream
     *
     * @param requestHeaders of ArrayList<HeaderField>
     * @param out of Bool
     *
     * @return Type of H2Stream
     * @since 0.34.3
     */
    public func newStream(request: Request): H2Stream {
        let h2Stream: H2Stream
        let outFinished: Bool = !has(request.getBody())
        var flushHeaders: Bool = false
        var streamId: UInt32 = 0

        synchronized(muLock) {
            if (Int32(nextStreamID) > Int32.Max / 2) {
                shutdownConn(REFUSED_STREAM)
            }
            if (shutdown) {
                throw ConnectionShutdownException("connection shutdown")
            }
            streamId = nextStreamID
            nextStreamID += 2
            flushHeaders = outFinished || bytesLeftInWriteWindow == 0
            h2Stream = H2Stream(streamId, this, outFinished)
            h2Stream.start()
            if (h2Stream.isOpen()) {
                streamMap.add(streamId, h2Stream)
            }
            let requestHeaders: ArrayList<HeaderField> = Http2ExchangeCodec.http2HeadersList(request)
            writer.headers(outFinished, streamId, requestHeaders)
        }
        if (flushHeaders) {
            writer.flush()
        }
        return h2Stream
    }

    /**
     * The Function is getMaxConcurrentStreams
     *
     * @since 0.34.3
     */
    public func getMaxConcurrentStreams() {
        return peerSettings.getMaxConcurrentStreams(100)
    }

    /**
     * Flush the writer
     *
     * @return Type of Unit
     * @since 0.34.3
     */
    public func flush(): Unit {
        writer.flush()
    }
}