import { memory } from './types';

const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';

/** Encode an array of bytes in base64. */
export function base64_encode(data: null | undefined): undefined;
export function base64_encode(data: memory): string;
export function base64_encode(data: memory | null | undefined): string | undefined {
    // Twacked by Will Scullin to handle arrays of 'bytes'

    // http://kevin.vanzonneveld.net
    // +   original by: Tyler Akins (http://rumkin.com)
    // +   improved by: Bayron Guevara
    // +   improved by: Thunder.m
    // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
    // +   bugfixed by: Pellentesque Malesuada
    // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
    // -    depends on: utf8_encode
    // *     example 1: base64_encode('Kevin van Zonneveld');
    // *     returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA=='

    // mozilla has this native
    // - but breaks in 2.0.0.12!
    //if (typeof this.window['atob'] == 'function') {
    //    return atob(data);
    //}


    let o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, enc='';
    const tmp_arr = [];

    if (!data) {
        return undefined;
    }

    do { // pack three octets into four hexets
        o1 = data[i++];
        o2 = data[i++];
        o3 = data[i++];

        bits = o1<<16 | o2<<8 | o3;

        h1 = bits>>18 & 0x3f;
        h2 = bits>>12 & 0x3f;
        h3 = bits>>6 & 0x3f;
        h4 = bits & 0x3f;

        // use hexets to index into b64, and append result to encoded string
        tmp_arr[ac++] = B64.charAt(h1) + B64.charAt(h2) + B64.charAt(h3) + B64.charAt(h4);
    } while (i < data.length);

    enc = tmp_arr.join('');

    switch (data.length % 3) {
        case 1:
            enc = enc.slice(0, -2) + '==';
            break;
        case 2:
            enc = enc.slice(0, -1) + '=';
            break;
    }

    return enc;
}

/** Returns undefined if the input is null or undefined. */
export function base64_decode(data: null | undefined): undefined;
/** Returns an array of bytes from the given base64-encoded string. */
export function base64_decode(data: string): memory;
/** Returns an array of bytes from the given base64-encoded string. */
export function base64_decode(data: string | null | undefined): memory | undefined {
    // Twacked by Will Scullin to handle arrays of 'bytes'

    // http://kevin.vanzonneveld.net
    // +   original by: Tyler Akins (http://rumkin.com)
    // +   improved by: Thunder.m
    // +      input by: Aman Gupta
    // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
    // +   bugfixed by: Onno Marsman
    // +   bugfixed by: Pellentesque Malesuada
    // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
    // +      input by: Brett Zamir (http://brett-zamir.me)
    // +   bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
    // -    depends on: utf8_decode
    // *     example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA==');
    // *     returns 1: 'Kevin van Zonneveld'

    // mozilla has this native
    // - but breaks in 2.0.0.12!
    //if (typeof this.window['btoa'] == 'function') {
    //    return btoa(data);
    //}

    let o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0;
    const tmp_arr = [];

    if (!data) {
        return undefined;
    }

    do {  // unpack four hexets into three octets using index points in B64
        h1 = B64.indexOf(data.charAt(i++));
        h2 = B64.indexOf(data.charAt(i++));
        h3 = B64.indexOf(data.charAt(i++));
        h4 = B64.indexOf(data.charAt(i++));

        bits = h1<<18 | h2<<12 | h3<<6 | h4;

        o1 = bits>>16 & 0xff;
        o2 = bits>>8 & 0xff;
        o3 = bits & 0xff;

        tmp_arr[ac++] = o1;
        if (h3 !== 64) {
            tmp_arr[ac++] = o2;
        }
        if (h4 !== 64) {
            tmp_arr[ac++] = o3;
        }
    } while (i < data.length);

    return new Uint8Array(tmp_arr);
}

const DATA_URL_PREFIX = 'data:application/octet-stream;base64,';

export function base64_json_parse(json: string): unknown {
    const reviver = (_key: string, value: unknown) => {
        if (typeof value ==='string' && value.startsWith(DATA_URL_PREFIX)) {
            return base64_decode(value.slice(DATA_URL_PREFIX.length));
        }
        return value;
    };

    return JSON.parse(json, reviver);
}

export function base64_json_stringify(json: unknown) {
    const replacer = (_key: string, value: unknown) => {
        if (value instanceof Uint8Array) {
            return DATA_URL_PREFIX + base64_encode(value);
        }
        return value;
    };

    return JSON.stringify(json, replacer);
}
