/**
 * Base64 Utility Functions in TypeScript
 */

export {
    encode,
    decode
}



/**
 * Encode an array of bytes as a Uint8Array in base64 notation.
 */
function
encode
    (bytes: Uint8Array)
    : string
{
    // slice the array
    // length of head is a multiple of 3
    // treat the tail as a special case
    let {head, tail, tail_len} = slice3k(bytes);
    let head_str : string = encode_head(head);
    let tail_str : string = encode_tail(tail, tail_len);
    return head_str + tail_str;
}



type slice3k
    = {head     : Uint8Array,
       tail     : Uint8Array,
       tail_len : number};

/**
 * Take a Uint8Array, take the first 3k (k >= 0) bytes, put them in head, and
 * the remaining 0,1, or 2 bytes, put them in tail
 *
 * @internal
 */
function
slice3k
    (bytes: Uint8Array)
    : slice3k
{
    let len      : number = bytes.length;
    // too lazy to look up how to do integer division in js so this will do
    let tail_len : number = len % 3;
    let head_len : number = len - tail_len;
    // for slice:
    // first argument is the 0-index of the start
    // second - first is the length of the slice
    let head : Uint8Array = bytes.slice(0, head_len);
    // empty second argument means go to the end
    let tail : Uint8Array = bytes.slice(head_len);
    return {head     : head,
            tail     : tail,
            tail_len : tail_len};
}



/**
 * Encode a Uint8Array whose length is known to be a multiple of 3
 *
 * @internal
 */
function
encode_head
    (head_bytes: Uint8Array)
    : string
{
    // can assume length of bytes is a multiple of 3
    // start index at 0
    // increment by 3
    let head_bytes_len : number = head_bytes.length;
    let max_idx0       : number = head_bytes_len - 1;

    let head_str_acc : string = '';
    for(let this_3slice_start_idx0  = 0;
            this_3slice_start_idx0 <= max_idx0;
            this_3slice_start_idx0 += 3)
    {
        let this_3slice_bytes : Uint8Array = head_bytes.slice(this_3slice_start_idx0, this_3slice_start_idx0 + 3);
        let this_3slice_str   : string     = encode3(this_3slice_bytes);
        head_str_acc += this_3slice_str;
    }

    return head_str_acc;
}



/**
 * Encode a 3 bytes into base64 notation
 *
 * @internal
 */
function
encode3
    (bytes: Uint8Array)
    : string
{
    let b0 : number = bytes[0];
    let b1 : number = bytes[1];
    let b2 : number = bytes[2];

    // ABCDEFGH 12345678 abcdefgh
    //    b0       b1       b2
    // ABCDEF GH1234 5678ab cdefgh
    //   n0     n1     n2     n3
    let n0 : number = b0 >> 2;
    // b0            = ABCDEFGH
    // 4             = _____1__
    // b0 % 4        = ______GH
    // (b0 % 4) << 4 = __GH____
    // b1            = 12345678
    // b1 >> 4       = ____1234
    // n1            = __GH1234
    let n1 : number = ((b0 % 4) << 4) + (b1 >> 4);
    // b1             = 12345678
    // 16             = ___1____
    // b1 % 16        = ____5678
    // (b1 % 16) << 2 = __5678__
    // b2             = abcdefgh
    // b2 >> 6        = ______ab
    // n2             = __5678ab
    let n2 : number = ((b1 % 16) << 2) + (b2 >> 6);
    // b2 = abcdefgh
    // 64 = _1______
    // n3 = __cdefgh
    let n3 : number = b2 % 64;

    // convert to chars
    let s0 : string = int2char(n0);
    let s1 : string = int2char(n1);
    let s2 : string = int2char(n2);
    let s3 : string = int2char(n3);

    // retrvn
    return s0 + s1 + s2 + s3;
}



/**
 * Encode the final 0, 1, or 2 bytes
 *
 * @internal
 */
function
encode_tail
    (tail_bytes : Uint8Array,
     tail_len   : number)
    : string
{
    switch(tail_len) {
        case 0: return '';
        case 1: return encode1(tail_bytes);
        case 2: return encode2(tail_bytes);
        default:
            throw new Error('encode_tail with tail_len = ' + tail_len);
    }
}



