/*
 * Copyright (C) 2022 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.window;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.util.Log;
import android.util.Pair;
import android.window.WindowOnBackInvokedDispatcher.Checker;

import java.util.ArrayList;
import java.util.List;

/**
 * {@link OnBackInvokedDispatcher} only used to hold callbacks while an actual
 * dispatcher becomes available. <b>It does not dispatch the back events</b>.
 * <p>
 * Once the actual {@link OnBackInvokedDispatcher} becomes available,
 * {@link #setActualDispatcher(OnBackInvokedDispatcher)} needs to
 * be called and this {@link ProxyOnBackInvokedDispatcher} will pass the callback registrations
 * onto it.
 * <p>
 * This dispatcher will continue to keep track of callback registrations and when a dispatcher is
 * removed or set it will unregister the callbacks from the old one and register them on the new
 * one unless {@link #reset()} is called before.
 *
 * @hide
 */
public class ProxyOnBackInvokedDispatcher implements OnBackInvokedDispatcher {

    /**
     * List of pair representing an {@link OnBackInvokedCallback} and its associated priority.
     *
     * @see OnBackInvokedDispatcher#registerOnBackInvokedCallback(int, OnBackInvokedCallback)
     */
    private final List<Pair<OnBackInvokedCallback, Integer>> mCallbacks = new ArrayList<>();
    private final Object mLock = new Object();
    private OnBackInvokedDispatcher mActualDispatcher = null;
    private ImeOnBackInvokedDispatcher mImeDispatcher;
    private final Checker mChecker;

    public ProxyOnBackInvokedDispatcher(@NonNull Context context) {
        mChecker = new Checker(context);
    }

    @Override
    public void registerOnBackInvokedCallback(
            int priority, @NonNull OnBackInvokedCallback callback) {
        if (DEBUG) {
            Log.v(TAG, String.format("Proxy register %s. mActualDispatcher=%s", callback,
                    mActualDispatcher));
        }
        if (mChecker.checkApplicationCallbackRegistration(priority, callback)) {
            registerOnBackInvokedCallbackUnchecked(callback, priority);
        }
    }

    @Override
    public void registerSystemOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) {
        registerOnBackInvokedCallbackUnchecked(callback, PRIORITY_SYSTEM);
    }

    @Override
    public void unregisterOnBackInvokedCallback(
            @NonNull OnBackInvokedCallback callback) {
        if (DEBUG) {
            Log.v(TAG, String.format("Proxy unregister %s. Actual=%s", callback,
                    mActualDispatcher));
        }
        synchronized (mLock) {
            mCallbacks.removeIf((p) -> p.first.equals(callback));
            if (mActualDispatcher != null) {
                mActualDispatcher.unregisterOnBackInvokedCallback(callback);
            }
        }
    }

    private void registerOnBackInvokedCallbackUnchecked(
            @NonNull OnBackInvokedCallback callback, int priority) {
        synchronized (mLock) {
            mCallbacks.add(Pair.create(callback, priority));
            if (mActualDispatcher != null) {
                if (priority <= PRIORITY_SYSTEM) {
                    mActualDispatcher.registerSystemOnBackInvokedCallback(callback);
                } else {
                    mActualDispatcher.registerOnBackInvokedCallback(priority, callback);
                }
            }
        }
    }

    /**
     * Transfers all the pending callbacks to the provided dispatcher.
     * <p>
     * The callbacks are registered on the dispatcher in the same order as they were added on this
     * proxy dispatcher.
     */
    private void transferCallbacksToDispatcher() {
        if (mActualDispatcher == null) {
            return;
        }
        if (DEBUG) {
            Log.v(TAG, String.format("Proxy transferring %d callbacks to %s", mCallbacks.size(),
                    mActualDispatcher));
        }
        if (mImeDispatcher != null) {
            mActualDispatcher.setImeOnBackInvokedDispatcher(mImeDispatcher);
        }
        for (Pair<OnBackInvokedCallback, Integer> callbackPair : mCallbacks) {
            int priority = callbackPair.second;
            if (priority >= PRIORITY_DEFAULT) {
                mActualDispatcher.registerOnBackInvokedCallback(priority, callbackPair.first);
            } else {
                mActualDispatcher.registerSystemOnBackInvokedCallback(callbackPair.first);
            }
        }
        mCallbacks.clear();
        mImeDispatcher = null;
    }

    private void clearCallbacksOnDispatcher() {
        if (mActualDispatcher == null) {
            return;
        }
        for (Pair<OnBackInvokedCallback, Integer> callback : mCallbacks) {
            mActualDispatcher.unregisterOnBackInvokedCallback(callback.first);
        }
    }

    /**
     * Resets this {@link ProxyOnBackInvokedDispatcher} so it loses track of the currently
     * registered callbacks.
     * <p>
     * Using this method means that when setting a new {@link OnBackInvokedDispatcher}, the
     * callbacks registered on the old one won't be removed from it and won't be registered on
     * the new one.
     */
    public void reset() {
        if (DEBUG) {
            Log.v(TAG, "Proxy: reset callbacks");
        }
        synchronized (mLock) {
            mCallbacks.clear();
            mImeDispatcher = null;
        }
    }

    /**
     * Sets the actual {@link OnBackInvokedDispatcher} onto which the callbacks will be registered.
     * <p>
     * If any dispatcher was already present, all the callbacks that were added via this
     * {@link ProxyOnBackInvokedDispatcher} will be unregistered from the old one and registered
     * on the new one if it is not null.
     * <p>
     * If you do not wish for the previously registered callbacks to be reassigned to the new
     * dispatcher, {@link #reset} must be called beforehand.
     */
    public void setActualDispatcher(@Nullable OnBackInvokedDispatcher actualDispatcher) {
        if (DEBUG) {
            Log.v(TAG, String.format("Proxy setActual %s. Current %s",
                            actualDispatcher, mActualDispatcher));
        }
        synchronized (mLock) {
            if (actualDispatcher == mActualDispatcher) {
                return;
            }
            clearCallbacksOnDispatcher();
            mActualDispatcher = actualDispatcher;
            transferCallbacksToDispatcher();
        }
    }

    @Override
    public void setImeOnBackInvokedDispatcher(
            @NonNull ImeOnBackInvokedDispatcher imeDispatcher) {
        if (mActualDispatcher != null) {
            mActualDispatcher.setImeOnBackInvokedDispatcher(imeDispatcher);
        } else {
            mImeDispatcher = imeDispatcher;
        }
    }
}