// Copyright (c) Microsoft Corporation.
// All rights reserved.
//
// This code is licensed under the MIT License.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files(the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions :
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package com.microsoft.aad.adal;

import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Looper;
import android.os.NetworkOnMainThreadException;
import android.text.TextUtils;
import android.util.SparseArray;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;

import com.googleblog.android_developers.PRNGFixes;
import com.microsoft.aad.adal.AuthenticationRequest.UserIdentifierType;
import com.microsoft.identity.common.adal.internal.AuthenticationConstants;
import com.microsoft.identity.common.adal.internal.util.StringExtensions;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicReference;

import static com.microsoft.identity.common.java.AuthenticationConstants.UIRequest.BROWSER_FLOW;

/**
 * ADAL context to get access token, refresh token, and lookup from cache.
 */
public class AuthenticationContext {

    private static final int EXCLUDE_INDEX = 8;

    private static final String TAG = "AuthenticationContext";

    private static final String REQUEST_ID = "requestId:";

    private Context mContext;

    private String mAuthority;

    private boolean mValidateAuthority;

    private boolean mIsAuthorityValidated;

    private ITokenCacheStore mTokenCacheStore;

    private BrokerProxy mBrokerProxy = null;

    private boolean mExtendedLifetimeEnabled = false;

    private List<String> mClientCapabilites = null;
    /**
     * Delegate map is needed to handle activity recreate without asking
     * developer to handle context instance for config changes.
     */
    private static final SparseArray<AuthenticationRequestState> DELEGATE_MAP = new SparseArray<>();

    /**
     * CorrelationId set by user or generated by ADAL.
     */
    private UUID mRequestCorrelationId = null;

    /**
     * Constructs context to use with known authority to get the token. It uses
     * default cache that stores encrypted tokens.
     *
     * @param appContext        It needs to have handle to the {@link Context} to use
     *                          the SharedPreferencesFileManager as a Default cache storage. It does not
     *                          need to be activity.
     * @param authority         Authority url to send code and token requests
     * @param validateAuthority validate authority before sending token request
     */
    public AuthenticationContext(@NonNull final Context appContext,
                                 @NonNull final String authority,
                                 final boolean validateAuthority) {
        // Fixes are required for SDK 16-18
        // The fixes need to be applied before any use of Java Cryptography
        // Architecture primitives. Default cache uses encryption
        PRNGFixes.apply();
        initialize(appContext, authority, new DefaultTokenCacheStore(appContext), validateAuthority, true);
    }

    /**
     * Constructs context to use with known authority to get the token. It uses
     * provided cache.
     *
     * @param appContext        {@link Context}
     * @param authority         Authority Url
     * @param validateAuthority true/false for validation
     * @param tokenCacheStore   Set to null if you don't want cache.
     */
    public AuthenticationContext(@NonNull final Context appContext,
                                 @NonNull final String authority,
                                 final boolean validateAuthority,
                                 @Nullable final ITokenCacheStore tokenCacheStore) {
        initialize(appContext, authority, tokenCacheStore, validateAuthority, false);
    }

    /**
     * It will verify the authority and use the given cache. If cache is null,
     * it will not use cache.
     *
     * @param appContext      {@link Context}
     * @param authority       Authority Url
     * @param tokenCacheStore Cache {@link ITokenCacheStore} used to store
     *                        tokens. Set to null if you don't want cache.
     */
    public AuthenticationContext(@NonNull final Context appContext,
                                 @NonNull final String authority,
                                 @Nullable final ITokenCacheStore tokenCacheStore) {
        initialize(appContext, authority, tokenCacheStore, true, false);
    }

    private void initialize(@NonNull final Context appContext,
                            @NonNull final String authority,
                            @Nullable final ITokenCacheStore tokenCacheStore,
                            final boolean validateAuthority,
                            final boolean defaultCache) {
        if (appContext == null) {
            throw new IllegalArgumentException("appContext");
        }

        if (authority == null) {
            throw new IllegalArgumentException("authority");
        }

        mBrokerProxy = new BrokerProxy(appContext);

        if (!defaultCache && !mBrokerProxy.canUseLocalCache(authority)) {
            throw new UnsupportedOperationException("Local cache is not supported for broker usage");
        }

        mContext = appContext;
        checkInternetPermission();
        mAuthority = extractAuthority(authority);
        mValidateAuthority = validateAuthority;

        if (null != tokenCacheStore) {
            mTokenCacheStore = new DelegatingCache(mContext, tokenCacheStore);
        }
    }

    /**
     * Returns referenced cache. You can use default cache, which uses
     * SharedPreferencesFileManager and handles synchronization by itself.
     *
     * @return ITokenCacheStore Current cache used
     */
    public ITokenCacheStore getCache() {
        return mTokenCacheStore;
    }

    /**
     * Gets if the ExtendedLifetime mode is enabled.
     *
     * @return True when ExtendedLifetime mode is enabled
     */
    public boolean getExtendedLifetimeEnabled() {
        return mExtendedLifetimeEnabled;
    }

    /**
     * The client need to set the flag to true explicitly to enable the ExtendedLifetime mode
     * The default value of flag is false.
     * ADAL will return the stale token when ExtendedLifetime mode is enabled and the server is down
     *
     * @param extendedLifetimeEnabled true if the ExtendedLifetime mode is on, false otherwise
     */
    public void setExtendedLifetimeEnabled(final boolean extendedLifetimeEnabled) {
        mExtendedLifetimeEnabled = extendedLifetimeEnabled;
    }

    /**
     * Gets authority that is used for this object of AuthenticationContext.
     *
     * @return Authority
     */
    public String getAuthority() {
        return mAuthority;
    }

    /**
     * @return True If developer turn on the authority validation, false otherwise.
     */
    public boolean getValidateAuthority() {
        return mValidateAuthority;
    }

    /**
     * Gets username for current broker user.
     *
     * @return Username
     */
    public String getBrokerUser() {
        if (mBrokerProxy != null) {
            return mBrokerProxy.getCurrentUser();
        }

        return null;
    }

    /**
     * Gets user info from broker. This should not be called on main thread.
     *
     * @return An array of {@link UserInfo} that haven been authenticated via broker(can be null).
     * @throws IOException                if the broker returned an error response that indicates that it encountered an IOException
     *                                    while communicating with the authentication server.
     * @throws AuthenticatorException     if there was an error communicating with the authenticator or if the
     *                                    authenticator returned an invalid response.
     * @throws OperationCanceledException if the request was canceled for any reason.
     */
    public UserInfo[] getBrokerUsers() throws OperationCanceledException, AuthenticatorException,
            IOException {
        return mBrokerProxy != null ? mBrokerProxy.getBrokerUsers() : null;
    }

    /**
     * Get expected redirect Uri for your app to use in broker. You need to
     * register this redirectUri in order to get token from Broker.
     *
     * @return RedirectUri string to use for broker requests.
     */
    public String getRedirectUriForBroker() {
        final String methodName = ":getRedirectUriForBroker";
        final PackageHelper packageHelper = new PackageHelper(mContext);
        final String packageName = mContext.getPackageName();

        // First available signature. Applications can be signed with multiple
        // signatures.
        final String signatureDigest = packageHelper.getSha1SignatureForPackage(packageName);
        final String redirectUri = PackageHelper.getBrokerRedirectUrl(packageName, signatureDigest);
        Logger.v(TAG + methodName, "Get expected redirect Uri. ", "Broker redirectUri:" + redirectUri + " packagename:" + packageName
                + " signatureDigest:" + signatureDigest, null);
        return redirectUri;
    }

    /**
     * acquireToken will start interactive flow if needed. It checks the cache
     * to return existing result if not expired. It tries to use refresh token
     * if available. If it fails to get token with refresh token, it will remove
     * this refresh token from cache and start authentication.
     *
     * @param activity    required to launch authentication activity.
     * @param resource    required resource identifier.
     * @param clientId    required client identifier
     * @param redirectUri Optional. It will use package name info if not
     *                    provided.
     * @param loginHint   login hint (optional if validateAuthority == false)
     * @param callback    required
     */
    public void acquireToken(Activity activity, String resource, String clientId,
                             @Nullable String redirectUri, @Nullable String loginHint,
                             AuthenticationCallback<AuthenticationResult> callback) {

        acquireToken(resource, clientId, redirectUri, loginHint, PromptBehavior.Auto, null,
                null, callback, EventStrings.ACQUIRE_TOKEN_1, wrapActivity(activity), false);
    }

