package Base64

import Bitwise
import StringUtils
import ByteBuffer
import ChunkedString
import Execute
import ErrorHandling

/*  Base64 package provides encoding and decoding functions for the common base64 format
    that allows serializing and deserializing of any data by writing integers into the Encoder
    and reading integers from the Decoder. You'll have to implement writing/reading functions yourself.

    Encode Data using a builder pattern:
    > let encoder = new Base64Encoder()
    > encoder.writeByte(25)
    > encoder.writeInt(100500)
    > let data = encoder.intoData() // the encoder is consumed, no need to destroy it

    Decode Data using a builder pattern:
    > let decoder = new Base64Decoder()
    > while data.hasChunk()
    >   decoder.append(data.readChunk())
    > let bytes = decoder.intoData() // the decoder is consumed, no need to destroy it
    > let byte = bytes.readByte()
    > let number = bytes.readInt()

    This package also provides convenient methods for ChunkedStrings and ByteBuffers.

    Encode Data:
    > let data = bytes.encodeBase64() // `data` is of type `ChunkedString`, `bytes` is of type `ByteBuffer`

    Decode Data:
    > let bytes = data.decodeBase64() // `data` is of type `ChunkedString`, `bytes` is of type `ByteBuffer`
*/

/**
    Specifies how many characters to encode per a single `execute()` call.
    This value has been tuned to work under all optimization settings and
    with stacktraces included.
**/
@configurable public constant ENCODES_PER_ROUND = 1000
/**
    Specifies how many chunks to decode per a single `execute()` call.
    This value has been tuned to work under all optimization settings and
    with stacktraces included.
**/
@configurable public constant DECODES_PER_ROUND = 25

// RFC 4648 compliant Base64 charmap.
constant CHARMAP = [
    "A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P",
    "Q","R","S","T","U","V","W","X","Y","Z","a","b","c","d","e","f",
    "g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v",
    "w","x","y","z","0","1","2","3","4","5","6","7","8","9","+","/"
]

// RFC 4648 compliant Base64 reverse charmap.
constant REVERSE_CHARMAP = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 63, 52, 53, 54,
    55, 56, 57, 58, 59, 60, 61, 0, 0, 0, 0, 0, 0, 0, 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, 0, 0, 0, 0, 0, 0, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]

public class Base64Encoder
    private ChunkedString chunkedString

    // We append only one char each time, and strings in WC3 get fully copied on each concatenation.
    // To optimize it and lower the times of copying quite big chunks, append by smaller chunks.
    // These chunks are stored in the `stringBuffer` before appended.
    private var stringBuffer = ""
    private var chars = 0
    private static constant MAX_CHARS = 32

    private var buffer = 0
    private var bytes = 0
    private static constant MAX_BYTES = 3

    construct()
        this.chunkedString = new ChunkedString()

    construct(int maxChunkLength)
        this.chunkedString = new ChunkedString(maxChunkLength)

    ondestroy
        if chunkedString != null // The string is null after calling `intoData()`.
            destroy chunkedString

    // We're not using `executeWhile()` here to avoid additional overhead in this tight loop.
    private function writeData(ByteBuffer data)
        execute() ->
            var i = 0
            while data.hasByte() and i < ENCODES_PER_ROUND
                writeByteUnsafe(data.readByte())
                i++

            if data.hasByte()
                writeData(data)

    private function flushStringBuffer()
        chunkedString.append(stringBuffer)
        stringBuffer = ""
        chars = 0

    private function append(string char)
        stringBuffer += char
        chars++
        if chars == MAX_CHARS
            flushStringBuffer()

    private function encode(int byte, int count)
        var remaining = byte

        for i = 0 to count
            let c = remaining.bitAnd(compiletime("11111100 00000000 00000000".fromBitString())).shiftr(18)
            append(CHARMAP[c])
            remaining = remaining.shiftl(6)

    /** Writes an unsigned byte to be serialized into Base64. 
    You must be sure that the provided integer is in the range [0, 255]. */
    private function writeByteUnsafe(int n)
        buffer = buffer.shiftl(8) + n
        bytes++
        if bytes == MAX_BYTES
            encode(buffer, 3)
            bytes = 0

    /** Writes an unsigned short to be serialized into Base64.
    You must be sure that the provided integer is in the range [0, 65535]. */
    private function writeShortUnsafe(int n)
        writeByteUnsafe(n.bitAnd(compiletime("11111111".fromBitString())))
        writeByteUnsafe(n.shiftr(8))

    /** Writes an unsigned byte to be serialized into Base64. */
    function writeByte(int n)
        writeByteUnsafe(n.bitAnd(compiletime("11111111".fromBitString())))

    /** Writes an unsigned short to be serialized into Base64. */
    function writeShort(int n)
        writeShortUnsafe(n.bitAnd(compiletime("11111111 11111111".fromBitString())))

    /** Writes a signed integer to be serialized into Base64. */
    function writeInt(int n)
        writeShortUnsafe(n.bitAnd(compiletime("11111111 11111111".fromBitString())))
        writeShortUnsafe(n.shiftr(16))

    /** Writes all bytes from the buffer to be serialized into Base64. */
    function write(ByteBuffer data)
        writeData(data)
        data.resetRead()
    
    /** Writes all bytes from the buffer to be serialized into Base64 and destroys the buffer. */
    function consume(ByteBuffer data)
        writeData(data)
        destroy data

    /** Consumes this encoder and returns the encoded data as a ChunkedString. */
    function intoData() returns ChunkedString
        if bytes != 0
            encode(buffer.shiftl(8 * (3 - bytes)), bytes)
            append("=")
            if bytes == 1
                append("=")
        flushStringBuffer()

        let tmp = chunkedString
        chunkedString = null
        destroy this

        return tmp

