/**
 * Base64 functions.
 *
 * @author n1474335 [n1474335@gmail.com]
 * @copyright Crown Copyright 2016
 * @license Apache-2.0
 */

import Utils from "../Utils.mjs";
import OperationError from "../errors/OperationError.mjs";

/**
 * Base64's the input byte array using the given alphabet, returning a string.
 *
 * @param {byteArray|Uint8Array|ArrayBuffer|string} data
 * @param {string} [alphabet="A-Za-z0-9+/="]
 * @returns {string}
 *
 * @example
 * // returns "SGVsbG8="
 * toBase64([72, 101, 108, 108, 111]);
 *
 * // returns "SGVsbG8="
 * toBase64("Hello");
 */
export function toBase64(data, alphabet="A-Za-z0-9+/=") {
    if (!data) return "";
    if (data instanceof ArrayBuffer) {
        data = new Uint8Array(data);
    }
    if (typeof data == "string") {
        data = Utils.strToByteArray(data);
    }

    alphabet = Utils.expandAlphRange(alphabet).join("");
    if (alphabet.length !== 64 && alphabet.length !== 65) { // Allow for padding
        throw new OperationError(`Invalid Base64 alphabet length (${alphabet.length}): ${alphabet}`);
    }

    let output = "",
        chr1, chr2, chr3,
        enc1, enc2, enc3, enc4,
        i = 0;

    while (i < data.length) {
        chr1 = data[i++];
        chr2 = data[i++];
        chr3 = data[i++];

        enc1 = chr1 >> 2;
        enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
        enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
        enc4 = chr3 & 63;

        if (isNaN(chr2)) {
            enc3 = enc4 = 64;
        } else if (isNaN(chr3)) {
            enc4 = 64;
        }

        output += alphabet.charAt(enc1) + alphabet.charAt(enc2) +
            alphabet.charAt(enc3) + alphabet.charAt(enc4);
    }

    return output;
}


/**
 * UnBase64's the input string using the given alphabet, returning a byte array.
 *
 * @param {string} data
 * @param {string} [alphabet="A-Za-z0-9+/="]
 * @param {string} [returnType="string"] - Either "string" or "byteArray"
 * @param {boolean} [removeNonAlphChars=true]
 * @returns {byteArray}
 *
 * @example
 * // returns "Hello"
 * fromBase64("SGVsbG8=");
 *
 * // returns [72, 101, 108, 108, 111]
 * fromBase64("SGVsbG8=", null, "byteArray");
 */
export function fromBase64(data, alphabet="A-Za-z0-9+/=", returnType="string", removeNonAlphChars=true, strictMode=false) {
    if (!data) {
        return returnType === "string" ? "" : [];
    }

    alphabet = alphabet || "A-Za-z0-9+/=";
    alphabet = Utils.expandAlphRange(alphabet).join("");

    // Confirm alphabet is a valid length
    if (alphabet.length !== 64 && alphabet.length !== 65) { // Allow for padding
        throw new OperationError(`Error: Base64 alphabet should be 64 characters long, or 65 with a padding character. Found ${alphabet.length}: ${alphabet}`);
    }

    // Remove non-alphabet characters
    if (removeNonAlphChars) {
        const re = new RegExp("[^" + alphabet.replace(/[[\]\\\-^$]/g, "\\$&") + "]", "g");
        data = data.replace(re, "");
    }

    if (strictMode) {
        // Check for incorrect lengths (even without padding)
        if (data.length % 4 === 1) {
            throw new OperationError(`Error: Invalid Base64 input length (${data.length}). Cannot be 4n+1, even without padding chars.`);
        }

        if (alphabet.length === 65) { // Padding character included
            const pad = alphabet.charAt(64);
            const padPos = data.indexOf(pad);
            if (padPos >= 0) {
                // Check that the padding character is only used at the end and maximum of twice
                if (padPos < data.length - 2 || data.charAt(data.length - 1) !== pad) {
                    throw new OperationError(`Error: Base64 padding character (${pad}) not used in the correct place.`);
                }

                // Check that input is padded to the correct length
                if (data.length % 4 !== 0) {
                    throw new OperationError("Error: Base64 not padded to a multiple of 4.");
                }
            }
        }
    }

    const output = [];
    let chr1, chr2, chr3,
        enc1, enc2, enc3, enc4,
        i = 0;

    while (i < data.length) {
        // Including `|| null` forces empty strings to null so that indexOf returns -1 instead of 0
        enc1 = alphabet.indexOf(data.charAt(i++) || null);
        enc2 = alphabet.indexOf(data.charAt(i++) || null);
        enc3 = alphabet.indexOf(data.charAt(i++) || null);
        enc4 = alphabet.indexOf(data.charAt(i++) || null);

        if (strictMode && (enc1 < 0 || enc2 < 0 || enc3 < 0 || enc4 < 0)) {
            throw new OperationError("Error: Base64 input contains non-alphabet char(s)");
        }

        chr1 = (enc1 << 2) | (enc2 >> 4);
        chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
        chr3 = ((enc3 & 3) << 6) | enc4;

        if (chr1 >= 0 && chr1 < 256) {
            output.push(chr1);
        }
        if (chr2 >= 0 && chr2 < 256 && enc3 !== 64) {
            output.push(chr2);
        }
        if (chr3 >= 0 && chr3 < 256 && enc4 !== 64) {
            output.push(chr3);
        }
    }

    return returnType === "string" ? Utils.byteArrayToUtf8(output) : output;
}


