/*
 * 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.
 */

/**
 * @file
 *
 * This is a library for Encoder class.
 */

package stdx.net.http

import std.collection.HashSet
import stdx.log.*

/**
 * HPACK Encoder
 * A encoder encode HTTP header list to HPACK-encoded data frame.
 */
class Encoder {
    private let headerTable: HeaderTable
    /**
     * The value of Server.headerTableSize.
     * The value will be used when receiving the SETTINGS_HEADER_TABLE_SIZE from peer.
     * The new maximum table size will be `min(headerTableSizeLimit, SETTINGS_HEADER_TABLE_SIZE)`.
     * The new maximum table size chosen by encoder will be encoded at the beginning of the first HeaderBlock.
     */
    private var headerTableSizeLimit: Int64 = 4096

    /**
     * Sensitive fields will set by user from decoder.
     */
    private let sensitiveFields = HashSet<String>()

    /**
     * For encoder, this value will set by peer by using the SETTINGS_MAX_HEADER_LIST_SIZE.
     * The initial value of this setting is unlimited.
     */
    var maxHeaderListSize = -1

    /**
     * New size should be encoded at the begining of the first HeaderBlock.
     */
    private var headerTableSizeChanged = false

    let name: String
    var _logger: Logger

    /**
     * Constructor
     */
    init(name!: String = "unknown", _logger!: Logger = mutexLogger()) {
        headerTable = HeaderTable("${name}.encoder", _logger)
        this._logger = _logger
        this.name = name
    }

    /**
     * Logger
     */
    mut prop logger: Logger {
        get() {
            return _logger
        }
        set(v) {
            _logger = v
            headerTable.logger = v
        }
    }

