/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
 * This source file is part of the Cangjie project, licensed under Apache-2.0
 * with Runtime Library Exception.
 *
 * See https://cangjie-lang.cn/pages/LICENSE for license information.
 */

package stdx.net.http

import std.collection.{ArrayList, HashMap}

let H2_EXCLUDE_HEADERS: Array<String> = ["host", "connection", "transfer-encoding", "keep-alive", "upgrade",
    "proxy-connection"]

// connection preface, "".toArray() can't auto escape, must use toArray()
let PREFACE: Array<UInt8> = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".toArray()

// flag types of frame
// bit value 1 means the flag is true
const STREAM_END: UInt8 = 0b0000_0001
const HEADERS_END: UInt8 = 0b0000_0100
const PADDED: UInt8 = 0b0000_1000
const HAS_PRIORITY: UInt8 = 0b0010_0000
const ACK: UInt8 = 0b0000_0001

// All frames begin with a fixed 9-octet header followed by a variable-length frame payload.
// use upper case
const FRAME_HEAD_LEN = 9

// for priority in level queue
const CONTROL_PRIORITY = 1
const MESSAGE_PRIORITY = 0
const MAX_URGENCY = 7
const DEFAULT_URGENCY = 3
let h2UpgradeProtocols: Array<String> = ["HTTP", "TLS", "WebSocket", "websocket", "h2c", "connect-udp", "connect-ip"]

// cjlint-ignore -start !G.OTH.03
// https://www.rfc-editor.org/rfc/rfc9113.html#SETTINGS
// cjlint-ignore -end
enum H2Setting {
    SettingsHeaderTableSize
    | SettingsEnablePush
    | SettingsMaxConcurrentStreams
    | SettingsInitialWindowSize
    | SettingsMaxFrameSize
    | SettingsMaxHeaderListSize
    | SettingsEnableConnectProtocol
    | SettingsNoRfc7540Priorities
    | UnknownSettings

    prop code: UInt16 {
        get() {
            match (this) {
                case SettingsHeaderTableSize => 1
                case SettingsEnablePush => 2
                case SettingsMaxConcurrentStreams => 3
                case SettingsInitialWindowSize => 4
                case SettingsMaxFrameSize => 5
                case SettingsMaxHeaderListSize => 6
                case SettingsEnableConnectProtocol => 8
                case SettingsNoRfc7540Priorities => 9
                case _ => 0
            }
        }
    }
}

func getSettingByCode(code: UInt16): H2Setting {
    match (code) {
        case 1 => SettingsHeaderTableSize
        case 2 => SettingsEnablePush
        case 3 => SettingsMaxConcurrentStreams
        case 4 => SettingsInitialWindowSize
        case 5 => SettingsMaxFrameSize
        case 6 => SettingsMaxHeaderListSize
        case 8 => SettingsEnableConnectProtocol
        case 9 => SettingsNoRfc7540Priorities
        case _ => UnknownSettings
    }
}

func validSetting(setting: H2Setting, value: UInt32) {
    match (setting) {
        case SettingsHeaderTableSize => () // no limit
        case SettingsEnablePush => zeroOrOne(value)
        case SettingsMaxConcurrentStreams => () // no limit
        case SettingsInitialWindowSize =>
            if (value > MAX_WINDOW) {
                throw HttpConnectionException(FlowControlError,
                    "Illegal HTTP/2 setting SettingsInitialWindowSize value:${value}.")
            }
        case SettingsMaxFrameSize =>
            if (value < MIN_FRAME_SIZE || value > MAX_FRAME_SIZE) {
                throw HttpConnectionException(ProtocolError,
                    "Illegal HTTP/2 setting SettingsMaxFrameSize value:${value}.")
            }
        case SettingsMaxHeaderListSize => () // no limit
        case SettingsEnableConnectProtocol => zeroOrOne(value)
        case SettingsNoRfc7540Priorities => zeroOrOne(value)
        case _ => throw HttpException("Unknown HTTP/2 setting.")
    }
}

func validSetting(setting: UInt16, value: UInt32) {
    validSetting(getSettingByCode(setting), value)
}

func zeroOrOne(value: UInt32): Unit {
    if (value != 0 && value != 1) {
        throw HttpConnectionException(ProtocolError, "Illegal HTTP/2 setting value:${value}, should be 0 or 1.")
    }
}