/**
 * Base64 alphabets.
 */
export const ALPHABET_OPTIONS = [
    {name: "Standard (RFC 4648): A-Za-z0-9+/=", value: "A-Za-z0-9+/="},
    {name: "URL safe (RFC 4648 \u00A75): A-Za-z0-9-_", value: "A-Za-z0-9-_"},
    {name: "Filename safe: A-Za-z0-9+-=", value: "A-Za-z0-9+\\-="},
    {name: "itoa64: ./0-9A-Za-z=", value: "./0-9A-Za-z="},
    {name: "XML: A-Za-z0-9_.", value: "A-Za-z0-9_."},
    {name: "y64: A-Za-z0-9._-", value: "A-Za-z0-9._-"},
    {name: "z64: 0-9a-zA-Z+/=", value: "0-9a-zA-Z+/="},
    {name: "Radix-64 (RFC 4880): 0-9A-Za-z+/=", value: "0-9A-Za-z+/="},
    {name: "Uuencoding: [space]-_", value: " -_"},
    {name: "Xxencoding: +-0-9A-Za-z", value: "+\\-0-9A-Za-z"},
    {name: "BinHex: !-,-0-689@A-NP-VX-Z[`a-fh-mp-r", value: "!-,-0-689@A-NP-VX-Z[`a-fh-mp-r"},
    {name: "ROT13: N-ZA-Mn-za-m0-9+/=", value: "N-ZA-Mn-za-m0-9+/="},
    {name: "UNIX crypt: ./0-9A-Za-z", value: "./0-9A-Za-z"},
    {name: "Atom128: /128GhIoPQROSTeUbADfgHijKLM+n0pFWXY456xyzB7=39VaqrstJklmNuZvwcdEC", value: "/128GhIoPQROSTeUbADfgHijKLM+n0pFWXY456xyzB7=39VaqrstJklmNuZvwcdEC"},
    {name: "Megan35: 3GHIJKLMNOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF5", value: "3GHIJKLMNOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF5"},
    {name: "Zong22: ZKj9n+yf0wDVX1s/5YbdxSo=ILaUpPBCHg8uvNO4klm6iJGhQ7eFrWczAMEq3RTt2", value: "ZKj9n+yf0wDVX1s/5YbdxSo=ILaUpPBCHg8uvNO4klm6iJGhQ7eFrWczAMEq3RTt2"},
    {name: "Hazz15: HNO4klm6ij9n+J2hyf0gzA8uvwDEq3X1Q7ZKeFrWcVTts/MRGYbdxSo=ILaUpPBC5", value: "HNO4klm6ij9n+J2hyf0gzA8uvwDEq3X1Q7ZKeFrWcVTts/MRGYbdxSo=ILaUpPBC5"}
];