/**
 * Encode a single byte
 *
 * @internal
 */
function
encode1
    (bytes: Uint8Array)
    : string
{
    let b0 : number = bytes[0];
    // n0            = __ABCDEF
    // b0            = ABCDEFGH
    // b0 >> 2       = __ABCDEF
    let n0 : number = b0 >> 2;
    // n1            = __GH____
    // b0            = ABCDEFGH
    //  4            = _____1__
    // b0 % 4        = ______GH
    // (b0 % 4) << 4 = __GH____
    let n1 : number = (b0 % 4) << 4;

    return int2char(n0) + int2char(n1) + '==';
}



/**
 * Encode two bytes
 *
 * @internal
 */
function
encode2
    (bytes: Uint8Array)
    : string
{
    let b0 : number = bytes[0];
    let b1 : number = bytes[1];

    // ABCDEFGH 12345678
    //    b0       b1
    // ABCDEF GH1234 5678__
    //   n0     n1     n2
    let n0 : number = b0 >> 2;
    // b0            = ABCDEFGH
    // 4             = _____1__
    // b0 % 4        = ______GH
    // (b0 % 4) << 4 = __GH____
    // b1            = 12345678
    // b1 >> 4       = ____1234
    // n1            = __GH1234
    let n1 : number = ((b0 % 4) << 4) + (b1 >> 4);
    // b1             = 12345678
    // 16             = ___1____
    // b1 % 16        = ____5678
    // (b1 % 16) << 2 = __5678__
    // n2             = __5678__
    let n2 : number = (b1 % 16) << 2;

    // convert to chars
    let s0 : string = int2char(n0);
    let s1 : string = int2char(n1);
    let s2 : string = int2char(n2);

    // retrvn
    return s0 + s1 + s2 + '=';
}



/**
 * Decode a base64-encoded string
 */
function
decode
    (base64_str : string)
    : Uint8Array
{
    // length of the string is guaranteed to be a multiple of 4
    // if the string is empty, return the empty array
    let len = base64_str.length;
    // this branching contains the implicit assertion that the length is a
    // multiple of 4. If this is not true, the bottom branch is triggered.
    // general case goes first because speeeeeed
    if (   (4 < len)
        && (0 === (len % 4)))
    {
        // split the head and tail
        let tail_start_idx0 : number        = len - 4;
        let head_s          : string        = base64_str.slice(0, tail_start_idx0);
        let tail_s          : string        = base64_str.slice(tail_start_idx0);
        // Using arrays because Uint8Arrays don't have a concat operation
        let head_arr        : Array<number> = decode_head(head_s);
        let tail_arr        : Array<number> = decode_tail(tail_s);
        // silly to put these in variables but this is exactly the type of
        // situation where JS type insanity shows up
        //
        // see: i forgot
        // > [1,2,3] + [4,5,6]
        // '1,2,34,5,6'
        //
        // Originally, I used + like some sort of moron who codes in a sane
        // language
        //
        // seriously what is this language
        //
        // this is some clown behavior
        let total_arr       : Array<number> = head_arr.concat(tail_arr);
        return new Uint8Array(total_arr);
    }
    // special case if the length is exactly 4
    else if (4 === len)
    {
        // it's just a tail
        return new Uint8Array(decode_tail(base64_str));
    }
    // empty string
    else if (0 === len)
    {
        return new Uint8Array([]);
    }
    else
    {
        throw new Error('base64 decode: invalid string length: ' + len);
    }
}



/**
 * Decode a string known to not have any padding
 *
 * @internal
 */
function
decode_head
    (s: string)
    : Array<number>
{
    // go 4 characters at a time
    let max_i0      : number        = s.length - 1;
    let decoded_acc : Array<number> = [];
    for(let i0  = 0;
            i0 <= max_i0;
            i0 += 4)
    {
        let this_slice_s   : string        = s.slice(i0, i0 + 4);
        let this_slice_arr : Array<number> = decode3(this_slice_s);
        // update accumulator
        decoded_acc = decoded_acc.concat(this_slice_arr);
    }
    return decoded_acc;
}