func initializeSettings(): HashMap<UInt16, UInt32> {
    let settings = HashMap<UInt16, UInt32>()
    settings.add(1, 4096)
    settings.add(2, 1)
    settings.add(3, 100) // should NOT be smaller than 100
    settings.add(4, UInt32(2 ** 16 - 1))
    settings.add(5, MIN_FRAME_SIZE)
    settings.add(6, UInt32.Max) // no limit
    settings.add(8, 0) // SettingsEnableConnectProtocol
    settings.add(9, 0) // SettingsNoRfc7540Priorities
    return settings
}

class HttpConnectionException <: Exception {
    let h2Error: H2Error

    init(h2Error: H2Error, message: String) {
        super(message)
        this.h2Error = h2Error
    }

    protected override func getClassName(): String {
        return "HttpConnectionException"
    }
}

class HttpStreamException <: Exception {
    let h2Error: H2Error
    init(h2Error: H2Error, message: String) {
        super(message)
        this.h2Error = h2Error
    }
    protected override func getClassName(): String {
        return "HttpStreamException"
    }
}

// cjlint-ignore -start !G.OTH.03
/*
 * https://www.rfc-editor.org/rfc/rfc9113.html#ErrorCodes
 */
// cjlint-ignore -end
enum H2Error {
    | NoError
    | ProtocolError
    | InternalError
    | FlowControlError
    | SettingsTimeout
    | StreamClosed
    | FrameSizeError
    | RefusedStream
    | Cancel
    | CompressionError
    | ConnectError
    | EnhanceYourCalm
    | InadequateSecurity
    | Http_1_1_Required

    prop code: UInt32 {
        get() {
            match (this) {
                case NoError => 0
                case ProtocolError => 1
                case InternalError => 2
                case FlowControlError => 3
                case SettingsTimeout => 4
                case StreamClosed => 5
                case FrameSizeError => 6
                case RefusedStream => 7
                case Cancel => 8
                case CompressionError => 9
                case ConnectError => 10
                case EnhanceYourCalm => 11
                case InadequateSecurity => 12
                case Http_1_1_Required => 13
            }
        }
    }
}

enum Status <: ToString & Equatable<Status> {
    | Idle
    | ReservedLocal
    | ReservedRemote
    | Open
    | HalfClosedLocal
    | HalfClosedRemote
    | Closed

    public func toString(): String {
        match (this) {
            case Idle => "Idle"
            case ReservedLocal => "ReservedLocal"
            case ReservedRemote => "ReservedRemote"
            case Open => "Open"
            case HalfClosedLocal => "HalfClosedLocal"
            case HalfClosedRemote => "HalfClosedRemote"
            case Closed => "Closed"
        }
    }

    public operator func ==(rhs: Status): Bool {
        match ((this, rhs)) {
            case (Idle, Idle) => return true
            case (ReservedLocal, ReservedLocal) => return true
            case (ReservedRemote, ReservedRemote) => return true
            case (Open, Open) => return true
            case (HalfClosedLocal, HalfClosedLocal) => return true
            case (HalfClosedRemote, HalfClosedRemote) => return true
            case (Closed, Closed) => return true
            case _ => return false
        }
    }

    public operator func !=(rhs: Status): Bool {
        return !(this == rhs)
    }
}

/********************************************** frame util ********************************************/

// stream id is a 31bit number, first bit of UInt32 must be zero
func streamIdOverflow(id: UInt32): Bool {
    return id > MAX_STREAM_ID
}

// normal stream id cannot be 0
func checkStreamId(id: UInt32): Bool {
    return id != 0 && !streamIdOverflow(id)
}

// padding cannot have non zero byte
func checkPad(pad: Array<UInt8>): Bool {
    for (i in pad) {
        if (i != 0) {
            return false
        }
    }
    return true
}

func decodeFields(decoder: Decoder, fields: ArrayList<UInt8>): FieldsList {
    decodeFields(decoder, unsafe { fields.getRawArray()[..fields.size] })
}

func decodeFields(decoder: Decoder, fields: Array<UInt8>): FieldsList {
    let fieldsList: FieldsList
    try {
        fieldsList = decoder.decode(fields)
    } catch (e: HpackException) {
        throw HttpConnectionException(CompressionError, e.message)
    }
    return fieldsList
}

