const loadingCallback = 'OnLoading';
const errorCallback = 'OnError';
const completedCallback = 'OnCompleted';
var credentialInstance;
var assertionInstance;

function initCredentialInstance(instance) {
    credentialInstance = instance;
}

async function makeCredential(credentialsOptionsJson) {
    credentialInstance.invokeMethodAsync(loadingCallback);

    try {
        var credentials = await createCredentials(credentialsOptionsJson);
    } catch (e) {
        await credentialInstance.invokeMethodAsync(errorCallback, e.message);
        return;
    }

    await credentialInstance.invokeMethodAsync(completedCallback, credentials);
}

function initAssertionInstance(instance) {
    assertionInstance = instance;
}

async function makeAssertion(userEmail) {
    assertionInstance.invokeMethodAsync(loadingCallback);

    if (userEmail === undefined) {
        userEmail = '';
    }

    var id = generateId();
    var data = new FormData();
    data.append('userEmail', userEmail);

    let makeAssertionOptions;
    try {
        var res = await fetch('/api/Identity/FIDO2AssertionOptions?id=' + id, {
            method: 'POST',
            body: data,
            headers: {
                'Accept': 'application/json'
            }
        });

        makeAssertionOptions = await res.json();
    } catch (e) {
        await assertionInstance.invokeMethodAsync(errorCallback, e.message);
        return;
    }

    let credential;
    try {
        credential = await getCredentials(makeAssertionOptions);
    } catch (e) {
        await assertionInstance.invokeMethodAsync(errorCallback, e.message);
        return;
    }

    let authorizationResponse;
    try {
        var result = await fetch('/api/Identity/FIDO2AuthenticatorSignIn?id=' + id, {
            method: 'POST',
            body: credential,
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            }
        });

        authorizationResponse = await result.text();
    } catch (e) {
        await assertionInstance.invokeMethodAsync(errorCallback, e.message);
        return;
    }

    await assertionInstance.invokeMethodAsync(completedCallback, authorizationResponse);
}

async function createCredentials(credentialOptionsJson) {
    let credentialOptions = JSON.parse(credentialOptionsJson);
    credentialOptions.challenge = coerceToArrayBuffer(credentialOptions.challenge);
    credentialOptions.user.id = coerceToArrayBuffer(credentialOptions.user.id);
    credentialOptions.excludeCredentials = credentialOptions.excludeCredentials.map((c) => {
        c.id = coerceToArrayBuffer(c.id);
        return c;
    });

    let credential = await navigator.credentials.create({
        publicKey: credentialOptions
    });

    let attestationObject = new Uint8Array(credential.response.attestationObject);
    let clientDataJSON = new Uint8Array(credential.response.clientDataJSON);
    let rawId = new Uint8Array(credential.rawId);

    const data = {
        id: credential.id,
        rawId: coerceToBase64Url(rawId),
        type: credential.type,
        extensions: credential.getClientExtensionResults(),
        response: {
            attestationObject: coerceToBase64Url(attestationObject),
            clientDataJSON: coerceToBase64Url(clientDataJSON)
        }
    };

    return JSON.stringify(data);
}

async function getCredentials(assertionOptionsJson) {
    let assertionOptions = JSON.parse(assertionOptionsJson);
    // todo: switch this to coercebase64
    const challenge = assertionOptions.challenge.replace(/-/g, "+").replace(/_/g, "/");
    assertionOptions.challenge = Uint8Array.from(atob(challenge), c => c.charCodeAt(0));
    // fix escaping. Change this to coerce
    assertionOptions.allowCredentials.forEach(function (listItem) {
        var fixedId = listItem.id.replace(/\_/g, "/").replace(/\-/g, "+");
        listItem.id = Uint8Array.from(atob(fixedId), c => c.charCodeAt(0));
    });

    let credential = await navigator.credentials.get({ publicKey: assertionOptions })

    // Move data into Arrays incase it is super long
    let authData = new Uint8Array(credential.response.authenticatorData);
    let clientDataJSON = new Uint8Array(credential.response.clientDataJSON);
    let rawId = new Uint8Array(credential.rawId);
    let sig = new Uint8Array(credential.response.signature);
    //let userHandle = new Uint8Array(credential.response.userHandle);
    const data = {
        id: credential.id,
        rawId: coerceToBase64Url(rawId),
        type: credential.type,
        extensions: credential.getClientExtensionResults(),
        response: {
            authenticatorData: coerceToBase64Url(authData),
            clientDataJson: coerceToBase64Url(clientDataJSON),
            //userHandle: userHandle !== null ? coerceToBase64Url(userHandle) : null,
            signature: coerceToBase64Url(sig)
        }
    };

    return JSON.stringify(data);
}

coerceToArrayBuffer = function (thing, name) {
    if (typeof thing === "string") {
        // base64url to base64
        thing = thing.replace(/-/g, "+").replace(/_/g, "/");

        // base64 to Uint8Array
        var str = window.atob(thing);
        var bytes = new Uint8Array(str.length);
        for (var i = 0; i < str.length; i++) {
            bytes[i] = str.charCodeAt(i);
        }
        thing = bytes;
    }

    // Array to Uint8Array
    if (Array.isArray(thing)) {
        thing = new Uint8Array(thing);
    }

    // Uint8Array to ArrayBuffer
    if (thing instanceof Uint8Array) {
        thing = thing.buffer;
    }

    // error if none of the above worked
    if (!(thing instanceof ArrayBuffer)) {
        throw new TypeError("could not coerce '" + name + "' to ArrayBuffer");
    }

    return thing;
};

coerceToBase64Url = function (thing) {
    // Array or ArrayBuffer to Uint8Array
    if (Array.isArray(thing)) {
        thing = Uint8Array.from(thing);
    }

    if (thing instanceof ArrayBuffer) {
        thing = new Uint8Array(thing);
    }

    // Uint8Array to base64
    if (thing instanceof Uint8Array) {
        var str = "";
        var len = thing.byteLength;

        for (var i = 0; i < len; i++) {
            str += String.fromCharCode(thing[i]);
        }
        thing = window.btoa(str);
    }

    if (typeof thing !== "string") {
        throw new Error("could not coerce to string");
    }

    // base64 to base64url
    // NOTE: "=" at the end of challenge is optional, strip it off here
    thing = thing.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");

    return thing;
};

function generateId() {
    function S4() {
        return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
    }
    return (S4() + S4() + "-" + S4() + "-4" + S4().substr(0, 3) + "-" + S4() + "-" + S4() + S4() + S4()).toLowerCase();
}