/*
 * Passkey Implementation
 */
let passkey_debug = false;

/**
 * Convert a ArrayBuffer to Base64
 * @param {ArrayBuffer} buffer
 * @returns {String}
 */
function arrayBufferToBase64(buffer) {
	let binary = '';
	let bytes = new Uint8Array(buffer);
	let len = bytes.byteLength;
	for (let i = 0; i < len; i++) {
		binary += String.fromCharCode( bytes[ i ] );
	}
	return window.btoa(binary);
}

/**
 * convert RFC 1342-like base64 strings to array buffer
 * @param {mixed} obj
 * @returns {undefined}
 */
function recursiveBase64StrToArrayBuffer(obj) {
	let prefix = '=?BINARY?B?';
	let suffix = '?=';
	if (typeof obj === 'object') {
		for (let key in obj) {
			if (typeof obj[key] === 'string') {
				let str = obj[key];
				if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) {
					str = str.substring(prefix.length, str.length - suffix.length);

					let binary_string = window.atob(str);
					let len = binary_string.length;
					let bytes = new Uint8Array(len);
					for (let i = 0; i < len; i++)        {
						bytes[i] = binary_string.charCodeAt(i);
					}
					obj[key] = bytes.buffer;
				}
			} else {
				recursiveBase64StrToArrayBuffer(obj[key]);
			}
		}
	}
}

function passkey_check_browser()
{
	// check browser support
	if ((! window.fetch) || (! navigator.credentials) || (! navigator.credentials.create))
		throw new Error('Browser not supported.');

	/*
	// Availability of `window.PublicKeyCredential` means WebAuthn is usable.
	// `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable.
	// `isConditionalMediationAvailable` means the feature detection is usable.
	if (window.PublicKeyCredential &&
		PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
		PublicKeyCredential.isConditionalMediationAvailable) {
		// Check if user verifying platform authenticator is available.
		Promise.all([
			PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),
			PublicKeyCredential.isConditionalMediationAvailable(),
		]).then(results => {
			if (results.every(r => r === true)) {
				// Display "Create a new passkey" button
			}
		});
	}
	*/

	if (passkey_debug)
		console.log('Passkey: Browser OK');

	return true;
}

/**
 * Register/Create a passkey for a user
 */
async function passkey_register(csrf_token,icon_dom,icon,icon_shell_success,icon_shell_fail)
{
	try {
		if (! passkey_check_browser())
			return;

		// Change our icon so that it is obvious we are doing something
		icon_dom.find('i').removeClass(icon).addClass('spinner-grow spinner-grow-sm');

		// Get our arguments
		var createArgs;
		$.ajax({
			url: '/passkey/register',
			type: 'GET',
			dataType: 'json',
			async: false,
			cache: false,
			success: function(data) {
				if (passkey_debug)
					console.log('Passkey: Get Register Success');

				recursiveBase64StrToArrayBuffer(data);
				createArgs = data;
			},
			error: function(e,status,error) {
				throw new Error(status || 'Unknown error occurred');
			}
		});

		// Create credentials
		try {
			const cred = await navigator.credentials.create(createArgs);

			const authenticatorAttestationResponse = {
				id: cred.id,
				rawId: arrayBufferToBase64(cred.rawId),
				transports: cred.response.getTransports ? cred.response.getTransports() : null,
				clientDataJSON: cred.response.clientDataJSON  ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
				attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null,
				authenticatorAttachment: cred.authenticatorAttachment,
				_token: csrf_token,
			};

			$.ajax({
				url: '/passkey/check',
				type: 'POST',
				data: authenticatorAttestationResponse,
				cache: false,
				success: function(data) {
					if (passkey_debug)
						console.log('Passkey: Registration Success');

					icon_dom.find('i').addClass(icon).removeClass('spinner-grow spinner-grow-sm');
					icon_dom.removeClass('btn-outline-primary').addClass('btn-primary');
				},
				error: function(e,status,error) {
					throw new Error(status || 'Unknown error occurred');
				}
			});

		} catch (status) {
			if (passkey_debug)
				console.log(status || 'Passkey: User Aborted Register');

			// Restore the icon
			icon_dom.find('i').addClass(icon).removeClass('spinner-grow spinner-grow-sm');

			return;
		}

	} catch (err) {
		window.alert(err || 'An UNKNOWN error occurred?');
	}
}

/**
 * Check a passkey being presented
 */
async function passkey_check(csrf_token,redirect)
{
	if (passkey_debug)
		console.log('Passkey: Check User Passkey');

	try {
		if (! passkey_check_browser())
			return;

		// Get our arguments
		var getArgs;
		$.ajax({
			url: '/passkey/get',
			type: 'GET',
			dataType: 'json',
			async: false,
			cache: false,
			success: function(data) {
				if (passkey_debug)
					console.log('Passkey: Get Args Success');

				recursiveBase64StrToArrayBuffer(data);
				getArgs = data;
			},
			error: function(e,status,error) {
				throw new Error(status || 'Unknown error occurred');
			}
		});

		// check credentials with hardware
		const cred = await navigator.credentials.get(getArgs);

		// create object for transmission to server
		const authenticatorAttestationResponse = {
			id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
			clientDataJSON: cred.response.clientDataJSON  ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
			authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
			signature: cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null,
			userHandle: cred.response.userHandle ? arrayBufferToBase64(cred.response.userHandle) : null,
			_token: csrf_token
		};

		$.ajax({
			url: '/passkey/process',
			type: 'POST',
			data: authenticatorAttestationResponse,
			cache: false,
			success: function(data) {
				if (passkey_debug)
					console.log('Passkey: Process Success');

				// Direct to the home page
				if (data.success)
					window.location.href = (redirect !== undefined) ? redirect : '/';
				else
					alert(data.msg || 'Unknown error occurred');
			},
			error: function(e,status,error) {
				throw new Error(status || 'Unknown error occurred');
			}
		});

	} catch (err) {
		window.alert(err || 'An UNKNOWN error occurred?');
	}
}