    /**
     * Encode HTTP header list to HPACK-encoded data frame.
     *
     * @param headerList HTTP header list.
     * @param maxBlockSize max block size for encoded data.
     * @return LinkedList<HeaderBlock> HPACK-encoded data frame split into list based on `maxBlockSize`.
     *
     * @throws HpackException, if encoded size greater than SettingsMaxHeaderListSize .
     */
    func encodeTo(headerList: Iterable<(String, String)>, writer: FieldsWriter): Unit {
        // cjlint-ignore -start !G.OTH.03
        /*
         * Dynamic Table Size Update
         * Dynamic table size update MUST occur at the beginning of the first header block following the change to the dynamic table size.
         * https://www.rfc-editor.org/rfc/rfc7541#section-4.2
         * https://www.rfc-editor.org/rfc/rfc7541#section-6.3
         *
         *    0   1   2   3   4   5   6   7
         *  +---+---+---+---+---+---+---+---+
         *  | 0 | 0 | 1 |   Max size (5+)   |
         *  +---+---------------------------+
         */
        // cjlint-ignore -end
        if (headerTableSizeChanged) {
            encodeIntTo(headerTable.headerTableSize, "001", writer) // set headerTableSize
            headerTableSizeChanged = false
        }

        var totalHeaderListSize: Int64 = 0 // headerSize = name.size + value.size + 32

        for (field in headerList) {
            // check total header list size
            if (maxHeaderListSize != -1) {
                totalHeaderListSize += fieldSize(field) //k.size + v.size + 32
                if (totalHeaderListSize > maxHeaderListSize) {
                    throw HpackException(
                        "Total size:${totalHeaderListSize} out of SettingsMaxHeaderListSize :${maxHeaderListSize}.")
                }
            }

            if (isSensitiveKey(field[0])) {
                // Literal Header Field Never Indexed
                let index = headerTable.indexOf((field[0], ""))
                match (index) {
                    // May match the index entry with out value
                    case EntryIndex(idx) =>
                        encodeIntTo(idx, "0001", writer) // process as name index
                        if (logger.enabled(LogLevel.TRACE)) {
                            httpLogTrace(logger,
                                "[${this.name}.Encoder#encode] sensitive header:(${field[0]}: ${field[1]}), EntryIndex(${idx})"
                            )
                        }
                    // cjlint-ignore -start !G.OTH.03
                    /*
                     * Literal Header Field Never Indexed
                     * https://www.rfc-editor.org/rfc/rfc7541#section-6.2.3
                     *
                     *    0   1   2   3   4   5   6   7
                     *  +---+---+---+---+---+---+---+---+
                     *  | 0 | 0 | 0 | 1 |  Index (4+)   |
                     *  +---+---+-----------------------+
                     *  | H |     Value Length (7+)     |
                     *  +---+---------------------------+
                     *  | Value String (Length octets)  |
                     *  +-------------------------------+
                     */
                    // cjlint-ignore -end
                    case NameIndex(idx) =>
                        encodeIntTo(idx, "0001", writer) // encode name index
                        if (logger.enabled(LogLevel.TRACE)) {
                            httpLogTrace(logger,
                                "[${this.name}.Encoder#encode] sensitive header:(${field[0]}: ${field[1]}), NameIndex(${idx})"
                            )
                        }
                    // cjlint-ignore -start !G.OTH.03
                    /*
                     * Literal Header Field Never Indexed
                     * https://www.rfc-editor.org/rfc/rfc7541#section-6.2.3
                     *
                     *    0   1   2   3   4   5   6   7
                     *  +---+---+---+---+---+---+---+---+
                     *  | 0 | 0 | 0 | 1 |       0       |
                     *  +---+---+-----------------------+
                     *  | H |     Name Length (7+)      |
                     *  +---+---------------------------+
                     *  |  Name String (Length octets)  |
                     *  +---+---------------------------+
                     *  | H |     Value Length (7+)     |
                     *  +---+---------------------------+
                     *  | Value String (Length octets)  |
                     *  +-------------------------------+
                     */
                    // cjlint-ignore -end
                    case NoneIndex =>
                        0x10 |> writer.write
                        encodeStringTo(field[0], writer)
                        if (logger.enabled(LogLevel.TRACE)) {
                            httpLogTrace(logger,
                                "[${this.name}.Encoder#encode] sensitive header:(${field[0]}: ${field[1]}), NoneIndex")
                        }
                }
                encodeStringTo(field[1], writer)
            } else {
                let index = headerTable.indexOf(field)
                match (index) {
                    // cjlint-ignore -start !G.OTH.03
                    /*
                     * Indexed Header Field Representation
                     * https://www.rfc-editor.org/rfc/rfc7541#section-6.1
                     *
                     *    0   1   2   3   4   5   6   7
                     *  +---+---+---+---+---+---+---+---+
                     *  | 1 |        Index (7+)         |
                     *  +---+---------------------------+
                     */
                    // cjlint-ignore -end
                    case EntryIndex(idx) =>
                        encodeIntTo(idx, "1", writer)
                        if (logger.enabled(LogLevel.TRACE)) {
                            httpLogTrace(logger,
                                "[${this.name}.Encoder#encode] header:(${field[0]}: ${field[1]}), EntryIndex(${idx})")
                        }
                    // cjlint-ignore -start !G.OTH.03
                    /*
                     * Literal Header Field with Incremental Indexing
                     * https://www.rfc-editor.org/rfc/rfc7541#section-6.2.1
                     *
                     *    0   1   2   3   4   5   6   7
                     *  +---+---+---+---+---+---+---+---+
                     *  | 0 | 1 |      Index (6+)       |
                     *  +---+---+-----------------------+
                     *  | H |     Value Length (7+)     |
                     *  +---+---------------------------+
                     *  | Value String (Length octets)  |
                     *  +-------------------------------+
                     */
                    // cjlint-ignore -end
                    case NameIndex(idx) =>
                        encodeIntTo(idx, "01", writer)
                        encodeStringTo(field[1], writer)

                        headerTable.insert(field) // add entry to table
                        if (logger.enabled(LogLevel.TRACE)) {
                            httpLogTrace(logger,
                                "[${this.name}.Encoder#encode] header:(${field[0]}: ${field[1]}), NameIndex(${idx})")
                        }

                    // cjlint-ignore -start !G.OTH.03
                    /*
                     * Literal Header Field with Incremental Indexing
                     * https://www.rfc-editor.org/rfc/rfc7541#section-6.2.1
                     *
                     *    0   1   2   3   4   5   6   7
                     *  +---+---+---+---+---+---+---+---+
                     *  | 0 | 1 |           0           |
                     *  +---+---+-----------------------+
                     *  | H |     Name Length (7+)      |
                     *  +---+---------------------------+
                     *  |  Name String (Length octets)  |
                     *  +---+---------------------------+
                     *  | H |     Value Length (7+)     |
                     *  +---+---------------------------+
                     *  | Value String (Length octets)  |
                     *  +-------------------------------+
                     */
                    // cjlint-ignore -end
                    case NoneIndex =>
                        0x40 |> writer.write
                        encodeStringTo(field[0], writer)
                        encodeStringTo(field[1], writer)

                        headerTable.insert(field) // add entry to table
                        if (logger.enabled(LogLevel.TRACE)) {
                            httpLogTrace(logger,
                                "[${this.name}.Encoder#encode] header:(${field[0]}: ${field[1]}), NoneIndex")
                        }
                }
            }
        }
        writer.finish()
    }

    func setHeaderTableSizeLimit(limit: Int64): Unit {
        this.headerTableSizeLimit = limit
        headerTable.headerTableSize = limit
    }

