/*
 * Copyright (C) 2020 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 com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SYNC_ENGINE;

import android.annotation.NonNull;
import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseArray;
import android.view.SurfaceControl;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;

/**
 * Utility class for collecting WindowContainers that will merge transactions.
 * For example to use to synchronously resize all the children of a window container
 *   1. Open a new sync set, and pass the listener that will be invoked
 *        int id startSyncSet(TransactionReadyListener)
 *      the returned ID will be eventually passed to the TransactionReadyListener in combination
 *      with a set of WindowContainers that are ready, meaning onTransactionReady was called for
 *      those WindowContainers. You also use it to refer to the operation in future steps.
 *   2. Ask each child to participate:
 *       addToSyncSet(int id, WindowContainer wc)
 *      if the child thinks it will be affected by a configuration change (a.k.a. has a visible
 *      window in its sub hierarchy, then we will increment a counter of expected callbacks
 *      At this point the containers hierarchy will redirect pendingTransaction and sub hierarchy
 *      updates in to the sync engine.
 *   3. Apply your configuration changes to the window containers.
 *   4. Tell the engine that the sync set is ready
 *       setReady(int id)
 *   5. If there were no sub windows anywhere in the hierarchy to wait on, then
 *      transactionReady is immediately invoked, otherwise all the windows are poked
 *      to redraw and to deliver a buffer to {@link WindowState#finishDrawing}.
 *      Once all this drawing is complete, all the transactions will be merged and delivered
 *      to TransactionReadyListener.
 *
 * This works primarily by setting-up state and then watching/waiting for the registered subtrees
 * to enter into a "finished" state (either by receiving drawn content or by disappearing). This
 * checks the subtrees during surface-placement.
 */
class BLASTSyncEngine {
    private static final String TAG = "BLASTSyncEngine";

    interface TransactionReadyListener {
        void onTransactionReady(int mSyncId, SurfaceControl.Transaction transaction);
    }

    /**
     * Holds state associated with a single synchronous set of operations.
     */
    class SyncGroup {
        final int mSyncId;
        final TransactionReadyListener mListener;
        final Runnable mOnTimeout;
        boolean mReady = false;
        final ArraySet<WindowContainer> mRootMembers = new ArraySet<>();
        private SurfaceControl.Transaction mOrphanTransaction = null;

        private SyncGroup(TransactionReadyListener listener, int id) {
            mSyncId = id;
            mListener = listener;
            mOnTimeout = () -> {
                Slog.w(TAG, "Sync group " + mSyncId + " timeout");
                synchronized (mWm.mGlobalLock) {
                    onTimeout();
                }
            };
        }

        /**
         * Gets a transaction to dump orphaned operations into. Orphaned operations are operations
         * that were on the mSyncTransactions of "root" subtrees which have been removed during the
         * sync period.
         */
        @NonNull
        SurfaceControl.Transaction getOrphanTransaction() {
            if (mOrphanTransaction == null) {
                // Lazy since this isn't common
                mOrphanTransaction = mWm.mTransactionFactory.get();
            }
            return mOrphanTransaction;
        }

        private void onSurfacePlacement() {
            if (!mReady) return;
            ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: onSurfacePlacement checking %s",
                    mSyncId, mRootMembers);
            for (int i = mRootMembers.size() - 1; i >= 0; --i) {
                final WindowContainer wc = mRootMembers.valueAt(i);
                if (!wc.isSyncFinished()) {
                    ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d:  Unfinished container: %s",
                            mSyncId, wc);
                    return;
                }
            }
            finishNow();
        }

        private void finishNow() {
            ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Finished!", mSyncId);
            SurfaceControl.Transaction merged = mWm.mTransactionFactory.get();
            if (mOrphanTransaction != null) {
                merged.merge(mOrphanTransaction);
            }
            for (WindowContainer wc : mRootMembers) {
                wc.finishSync(merged, false /* cancel */);
            }
            mListener.onTransactionReady(mSyncId, merged);
            mActiveSyncs.remove(mSyncId);
            mWm.mH.removeCallbacks(mOnTimeout);
        }

        private void setReady(boolean ready) {
            ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Set ready", mSyncId);
            mReady = ready;
            if (!ready) return;
            mWm.mWindowPlacerLocked.requestTraversal();
        }

        private void addToSync(WindowContainer wc) {
            if (!mRootMembers.add(wc)) {
                return;
            }
            ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Adding to group: %s", mSyncId, wc);
            wc.setSyncGroup(this);
            wc.prepareSync();
            mWm.mWindowPlacerLocked.requestTraversal();
        }

        void onCancelSync(WindowContainer wc) {
            mRootMembers.remove(wc);
        }

        private void onTimeout() {
            if (!mActiveSyncs.contains(mSyncId)) return;
            for (int i = mRootMembers.size() - 1; i >= 0; --i) {
                final WindowContainer<?> wc = mRootMembers.valueAt(i);
                if (!wc.isSyncFinished()) {
                    Slog.i(TAG, "Unfinished container: " + wc);
                }
            }
            finishNow();
        }
    }

    private final WindowManagerService mWm;
    private int mNextSyncId = 0;
    private final SparseArray<SyncGroup> mActiveSyncs = new SparseArray<>();

    BLASTSyncEngine(WindowManagerService wms) {
        mWm = wms;
    }

    int startSyncSet(TransactionReadyListener listener) {
        return startSyncSet(listener, WindowState.BLAST_TIMEOUT_DURATION);
    }

    int startSyncSet(TransactionReadyListener listener, long timeoutMs) {
        final int id = mNextSyncId++;
        final SyncGroup s = new SyncGroup(listener, id);
        mActiveSyncs.put(id, s);
        ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Started for listener: %s", id, listener);
        scheduleTimeout(s, timeoutMs);
        return id;
    }

    @VisibleForTesting
    void scheduleTimeout(SyncGroup s, long timeoutMs) {
        mWm.mH.postDelayed(s.mOnTimeout, timeoutMs);
    }

    void addToSyncSet(int id, WindowContainer wc) {
        mActiveSyncs.get(id).addToSync(wc);
    }

    void setReady(int id, boolean ready) {
        mActiveSyncs.get(id).setReady(ready);
    }

    void setReady(int id) {
        setReady(id, true);
    }

    boolean isReady(int id) {
        return mActiveSyncs.get(id).mReady;
    }

    /**
     * Aborts the sync (ie. it doesn't wait for ready or anything to finish)
     */
    void abort(int id) {
        mActiveSyncs.get(id).finishNow();
    }

    void onSurfacePlacement() {
        // backwards since each state can remove itself if finished
        for (int i = mActiveSyncs.size() - 1; i >= 0; --i) {
            mActiveSyncs.valueAt(i).onSurfacePlacement();
        }
    }
}