    /**
     * acquireToken will start interactive flow if needed. It checks the cache
     * to return existing result if not expired. It tries to use the refresh
     * token if available. If it fails to get token with refresh token, it will
     * remove this refresh token from cache and fall back on the UI.
     *
     * @param activity             Calling activity
     * @param resource             required resource identifier.
     * @param clientId             required client identifier
     * @param redirectUri          Optional. It will use packagename and provided suffix
     *                             for this.
     * @param loginHint            Optional if validateAuthority == null. This parameter will
     *                             be used to pre-populate the username field in the authentication
     *                             form. Please note that the end user can still edit the username
     *                             field and authenticate as a different user.
     * @param extraQueryParameters Optional. This parameter will be appended as
     *                             is to the query string in the HTTP authentication request to
     *                             the authority. The parameter can be null.
     * @param callback             required {@link AuthenticationCallback} object for async
     *                             call.
     */
    public void acquireToken(Activity activity, String resource, String clientId,
                             @Nullable String redirectUri, @Nullable String loginHint, @Nullable String extraQueryParameters,
                             AuthenticationCallback<AuthenticationResult> callback) {

        acquireToken(resource, clientId, redirectUri, loginHint, PromptBehavior.Auto, extraQueryParameters,
                null, callback, EventStrings.ACQUIRE_TOKEN_2, wrapActivity(activity), false);
    }

    /**
     * acquireToken will start interactive flow if needed. It checks the cache
     * to return existing result if not expired. It tries to use refresh token
     * if available. If it fails to get token with refresh token, behavior will
     * depend on options. If {@link PromptBehavior} is AUTO, it will remove this
     * refresh token from cache and fall back on the UI. Default is AUTO. if
     * {@link PromptBehavior} is Always, it will display prompt screen.
     *
     * @param activity    Calling activity
     * @param resource    required resource identifier.
     * @param clientId    required client identifier.
     * @param redirectUri Optional. It will use packagename and provided suffix
     *                    for this.
     * @param prompt      Optional. {@link PromptBehavior} added as query parameter
     *                    to authorization url
     * @param callback    required {@link AuthenticationCallback} object for async
     *                    call.
     */
    public void acquireToken(Activity activity, String resource, String clientId,
                             @Nullable String redirectUri, @Nullable PromptBehavior prompt,
                             AuthenticationCallback<AuthenticationResult> callback) {

        acquireToken(resource, clientId, redirectUri, null, prompt, null,
                null, callback, EventStrings.ACQUIRE_TOKEN_3, wrapActivity(activity), false);
    }

    /**
     * acquireToken will start interactive flow if needed. It checks the cache
     * to return existing result if not expired. It tries to use refresh token
     * if available. If it fails to get token with refresh token, behavior will
     * depend on options. If promptbehavior is AUTO, it will remove this refresh
     * token from cache and fall back on the UI if activitycontext is not null.
     * Default is AUTO.
     *
     * @param activity             Calling activity
     * @param resource             required resource identifier.
     * @param clientId             required client identifier.
     * @param redirectUri          Optional. It will use packagename and provided suffix
     *                             for this.
     * @param prompt               Optional. added as query parameter to authorization url
     * @param extraQueryParameters Optional. added to authorization url
     * @param callback             required {@link AuthenticationCallback} object for async
     *                             call.
     */
    public void acquireToken(Activity activity, String resource, String clientId,
                             @Nullable String redirectUri, @Nullable PromptBehavior prompt, @Nullable String extraQueryParameters,
                             AuthenticationCallback<AuthenticationResult> callback) {
        acquireToken(resource, clientId, redirectUri, null, prompt, extraQueryParameters,
                null, callback, EventStrings.ACQUIRE_TOKEN_4, wrapActivity(activity), false);
    }

    /**
     * acquireToken will start interactive flow if needed. It checks the cache
     * to return existing result if not expired. It tries to use refresh token
     * if available. If it fails to get token with refresh token, behavior will
     * depend on options. If promptbehavior is AUTO, it will remove this refresh
     * token from cache and fall back on the UI if activitycontext is not null.
     * Default is AUTO.
     *
     * @param activity             Calling activity
     * @param resource             required resource identifier.
     * @param clientId             required client identifier.
     * @param redirectUri          Optional. It will use packagename and provided suffix
     *                             for this.
     * @param loginHint            Optional if validateAuthority == null. It is used for cache and as a loginhint at
     *                             authentication.
     * @param prompt               Optional. added as query parameter to authorization url
     * @param extraQueryParameters Optional. added to authorization url
     * @param callback             required {@link AuthenticationCallback} object for async
     *                             call.
     */
    public void acquireToken(Activity activity, String resource, String clientId,
                             @Nullable String redirectUri, @Nullable String loginHint, @Nullable PromptBehavior prompt,
                             @Nullable String extraQueryParameters, AuthenticationCallback<AuthenticationResult> callback) {

        acquireToken(resource, clientId, redirectUri, loginHint, prompt, extraQueryParameters,
                null, callback, EventStrings.ACQUIRE_TOKEN_5, wrapActivity(activity), false);
    }

    /**
     * acquireToken will start an interactive auth flow to acquire new tokens
     * with the requested claims. Bypasses token cache if promptbehavior is not AUTO or claims are passed.
     *
     * @param activity             Calling activity
     * @param resource             required resource identifier.
     * @param clientId             required client identifier.
     * @param redirectUri          Optional. It will use packagename and provided suffix
     *                             for this.
     * @param loginHint            Optional if validateAuthority == null. It is used for cache and as a loginhint at
     *                             authentication.
     * @param prompt               Optional. added as query parameter to authorization url
     * @param extraQueryParameters Optional. added to authorization url
     * @param claims               Optional. The claims challenge returned from middle tier service, will be added as query string
     *                             to authorize endpoint.
     * @param callback             required {@link AuthenticationCallback} object for async
     *                             call.
     */
    public void acquireToken(final Activity activity, final String resource, final String clientId, @Nullable String redirectUri,
                             @Nullable final String loginHint, @Nullable final PromptBehavior prompt, @Nullable String extraQueryParameters,
                             @Nullable final String claims, final AuthenticationCallback<AuthenticationResult> callback) {

        acquireToken(resource, clientId, redirectUri, loginHint, prompt, extraQueryParameters,
                claims, callback, EventStrings.ACQUIRE_TOKEN_8, wrapActivity(activity), false);
    }

    /**
     * It will start interactive flow if needed. It checks the cache to return
     * existing result if not expired. It tries to use refresh token if
     * available. If it fails to get token with refresh token, behavior will
     * depend on options. If promptbehavior is AUTO, it will remove this refresh
     * token from cache and fall back on the UI. Default is AUTO.
     *
     * @param fragment             It accepts both type of fragments.
     * @param resource             required resource identifier.
     * @param clientId             required client identifier.
     * @param redirectUri          Optional. It will use packagename and provided suffix
     *                             for this.
     * @param loginHint            Optional if validateAuthority == null. It is used for cache and as a loginhint at
     *                             authentication.
     * @param prompt               Optional. added as query parameter to authorization url
     * @param extraQueryParameters Optional. added to authorization url
     * @param callback             required {@link AuthenticationCallback} object for async
     *                             call.
     */
    public void acquireToken(IWindowComponent fragment, String resource, String clientId,
                             @Nullable String redirectUri, @Nullable String loginHint, @Nullable PromptBehavior prompt,
                             @Nullable String extraQueryParameters, AuthenticationCallback<AuthenticationResult> callback) {

        acquireToken(resource, clientId, redirectUri, loginHint, prompt, extraQueryParameters,
                null, callback, EventStrings.ACQUIRE_TOKEN_6, fragment, false);

    }