    func receiveSettingsHeaderTableSize(settings: Int64): Unit {
        let newMaxSize = min(settings, this.headerTableSizeLimit)

        headerTable.headerTableSize = newMaxSize
        headerTableSizeChanged = true
    }

    func setSensitive(headerField: HeaderField) {
        sensitiveFields.add(headerField[0])
    }

    func setInsensitive(headerField: HeaderField) {
        sensitiveFields.remove(headerField[0])
    }

    func isSensitiveKey(key: String): Bool {
        return sensitiveFields.contains(key.toAsciiLower())
    }

    // cjlint-ignore -start !G.OTH.03
    /**
     * String Literal Representation
     * https://www.rfc-editor.org/rfc/rfc7541#section-5.2
     *
     *      0   1   2   3   4   5   6   7
     *  +---+---+---+---+---+---+---+---+
     *  | H |    String Length (7+)     |
     *  +---+---------------------------+
     *  |  String Data (Length octets)  |
     *  +-------------------------------+
     */
    // cjlint-ignore -end
    func encodeStringTo(string: String, writer: FieldsWriter): Unit {
        let rawBytes = unsafe { string.rawData() }
        let huffmanLength = QuickHuffmanEncoder.lengthOf(rawBytes)
        let isHuff = rawBytes.size >= huffmanLength // Should use >=, see rfc7541: `C.6.2.  Second Response`

        let prefix = if (isHuff) {
            "1"
        } else {
            "0"
        }
        let encodedLength = if (isHuff) {
            huffmanLength
        } else {
            rawBytes.size
        }

        encodeIntTo(encodedLength, prefix, writer)

        if (logger.enabled(LogLevel.TRACE)) {
            httpLogTrace(logger, "[${this.name}#encodeString] `${string}` -->")
        }

        if (isHuff) {
            QuickHuffmanEncoder.encodeTo(rawBytes, writer)
            if (logger.enabled(LogLevel.TRACE)) {
                httpLogTrace(logger, "\t+---+---------------------------+")
                httpLogTrace(logger, "\t| H | Value Length: ${encodedLength} |")
                httpLogTrace(logger, "\t+---+---------------------------+")
            }
        } else {
            writer.write(rawBytes)
            if (logger.enabled(LogLevel.TRACE)) {
                httpLogTrace(logger, "\t+---+---------------------------+")
                httpLogTrace(logger, "\t| 0 | Value Length: ${encodedLength} |")
                httpLogTrace(logger, "\t+---+---------------------------+")
                httpLogTrace(logger, "\t| ${rawBytes} |")
                httpLogTrace(logger, "\t+-------------------------------+")
            }
        }
    }

    // cjlint-ignore -start !G.OTH.03
    /**
     * Integer Representation
     * https://www.rfc-editor.org/rfc/rfc7541#section-5.1
     *
     *    0   1   2   3   4   5   6   7
     *  +---+---+---+---+---+---+---+---+
     *  | ? | ? | ? |       Value       |
     *  +---+---+---+-------------------+
     *
     * or
     *
     *    0   1   2   3   4   5   6   7
     *  +---+---+---+---+---+---+---+---+
     *  | ? | ? | ? | 1   1   1   1   1 |
     *  +---+---+---+-------------------+
     *  | 1 |    Value-(2^N-1) LSB      |
     *  +---+---------------------------+
     *                 ...
     *  +---+---------------------------+
     *  | 0 |    Value-(2^N-1) MSB      |
     *  +---+---------------------------+
     */
    // cjlint-ignore -end
    func encodeIntTo(value: Int64, prefix: String, writer: FieldsWriter): Unit {
        let n = 8 - prefix.size
        if (n <= 0) {
            throw HpackException("Invalid prefix: ${prefix}.")
        }

        let maskN = (1 << n) - 1
        if (value < maskN) {
            let b = convert2Byte(prefix) | UInt8(value) // `Byte(value)` not support now
            writer.write(b)
            return
        }

        var b = convert2Byte(prefix) | UInt8(maskN)
        writer.write(b)
        var v = value - maskN

        let mask7 = (1 << 7) - 1
        while (v > mask7) {
            b = 0x80 | UInt8(v & mask7)
            writer.write(b)
            v >>= 7
        }
        writer.write(UInt8(v))
        return
    }

    func convert2Byte(strValue: String): Byte {
        var value: Byte = 0
        let n = strValue.size
        var mask: Byte = 0x80
        for (i in 0..n) {
            if (mask == 0) {
                throw HpackException("Invalid strValue: ${strValue}.")
            }

            match (strValue.get(i)) {
                case Some(48) => // r'0'
                    () // continue
                case Some(49) => // r'1'
                    value |= mask
                case _ => throw HpackException("Invalid strValue: ${strValue}.")
            }
            mask >>= 1
        }
        return value
    }
}