/*
 * This file is part of LibEuFin.
 * Copyright (C) 2024-2025 Taler Systems S.A.

 * LibEuFin is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation; either version 3, or
 * (at your option) any later version.

 * LibEuFin is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
 * Public License for more details.

 * You should have received a copy of the GNU Affero General Public
 * License along with LibEuFin; see the file COPYING.  If not, see
 * <http://www.gnu.org/licenses/>
 */

package tech.libeufin.common

/** Crockford's Base32 implementation */
object Base32Crockford {
    /** Crockford's Base32 alphabet */
    const val ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
    /** Base32 mark to extract 5 bits chunks */
    private const val MASK = 0b11111
    /** Crockford's Base32 inversed alphabet */
    private val INV = intArrayOf(
        0,  1,  2,  3,  4,  5,  6,  7,  8,  9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12,
       13, 14, 15, 16, 17,  1, 18, 19,  1, 20, 21,  0, 22, 23, 24, 25, 26, 27, 27, 28,
       29, 30, 31, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, 17,  1, 18, 19,
        1, 20, 21,  0, 22, 23, 24, 25, 26, 27, 27, 28, 29, 30, 31,
    )

    fun encode(data: ByteArray): String = buildString(encodedSize(data.size)) {
        var buffer = 0
        var bitsLeft = 0

        for (byte in data) {
            // Read input
            buffer = (buffer shl 8) or (byte.toInt() and 0xFF)
            bitsLeft += 8
            // Write symbols
            while (bitsLeft >= 5) {
                append(ALPHABET[(buffer shr (bitsLeft - 5)) and MASK])
                bitsLeft -= 5
            }
        }

        if (bitsLeft > 0) {
            // Write remaining bits
            append(ALPHABET[(buffer shl (5 - bitsLeft)) and MASK])
        }
    }

    fun decode(encoded: String): ByteArray {
        val out = ByteArray(decodedSize(encoded.length))

        var bitsLeft = 0
        var buffer = 0
        var cursor = 0

        for (char in encoded) {
            // Read input
            val index = char - '0'
            require(index in 0..INV.size) { "invalid Base32 character: $char" }
            val decoded = INV[index]
            require(decoded != -1) { "invalid Base32 character: $char" }
            buffer = (buffer shl 5) or decoded
            bitsLeft += 5
            // Write bytes
            if (bitsLeft >= 8) {
                out[cursor++] = (buffer shr (bitsLeft - 8)).toByte()
                bitsLeft -= 8 // decrease of written bits.
            }
        }

        return out
    }

    /**
     * Compute the length of the resulting string when encoding data of the given size
     * in bytes.
     *
     * @param dataSize size of the data to encode in bytes
     * @return size of the string that would result from encoding
     */
    fun encodedSize(dataSize: Int): Int {
        return (dataSize * 8 + 4) / 5
    }

    /**
     * Compute the length of the resulting data in bytes when decoding a (valid) string of the
     * given size.
     *
     * @param stringSize size of the string to decode
     * @return size of the resulting data in bytes
     */
    fun decodedSize(stringSize: Int): Int {
        return (stringSize * 5) / 8
    }
}
 
 