/**
 * Decode 4 characters that correspond to either 3 bytes, 2, bytes, or 1 byte
 *
 * @internal
 */
function
decode_tail
    (s: string)
    : Array<number>
{
    // all that matters right now is the last 2 chars
    // s0, s1, s2, s3
    // 0 based indexing is so annoying
    let s2 = s[2];
    let s3 = s[3];

    // braaaaaaaaaaaaaaaaaench
    // two equals signs means 1 byte
    if (('=' === s3) && ('=' === s2)) {
        return decode1(s);
    }
    // one equals sign means 2 bytes
    else if (('=' === s3)) {
        return decode2(s);
    }
    // 0 equals signs means 3 bytes
    else {
        return decode3(s);
    }
}



/**
 * Decode a 4-character long base64 string corresponding to 3 bytes
 *
 * @internal
 */
function
decode3
    (s: string)
    : Array<number>
{
    // pull out strings
    let s0 : string = s[0];
    let s1 : string = s[1];
    let s2 : string = s[2];
    let s3 : string = s[3];

    // convert to numbers
    let n0 : number = char2int(s0);
    let n1 : number = char2int(s1);
    let n2 : number = char2int(s2);
    let n3 : number = char2int(s3);

    // abcdef gh1234 5678ab cdefgh
    //   n0     n1    n2      n3
    // abcdefgh 12345678 abcdefgh
    //   b0        b1       b2

    // n0      = __abcdef
    // n1      = __gh1234
    // n0 << 2 = abcdef__
    // n1 >> 4 = ______gh
    // b0      = abcdefgh
    let b0 : number = (n0 << 2) + (n1 >> 4);
    // n1             = __gh1234
    // 16             = ___1____
    // n1 % 16        = ____1234
    // (n1 % 16) << 4 = 1234____
    // n2             = __5678ab
    // n2 >> 2        = ____5678
    // b1             = 12345678
    let b1 : number = ((n1 % 16) << 4) + (n2 >> 2);
    // n2             = __5678ab
    // 4              = _____1__
    // n2 % 4         = ______ab
    // (n2 % 4) << 6  = ab______
    // n3             = __cdefgh
    let b2 : number = ((n2 % 4) << 6) + n3;

    return [b0, b1, b2];
}



/**
 * Decode a 4-character long base64 string corresponding to 2 bytes
 *
 * @internal
 */
function
decode2
    (s: string)
    : Array<number>
{
    // xyz=
    // pull out strings
    let s0 : string = s[0];
    let s1 : string = s[1];
    let s2 : string = s[2];

    // convert to numbers
    let n0 : number = char2int(s0);
    let n1 : number = char2int(s1);
    let n2 : number = char2int(s2);

    // abcdef gh1234 5678__
    //   n0     n1    n2
    // abcdefgh 12345678
    //   b0        b1

    // n0      = __abcdef
    // n1      = __gh1234
    // n0 << 2 = abcdef__
    // n1 >> 4 = ______gh
    // b0      = abcdefgh
    let b0 : number = (n0 << 2) + (n1 >> 4);
    // n1             = __gh1234
    // 16             = ___1____
    // n1 % 16        = ____1234
    // (n1 % 16) << 4 = 1234____
    // n2             = __5678__
    // n2 >> 2        = ____5678
    // b1             = 12345678
    let b1 : number = ((n1 % 16) << 4) + (n2 >> 2);

    return [b0, b1];
}



/**
 * Decode a 4-character long base64 string corresponding to 2 bytes
 *
 * @internal
 */
function
decode1
    (s: string)
    : Array<number>
{
    // xy==
    // pull out strings
    let s0 : string = s[0];
    let s1 : string = s[1];

    // convert to numbers
    let n0 : number = char2int(s0);
    let n1 : number = char2int(s1);

    // abcdef gh____
    //   n0     n1
    // abcdefgh
    //   b0

    // n0      = __abcdef
    // n1      = __gh____
    // n0 << 2 = abcdef__
    // n1 >> 4 = ______gh
    // b0      = abcdefgh
    let b0 : number = (n0 << 2) + (n1 >> 4);

    return [b0];
}




