// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.webauthn;

import android.content.Context;

import androidx.annotation.VisibleForTesting;

import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.blink.mojom.AuthenticatorStatus;
import org.chromium.blink.mojom.PaymentOptions;
import org.chromium.blink.mojom.PublicKeyCredentialCreationOptions;
import org.chromium.blink.mojom.PublicKeyCredentialRequestOptions;
import org.chromium.content_public.browser.RenderFrameHost;
import org.chromium.content_public.browser.WebAuthenticationDelegate;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsStatics;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.url.Origin;

import java.nio.ByteBuffer;

/**
 * Acts as a bridge from InternalAuthenticator declared in
 * //components/webauthn/android/internal_authenticator_android.h to AuthenticatorImpl.
 *
 * The origin associated with requests on InternalAuthenticator should be set by calling
 * setEffectiveOrigin() first.
 */
@JNINamespace("webauthn")
public class InternalAuthenticator {
    private long mNativeInternalAuthenticatorAndroid;
    private final AuthenticatorImpl mAuthenticator;

    private InternalAuthenticator(long nativeInternalAuthenticatorAndroid, Context context,
            WebAuthenticationDelegate.IntentSender intentSender, RenderFrameHost renderFrameHost,
            Origin topOrigin) {
        mNativeInternalAuthenticatorAndroid = nativeInternalAuthenticatorAndroid;
        mAuthenticator = new AuthenticatorImpl(context, intentSender, renderFrameHost, topOrigin);
    }

    public static InternalAuthenticator createForTesting(Context context,
            WebAuthenticationDelegate.IntentSender intentSender, RenderFrameHost renderFrameHost,
            Origin topOrigin) {
        return new InternalAuthenticator(-1, context, intentSender, renderFrameHost, topOrigin);
    }

    @CalledByNative
    public static InternalAuthenticator create(
            long nativeInternalAuthenticatorAndroid, RenderFrameHost renderFrameHost) {
        final WebContents webContents = WebContentsStatics.fromRenderFrameHost(renderFrameHost);
        final WindowAndroid window = webContents.getTopLevelNativeWindow();
        final Context context = window.getActivity().get();
        final Origin topOrigin = webContents.getMainFrame().getLastCommittedOrigin();
        return new InternalAuthenticator(nativeInternalAuthenticatorAndroid, context,
                new AuthenticatorImpl.WindowIntentSender(window), renderFrameHost, topOrigin);
    }

    @CalledByNative
    public void clearNativePtr() {
        mNativeInternalAuthenticatorAndroid = 0;
    }

    @CalledByNative
    public void setEffectiveOrigin(Origin origin) {
        mAuthenticator.setEffectiveOrigin(origin);
    }

    @CalledByNative
    public void setPaymentOptions(ByteBuffer payment) {
        mAuthenticator.setPaymentOptions(PaymentOptions.deserialize(payment));
    }

    /**
     * Called by InternalAuthenticatorAndroid, which facilitates WebAuthn for processes that
     * originate from the browser process.
     */
    @CalledByNative
    public void makeCredential(ByteBuffer optionsByteBuffer) {
        mAuthenticator.makeCredential(
                PublicKeyCredentialCreationOptions.deserialize(optionsByteBuffer),
                (status, response, domExceptionDetails) -> {
                    // DOMExceptions can only be passed through the webAuthenticationProxy
                    // extensions API, which doesn't exist on Android.
                    assert status != AuthenticatorStatus.ERROR_WITH_DOM_EXCEPTION_DETAILS
                            && domExceptionDetails == null;
                    if (mNativeInternalAuthenticatorAndroid != 0) {
                        InternalAuthenticatorJni.get().invokeMakeCredentialResponse(
                                mNativeInternalAuthenticatorAndroid, status.intValue(),
                                response == null ? null : response.serialize());
                    }
                });
    }

    /**
     * Called by InternalAuthenticatorAndroid, which facilitates WebAuthn for processes that
     * originate from the browser process.
     */
    @CalledByNative
    public void getAssertion(ByteBuffer optionsByteBuffer) {
        mAuthenticator.getAssertion(
                PublicKeyCredentialRequestOptions.deserialize(optionsByteBuffer),
                (status, response, domExceptionDetails) -> {
                    // DOMExceptions can only be passed through the webAuthenticationProxy
                    // extensions API, which doesn't exist on Android.
                    assert status != AuthenticatorStatus.ERROR_WITH_DOM_EXCEPTION_DETAILS
                            && domExceptionDetails == null;
                    if (mNativeInternalAuthenticatorAndroid != 0) {
                        InternalAuthenticatorJni.get().invokeGetAssertionResponse(
                                mNativeInternalAuthenticatorAndroid, status.intValue(),
                                response == null ? null : response.serialize());
                    }
                });
    }

    /**
     * Called by InternalAuthenticatorAndroid, which facilitates WebAuthn for processes that
     * originate from the browser process. The response will be passed through
     * |invokeIsUserVerifyingPlatformAuthenticatorAvailableResponse()|.
     */
    @CalledByNative
    public void isUserVerifyingPlatformAuthenticatorAvailable() {
        mAuthenticator.isUserVerifyingPlatformAuthenticatorAvailable((isUVPAA) -> {
            if (mNativeInternalAuthenticatorAndroid != 0) {
                InternalAuthenticatorJni.get()
                        .invokeIsUserVerifyingPlatformAuthenticatorAvailableResponse(
                                mNativeInternalAuthenticatorAndroid, isUVPAA);
            }
        });
    }

    /**
     * Called by InternalAuthenticatorAndroid, which facilitates WebAuthn for processes that
     * originate from the browser process.
     */
    @CalledByNative
    public boolean isGetMatchingCredentialIdsSupported() {
        return mAuthenticator.isGetMatchingCredentialIdsSupported();
    }

    /**
     * Called by InternalAuthenticatorAndroid, which facilitates WebAuthn for processes that
     * originate from the browser process. The response will be passed through
     * |invokeGetMatchingCredentialIdsResponse()|.
     */
    @CalledByNative
    public void getMatchingCredentialIds(
            String relyingPartyId, byte[][] credentialIds, boolean requireThirdPartyPayment) {
        mAuthenticator.getMatchingCredentialIds(relyingPartyId, credentialIds,
                requireThirdPartyPayment, (matchingCredentialIds) -> {
                    if (mNativeInternalAuthenticatorAndroid != 0) {
                        InternalAuthenticatorJni.get().invokeGetMatchingCredentialIdsResponse(
                                mNativeInternalAuthenticatorAndroid,
                                matchingCredentialIds.toArray(new byte[0][]));
                    }
                });
    }

    @CalledByNative
    public void cancel() {
        mAuthenticator.cancel();
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    @NativeMethods
    public interface Natives {
        void invokeMakeCredentialResponse(
                long nativeInternalAuthenticatorAndroid, int status, ByteBuffer byteBuffer);
        void invokeGetAssertionResponse(
                long nativeInternalAuthenticatorAndroid, int status, ByteBuffer byteBuffer);
        void invokeIsUserVerifyingPlatformAuthenticatorAvailableResponse(
                long nativeInternalAuthenticatorAndroid, boolean isUVPAA);
        void invokeGetMatchingCredentialIdsResponse(
                long nativeInternalAuthenticatorAndroid, byte[][] matchingCredentialIds);
    }
}