/********************************************** byte util ********************************************/

// convert 4 UIn8 to UInt32
func convertUInt32(a: UInt8, b: UInt8, c: UInt8, d: UInt8): UInt32 {
    return (UInt32(a) << 24) + (UInt32(b) << 16) + (UInt32(c) << 8) + UInt32(d)
}

// convert Array<UInt8> to UInt32, the size must le 4
func readUInt32(src: Array<UInt8>): UInt32 {
    if (src.size > 4 || src.size <= 1) {
        throw IllegalArgumentException("Illegal src.")
    }
    if (src.size == 2) {
        return UInt32(readUInt16(src))
    }
    if (src.size == 3) {
        return convertUInt32(0, src[0], src[1], src[2])
    }
    convertUInt32(src[0], src[1], src[2], src[3])
}

// convert 2 UIn8 to UInt16
func convertUInt16(a: UInt8, b: UInt8): UInt16 {
    return (UInt16(a) << 8) + UInt16(b)
}

// convert Array<UInt8> to UInt16, the size must equals 2
func readUInt16(src: Array<UInt8>): UInt16 {
    if (src.size != 2) {
        throw IllegalArgumentException("Illegal size.")
    }
    convertUInt16(src[0], src[1])
}

func isPushStream(id: UInt32): Bool {
    return id % 2 == 0
}

public class HttpResponsePusher {
    private HttpResponsePusher(let engine: HttpEngineConn2) {}

    /**
     * Return None if get pusher failed.
     *
     * @param ctx the request context.
     * @return Option HttpResponsePusher.
     */
    public static func getPusher(ctx: HttpContext): ?HttpResponsePusher {
        match (ctx.httpConn) {
            case engineConn: HttpEngineConn2 =>
                if (engineConn.enablePush) {
                    return HttpResponsePusher(engineConn)
                }
                return None
            case _ => return None
        }
    }

    /**
     * Push a request to client.
     *
     * @param path the request's path.
     * @param method the request's method.
     * @param header the request's header.
     */
    public func push(path: String, method: String, header: HttpHeaders): Unit {
        engine.writePush(path, method, header)
    }
}

extend HttpResponse {
    /**
     * Obtains the server push response.
     * If None is returned, the server push function is disabled.
     * If ArrayList is empty, no server push response is returned.
     *
     * @return Option<ArrayList<HttpResponse>>
     */
    public func getPush(): Option<ArrayList<HttpResponse>> {
        return match (pushResponses) {
            case Some(pushStreams) =>
                let responses = ArrayList<HttpResponse>(pushStreams.size)
                for (stream in pushStreams) {
                    let response = if (let Some(v) <- (stream as ClientStream)?.constructPushResponse()) {
                        v
                    } else {
                        continue
                    }
                    responses.add(response)
                }
                responses
            case None => None
        }
    }
}

class BytesIOStream {
    private var buffer = Array<Byte>(32, repeat: 0)
    private var idx = 0
    private var len = 0

    prop remainLength: Int64 {
        get() {
            len
        }
    }

    prop remainCapacity: Int64 {
        get() {
            buffer.size - len
        }
    }

    func read(dst: Array<Byte>): Int64 {
        if (dst.size == 0) {
            return 0
        }

        let readLen = if (dst.size < len) {
            dst.size
        } else {
            len
        }

        /**
         *  Copy from cycle buffer to destination buffer.
         *
         *  Case 1:
         *
         *        idx
         *         |
         *      |--012345678---------|
         *      |  <--len-->         |
         *      |       buffer       |
         *
         *  Case 2:
         *
         *                     idx
         *                      |
         *      |5678-----------01234|
         *      |--->           <-len|
         *      |       buffer       |
         */
        match (idx + len <= buffer.size) {
            case true => buffer.copyTo(dst, idx, 0, readLen)
            case false =>
                let len1 = idx + len - buffer.size
                match (readLen <= len1) {
                    case true => buffer.copyTo(dst, idx, 0, readLen)
                    case false =>
                        buffer.copyTo(dst, idx, 0, len1)
                        buffer.copyTo(dst, 0, len1, readLen - len1)
                }
        }

        idx += readLen
        len -= readLen
        return readLen
    }