// FIXME: these tables would *probably* be faster if they were made into objects

/**
 * Conversion table for base64 encode
 *
 * @internal
 */
function
int2char
    (n: number)
    : string
{
    switch(n) {
        case  0: return 'A';
        case  1: return 'B';
        case  2: return 'C';
        case  3: return 'D';
        case  4: return 'E';
        case  5: return 'F';
        case  6: return 'G';
        case  7: return 'H';
        case  8: return 'I';
        case  9: return 'J';
        case 10: return 'K';
        case 11: return 'L';
        case 12: return 'M';
        case 13: return 'N';
        case 14: return 'O';
        case 15: return 'P';
        case 16: return 'Q';
        case 17: return 'R';
        case 18: return 'S';
        case 19: return 'T';
        case 20: return 'U';
        case 21: return 'V';
        case 22: return 'W';
        case 23: return 'X';
        case 24: return 'Y';
        case 25: return 'Z';
        case 26: return 'a';
        case 27: return 'b';
        case 28: return 'c';
        case 29: return 'd';
        case 30: return 'e';
        case 31: return 'f';
        case 32: return 'g';
        case 33: return 'h';
        case 34: return 'i';
        case 35: return 'j';
        case 36: return 'k';
        case 37: return 'l';
        case 38: return 'm';
        case 39: return 'n';
        case 40: return 'o';
        case 41: return 'p';
        case 42: return 'q';
        case 43: return 'r';
        case 44: return 's';
        case 45: return 't';
        case 46: return 'u';
        case 47: return 'v';
        case 48: return 'w';
        case 49: return 'x';
        case 50: return 'y';
        case 51: return 'z';
        case 52: return '0';
        case 53: return '1';
        case 54: return '2';
        case 55: return '3';
        case 56: return '4';
        case 57: return '5';
        case 58: return '6';
        case 59: return '7';
        case 60: return '8';
        case 61: return '9';
        case 62: return '+';
        case 63: return '/';
        default:  throw new Error("invalid base64 encode byte: " + n);
    }
}



/**
 * Conversion table for base64 decode
 *
 * @internal
 */
function
char2int
    (s: string)
    : number
{
    switch(s) {
        case 'A': return  0;
        case 'B': return  1;
        case 'C': return  2;
        case 'D': return  3;
        case 'E': return  4;
        case 'F': return  5;
        case 'G': return  6;
        case 'H': return  7;
        case 'I': return  8;
        case 'J': return  9;
        case 'K': return 10;
        case 'L': return 11;
        case 'M': return 12;
        case 'N': return 13;
        case 'O': return 14;
        case 'P': return 15;
        case 'Q': return 16;
        case 'R': return 17;
        case 'S': return 18;
        case 'T': return 19;
        case 'U': return 20;
        case 'V': return 21;
        case 'W': return 22;
        case 'X': return 23;
        case 'Y': return 24;
        case 'Z': return 25;
        case 'a': return 26;
        case 'b': return 27;
        case 'c': return 28;
        case 'd': return 29;
        case 'e': return 30;
        case 'f': return 31;
        case 'g': return 32;
        case 'h': return 33;
        case 'i': return 34;
        case 'j': return 35;
        case 'k': return 36;
        case 'l': return 37;
        case 'm': return 38;
        case 'n': return 39;
        case 'o': return 40;
        case 'p': return 41;
        case 'q': return 42;
        case 'r': return 43;
        case 's': return 44;
        case 't': return 45;
        case 'u': return 46;
        case 'v': return 47;
        case 'w': return 48;
        case 'x': return 49;
        case 'y': return 50;
        case 'z': return 51;
        case '0': return 52;
        case '1': return 53;
        case '2': return 54;
        case '3': return 55;
        case '4': return 56;
        case '5': return 57;
        case '6': return 58;
        case '7': return 59;
        case '8': return 60;
        case '9': return 61;
        case '+': return 62;
        case '/': return 63;
        default:  throw new Error("invalid base64 character: " + s);
    }
}
