// Copyright 2017 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.autofill;

import android.content.ComponentName;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.view.View;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillValue;

import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.CollectionUtil;
import org.chromium.base.Log;
import org.chromium.build.annotations.DoNotStripLogs;

import java.lang.ref.WeakReference;
import java.util.ArrayList;

/**
 * The class to call Android's AutofillManager.
 */
@RequiresApi(Build.VERSION_CODES.O)
public class AutofillManagerWrapper {
    // Don't change TAG, it is used for runtime log.
    // NOTE: As a result of the above, the tag below still references the name of this class from
    // when it was originally developed specifically for Android WebView.
    public static final String TAG = "AwAutofillManager";
    private static final String AWG_COMPONENT_NAME =
            "com.google.android.gms/com.google.android.gms.autofill.service.AutofillService";
    /**
     * The observer of suggestion window.
     */
    public static interface InputUIObserver { void onInputUIShown(); }

    private static class AutofillInputUIMonitor extends AutofillManager.AutofillCallback {
        private WeakReference<AutofillManagerWrapper> mManager;

        public AutofillInputUIMonitor(AutofillManagerWrapper manager) {
            mManager = new WeakReference<AutofillManagerWrapper>(manager);
        }

        @Override
        public void onAutofillEvent(View view, int virtualId, int event) {
            AutofillManagerWrapper manager = mManager.get();
            if (manager == null) return;
            manager.mIsAutofillInputUIShowing = (event == EVENT_INPUT_SHOWN);
            if (event == EVENT_INPUT_SHOWN) manager.notifyInputUIChange();
        }
    }

    private static boolean sIsLoggable;
    private AutofillManager mAutofillManager;
    private boolean mIsAutofillInputUIShowing;
    private AutofillInputUIMonitor mMonitor;
    private boolean mDestroyed;
    private boolean mDisabled;
    private ArrayList<WeakReference<InputUIObserver>> mInputUIObservers;
    // Indicates if AwG is the current Android autofill service.
    private final boolean mIsAwGCurrentAutofillService;

    public AutofillManagerWrapper(Context context) {
        updateLogStat();
        if (isLoggable()) log("constructor");
        mAutofillManager = context.getSystemService(AutofillManager.class);
        mDisabled = mAutofillManager == null || !mAutofillManager.isEnabled();

        if (mDisabled) {
            mIsAwGCurrentAutofillService = false;
            if (isLoggable()) log("disabled");
            return;
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            ComponentName componentName = null;
            try {
                componentName = mAutofillManager.getAutofillServiceComponentName();
            } catch (Exception e) {
                // Can't catch com.android.internal.util.SyncResultReceiver.TimeoutException,
                // because
                // - The exception isn't Android API.
                // - Different version of Android handle it differently.
                // Uses Exception to catch various cases. (refer to crbug.com/1186406)
                Log.e(TAG, "getAutofillServiceComponentName", e);
            }
            if (componentName != null) {
                mIsAwGCurrentAutofillService =
                        AWG_COMPONENT_NAME.equals(componentName.flattenToString());
                AutofillProviderUMA.logCurrentProvider(componentName.getPackageName());
            } else {
                mIsAwGCurrentAutofillService = false;
            }
        } else {
            mIsAwGCurrentAutofillService = false;
        }
        mMonitor = new AutofillInputUIMonitor(this);
        mAutofillManager.registerCallback(mMonitor);
    }

    public void notifyVirtualValueChanged(View parent, int childId, AutofillValue value) {
        if (mDisabled || checkAndWarnIfDestroyed()) return;
        if (isLoggable()) log("notifyVirtualValueChanged");
        mAutofillManager.notifyValueChanged(parent, childId, value);
    }

    public void commit(int submissionSource) {
        if (mDisabled || checkAndWarnIfDestroyed()) return;
        if (isLoggable()) log("commit source:" + submissionSource);
        mAutofillManager.commit();
    }

    public void cancel() {
        if (mDisabled || checkAndWarnIfDestroyed()) return;
        if (isLoggable()) log("cancel");
        mAutofillManager.cancel();
    }

