/*
 * Copyright (C) 2018 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 com.android.server.wm;

import static android.internal.perfetto.protos.Windowmanagerservice.DisplayContentProto.IME_INSETS_SOURCE_PROVIDER;
import static android.internal.perfetto.protos.Windowmanagerservice.DisplayContentProto.INSETS_SOURCE_PROVIDERS;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.view.InsetsSource.FLAG_FORCE_CONSUMING;
import static android.view.InsetsSource.ID_IME;
import static android.view.WindowInsets.Type.displayCutout;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsets.Type.mandatorySystemGestures;
import static android.view.WindowInsets.Type.systemGestures;

import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_IME;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Trace;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.SparseArray;
import android.util.SparseLongArray;
import android.util.proto.ProtoOutputStream;
import android.view.InsetsSource;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.WindowInsets;
import android.view.WindowInsets.Type.InsetsType;
import android.view.inputmethod.ImeTracker;

import com.android.internal.protolog.ProtoLog;
import com.android.server.inputmethod.InputMethodManagerInternal;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.function.Consumer;

/**
 * Manages global window inset state in the system represented by {@link InsetsState}.
 */
class InsetsStateController {

    @NonNull
    private final InsetsState mLastState = new InsetsState();
    @NonNull
    private final InsetsState mState = new InsetsState();
    @NonNull
    private final DisplayContent mDisplayContent;
    @NonNull
    private final SparseArray<InsetsSourceProvider> mProviders = new SparseArray<>();
    @NonNull
    private final SparseLongArray mSurfaceTransactionIds = new SparseLongArray();
    @NonNull
    private final ArrayMap<InsetsControlTarget, ArrayList<InsetsSourceProvider>>
            mControlTargetProvidersMap = new ArrayMap<>();
    @NonNull
    private final ArrayMap<InsetsControlTarget, ArrayList<InsetsSourceProvider>>
            mPendingTargetProvidersMap = new ArrayMap<>();
    @NonNull
    private final SparseArray<InsetsControlTarget> mIdControlTargetMap = new SparseArray<>();
    @NonNull
    private final SparseArray<InsetsControlTarget> mIdFakeControlTargetMap = new SparseArray<>();

    @NonNull
    private final Consumer<WindowState> mDispatchInsetsChanged = w -> {
        if (w.isReadyToDispatchInsetsState()) {
            w.notifyInsetsChanged();
        }
    };

    /**
     * Empty instance of the IME control target, to enable hiding the IME when there is no real
     * control target.
     */
    @NonNull
    private final InsetsControlTarget mEmptyImeControlTarget = new InsetsControlTarget() {
        @Override
        public void notifyInsetsControlChanged(int displayId) {
            InsetsSourceControl[] controls = getControlsForDispatch(this);
            if (controls == null) {
                return;
            }
            for (InsetsSourceControl control : controls) {
                if (control.getType() == WindowInsets.Type.ime()) {
                    mDisplayContent.mWmService.mH.post(() ->
                            InputMethodManagerInternal.get().removeImeSurface(displayId));
                }
            }
        }
    };

    @InsetsType
    private int mForcedConsumingTypes;

    InsetsStateController(@NonNull DisplayContent displayContent) {
        mDisplayContent = displayContent;
    }

    @NonNull
    InsetsState getRawInsetsState() {
        return mState;
    }

    @Nullable
    InsetsSourceControl[] getControlsForDispatch(@NonNull InsetsControlTarget target) {
        final ArrayList<InsetsSourceProvider> controlled = mControlTargetProvidersMap.get(target);
        if (controlled == null) {
            return null;
        }
        final int size = controlled.size();
        final var result = new InsetsSourceControl[size];
        for (int i = 0; i < size; i++) {
            result[i] = controlled.get(i).getControl(target);
        }
        return result;
    }

    @NonNull
    SparseArray<InsetsSourceProvider> getSourceProviders() {
        return mProviders;
    }

    /**
     * @return The provider of a specific source ID.
     */
    @NonNull
    InsetsSourceProvider getOrCreateSourceProvider(int id, @InsetsType int type) {
        InsetsSourceProvider provider = mProviders.get(id);
        if (provider != null) {
            return provider;
        }
        final InsetsSource source = mState.getOrCreateSource(id, type);
        provider = id == ID_IME
                ? new ImeInsetsSourceProvider(source, this, mDisplayContent)
                : new InsetsSourceProvider(source, this, mDisplayContent);
        provider.setFlags(
                (mForcedConsumingTypes & type) != 0
                        ? FLAG_FORCE_CONSUMING
                        : 0,
                FLAG_FORCE_CONSUMING);
        mProviders.put(id, provider);
        return provider;
    }

