/*
 * 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 file defines some base64 convert methods.
 *
 */

package stdx.encoding.base64

const SIXTEEN_BIT = 16
const EIGHT_BIT = 8
let BASE64_ENCODE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".toArray()
let ArrayBASE64: Array<Int64> = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54,
    55, 56, 57, 58, 59, 60, 61, -1, -1, -1, 64, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
    18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1]

/*
 * Converting Base64 arrays into UInt8 sets (Base64ToString decoding).
 *
 * @Param data of Array<UInt8>.
 * @return Parameters of Option<Array<UInt8>>.
 *
 * @since 0.17.4
 */
func fromBase64(data: Array<UInt8>): Option<Array<UInt8>> {
    if (!verifyBase64(data)) {
        if (data.size == 0) {
            return Option<Array<UInt8>>.Some(Array<UInt8>())
        }
        return Option<Array<UInt8>>.None
    }
    var padding: Int64 = 0
    if (data[data.size - 1] == b'=') {
        padding++
    }
    if (data[data.size - 2] == b'=') {
        padding++
    }
    var output: Array<UInt8> = Array<UInt8>((data.size / 4 * 3) - padding, repeat: 0)
    var i: Int64 = 0
    var index: Int64 = 0
    while (i < data.size) {
        var chr1: UInt8 = 0
        var chr2: UInt8 = 0
        var chr3: UInt8 = 0

        /*
         * Get values for each group of four base 64 characters
         * If data[i] is not in the range, an error will be reported during transformation
         */
        var num: Int64 = Int64(UInt32(data[i]))
        var c1: Int64 = ArrayBASE64[num]
        var enc1: UInt8 = UInt8(c1)
        i++
        var num2: Int64 = Int64(UInt32(data[i]))
        var c2: Int64 = ArrayBASE64[num2]
        var enc2: UInt8 = UInt8(c2)
        i++
        var num3: Int64 = Int64(UInt32(data[i]))
        var c3: Int64 = ArrayBASE64[num3]
        var enc3: UInt8 = UInt8(c3)
        i++
        var num4: Int64 = Int64(UInt32(data[i]))
        var c4: Int64 = ArrayBASE64[num4]
        var enc4: UInt8 = UInt8(c4)
        i++

        /* Take the first 2 bits of enc1 + enc2 to form 8 bits or 1 byte */
        chr1 = UInt8((enc1 << 2) | (enc2 >> 4))

        /* Take the last 4 bits of enc2 + the first 4 bits of enc3 to form 8 bits or 1 byte */
        chr2 = UInt8(((enc2 & 15) << 4) | (enc3 >> 2))

        /* Take the first 2 bits of enc3 + enc4 to form 8 bits or 1 byte */
        chr3 = UInt8(((enc3 & 3) << 6) | enc4)

        /* Add the byte to the return value if it isn't part of an r'=' character (indicated by 64) */
        if (enc2 != 64) {
            output[index] = chr1
            index++
        }
        if (enc3 != 64) {
            output[index] = chr2
            index++
        }
        if (enc4 != UInt8(64)) {
            output[index] = chr3
            index++
        }
    }

    return Option<Array<UInt8>>.Some(output)
}

func verifyBase64(value: Array<UInt8>): Bool {
    if (value.size == 0 || value.size % 4 != 0) {
        return false
    }

    /* 98% of all non base64 values are invalidated by this time. */
    var index: Int64 = value.size - 1

    /* if there is padding step back */
    if (value[index] == b'=') {
        index--
    }

    /* if there are two padding chars step back a second time */
    if (value[index] == b'=') {
        index--
    }

    /* Back-to-front traversal can reduce boundary checks and improve performance */
    for (i in index..=0 : -1) {
        /* If any of the character is not from the allowed list */
        var num: Int64 = Int64(UInt32(value[i]))
        if (num < 0 || num > 123) {
            return false
        }
        if (num == 61) {
            return false
        }
        var c1: Int64 = ArrayBASE64[num]
        if (c1 == Int64(-1)) {
            return false
        }
    }
    let last = value.size - 1
    if (value[last] == b'=' && value[last - 1] == b'=') {
        let second = ArrayBASE64[Int64(UInt32(value[last - 2]))] // End with "=="
        if ((second & 0x0F) != 0) { // 0b00001111: If lower 4 bits of the 2st character are not 0, then it's not valid
            return false
        }
    } else if (value[last] == b'=') {
        let third = ArrayBASE64[Int64(UInt32(value[last - 1]))] // End with "="
        if ((third & 0x03) != 0) { // 0b00000011: If lower 2 bits of the 3rd character are not 0, then it's not valid
            return false
        }
    }
    return true
}