    /**
     * acquireToken will start an interactive auth flow to acquire new tokens
     * with the requested claims. Bypasses token cache if promptbehavior is not AUTO or claims are passed.
     *
     * @param fragment             It accepts both type of fragments.
     * @param resource             required resource identifier.
     * @param clientId             required client identifier.
     * @param redirectUri          Optional. It will use packagename and provided suffix
     *                             for this.
     * @param loginHint            Optional if validateAuthority == null. It is used for cache and as a loginhint at
     *                             authentication.
     * @param prompt               Optional. added as query parameter to authorization url
     * @param extraQueryParameters Optional. added to authorization url
     * @param claims               Optional. The claims challenge returned from middle tier service, will be added as query string
     *                             to authorize endpoint.
     * @param callback             required {@link AuthenticationCallback} object for async
     *                             call.
     */
    public void acquireToken(final IWindowComponent fragment, final String resource, final String clientId, @Nullable String redirectUri,
                             @Nullable final String loginHint, @Nullable final PromptBehavior prompt, @Nullable String extraQueryParameters,
                             @Nullable final String claims, final AuthenticationCallback<AuthenticationResult> callback) {

        acquireToken(resource, clientId, redirectUri, loginHint, prompt, extraQueryParameters,
                claims, callback, EventStrings.ACQUIRE_TOKEN_9, fragment, false);
    }

    /**
     * acquireToken will authorize an end user to call the specified resource.
     * The access token returned from the STS will be sent to the {@link AuthenticationCallback}
     * and can be used to call the specified protected resource.
     * By default, acquireToken will attempt to fulfill the request silently, and
     * perform an interactive login if needed or explicitly specified in
     * the request. This overload uses an
     * [AlertDialog](https://developer.android.com/guide/topics/ui/dialogs)
     * when user interaction is required. This overload does not support any flow
     * requiring a
     * [token broker](https://github.com/AzureAD/azure-activedirectory-library-for-android/wiki/Broker).
     *
     * @param resource             required resource identifier.
     * @param clientId             required client identifier.
     * @param redirectUri          Optional. It will use packagename and provided suffix
     *                             for this.
     * @param loginHint            Optional if validateAuthority == null. It is used for cache and as a loginhint at
     *                             authentication.
     * @param prompt               Optional. added as query parameter to authorization url
     * @param extraQueryParameters Optional. added to authorization url
     * @param callback             required {@link AuthenticationCallback} object for async
     *                             call.
     */
    public void acquireToken(String resource, String clientId, @Nullable String redirectUri,
                             @Nullable String loginHint, @Nullable PromptBehavior prompt, @Nullable String extraQueryParameters,
                             AuthenticationCallback<AuthenticationResult> callback) {

        acquireToken(resource, clientId, redirectUri, loginHint, prompt, extraQueryParameters,
                null, callback, EventStrings.ACQUIRE_TOKEN_7, null, true);
    }

    /**
     * acquireToken will authorize an end user to call the specified resource.
     * The access token returned from the STS will be sent to the {@link AuthenticationCallback}
     * and can be used to call the specified protected resource.
     * Bypasses token cache if @param prompt is not AUTO or claims are passed.
     * This overload uses an
     * [AlertDialog](https://developer.android.com/guide/topics/ui/dialogs)
     * when user interaction is required. This overload does not support any flow
     * requiring a
     * [token broker](https://github.com/AzureAD/azure-activedirectory-library-for-android/wiki/Broker).
     *
     * @param resource             required resource identifier.
     * @param clientId             required client identifier.
     * @param redirectUri          Optional. It will use packagename and provided suffix
     *                             for this.
     * @param loginHint            Optional if validateAuthority == null. It is used for cache and as a loginhint at
     *                             authentication.
     * @param prompt               Optional. added as query parameter to authorization url
     * @param extraQueryParameters Optional. added to authorization url
     * @param claims               Optional. The claims challenge returned from middle tier service, will be added as query string
     *                             to authorize endpoint.
     * @param callback             required {@link AuthenticationCallback} object for async
     *                             call.
     */
    public void acquireToken(final String resource, final String clientId, @Nullable String redirectUri,
                             @Nullable final String loginHint, @Nullable final PromptBehavior prompt, @Nullable String extraQueryParameters,
                             @Nullable final String claims, final AuthenticationCallback<AuthenticationResult> callback) {

        acquireToken(resource, clientId, redirectUri, loginHint, prompt, extraQueryParameters,
                claims, callback, EventStrings.ACQUIRE_TOKEN_10, null, false);

    }