    @NonNull
    ImeInsetsSourceProvider getImeSourceProvider() {
        return (ImeInsetsSourceProvider) getOrCreateSourceProvider(ID_IME, ime());
    }

    void removeSourceProvider(int id) {
        if (id != ID_IME) {
            mState.removeSource(id);
            mProviders.remove(id);
        }
    }

    void setForcedConsumingTypes(@InsetsType int types) {
        if (mForcedConsumingTypes != types) {
            mForcedConsumingTypes = types;
            boolean changed = false;
            for (int i = mProviders.size() - 1; i >= 0; i--) {
                final InsetsSourceProvider provider = mProviders.valueAt(i);
                changed |= provider.setFlags(
                        (types & provider.getSource().getType()) != 0
                                ? FLAG_FORCE_CONSUMING
                                : 0,
                        FLAG_FORCE_CONSUMING);
            }
            if (changed) {
                notifyInsetsChanged();
            }
        }
    }

    /**
     * Called before a layout pass will occur.
     */
    void onPreLayout() {
        if (!android.view.inputmethod.Flags.setServerVisibilityOnprelayout()) {
            return;
        }
        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "ISC.onPreLayout");
        for (int i = mProviders.size() - 1; i >= 0; i--) {
            mProviders.valueAt(i).onPreLayout();
        }
        Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
    }

    /**
     * Called after a layout pass has occurred.
     */
    void onPostLayout() {
        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "ISC.onPostLayout");
        for (int i = mProviders.size() - 1; i >= 0; i--) {
            mProviders.valueAt(i).onPostLayout();
        }
        if (!mLastState.equals(
                mState,
                false /* excludesCaptionBar */,
                false /* excludesInvisibleIme */,
                true /* excludesInvalidSource */)) {
            mLastState.set(mState, true /* copySources */);
            notifyInsetsChanged();
        }
        Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
    }

    /**
     * Updates {@link WindowState#mAboveInsetsState} for all windows in the display.
     *
     * @param notifyInsetsChange {@code true} if the clients should be notified about the change.
     */
    void updateAboveInsetsState(boolean notifyInsetsChange) {
        final var aboveInsetsState = new InsetsState();
        aboveInsetsState.set(mState,
                displayCutout() | systemGestures() | mandatorySystemGestures());
        final var localInsetsSourcesFromParent = new SparseArray<InsetsSource>();
        final var insetsChangedWindows = new ArraySet<WindowState>();

        // This method will iterate on the entire hierarchy in top to bottom z-order manner. The
        // aboveInsetsState will be modified as per the insets provided by the WindowState being
        // visited.
        mDisplayContent.updateAboveInsetsState(aboveInsetsState, localInsetsSourcesFromParent,
                insetsChangedWindows);

        if (notifyInsetsChange) {
            for (int i = insetsChangedWindows.size() - 1; i >= 0; i--) {
                mDispatchInsetsChanged.accept(insetsChangedWindows.valueAt(i));
            }
        }
    }

    void onDisplayFramesUpdated(boolean notifyInsetsChange) {
        final ArrayList<WindowState> insetsChangedWindows = new ArrayList<>();
        mDisplayContent.forAllWindows(w -> {
            w.mAboveInsetsState.set(mState, displayCutout());
            insetsChangedWindows.add(w);
        }, true /* traverseTopToBottom */);
        if (notifyInsetsChange) {
            for (int i = insetsChangedWindows.size() - 1; i >= 0; i--) {
                mDispatchInsetsChanged.accept(insetsChangedWindows.get(i));
            }
        }
    }

    void onRequestedVisibleTypesChanged(@NonNull InsetsTarget caller, @InsetsType int changedTypes,
            @Nullable ImeTracker.Token statsToken) {
        boolean changed = false;
        for (int i = mProviders.size() - 1; i >= 0; i--) {
            final InsetsSourceProvider provider = mProviders.valueAt(i);
            final @InsetsType int type = provider.getSource().getType();
            final boolean isImeProvider = type == WindowInsets.Type.ime();
            if ((type & changedTypes) != 0) {
                changed |= provider.updateClientVisibility(caller,
                        isImeProvider ? statsToken : null)
                        // Fake control target cannot change the client visibility, but it should
                        // change the insets with its newly requested visibility.
                        || (caller == provider.getFakeControlTarget());
            } else if (isImeProvider) {
                ImeTracker.forLogging().onCancelled(statsToken,
                        ImeTracker.PHASE_WM_SET_REMOTE_TARGET_IME_VISIBILITY);
            }
        }
        if (changed) {
            notifyInsetsChanged();
            mDisplayContent.updateSystemGestureExclusion();

            mDisplayContent.getDisplayPolicy().updateSystemBarAttributes();
        }
    }

    @InsetsType
    int getFakeControllingTypes(@NonNull InsetsTarget target) {
        @InsetsType int types = 0;
        for (int i = mProviders.size() - 1; i >= 0; i--) {
            final InsetsSourceProvider provider = mProviders.valueAt(i);
            final InsetsControlTarget fakeControlTarget = provider.getFakeControlTarget();
            if (target == fakeControlTarget) {
                types |= provider.getSource().getType();
            }
        }
        return types;
    }

    void onImeControlTargetChanged(@Nullable InsetsControlTarget target) {
        // Make sure that we always have a control target for the IME, even if the IME control
        // target is null. Otherwise there is no leash that will hide it and IME becomes "randomly"
        // visible.
        final var realOrEmptyTarget = target != null ? target : mEmptyImeControlTarget;
        onControlTargetChanged(getImeSourceProvider(), realOrEmptyTarget, false /* fake */);
        ProtoLog.d(WM_DEBUG_IME, "onImeControlTargetChanged %s",
                realOrEmptyTarget.getWindow() != null ? realOrEmptyTarget.getWindow()
                        : realOrEmptyTarget);
        notifyPendingInsetsControlChanged();
    }

    /**
     * Called when the focused window that is able to control the system bars changes.
     *
     * @param statusControlling The target that is now able to control the status bar appearance
     *                          and visibility.
     * @param navControlling The target that is now able to control the nav bar appearance
     *                       and visibility.
     */
    void onBarControlTargetChanged(@Nullable InsetsControlTarget statusControlling,
            @Nullable InsetsControlTarget fakeStatusControlling,
            @Nullable InsetsControlTarget navControlling,
            @Nullable InsetsControlTarget fakeNavControlling) {
        for (int i = mProviders.size() - 1; i >= 0; i--) {
            final InsetsSourceProvider provider = mProviders.valueAt(i);
            final @InsetsType int type = provider.getSource().getType();
            if (type == WindowInsets.Type.statusBars()) {
                onControlTargetChanged(provider, statusControlling, false /* fake */);
                onControlTargetChanged(provider, fakeStatusControlling, true /* fake */);
            } else if (type == WindowInsets.Type.navigationBars()) {
                onControlTargetChanged(provider, navControlling, false /* fake */);
                onControlTargetChanged(provider, fakeNavControlling, true /* fake */);
            }
        }
        notifyPendingInsetsControlChanged();
    }

    void notifyControlTargetChanged(@Nullable InsetsControlTarget target,
            @NonNull InsetsSourceProvider provider) {
        onControlTargetChanged(provider, target, false /* fake */);
        notifyPendingInsetsControlChanged();
    }

    void notifyControlRevoked(@NonNull InsetsControlTarget previousControlTarget,
            @NonNull InsetsSourceProvider provider) {
        removeFromControlMaps(previousControlTarget, provider, false /* fake */);
        addToPendingControlMaps(previousControlTarget, provider);
        notifyPendingInsetsControlChanged();
    }

    private void onControlTargetChanged(@NonNull InsetsSourceProvider provider,
            @Nullable InsetsControlTarget target, boolean fake) {
        final InsetsControlTarget lastTarget = fake
                ? mIdFakeControlTargetMap.get(provider.getSource().getId())
                : mIdControlTargetMap.get(provider.getSource().getId());
        if (target == lastTarget) {
            return;
        }
        if (!provider.isControllable()) {
            return;
        }
        if (fake) {
            // The fake target updated here will be used to pretend to the app that it's still under
            // control of the bars while it's not really, but we still need to find out the apps
            // intentions around showing/hiding. For example, when the transient bars are showing,
            // and the fake target requests to show system bars, the transient state will be
            // aborted.
            provider.updateFakeControlTarget(target);
        } else {
            // TODO(b/353463205) if the IME controlTarget changes, any pending requests should fail
            provider.updateControlForTarget(target, false /* force */,
                    null /* TODO(b/353463205) check if needed here */);

            // Get control target again in case the provider didn't accept the one we passed to it.
            target = provider.getControlTarget();
            if (target == lastTarget) {
                return;
            }
        }
        if (lastTarget != null) {
            removeFromControlMaps(lastTarget, provider, fake);
            addToPendingControlMaps(lastTarget, provider);
        }
        if (target != null) {
            addToControlMaps(target, provider, fake);
            addToPendingControlMaps(target, provider);
        }
    }

    private void removeFromControlMaps(@NonNull InsetsControlTarget target,
            @NonNull InsetsSourceProvider provider, boolean fake) {
        final ArrayList<InsetsSourceProvider> array = mControlTargetProvidersMap.get(target);
        if (array == null) {
            return;
        }
        array.remove(provider);
        if (array.isEmpty()) {
            mControlTargetProvidersMap.remove(target);
        }
        if (fake) {
            mIdFakeControlTargetMap.remove(provider.getSource().getId());
        } else {
            mIdControlTargetMap.remove(provider.getSource().getId());
        }
    }

    private void addToControlMaps(@NonNull InsetsControlTarget target,
            @NonNull InsetsSourceProvider provider, boolean fake) {
        final ArrayList<InsetsSourceProvider> array = mControlTargetProvidersMap.computeIfAbsent(
                target, key -> new ArrayList<>());
        array.add(provider);
        if (fake) {
            mIdFakeControlTargetMap.put(provider.getSource().getId(), target);
        } else {
            mIdControlTargetMap.put(provider.getSource().getId(), target);
        }
    }

    private void addToPendingControlMaps(@NonNull InsetsControlTarget target,
            @NonNull InsetsSourceProvider provider) {
        final ArrayList<InsetsSourceProvider> array =
                mPendingTargetProvidersMap.computeIfAbsent(target, key -> new ArrayList<>());
        array.add(provider);
    }

    void notifyControlChanged(@NonNull InsetsControlTarget target,
            @NonNull InsetsSourceProvider provider) {
        addToPendingControlMaps(target, provider);
        notifyPendingInsetsControlChanged();
    }

    void notifySurfaceTransactionReady(@NonNull InsetsSourceProvider provider, long id,
            boolean ready) {
        if (ready) {
            mSurfaceTransactionIds.put(provider.getSource().getId(), id);
        } else {
            mSurfaceTransactionIds.delete(provider.getSource().getId());
        }
    }

    void onAnimatingTypesChanged(@NonNull InsetsControlTarget target,
            @Nullable ImeTracker.Token statsToken) {
        for (int i = mProviders.size() - 1; i >= 0; i--) {
            final InsetsSourceProvider provider = mProviders.valueAt(i);
            final boolean isImeProvider = provider.getSource().getType() == WindowInsets.Type.ime();
            provider.onAnimatingTypesChanged(target, isImeProvider ? statsToken : null);
        }
    }

    private void notifyPendingInsetsControlChanged() {
        if (mPendingTargetProvidersMap.isEmpty()) {
            return;
        }
        final int size = mSurfaceTransactionIds.size();
        final SparseLongArray surfaceTransactionIds = new SparseLongArray(size);
        for (int i = 0; i < size; i++) {
            surfaceTransactionIds.append(
                    mSurfaceTransactionIds.keyAt(i), mSurfaceTransactionIds.valueAt(i));
        }
        mDisplayContent.mWmService.mAnimator.addAfterPrepareSurfacesRunnable(() -> {
            for (int i = 0; i < size; i++) {
                final int sourceId = surfaceTransactionIds.keyAt(i);
                final InsetsSourceProvider provider = mProviders.get(sourceId);
                if (provider == null) {
                    continue;
                }
                provider.onSurfaceTransactionCommitted(surfaceTransactionIds.valueAt(i));
            }
            final ArraySet<InsetsControlTarget> newControlTargets = new ArraySet<>();
            int displayId = mDisplayContent.getDisplayId();
            final ArrayMap<InsetsControlTarget, ArrayList<InsetsSourceProvider>> pendingControlMap =
                    mPendingTargetProvidersMap;
            for (int i = pendingControlMap.size() - 1; i >= 0; i--) {
                final InsetsControlTarget target = pendingControlMap.keyAt(i);
                final ArrayList<InsetsSourceProvider> providers = pendingControlMap.valueAt(i);
                for (int p = providers.size() - 1; p >= 0; p--) {
                    final InsetsSourceProvider provider = providers.get(p);
                    if (provider.isLeashInitialized() || provider.getControlTarget() != target) {
                        // Stop waiting for this provider.
                        providers.remove(p);
                    }
                }
                if (providers.isEmpty()) {
                    pendingControlMap.removeAt(i);

                    // All controls of this target are ready to be dispatched.
                    target.notifyInsetsControlChanged(displayId);
                    if (mControlTargetProvidersMap.containsKey(target)) {
                        // We only collect targets who get controls, not lose controls.
                        newControlTargets.add(target);
                    }
                }
            }

            // This updates the insets visibilities AFTER sending current insets state and controls
            // to the clients, so that the clients can change the current visibilities to the
            // requested visibilities with animations.
            for (int i = newControlTargets.size() - 1; i >= 0; i--) {
                // TODO(b/353463205) the statsToken shouldn't be null as it is used later in the
                //  IME provider. Check if we have to create a new request here
                onRequestedVisibleTypesChanged(newControlTargets.valueAt(i),
                        WindowInsets.Type.all(), null /* statsToken */);
            }
            newControlTargets.clear();
        });
    }

    void notifyInsetsChanged() {
        mDisplayContent.notifyInsetsChanged(mDispatchInsetsChanged);
    }

    void notifyInsetsChanged(@NonNull ArraySet<WindowState> changedWindows) {
        for (int i = changedWindows.size() - 1; i >= 0; i--) {
            mDispatchInsetsChanged.accept(changedWindows.valueAt(i));
        }
    }

    /**
     * Checks if the control target has pending controls.
     *
     * @param target the control target to check.
     */
    boolean hasPendingControls(@NonNull InsetsControlTarget target) {
        return mPendingTargetProvidersMap.containsKey(target);
    }

    void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
        pw.println(prefix + "WindowInsetsStateController");
        prefix = prefix + "  ";
        mState.dump(prefix, pw);
        pw.println(prefix + "Control map:");
        for (int i = mControlTargetProvidersMap.size() - 1; i >= 0; i--) {
            final InsetsControlTarget controlTarget = mControlTargetProvidersMap.keyAt(i);
            pw.print(prefix + "  ");
            pw.print(controlTarget);
            pw.println(":");
            final ArrayList<InsetsSourceProvider> providers = mControlTargetProvidersMap.valueAt(i);
            for (int j = providers.size() - 1; j >= 0; j--) {
                final InsetsSourceProvider provider = providers.get(j);
                if (provider != null) {
                    pw.print(prefix + "    ");
                    if (controlTarget == provider.getFakeControlTarget()) {
                        pw.print("(fake) ");
                    }
                    pw.println(provider.getControl(controlTarget));
                }
            }
        }
        if (mControlTargetProvidersMap.isEmpty()) {
            pw.print(prefix + "  none");
        }
        pw.println(prefix + "InsetsSourceProviders:");
        for (int i = mProviders.size() - 1; i >= 0; i--) {
            mProviders.valueAt(i).dump(pw, prefix + "  ");
        }
        if (mForcedConsumingTypes != 0) {
            pw.println(prefix + "mForcedConsumingTypes="
                    + WindowInsets.Type.toString(mForcedConsumingTypes));
        }
    }

    void dumpDebug(@NonNull ProtoOutputStream proto, @WindowTracingLogLevel int logLevel) {
        for (int i = mProviders.size() - 1; i >= 0; i--) {
            final InsetsSourceProvider provider = mProviders.valueAt(i);
            provider.dumpDebug(proto,
                    provider.getSource().getType() == ime()
                            ? IME_INSETS_SOURCE_PROVIDER
                            : INSETS_SOURCE_PROVIDERS,
                    logLevel);
        }
    }
}