// cjlint-ignore -start !G.OTH.02
/*
 * Convert the UInt8 set to a character string (Base64ToString encoding).
 * In step 1, the ASCII values of "M", "a", and "n" are 77, 97, and 110 respectively
 * and the corresponding binary values are 01001101, 011000001,
 * and 01101110. Concatenate them into a 24-bit binary string 01001011010110000101110.
 * Step 2: Divide the 24-bit binary string into four groups with six binary bits in each group: 010011, 010110, 000101, and 101110.
 * Step 3: Add two 00s before each group to expand the group into 32 binary bits,
 * that is, four bytes: 00010011, 00010110, 00000101, and 00101110. Their decimal values are 19, 22, 5, and 46.
 * Step 4: According to the preceding table, obtain the Base64 codes corresponding to each value, that is, T, W, F, and u.
 *
 * @Param data of Array<UInt8>
 * @return Parameters of String
 *
 * @since 0.17.4
 */
// cjlint-ignore -end
func toBase64(data: Array<UInt8>): String {
    if (data.size <= 0) {
        return ""
    }
    var lengthDataBits: Int64 = data.size * 8

    /* Whether the length of the encrypted string exceeds 24 */
    var fewerThan24bits: Int64 = lengthDataBits % 24
    var numberTriplets: Int64 = lengthDataBits / 24

    /* Calculate the total number of characters after string encryption */
    var number: Int64 = numberTriplets
    if (fewerThan24bits != 0) {
        number = numberTriplets + 1
    }

    /* Used to save the results */
    var toBase64Text: Array<Byte> = Array<Byte>(number * 4, repeat: 0)
    var index: Int64 = 0
    var order: Int64 = 0
    for (_ in 0..numberTriplets) {
        var s1: UInt8 = data[index]
        index++
        var s2: UInt8 = data[index]
        index++
        var s3: UInt8 = data[index]
        index++

        /* The first 6 digits */
        toBase64Text[order] = BASE64_ENCODE_CHARS[Int64((s1 & 0xFC) >> 2)]
        order++

        /* The second 6 digits */
        toBase64Text[order] = BASE64_ENCODE_CHARS[Int64(((s1 & 0x03) << 4) + ((s2 & 0xF0) >> 4))]
        order++

        /* The Third  6 digits */
        toBase64Text[order] = BASE64_ENCODE_CHARS[Int64(((s2 & 0x0F) << 2) + ((s3 & 0xC0) >> 6))]
        order++

        /* The fourth 6 digits */
        toBase64Text[order] = BASE64_ENCODE_CHARS[Int64(s3 & 0x3f)]
        order++
    }

    /*
     * In the case of one byte: Add two 0s to the last group of the 8 binary bits of this byte,
     * and add 4 0s to the back. In this way, a two-bit Base64 encoding is obtained
     * Add two "=" signs at the end
     */
    if (fewerThan24bits == EIGHT_BIT) {
        var last: UInt8 = data[index]
        index++
        toBase64Text[order] = BASE64_ENCODE_CHARS[Int64((last & 0xFC) >> 2)]
        order++
        toBase64Text[order] = BASE64_ENCODE_CHARS[Int64((last & 0x03) << 4)]
        order++
        toBase64Text[order] = BASE64_ENCODE_CHARS[64]
        order++
        toBase64Text[order] = BASE64_ENCODE_CHARS[64]
        order++
    }

    /*
     * In the case of two bytes: convert a total of 16 binary bits of these two bytes into three groups.
     * In addition to the two 0s in the front, the last group also needs to be added with two 0s
     * In this way, a three-digit Base64 encoding is obtained, and a "=" sign is added at the end
     */
    if (fewerThan24bits == SIXTEEN_BIT) {
        var s1: UInt8 = data[index]
        index++
        var s2: UInt8 = data[index]
        index++
        toBase64Text[order] = BASE64_ENCODE_CHARS[Int64((s1 & 0xFC) >> 2)]
        order++
        toBase64Text[order] = BASE64_ENCODE_CHARS[Int64(((s1 & 0x03) << 4) + ((s2 & 0xF0) >> 4))]
        order++
        toBase64Text[order] = BASE64_ENCODE_CHARS[Int64((s2 & 0x0f) << 2)]
        order++
        toBase64Text[order] = BASE64_ENCODE_CHARS[64]
        order++
    }
    return unsafe { String.fromUtf8Unchecked(toBase64Text) }
}

/**
 * Provides the Base64 encoding conversion function and the Base64String to ByteArray function.
 * If decoding fails, Option is returned.
 *
 * @param data of String.
 * @return Parameters of Option<Array<UInt8>>
 *
 * @since 0.17.4
 */
public func fromBase64String(data: String): Option<Array<Byte>> {
    return fromBase64(unsafe { data.rawData() })
}

/**
 * Provides the Base64 encoding conversion function and the ByteArray to Base64String function.
 * If the encoding fails, Option is returned.
 *
 * @param data of String.
 * @return Parameters of String.
 *
 * @since 0.17.4
 */
public func toBase64String(data: Array<Byte>): String {
    return toBase64(data)
}