    private void acquireToken(final String resource, final String clientId, @Nullable String redirectUri,
                              @Nullable final String loginHint, @Nullable final PromptBehavior prompt,
                              @Nullable String extraQueryParameters, @Nullable final String claims,
                              final AuthenticationCallback<AuthenticationResult> callback, String apiEventString,
                              final IWindowComponent fragment, final boolean useDialog) {

        throwIfClaimsInBothExtraQpAndClaimsParameter(claims, extraQueryParameters);

        try {
            validateClaims(claims);
        } catch (AuthenticationException e) {
            callback.onError(e);
            return;
        }

        if (checkPreRequirements(resource, clientId, callback) && checkADFSValidationRequirements(loginHint, callback)) {
            redirectUri = getRedirectUri(redirectUri);
            final String requestId = Telemetry.registerNewRequest();
            final APIEvent apiEvent = createApiEvent(mContext, clientId, requestId, apiEventString);
            apiEvent.setPromptBehavior(prompt);

            final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource,
                    clientId, redirectUri, loginHint, prompt, extraQueryParameters,
                    getRequestCorrelationId(), getExtendedLifetimeEnabled(), claims);
            request.setTelemetryRequestId(requestId);
            setAppInfoToRequest(request);
            request.setClientCapabilities(mClientCapabilites);

            if (!StringExtensions.isNullOrBlank(loginHint)) {
                apiEvent.setLoginHint(loginHint);
                request.setUserIdentifierType(UserIdentifierType.LoginHint);
            }

            createAcquireTokenRequest(apiEvent).acquireToken(fragment, useDialog, request, callback);
        }

    }

    /**
     * This is sync function. It will first look at the cache and automatically
     * checks for the token expiration. Additionally, if no suitable access
     * token is found in the cache, but refresh token is available, the function
     * will use the refresh token automatically. This method will not show UI
     * for the user. If prompt is needed, the method will return an exception
     *
     * @param resource required resource identifier.
     * @param clientId required client identifier.
     * @param userId   UserID obtained from
     *                 {@link AuthenticationResult #getUserInfo()}
     * @return A {@link Future} object representing the
     * {@link AuthenticationResult} of the call. It contains Access
     * Token,the Access Token's expiration time, Refresh token, and
     * {@link UserInfo}.
     * @throws AuthenticationException If silent request fails to get the token back.
     * @throws InterruptedException    If the main thread is interrupted before or during the activity.
     */
    public AuthenticationResult acquireTokenSilentSync(String resource, String clientId, String userId)
            throws AuthenticationException, InterruptedException {
        return acquireTokenSilentSync(resource, clientId, userId, false, null, EventStrings.ACQUIRE_TOKEN_SILENT_SYNC);
    }

    /**
     * This is sync function. It will first look at the cache and automatically
     * checks for the token expiration. Additionally, if no suitable access
     * token is found in the cache, but refresh token is available, the function
     * will use the refresh token automatically. This method will not show UI
     * for the user. If prompt is needed, the method will return an exception
     *
     * @param resource required resource identifier.
     * @param clientId required client identifier.
     * @param userId   UserID obtained from
     *                 {@link AuthenticationResult #getUserInfo()}
     * @param claims   Optional. The claims challenge returned from middle tier service, will be added as query string
     *                 to authorize endpoint.
     * @return A {@link Future} object representing the
     * {@link AuthenticationResult} of the call. It contains Access
     * Token,the Access Token's expiration time, Refresh token, and
     * {@link UserInfo}.
     * @throws AuthenticationException If silent request fails to get the token back.
     * @throws InterruptedException    If the main thread is interrupted before or during the activity.
     */
    public AuthenticationResult acquireTokenSilentSync(String resource, String clientId, String userId, @Nullable String claims)
            throws AuthenticationException, InterruptedException {
        return acquireTokenSilentSync(resource, clientId, userId, false, claims, EventStrings.ACQUIRE_TOKEN_SILENT_SYNC_CLAIMS_CHALLENGE);
    }

    /**
     * This is sync function. It will first look at the cache and automatically
     * checks for the token expiration. Additionally, if no suitable access
     * token is found in the cache, but refresh token is available, the function
     * will use the refresh token automatically. This method will not show UI
     * for the user. If prompt is needed, the method will return an exception
     *
     * @param resource     required resource identifier.
     * @param clientId     required client identifier.
     * @param userId       UserID obtained from
     *                     {@link AuthenticationResult #getUserInfo()}
     * @param forceRefresh when true, access token is renewed using broker if available; otherwise, uses local refresh token
     * @return A {@link Future} object representing the
     * {@link AuthenticationResult} of the call. It contains Access
     * Token,the Access Token's expiration time, Refresh token, and
     * {@link UserInfo}.
     * @throws AuthenticationException If silent request fails to get the token back.
     * @throws InterruptedException    If the main thread is interrupted before or during the activity.
     */
    public AuthenticationResult acquireTokenSilentSync(String resource, String clientId, String userId, boolean forceRefresh)
            throws AuthenticationException, InterruptedException {
        return acquireTokenSilentSync(resource, clientId, userId, forceRefresh, null, EventStrings.ACQUIRE_TOKEN_SILENT_SYNC_FORCE_REFRESH);
    }

    private AuthenticationResult acquireTokenSilentSync(final String resource,
                                                        final String clientId,
                                                        final String userId,
                                                        final boolean forceRefresh,
                                                        final String claims,
                                                        final String apiEventString)
            throws AuthenticationException, InterruptedException {
        return acquireTokenSilentSync(null, null, resource, clientId, userId,
                UserIdentifierType.UniqueId, forceRefresh, claims, apiEventString);
    }

    /**
     * This function tries to acquire token silently. It will first look at the cache
     * and automatically checks for the token expiration. Additionally, if no suitable
     * access token is found in the cache, but refresh token is available, the function
     * will use the refresh token automatically. If both RT and AT are not present, then
     * it will use the provided assertion and its type to acquire the same.
     * This method will not show UI for the user. If prompt is needed, the method
     * will return an exception
     *
     * @param assertion     the actual saml assertion
     * @param assertionType version of saml assertion being used
     * @param resource      required resource identifier.
     * @param clientId      required client identifier.
     * @param userId        UserID obtained from
     *                      {@link AuthenticationResult #getUserInfo()}
     * @return A {@link Future} object representing the
     * {@link AuthenticationResult} of the call. It contains Access
     * Token,the Access Token's expiration time, Refresh token, and
     * {@link UserInfo}.
     * @throws AuthenticationException If silent request fails to get the token back.
     * @throws InterruptedException    If the main thread is interrupted before or during the activity.
     */
    public AuthenticationResult acquireTokenSilentSyncWithAssertion(@NonNull final String assertion,
                                                                    @NonNull final String assertionType,
                                                                    @NonNull final String resource,
                                                                    @NonNull final String clientId,
                                                                    @NonNull final String userId)
            throws AuthenticationException, InterruptedException {
        return acquireTokenSilentSync(assertion, assertionType, resource, clientId, userId, UserIdentifierType.LoginHint,
                false, null, EventStrings.ACQUIRE_TOKEN_WITH_SAML_ASSERTION);
    }

    private AuthenticationResult acquireTokenSilentSync(@Nullable final String assertion,
                                                        @Nullable final String assertionType,
                                                        @NonNull final String resource,
                                                        @NonNull final String clientId,
                                                        final String userId,
                                                        final UserIdentifierType identifier,
                                                        final boolean forceRefresh,
                                                        final String claims,
                                                        final String apiEventString)
            throws AuthenticationException, InterruptedException {
        final String methodName = ":acquireTokenSilentSync";
        validateClaims(claims);
        checkPreRequirements(resource, clientId);
        checkADFSValidationRequirements(null);

        final AtomicReference<AuthenticationResult> authenticationResult = new AtomicReference<>();
        final AtomicReference<Exception> exception = new AtomicReference<>();
        final CountDownLatch latch = new CountDownLatch(1);

        final String requestId = Telemetry.registerNewRequest();
        final APIEvent apiEvent = createApiEvent(mContext, clientId, requestId, apiEventString);
        apiEvent.setPromptBehavior(PromptBehavior.Auto.toString());
        final AuthenticationRequest request = new AuthenticationRequest(assertion, assertionType, mAuthority, resource,
                clientId, userId, getRequestCorrelationId(), getExtendedLifetimeEnabled(), forceRefresh, claims);


        request.setSilent(true);
        request.setPrompt(PromptBehavior.Auto);
        request.setUserIdentifierType(identifier);
        request.setTelemetryRequestId(requestId);
        request.setClientCapabilities(mClientCapabilites);
        setAppInfoToRequest(request);

        final Looper currentLooper = Looper.myLooper();
        if (currentLooper != null && currentLooper == mContext.getMainLooper()) {
            Logger.e(TAG + methodName,
                    "Sync network calls must not be invoked in main thread. "
                            + "This method will throw android.os.NetworkOnMainThreadException in next major release",
                    new NetworkOnMainThreadException());
        }
        createAcquireTokenRequest(apiEvent).acquireToken(null, false, request,
                new AuthenticationCallback<AuthenticationResult>() {
                    @Override
                    public void onSuccess(AuthenticationResult result) {
                        authenticationResult.set(result);
                        latch.countDown();
                    }

                    @Override
                    public void onError(Exception exc) {
                        exception.set(exc);
                        latch.countDown();
                    }
                });

        latch.await();

        Exception e = exception.get();
        if (e != null) {
            if (e instanceof AuthenticationException) {
                throw (AuthenticationException) e;
            } else if (e instanceof RuntimeException) {
                throw (RuntimeException) e;
            }
            if (e.getCause() != null) {
                if (e.getCause() instanceof AuthenticationException) {
                    throw (AuthenticationException) e.getCause();
                } else if (e.getCause() instanceof RuntimeException) {
                    throw (RuntimeException) e.getCause();
                } else {
                    throw new AuthenticationException(ADALError.ERROR_SILENT_REQUEST, e.getCause()
                            .getMessage(), e.getCause());
                }
            }
            throw new AuthenticationException(ADALError.ERROR_SILENT_REQUEST, e.getMessage(), e);
        }

        return authenticationResult.get();
    }

    /**
     * The function will first look at the cache and automatically checks for
     * the token expiration. Additionally, if no suitable access token is found
     * in the cache, but refresh token is available, the function will use the
     * refresh token automatically. This method will not show UI for the user.
     * If prompt is needed, the method will return an exception
     *
     * @param resource required resource identifier.
     * @param clientId required client identifier.
     * @param userId   UserId obtained from {@link UserInfo} inside
     *                 {@link AuthenticationResult}
     * @param callback required {@link AuthenticationCallback} object for async
     *                 call.
     * @return A {@link Future} object representing the
     * {@link AuthenticationResult} of the call. It contains Access
     * Token,the Access Token's expiration time, Refresh token, and
     * {@link UserInfo}.
     * @deprecated Use the {@link #acquireTokenSilentAsync} method.
     */
    @Deprecated
    public Future<AuthenticationResult> acquireTokenSilent(String resource, String clientId,
                                                           String userId, final AuthenticationCallback<AuthenticationResult> callback) {
        final SettableFuture<AuthenticationResult> futureTask = new SettableFuture<>();
        try {
            checkPreRequirements(resource, clientId);
            checkADFSValidationRequirements(null);
        } catch (final AuthenticationException e) {
            callback.onError(e);
            futureTask.setException(e);
            // AD FS validation is stipulated, but cannot be fulfilled.
            // Return execution with an Exception
            return futureTask;
        }

        final String requestId = Telemetry.registerNewRequest();
        final APIEvent apiEvent = createApiEvent(mContext, clientId, requestId, EventStrings.ACQUIRE_TOKEN_SILENT);
        apiEvent.setIsDeprecated(true);

        final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource,
                clientId, userId, getRequestCorrelationId(), getExtendedLifetimeEnabled());
        request.setSilent(true);
        request.setPrompt(PromptBehavior.Auto);
        request.setUserIdentifierType(UserIdentifierType.UniqueId);

        request.setTelemetryRequestId(requestId);
        setAppInfoToRequest(request);


        createAcquireTokenRequest(apiEvent).acquireToken(null, false, request,
                new AuthenticationCallback<AuthenticationResult>() {
                    @Override
                    public void onSuccess(AuthenticationResult result) {
                        apiEvent.setWasApiCallSuccessful(true, null);
                        apiEvent.setCorrelationId(request.getCorrelationId().toString());
                        apiEvent.setIdToken(result.getIdToken());
                        apiEvent.stopTelemetryAndFlush();

                        if (callback != null) {
                            callback.onSuccess(result);
                        }

                        futureTask.set(result);
                    }

                    @Override
                    @SuppressWarnings("unchecked")
                    public void onError(Exception exc) {
                        apiEvent.setWasApiCallSuccessful(false, exc);
                        apiEvent.setCorrelationId(request.getCorrelationId().toString());
                        apiEvent.stopTelemetryAndFlush();

                        if (callback != null) {
                            callback.onError(exc);
                        }

                        futureTask.setException(exc);
                    }
                });
        return futureTask;
    }

    /**
     * The function will first look at the cache and automatically checks for
     * the token expiration. Additionally, if no suitable access token is found
     * in the cache, but refresh token is available, the function will use the
     * refresh token automatically. This method will not show UI for the user.
     * If prompt is needed, the method will return an exception
     *
     * @param resource required resource identifier.
     * @param clientId required client identifier.
     * @param userId   UserId obtained from {@link UserInfo} inside
     *                 {@link AuthenticationResult}
     * @param callback required {@link AuthenticationCallback} object for async
     *                 call.
     */
    public void acquireTokenSilentAsync(String resource,
                                        String clientId,
                                        String userId,
                                        AuthenticationCallback<AuthenticationResult> callback) {
        acquireTokenSilentAsync(null, null, resource, clientId, userId, UserIdentifierType.UniqueId,
                false, null, EventStrings.ACQUIRE_TOKEN_SILENT_ASYNC, callback);
    }

    /**
     * The function will first look at the cache and automatically checks for
     * the token expiration. Additionally, if no suitable access token is found
     * in the cache, but refresh token is available, the function will use the
     * refresh token automatically. This method will not show UI for the user.
     * If prompt is needed, the method will return an exception
     *
     * @param resource     required resource identifier.
     * @param clientId     required client identifier.
     * @param userId       UserId obtained from {@link UserInfo} inside
     *                     {@link AuthenticationResult}
     * @param forceRefresh when true, access token is renewed using broker if available; otherwise, uses local refresh token
     * @param callback     required {@link AuthenticationCallback} object for async
     *                     call.
     */
    public void acquireTokenSilentAsync(String resource,
                                        String clientId,
                                        String userId,
                                        boolean forceRefresh,
                                        AuthenticationCallback<AuthenticationResult> callback) {
        acquireTokenSilentAsync(null, null, resource, clientId, userId, UserIdentifierType.UniqueId,
                forceRefresh, null, EventStrings.ACQUIRE_TOKEN_SILENT_ASYNC_FORCE_REFRESH, callback);
    }

    /**
     * The function will first look at the cache and automatically checks for
     * the token expiration. Additionally, if no suitable access token is found
     * in the cache, but refresh token is available, the function will use the
     * refresh token automatically. This method will not show UI for the user.
     * If prompt is needed, the method will return an exception
     *
     * @param resource required resource identifier.
     * @param clientId required client identifier.
     * @param userId   UserId obtained from {@link UserInfo} inside
     *                 {@link AuthenticationResult}
     * @param claims   Optional. The claims challenge returned from middle tier service, will be added as query string
     *                 to authorize endpoint.
     * @param callback required {@link AuthenticationCallback} object for async
     *                 call.
     */
    public void acquireTokenSilentAsync(String resource,
                                        String clientId,
                                        String userId,
                                        @Nullable String claims,
                                        AuthenticationCallback<AuthenticationResult> callback) {
        acquireTokenSilentAsync(null, null, resource, clientId, userId, UserIdentifierType.UniqueId,
                false, claims, EventStrings.ACQUIRE_TOKEN_SILENT_ASYNC_CLAIMS_CHALLENGE, callback);
    }

    /**
     * This function tries to acquire token silently. It will first look at the cache
     * and automatically checks for the token expiration. Additionally, if no suitable
     * access token is found in the cache, but refresh token is available, the function
     * will use the refresh token automatically. If both RT and AT are not present, then
     * it will use the provided assertion and its type to acquire the same.
     * This method will not show UI for the user. If prompt is needed, the method
     * will return an exception
     *
     * @param assertion     the actual saml assertion
     * @param assertionType version of saml assertion being used
     * @param resource      required resource identifier.
     * @param clientId      required client identifier.
     * @param userId        UserID obtained from
     *                      {@link AuthenticationResult #getUserInfo()}
     * @param callback      required {@link AuthenticationCallback} object for async
     *                      call.
     */
    public void acquireTokenSilentAsyncWithAssertion(@NonNull final String assertion,
                                                     @NonNull final String assertionType,
                                                     final String resource,
                                                     final String clientId,
                                                     final String userId,
                                                     AuthenticationCallback<AuthenticationResult> callback) {
        acquireTokenSilentAsync(assertion, assertionType, resource, clientId, userId, UserIdentifierType.LoginHint,
                false, null, EventStrings.ACQUIRE_TOKEN_WITH_SAML_ASSERTION, callback);
    }

    private void acquireTokenSilentAsync(final String assertion,
                                         final String assertionType,
                                         final String resource,
                                         final String clientId,
                                         final String userId,
                                         final UserIdentifierType identifierType,
                                         final boolean forceRefresh,
                                         final String claims,
                                         final String apiEventString,
                                         final AuthenticationCallback<AuthenticationResult> callback) {

        if (!checkPreRequirements(resource, clientId, callback) || !checkADFSValidationRequirements(null, callback)) {
            // AD FS validation cannot be perfomed, stop executing
            return;
        }
        try {
            validateClaims(claims);
        } catch (AuthenticationException e) {
            callback.onError(e);
            return;
        }

        final String requestId = Telemetry.registerNewRequest();
        final APIEvent apiEvent = createApiEvent(mContext, clientId, requestId,
                apiEventString);
        apiEvent.setPromptBehavior(PromptBehavior.Auto.toString());

        final AuthenticationRequest request = new AuthenticationRequest(assertion, assertionType, mAuthority, resource,
                clientId, userId, getRequestCorrelationId(), getExtendedLifetimeEnabled(), forceRefresh, claims);
        request.setSilent(true);
        request.setPrompt(PromptBehavior.Auto);
        request.setUserIdentifierType(identifierType);
        request.setClientCapabilities(mClientCapabilites);
        setAppInfoToRequest(request);

        request.setTelemetryRequestId(requestId);

        createAcquireTokenRequest(apiEvent).acquireToken(null, false, request, callback);

    }


    /**
     * acquire token using refresh token if cache is not used. Otherwise, use
     * acquireToken to let the ADAL handle the cache lookup and refresh token
     * request.
     *
     * @param refreshToken Required.
     * @param clientId     Required.
     * @param callback     Required
     * @deprecated {@link AuthenticationContext#acquireTokenByRefreshToken(String, String, AuthenticationCallback)}
     * will be removed from the next major release. We don't recommend developers taking dependency on refresh token,
     * ADAL will handle the cache lookup, and refresh the RT if necessary.
     */
    @Deprecated
    public void acquireTokenByRefreshToken(String refreshToken, String clientId,
                                           AuthenticationCallback<AuthenticationResult> callback) {
        if (!checkADFSValidationRequirements(null, callback)) {
            return;
        }

        if (StringExtensions.isNullOrBlank(refreshToken)) {
            throw new IllegalArgumentException("Refresh token is not provided");
        }

        if (StringExtensions.isNullOrBlank(clientId)) {
            throw new IllegalArgumentException("ClientId is not provided");
        }

        if (callback == null) {
            throw new IllegalArgumentException("Callback is not provided");
        }

        final String requestId = Telemetry.registerNewRequest();
        final APIEvent apiEvent = createApiEvent(mContext, clientId, requestId,
                EventStrings.ACQUIRE_TOKEN_WITH_REFRESH_TOKEN);
        apiEvent.setPromptBehavior(PromptBehavior.Auto.toString());
        apiEvent.setIsDeprecated(true);

        final AuthenticationRequest request = new AuthenticationRequest(mAuthority,
                null, clientId, getRequestCorrelationId(), getExtendedLifetimeEnabled(),
                AuthenticationConstants.OAuth2Scopes.OPEN_ID_SCOPE
        );

        // It is not using cache and refresh is not expected to
        // show authentication activity.
        request.setSilent(true);

        request.setTelemetryRequestId(requestId);

        // Broker is not supported for the Acquire Token By Refresh Token API
        createAcquireTokenRequest(apiEvent).refreshTokenWithoutCache(refreshToken, request, callback);
    }

    /**
     * acquire token using refresh token if cache is not used. Otherwise, use
     * acquireToken to let the ADAL handle the cache lookup and refresh token
     * request.
     *
     * @param refreshToken Required.
     * @param clientId     Required.
     * @param resource     Required resource identifier.
     * @param callback     Required
     * @deprecated {@link AuthenticationContext#acquireTokenByRefreshToken(String, String, AuthenticationCallback)}
     * will be removed from the next major release. We don't recommend developers taking dependency on refresh token,
     * ADAL will handle the cache lookup, and refresh the RT if necessary.
     */
    @Deprecated
    public void acquireTokenByRefreshToken(String refreshToken, String clientId, String resource,
                                           AuthenticationCallback<AuthenticationResult> callback) {
        if (!checkADFSValidationRequirements(null, callback)) {
            return;
        }

        if (StringExtensions.isNullOrBlank(refreshToken)) {
            throw new IllegalArgumentException("Refresh token is not provided");
        }

        if (StringExtensions.isNullOrBlank(clientId)) {
            throw new IllegalArgumentException("ClientId is not provided");
        }

        if (callback == null) {
            throw new IllegalArgumentException("Callback is not provided");
        }

        final String requestId = Telemetry.registerNewRequest();
        final APIEvent apiEvent = createApiEvent(mContext, clientId, requestId,
                EventStrings.ACQUIRE_TOKEN_WITH_REFRESH_TOKEN_2);
        apiEvent.setPromptBehavior(PromptBehavior.Auto.toString());
        apiEvent.setIsDeprecated(true);

        // Broker is not supported for the Acquire Token By Refresh Token API
        final AuthenticationRequest request = new AuthenticationRequest(mAuthority,
                resource, clientId, getRequestCorrelationId(), getExtendedLifetimeEnabled(),
                AuthenticationConstants.OAuth2Scopes.OPEN_ID_SCOPE
        );

        request.setTelemetryRequestId(requestId);

        // It is not using cache and refresh is not expected to
        // show authentication activity.
        request.setSilent(true);
        createAcquireTokenRequest(apiEvent).refreshTokenWithoutCache(refreshToken, request, callback);
    }

    /**
     * This method wraps the implementation for onActivityResult at the related
     * Activity class. This method is called at UI thread.
     *
     * @param requestCode Request code provided at the start of the activity.
     * @param resultCode  Result code set from the activity.
     * @param data        {@link Intent}
     */
    public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
        final String methodName = ":onActivityResult";
        if (requestCode == BROWSER_FLOW) {

            if (data == null) {
                // If data is null, RequestId is unknown. It could not find
                // callback to respond to this request.
                Logger.e(TAG + methodName, "onActivityResult BROWSER_FLOW data is null.", "",
                        ADALError.ON_ACTIVITY_RESULT_INTENT_NULL);
                return;
            }

            AuthenticationRequestState waitingRequest;
            final Bundle extras = data.getExtras();
            final int requestId = extras.getInt(AuthenticationConstants.Browser.REQUEST_ID);

            synchronized (DELEGATE_MAP) {
                waitingRequest = DELEGATE_MAP.get(requestId);
            }

            if (waitingRequest != null) {
                final AcquireTokenRequest acquireTokenRequest =
                        new AcquireTokenRequest(mContext, this, waitingRequest.getAPIEvent());
                acquireTokenRequest.onActivityResult(requestCode, resultCode, data);
            } else {
                Logger.e(TAG + methodName,
                        "onActivityResult did not find the waiting request. "
                                + REQUEST_ID + requestId,
                        null,
                        ADALError.ON_ACTIVITY_RESULT_INTENT_NULL);
            }
        }
    }

    /**
     * Active authentication activity can be cancelled if it exists. It may not
     * be cancelled if activity is not launched yet. RequestId is the hashcode
     * of your AuthenticationCallback.
     *
     * @param requestId Hash code value of your callback to cancel activity
     *                  launch
     * @return true: if there is a valid waiting request and cancel message sent
     * successfully or if no waiting request exists. false: If the request could not be cancelled
     * @throws AuthenticationException if failed to get the waiting request
     */
    public boolean cancelAuthenticationActivity(final int requestId) throws AuthenticationException {
        final String methodName = ":cancelAuthenticationActivity";
        final AuthenticationRequestState waitingRequest = getWaitingRequest(requestId);

        if (waitingRequest == null || waitingRequest.getDelegate() == null) {
            // there is not any waiting callback
            Logger.v(TAG + methodName, "Current callback is empty. There is not any active authentication.");
            return true;
        }

        final String currentCorrelationInfo;
        if (waitingRequest.getRequest() != null) {
            currentCorrelationInfo = String.format(" CorrelationId: %s",
                    waitingRequest.getRequest().getCorrelationId().toString());
        } else {
            currentCorrelationInfo = "No correlation id associated with waiting request";
        }
        Logger.v(TAG + methodName, "Current callback is not empty. There is an active authentication Activity."
                + currentCorrelationInfo);

        // intent to cancel. Authentication activity registers for this message
        // at onCreate event.
        final Intent intent = new Intent(AuthenticationConstants.Browser.ACTION_CANCEL);
        final Bundle extras = new Bundle();
        intent.putExtras(extras);
        intent.putExtra(AuthenticationConstants.Browser.REQUEST_ID, requestId);
        // send intent to cancel any active authentication activity.
        // it may not cancel it, if activity takes some time to launch.

        final boolean cancelResult = LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);
        if (cancelResult) {
            // clear callback if broadcast message was successful
            Logger.v(TAG + methodName, "Cancel broadcast message was successful." + currentCorrelationInfo);
            waitingRequest.setCancelled(true);
            waitingRequest.getDelegate().onError(new AuthenticationCancelError(
                    "Cancel broadcast message was successful."));
        } else {
            // Activity is not launched yet or receiver is not registered
            Logger.w(TAG + methodName, "Cancel broadcast message was not successful." + currentCorrelationInfo,
                    "", ADALError.BROADCAST_CANCEL_NOT_SUCCESSFUL);
        }

        return cancelResult;
    }

    /**
     * Get the CorrelationId set by user.
     *
     * @return UUID
     */
    public UUID getRequestCorrelationId() {
        if (mRequestCorrelationId == null) {
            return UUID.randomUUID();
        }

        return mRequestCorrelationId;
    }

    /**
     * Set CorrelationId to requests.
     *
     * @param requestCorrelationId The correlation id {@link UUID} to be set in the request.
     */
    public void setRequestCorrelationId(final UUID requestCorrelationId) {
        this.mRequestCorrelationId = requestCorrelationId;
        Logger.setCorrelationId(requestCorrelationId);
    }

    private void setAppInfoToRequest(AuthenticationRequest request) {
        String packageName = mContext.getPackageName();
        request.setAppName(packageName);
        try {
            PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(packageName, 0);
            request.setAppVersion(packageInfo.versionName);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
    }

    private IWindowComponent wrapActivity(final Activity activity) {
        if (activity == null) {
            throw new IllegalArgumentException("activity");
        }

        return new IWindowComponent() {
            private Activity mRefActivity = activity;

            @Override
            public void startActivityForResult(Intent intent, int requestCode) {
                // if user closed an app or switched to another activity
                // mRefActivity can die before this method got invoked
                if (mRefActivity != null) {
                    mRefActivity.startActivityForResult(intent, requestCode);
                }
            }
        };
    }

    /**
     * Util method to merge
     * <pre>
     * Sample input claim :
     *   {
     *       "userinfo": {
     *           "given_name": {"essential": true},
     *           "email": {"essential": true},
     *       },
     *       "id_token": {
     *           "auth_time": {"essential": true},
     *       }
     *   }
     *
     * Sample capabilities list : [CP1, CP2 CP3]
     *
     * Output merged claims :
     *   {
     *       "userinfo": {
     *           "given_name": {
     *               "essential": true
     *           },
     *           "email": {
     *               "essential": true
     *           }
     *       },
     *       "id_token": {
     *           "auth_time": {
     *               "essential": true
     *           }
     *       },
     *       "access_token": {
     *           "xms_cc": {
     *               "values": ["CP1", "CP2"]
     *           }
     *       }
     *   }
     * </pre>
     * * @param claims input claims passed on acquireToken call
     * * @return merged claims with capabilities
     * * @throws JSONException if input claims is an invalid JSON
     */
    public static String mergeClaimsWithClientCapabilities(final String claims,
                                                           final List<String> clientCapabilities) {
        if (clientCapabilities == null || clientCapabilities.isEmpty()) {
            return claims;
        }
        String mergedClaims;
        final JSONArray capabilitiesArray = new JSONArray();

        for (String capability : clientCapabilities) {
            capabilitiesArray.put(capability);
        }
        try {
            final JSONObject capabilities = new JSONObject();
            final JSONObject values = new JSONObject();
            values.put("values", capabilitiesArray);
            capabilities.put(AuthenticationConstants.OAuth2.CLIENT_CAPABILITIES_CLAIMS_LIST, values);

            if (!TextUtils.isEmpty(claims)) {
                final JSONObject claimsJson = new JSONObject(claims);

                if (claimsJson.has(AuthenticationConstants.OAuth2.CLIENT_CAPABILITY_ACCESS_TOKEN)) {
                    final JSONObject accessTokenClaim = claimsJson.getJSONObject(AuthenticationConstants.OAuth2.CLIENT_CAPABILITY_ACCESS_TOKEN);
                    accessTokenClaim.put(AuthenticationConstants.OAuth2.CLIENT_CAPABILITIES_CLAIMS_LIST, values);
                    claimsJson.put(AuthenticationConstants.OAuth2.CLIENT_CAPABILITY_ACCESS_TOKEN, accessTokenClaim);
                } else {
                    claimsJson.put(AuthenticationConstants.OAuth2.CLIENT_CAPABILITY_ACCESS_TOKEN, capabilities);
                }
                mergedClaims = claimsJson.toString();

            } else {
                JSONObject claimsObject = new JSONObject();
                claimsObject.put(AuthenticationConstants.OAuth2.CLIENT_CAPABILITY_ACCESS_TOKEN, capabilities);
                mergedClaims = claimsObject.toString();
            }
        } catch (JSONException e) {
            // This case should never hit as we validate claims at acquireToken request level
            Logger.e(TAG, "Invalid json format for claims or Client capabilities ", e);
            return claims;
        }

        return mergedClaims;
    }

    private boolean checkPreRequirements(final String resource, final String clientId) throws AuthenticationException {
        if (mContext == null) {
            throw new IllegalArgumentException("context", new AuthenticationException(
                    ADALError.DEVELOPER_CONTEXT_IS_NOT_PROVIDED));
        }

        if (AuthenticationSettings.INSTANCE.getUseBroker()) {
            mBrokerProxy.verifyBrokerPermissionsAPI22AndLess();
        }

        if (StringExtensions.isNullOrBlank(resource)) {
            throw new IllegalArgumentException("resource");
        }

        if (StringExtensions.isNullOrBlank(clientId)) {
            throw new IllegalArgumentException("clientId");
        }

        return true;
    }

    private boolean checkPreRequirements(final String resource, final String clientId, final AuthenticationCallback<AuthenticationResult> callback) {
        if (callback == null) {
            throw new IllegalArgumentException("callback");
        }

        try {
            return checkPreRequirements(resource, clientId);
        } catch (final AuthenticationException exception) {
            callback.onError(exception);
            return false;
        }
    }

    private boolean checkADFSValidationRequirements(@Nullable final String loginHint)
            throws AuthenticationException {
        final URL authorityURL = StringExtensions.getUrl(mAuthority);
        if (mAuthority == null || authorityURL == null) {
            throw new AuthenticationException(ADALError.DEVELOPER_AUTHORITY_IS_NOT_VALID_URL);
        }
        if (UrlExtensions.isADFSAuthority(authorityURL) // is it ADFS?
                && mValidateAuthority // should it be validated?
                && !mIsAuthorityValidated // has it not already been validated?
                && loginHint == null) { // did you provide insufficient info to perform the validation?
            throw new AuthenticationException(
                    ADALError.DEVELOPER_AUTHORITY_CAN_NOT_BE_VALIDED,
                    "AD FS validation requires a loginHint be provided or an "
                            + getClass().getSimpleName()
                            + " in which the current authority has previously been validated."
            );
        }
        return true;
    }

    private boolean checkADFSValidationRequirements(
            @Nullable final String loginHint,
            final AuthenticationCallback<AuthenticationResult> callback) {
        try {
            return checkADFSValidationRequirements(loginHint);
        } catch (AuthenticationException e) {
            callback.onError(e);
            return false;
        }
    }

    private String getRedirectUri(String inputRedirectUri) {
        final String redirectUri;
        if (StringExtensions.isNullOrBlank(inputRedirectUri)) {
            redirectUri = mContext.getApplicationContext().getPackageName();
        } else {
            redirectUri = inputRedirectUri;
        }

        return redirectUri;
    }

    private AcquireTokenRequest createAcquireTokenRequest(final APIEvent apiEvent) {
        return new AcquireTokenRequest(mContext, this, apiEvent);
    }

    private static String extractAuthority(String authority) {
        if (!StringExtensions.isNullOrBlank(authority)) {

            // excluding the starting https:// or http://
            int thirdSlash = authority.indexOf('/', EXCLUDE_INDEX);

            // third slash is not the last character
            if (thirdSlash >= 0 && thirdSlash != (authority.length() - 1)) {
                int fourthSlash = authority.indexOf("/", thirdSlash + 1);
                if (fourthSlash < 0 || fourthSlash > thirdSlash + 1) {
                    if (fourthSlash >= 0) {
                        return authority.substring(0, fourthSlash);
                    }

                    return authority;
                }
            }
        }

        throw new IllegalArgumentException("authority");
    }

    private void checkInternetPermission() {
        PackageManager pm = mContext.getPackageManager();
        if (PackageManager.PERMISSION_GRANTED != pm.checkPermission("android.permission.INTERNET",
                mContext.getPackageName())) {
            throw new IllegalStateException(new AuthenticationException(ADALError.DEVELOPER_INTERNET_PERMISSION_MISSING));
        }
    }

    /**
     * Internal API of ADAL to serialize the family token cache item for the
     * given user.
     * <p>
     * Verify if the input uniqueUserId is valid and the broker is not used.
     * Then check if this user has family refresh token item in the cache. If
     * true, create an SSOStateContainer object with the family refresh token
     * item of this user and continue the serialization process.
     *
     * @param uniqueUserId Unique user id used to lookup family token.
     * @return The serialized blob.
     * @throws AuthenticationException
     */
    String serialize(final String uniqueUserId) throws AuthenticationException {
        final String methodName = ":serialize";
        if (StringExtensions.isNullOrBlank(uniqueUserId)) {
            throw new IllegalArgumentException("uniqueUserId");
        }

        if (mBrokerProxy.canSwitchToBroker(mAuthority) != BrokerProxy.SwitchToBroker.CANNOT_SWITCH_TO_BROKER) {
            throw new UsageAuthenticationException(ADALError.FAIL_TO_EXPORT,
                    "Failed to export the family refresh token cache item because broker is enabled.");
        }

        /*
         * The current serialize/deserialize feature is for only supports MS
         * apps. So the client ID for the FoCI token cache item is hard coded
         * below.
         */
        final TokenCacheAccessor tokenCacheAccessor = new TokenCacheAccessor(this.mContext.getApplicationContext(), mTokenCacheStore, this.getAuthority(),
                Telemetry.registerNewRequest());
        final TokenCacheItem tokenItem;
        try {
            // TokenCacheAccessor has all the logic for token cache fallback lookup. If the metadata cache is not cached yet,
            // it basically means that there hasn't been any requests done with the authority, otherwise authority validation
            // cache is already filled up.
            tokenItem = tokenCacheAccessor.getFRTItem(AuthenticationConstants.MS_FAMILY_ID, uniqueUserId);
        } catch (final MalformedURLException e) {
            throw new AuthenticationException(ADALError.DEVELOPER_AUTHORITY_IS_NOT_VALID_URL, e.getMessage(), e);
        }

        if (tokenItem == null) {
            Logger.i(TAG + methodName, "Cannot find the family token cache item for this userID", "");
            throw new UsageAuthenticationException(ADALError.FAIL_TO_EXPORT,
                    "Failed to export the FID because no family token cache item is found.");
        }

        if (StringExtensions.isNullOrBlank(tokenItem.getFamilyClientId())) {
            throw new UsageAuthenticationException(ADALError.FAIL_TO_EXPORT, "tokenItem does not contain family refresh token");
        }

        return SSOStateSerializer.serialize(tokenItem);
    }

    /**
     * Internal API of ADAL to provide the deserialization to the TokenCacheItem
     * <p>
     * The method will take the serializedBlob string as input and deserialize
     * the string into a tokenCacheItem. The deserialized tokenCacheItem will be
     * stored into the cache. Exceptions will be thrown for invalid input or the
     * broker is enabled.
     *
     * @param serializedBlob The blob to be deserialized.
     * @throws AuthenticationException
     */
    void deserialize(final String serializedBlob) throws AuthenticationException {
        if (StringExtensions.isNullOrBlank(serializedBlob)) {
            throw new IllegalArgumentException("serializedBlob");
        }

        if (mBrokerProxy.canSwitchToBroker(mAuthority) != BrokerProxy.SwitchToBroker.CANNOT_SWITCH_TO_BROKER) {
            throw new UsageAuthenticationException(ADALError.FAIL_TO_IMPORT, "Failed to import the serialized blob "
                    + "because broker is enabled.");
        }

        // If the serialized blob is from an app which already integrates ADAL having authority migration support, the token cache
        // item already has the authority updated with preferred cache location (login.windows.net is the preferred cache location);
        // If the blob is from an old version of ADAL, the token cache item contains the passed in authority (Note: most of microsoft first
        // party app uses Token Share Library today are using login.windows.net).
        // If the app receives the blob has the version of adal supporting authority migration, lookup will always go to preferred location
        // first, we'll fall back to passed in authority host and all the aliased hosts if necessary; If app receives the blob has the old
        // version of adal, since all the apps using token share library are using login.windows.net, token lookup will keep working.
        final TokenCacheItem tokenCacheItem = SSOStateSerializer.deserialize(serializedBlob);
        if (StringExtensions.isNullOrBlank(tokenCacheItem.getAuthority()) ||
                (StringExtensions.isNullOrBlank(tokenCacheItem.getClientId()) && StringExtensions.isNullOrBlank(tokenCacheItem.getFamilyClientId()))) {
            throw new DeserializationAuthenticationException("Failed to deserialize the blob because authority or client id is null/empty.");
        }
        final String cacheKey = CacheKey.createCacheKey(tokenCacheItem);
        this.getCache().setItem(cacheKey, tokenCacheItem);
    }

    void setIsAuthorityValidated(final boolean isAuthorityValidated) {
        mIsAuthorityValidated = isAuthorityValidated;
    }

    boolean getIsAuthorityValidated() {
        return mIsAuthorityValidated;
    }

    AuthenticationRequestState getWaitingRequest(final int requestId) throws AuthenticationException {
        final String methodName = ":getWaitingRequest";
        Logger.v(TAG + methodName, "Get waiting request. " + REQUEST_ID + requestId);
        AuthenticationRequestState request;

        synchronized (DELEGATE_MAP) {
            request = DELEGATE_MAP.get(requestId);
        }

        if (request == null) {
            Logger.e(TAG + methodName, "Request callback is not available. "
                    + REQUEST_ID + requestId, null, ADALError.CALLBACK_IS_NOT_FOUND);
            throw new AuthenticationException(ADALError.CALLBACK_IS_NOT_FOUND,
                    "Request callback is not available for requestId:" + requestId);
        }

        return request;
    }

    void putWaitingRequest(final int requestId, final AuthenticationRequestState requestState) {
        if (requestState == null) {
            return;
        }

        Logger.v(TAG, "Put waiting request. "
                + REQUEST_ID + requestId + " "
                + getCorrelationInfoFromWaitingRequest(requestState));

        synchronized (DELEGATE_MAP) {
            DELEGATE_MAP.put(requestId, requestState);
        }
    }

    void removeWaitingRequest(int requestId) {
        Logger.v(TAG, "Remove waiting request. " + REQUEST_ID + requestId);

        synchronized (DELEGATE_MAP) {
            DELEGATE_MAP.remove(requestId);
        }
    }

    /**
     * If request has correlationID, ADAL should report that instead of current
     * CorrelationId.
     */
    String getCorrelationInfoFromWaitingRequest(final AuthenticationRequestState waitingRequest) {
        UUID requestCorrelationID = getRequestCorrelationId();
        if (waitingRequest.getRequest() != null) {
            requestCorrelationID = waitingRequest.getRequest().getCorrelationId();
        }

        return String.format(" CorrelationId: %s", requestCorrelationID.toString());
    }

    /**
     * Version name for ADAL not for the app itself.
     *
     * @return The current SDK version.
     */
    public static String getVersionName() {
        // Package manager does not report for ADAL
        // AndroidManifest files are not merged, so it is returning hard coded
        // value
        return BuildConfig.VERSION_NAME;
    }

    public List<String> getClientCapabilites() {
        return mClientCapabilites;
    }

    public void setClientCapabilites(List<String> clientCapabilites) {
        mClientCapabilites = clientCapabilites;
    }

    /**
     * A {@link Future}  whose result can be set by a {@link #set(Object)} or {@link #setException(Throwable)}.
     */
    static final class SettableFuture<V> extends FutureTask<V> {
        SettableFuture() {
            super(new Callable<V>() {
                @Override
                public V call() throws Exception {
                    return null;
                }
            });
        }

        @SuppressWarnings("PMD.UselessOverridingMethod")
        @Override
        public void set(V v) {
            super.set(v);
        }

        @SuppressWarnings("PMD.UselessOverridingMethod")
        @Override
        public void setException(Throwable t) {
            super.setException(t);
        }
    }

    private APIEvent createApiEvent(Context context, String clientId, String requestId, String apiId) {
        final APIEvent apiEvent = new APIEvent(EventStrings.API_EVENT, context, clientId);
        apiEvent.setRequestId(requestId);
        apiEvent.setAPIId(apiId);
        apiEvent.setAuthority(getAuthority());
        Telemetry.getInstance().startEvent(requestId, apiEvent.getEventName());
        return apiEvent;
    }

    private void throwIfClaimsInBothExtraQpAndClaimsParameter(final String claims, final String extraQP) {
        if (!StringExtensions.isNullOrBlank(claims) && !StringExtensions.isNullOrBlank(extraQP) && extraQP.contains(AuthenticationConstants.OAuth2.CLAIMS)) {
            throw new IllegalArgumentException("claims cannot be sent in claims parameter and extra qp.");
        }
    }

    private void validateClaims(final String claims) throws AuthenticationException {
        try {
            if (!TextUtils.isEmpty(claims)) {
                new JSONObject(claims);
            }
        } catch (JSONException e) {
            throw new AuthenticationException(ADALError.JSON_PARSE_ERROR, "Invalid claims request parameters", e);
        }
    }
}
