/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.view;

import static android.internal.perfetto.protos.Imefocuscontroller.ImeFocusControllerProto.HAS_IME_FOCUS;

import android.annotation.AnyThread;
import android.annotation.NonNull;
import android.annotation.UiThread;
import android.util.Log;
import android.util.proto.ProtoOutputStream;
import android.view.inputmethod.InputMethodManager;

import com.android.internal.inputmethod.InputMethodDebug;

/**
 * Responsible for IME focus handling inside {@link ViewRootImpl}.
 * @hide
 */
public final class ImeFocusController {
    private static final boolean DEBUG = false;
    private static final String TAG = "ImeFocusController";

    private final ViewRootImpl mViewRootImpl;
    private boolean mHasImeFocus = false;
    private InputMethodManagerDelegate mDelegate;

    @UiThread
    ImeFocusController(@NonNull ViewRootImpl viewRootImpl) {
        mViewRootImpl = viewRootImpl;
    }

    @NonNull
    private InputMethodManagerDelegate getImmDelegate() {
        if (mDelegate == null) {
            mDelegate = mViewRootImpl.mContext.getSystemService(
                    InputMethodManager.class).getDelegate();
        }
        return mDelegate;
    }

    /** Called when the view root is moved to a different display. */
    @UiThread
    void onMovedToDisplay() {
        // InputMethodManager managed its instances for different displays. So if the associated
        // display is changed, the delegate also needs to be refreshed (by getImmDelegate).
        // See the comment in {@link android.app.SystemServiceRegistry} for InputMethodManager
        // and {@link android.view.inputmethod.InputMethodManager#forContext}.
        mDelegate = null;
    }

    @UiThread
    void onTraversal(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute) {
        final boolean hasImeFocus = WindowManager.LayoutParams.mayUseInputMethod(
                windowAttribute.flags);
        if (!hasWindowFocus || isInLocalFocusMode(windowAttribute)) {
            return;
        }
        if (hasImeFocus == mHasImeFocus) {
            return;
        }
        mHasImeFocus = hasImeFocus;
        if (mHasImeFocus) {
            getImmDelegate().onPreWindowGainedFocus(mViewRootImpl);
            final View focusedView = mViewRootImpl.mView.findFocus();
            View viewForWindowFocus = focusedView != null ? focusedView : mViewRootImpl.mView;
            getImmDelegate().onPostWindowGainedFocus(viewForWindowFocus, windowAttribute);
        }
    }

    @UiThread
    void onPreWindowFocus(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute) {
        mHasImeFocus = WindowManager.LayoutParams.mayUseInputMethod(windowAttribute.flags);
        if (!hasWindowFocus || !mHasImeFocus || isInLocalFocusMode(windowAttribute)) {
            if (!hasWindowFocus) {
                getImmDelegate().onWindowLostFocus(mViewRootImpl);
            }
        } else {
            getImmDelegate().onPreWindowGainedFocus(mViewRootImpl);
        }
    }

    @UiThread
    void onPostWindowFocus(View focusedView, boolean hasWindowFocus,
            WindowManager.LayoutParams windowAttribute) {
        if (!hasWindowFocus || !mHasImeFocus || isInLocalFocusMode(windowAttribute)) {
            return;
        }
        View viewForWindowFocus = focusedView != null ? focusedView : mViewRootImpl.mView;
        if (DEBUG) {
            Log.v(TAG, "onWindowFocus: " + viewForWindowFocus
                    + " softInputMode=" + InputMethodDebug.softInputModeToString(
                    windowAttribute.softInputMode));
        }

        getImmDelegate().onPostWindowGainedFocus(viewForWindowFocus, windowAttribute);
    }

    /**
     * @see ViewRootImpl#dispatchCheckFocus()
     */
    @UiThread
    void onScheduledCheckFocus() {
        getImmDelegate().onScheduledCheckFocus(mViewRootImpl);
    }

    @UiThread
    void onViewFocusChanged(View view, boolean hasFocus) {
        getImmDelegate().onViewFocusChanged(view, hasFocus);
    }

    @UiThread
    void onViewDetachedFromWindow(View view) {
        getImmDelegate().onViewDetachedFromWindow(view, mViewRootImpl);

    }

    @UiThread
    void onWindowDismissed() {
        getImmDelegate().onWindowDismissed(mViewRootImpl);
        mHasImeFocus = false;
    }

    /**
     * @param windowAttribute {@link WindowManager.LayoutParams} to be checked.
     * @return Whether the window is in local focus mode or not.
     */
    @AnyThread
    private static boolean isInLocalFocusMode(WindowManager.LayoutParams windowAttribute) {
        return (windowAttribute.flags & WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE) != 0;
    }

    int onProcessImeInputStage(Object token, InputEvent event,
            WindowManager.LayoutParams windowAttribute,
            InputMethodManager.FinishedInputEventCallback callback) {
        if (!mHasImeFocus || isInLocalFocusMode(windowAttribute)) {
            return InputMethodManager.DISPATCH_NOT_HANDLED;
        }
        if (event instanceof KeyEvent keyEvent && keyEvent.getKeyCode() == KeyEvent.KEYCODE_BACK) {
            final var insetsController = mViewRootImpl.getInsetsController();
            if (insetsController.getAnimationType(WindowInsets.Type.ime())
                    == InsetsController.ANIMATION_TYPE_HIDE
                    || insetsController.isPredictiveBackImeHideAnimInProgress()) {
                // if there is an ongoing hide animation, the back event should not be dispatched
                // to the IME.
                return InputMethodManager.DISPATCH_NOT_HANDLED;
            }
        }
        final InputMethodManager imm =
                mViewRootImpl.mContext.getSystemService(InputMethodManager.class);
        if (imm == null) {
            return InputMethodManager.DISPATCH_NOT_HANDLED;
        }
        return imm.dispatchInputEvent(event, token, callback, mViewRootImpl.mHandler);
    }

    /**
     * A delegate implementing some basic {@link InputMethodManager} APIs.
     * @hide
     */
    public interface InputMethodManagerDelegate {
        void onPreWindowGainedFocus(ViewRootImpl viewRootImpl);
        void onPostWindowGainedFocus(View viewForWindowFocus,
                @NonNull WindowManager.LayoutParams windowAttribute);
        void onWindowLostFocus(@NonNull ViewRootImpl viewRootImpl);
        void onViewFocusChanged(@NonNull View view, boolean hasFocus);
        void onScheduledCheckFocus(@NonNull ViewRootImpl viewRootImpl);
        void onViewDetachedFromWindow(View view, ViewRootImpl viewRootImpl);
        void onWindowDismissed(ViewRootImpl viewRootImpl);
    }

    /**
     * Indicates whether the view's window has IME focused.
     */
    @UiThread
    boolean hasImeFocus() {
        return mHasImeFocus;
    }

    void dumpDebug(ProtoOutputStream proto, long fieldId) {
        final long token = proto.start(fieldId);
        proto.write(HAS_IME_FOCUS, mHasImeFocus);
        proto.end(token);
    }
}