    public void notifyVirtualViewEntered(View parent, int childId, Rect absBounds) {
        // Log warning only when the autofill is triggered.
        if (mDisabled) {
            Log.w(TAG, "Autofill is disabled: AutofillManager isn't available in given Context.");
            return;
        }
        if (checkAndWarnIfDestroyed()) return;
        if (isLoggable()) log("notifyVirtualViewEntered");
        mAutofillManager.notifyViewEntered(parent, childId, absBounds);
    }

    public void notifyVirtualViewExited(View parent, int childId) {
        if (mDisabled || checkAndWarnIfDestroyed()) return;
        if (isLoggable()) log("notifyVirtualViewExited");
        mAutofillManager.notifyViewExited(parent, childId);
    }

    public void requestAutofill(View parent, int virtualId, Rect absBounds) {
        if (mDisabled || checkAndWarnIfDestroyed()) return;
        if (isLoggable()) log("requestAutofill");
        mAutofillManager.requestAutofill(parent, virtualId, absBounds);
    }

    public boolean isAutofillInputUIShowing() {
        if (mDisabled || checkAndWarnIfDestroyed()) return false;
        if (isLoggable()) log("isAutofillInputUIShowing: " + mIsAutofillInputUIShowing);
        return mIsAutofillInputUIShowing;
    }

    public void destroy() {
        if (mDisabled || checkAndWarnIfDestroyed()) return;
        if (isLoggable()) log("destroy");
        try {
            // The binder in the autofill service side might already be dropped,
            // unregisterCallback() will cause various exceptions in this
            // scenario (see crbug.com/1078337), catching RuntimeException here prevents crash.
            mAutofillManager.unregisterCallback(mMonitor);
        } catch (RuntimeException e) {
            // We are not logging anything here since some of the exceptions are raised as 'generic'
            // RuntimeException which makes it difficult to catch and ignore separately; and the
            // RuntimeException seemed only happen in Android O, therefore, isn't actionable.
        } finally {
            mAutofillManager = null;
            mDestroyed = true;
        }
    }

    public boolean isDisabled() {
        return mDisabled;
    }

    /**
     * Only work for Android P and beyond. Always return false for Android O.
     * @return if the Autofill with Google is the current autofill service.
     */
    public boolean isAwGCurrentAutofillService() {
        return mIsAwGCurrentAutofillService;
    }

    private boolean checkAndWarnIfDestroyed() {
        if (mDestroyed) {
            Log.w(TAG, "Application attempted to call on a destroyed AutofillManagerWrapper",
                    new Throwable());
        }
        return mDestroyed;
    }

    public void addInputUIObserver(InputUIObserver observer) {
        if (observer == null) return;
        if (mInputUIObservers == null) {
            mInputUIObservers = new ArrayList<WeakReference<InputUIObserver>>();
        }
        mInputUIObservers.add(new WeakReference<InputUIObserver>(observer));
    }

    @VisibleForTesting
    public void notifyInputUIChange() {
        for (InputUIObserver observer : CollectionUtil.strengthen(mInputUIObservers)) {
            observer.onInputUIShown();
        }
    }

    public void notifyNewSessionStarted(boolean hasServerPrediction) {
        updateLogStat();
        if (isLoggable()) log("Session starts, has server prediction = " + hasServerPrediction);
    }

    public void onQueryDone(boolean success) {
        if (isLoggable()) log("Query " + (success ? "succeed" : "failed"));
    }

    /**
     * Always check isLoggable() before call this method.
     */
    public static void log(String log) {
        // Log.i() instead of Log.d() is used here because log.d() is stripped out in release build.
        Log.i(TAG, log);
    }

    public static boolean isLoggable() {
        return sIsLoggable;
    }

    @DoNotStripLogs
    private static void updateLogStat() {
        // Use 'setprop log.tag.AwAutofillManager DEBUG' to enable the log at runtime.
        // NOTE: See the comment on TAG above for why this is still AwAutofillManager.
        // Check the system setting directly.
        sIsLoggable = android.util.Log.isLoggable(TAG, Log.DEBUG);
    }
}