    func write(data: Array<Byte>): Unit {
        if (len == 0) { // fast reset
            idx = 0
        }

        if (data.size > remainCapacity) {
            grow(remainCapacity + data.size)
        }

        /**
         *  Copy data to cycle buffer.
         *
         *  Case 1:
         *
         *        idx      wBeg          wEnd
         *         |        |             |
         *      |--012345678-------------------|
         *      |  <--len--><--data.size-->    |
         *      |             buffer           |
         *
         *  Case 2:
         *
         *             wEnd   idx      wBeg
         *              |      |        |
         *      |--------------012345678-------|
         *      |.size-->      <--len--><--data|
         *      |             buffer           |
         *
         */
        let wBeg = idx + len
        let wEnd = wBeg + data.size - 1
        match (wEnd < buffer.size) {
            case true => data.copyTo(buffer, 0, wBeg, data.size)
            case false =>
                let len1 = wEnd - buffer.size + 1
                data.copyTo(buffer, 0, wBeg, len1)
                data.copyTo(buffer, len1, 0, data.size - len1)
        }

        len += data.size
    }

    private func grow(minCapacity: Int64): Unit {
        var newCapacity = buffer.size + (buffer.size >> 1)
        if (newCapacity < minCapacity) {
            newCapacity = minCapacity
        }
        let newBuffer = Array<Byte>(newCapacity, repeat: 0)

        len = read(newBuffer)
        idx = 0
        buffer = newBuffer
    }
}

class FieldsWriter {
    var buffer: Array<Byte>
    private var bufferIdx = 0
    var firstFrame = true
    var blockSize = 0
    var streamId: UInt32 = 0
    var streamEnd = false
    var pushId: UInt32 = 0

    FieldsWriter(let conn: BufferedWriter) {
        buffer = Array<Byte>(blockSize, repeat: 0)
        bufferIdx = 0
    }

    func write(input: Byte): Unit {
        if (bufferIdx == blockSize) {
            flush(false)
        }
        buffer[bufferIdx] = input
        bufferIdx++
    }

    func write(input: ArrayList<Byte>): Unit {
        let raw = unsafe { input.getRawArray() }
        var srcIdx = 0
        let srcEnd = input.size

        while (srcIdx < srcEnd) { // visit source data
            if (bufferIdx == blockSize) {
                flush(false)
            }

            let copyLen = min(srcEnd - srcIdx, blockSize - bufferIdx)
            raw.copyTo(buffer, srcIdx, bufferIdx, copyLen)

            srcIdx += copyLen
            bufferIdx += copyLen
        }
    }

    func write(input: Array<Byte>): Unit {
        var srcIdx = 0
        let srcEnd = input.size

        while (srcIdx < srcEnd) { // visit source data
            if (bufferIdx == blockSize) {
                flush(false)
            }

            let copyLen = min(srcEnd - srcIdx, blockSize - bufferIdx)
            input.copyTo(buffer, srcIdx, bufferIdx, copyLen)

            srcIdx += copyLen
            bufferIdx += copyLen
        }
    }

    func flush(headerEnd: Bool): Unit {
        if (firstFrame) {
            if (pushId == 0) { // header
                var flag: UInt8 = 0
                if (streamEnd) {
                    flag |= STREAM_END
                }
                if (headerEnd) {
                    flag |= HEADERS_END
                }
                writeHead(conn, Headers, streamId, flag, UInt32(bufferIdx))
            } else { // push
                var flag: UInt8 = 0
                if (headerEnd) {
                    flag |= HEADERS_END
                }
                writeHead(conn, PushPromise, streamId, flag, UInt32(bufferIdx + 4))
                conn.writeUInt32(pushId)
            }
            if (!headerEnd) {
                firstFrame = false
            }
        } else { // Continuation
            var flag: UInt8 = 0
            if (headerEnd) {
                flag |= HEADERS_END
                firstFrame = true
            }
            writeHead(conn, Continuation, streamId, flag, UInt32(bufferIdx))
        }
        conn.write(buffer[..bufferIdx])
        conn.flush()
        bufferIdx = 0
    }

    func finish(): Unit {
        if (!buffer.isEmpty()) {
            flush(true)
        }
    }
}