public class Base64Decoder
    private var byteBuffer = new ByteBuffer()

    private var buffer = ""
    private var bufferLength = 0
    private static constant MAX_CHARS = 4

    private var lastDecoded = ""

    ondestroy
        if byteBuffer != null // The buffer is null after calling `intoData()`.
            destroy byteBuffer

    // We're not using `executeWhile()` here to avoid additional overhead in this tight loop.
    private function appendData(ChunkedString data)
        execute() ->
            var i = 0
            while data.hasChunk() and i < DECODES_PER_ROUND
                append(data.readChunk())
                i++

            if data.hasChunk()
                appendData(data)

    /** Appends a part of the Base64-encoded data. */
    function append(string data)
        let len = data.length()
        if bufferLength + len < MAX_CHARS
            buffer += data
            bufferLength += len
            return
        var i = MAX_CHARS - bufferLength
        writeBytes(buffer + data.substring(0, i))
        while i + MAX_CHARS <= len
            writeBytes(data.substring(i, i + MAX_CHARS))
            i += MAX_CHARS
        buffer = data.substring(i)
        bufferLength = len - i

    /** Appends a part of the Base64-encoded data. */
    function append(ChunkedString data)
        appendData(data)
        data.resetRead()

    /** Appends a part of the Base64-encoded data and destroys it. */
    function consume(ChunkedString data)
        appendData(data)
        destroy data

    @inline private static function decode(string char) returns int
        return REVERSE_CHARMAP[char.toChar().toInt()]

    private static constant DECODE_MASK = compiletime("11111111 11111111 11111111".fromBitString())

    private function writeBytes(string chars)
        lastDecoded = chars

        var data = decode(chars.charAt(0)).shiftl(18) + decode(chars.charAt(1)).shiftl(12) + decode(chars.charAt(2)).shiftl(6) + decode(chars.charAt(3))

        byteBuffer.writeByte(data.bitAnd(DECODE_MASK).shiftr(16))
        data = data.shiftl(8)

        byteBuffer.writeByte(data.bitAnd(DECODE_MASK).shiftr(16))
        data = data.shiftl(8)

        byteBuffer.writeByte(data.bitAnd(DECODE_MASK).shiftr(16))

    /** Consumes this decoder and returns the decoded data as a ByteBuffer. **/
    function intoData() returns ByteBuffer
        if bufferLength > 0
            error("Base64 ERROR: The Base-64 encoded data should have length divisible by 4.")
        byteBuffer.truncate(byteBuffer.size() - lastDecoded.countOccurences("="))

        let tmp = byteBuffer
        byteBuffer = null
        destroy this

        return tmp

/** Encodes the bytes in this buffer to a string according to the Base64 format. */
public function ByteBuffer.encodeBase64() returns ChunkedString
    return new Base64Encoder()..consume(this).intoData()

/** Decodes the bytes encoded into this string according to the Base64 format. */
public function ChunkedString.decodeBase64() returns ByteBuffer
    return new Base64Decoder()..consume(this).intoData()

/** Decodes the bytes encoded into this string according to the Base64 format. */
public function string.decodeBase64() returns ByteBuffer
    return new Base64Decoder()..append(this).intoData()