/*
 * 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.wm.shell.common;

import android.annotation.BinderThread;
import android.annotation.NonNull;
import android.os.RemoteException;
import android.util.Slog;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.WindowContainerTransaction;
import android.window.WindowContainerTransactionCallback;
import android.window.WindowOrganizer;

import com.android.wm.shell.transition.LegacyTransitions;

import java.util.ArrayList;

/**
 * Helper for serializing sync-transactions and corresponding callbacks.
 */
public final class SyncTransactionQueue {
    private static final boolean DEBUG = false;
    private static final String TAG = "SyncTransactionQueue";

    // Just a little longer than the sync-engine timeout of 5s
    private static final int REPLY_TIMEOUT = 5300;

    private final TransactionPool mTransactionPool;
    private final ShellExecutor mMainExecutor;

    // Sync Transactions currently don't support nesting or interleaving properly, so
    // queue up transactions to run them serially.
    private final ArrayList<SyncCallback> mQueue = new ArrayList<>();

    private SyncCallback mInFlight = null;
    private final ArrayList<TransactionRunnable> mRunnables = new ArrayList<>();

    private final Runnable mOnReplyTimeout = () -> {
        synchronized (mQueue) {
            if (mInFlight != null && mQueue.contains(mInFlight)) {
                Slog.w(TAG, "Sync Transaction timed-out: " + mInFlight.mWCT);
                mInFlight.onTransactionReady(mInFlight.mId, new SurfaceControl.Transaction());
            }
        }
    };

    public SyncTransactionQueue(TransactionPool pool, ShellExecutor mainExecutor) {
        mTransactionPool = pool;
        mMainExecutor = mainExecutor;
    }

    /**
     * Queues a sync transaction to be sent serially to WM.
     */
    public void queue(WindowContainerTransaction wct) {
        if (wct.isEmpty()) {
            if (DEBUG) Slog.d(TAG, "Skip queue due to transaction change is empty");
            return;
        }
        SyncCallback cb = new SyncCallback(wct);
        synchronized (mQueue) {
            if (DEBUG) Slog.d(TAG, "Queueing up " + wct);
            mQueue.add(cb);
            if (mQueue.size() == 1) {
                cb.send();
            }
        }
    }

    /**
     * Queues a legacy transition to be sent serially to WM
     */
    public void queue(LegacyTransitions.ILegacyTransition transition,
            @WindowManager.TransitionType int type, WindowContainerTransaction wct) {
        if (wct.isEmpty()) {
            if (DEBUG) Slog.d(TAG, "Skip queue due to transaction change is empty");
            return;
        }
        SyncCallback cb = new SyncCallback(transition, type, wct);
        synchronized (mQueue) {
            if (DEBUG) Slog.d(TAG, "Queueing up legacy transition " + wct);
            mQueue.add(cb);
            if (mQueue.size() == 1) {
                cb.send();
            }
        }
    }

    /**
     * Queues a sync transaction only if there are already sync transaction(s) queued or in flight.
     * Otherwise just returns without queueing.
     * @return {@code true} if queued, {@code false} if not.
     */
    public boolean queueIfWaiting(WindowContainerTransaction wct) {
        if (wct.isEmpty()) {
            if (DEBUG) Slog.d(TAG, "Skip queueIfWaiting due to transaction change is empty");
            return false;
        }
        synchronized (mQueue) {
            if (mQueue.isEmpty()) {
                if (DEBUG) Slog.d(TAG, "Nothing in queue, so skip queueing up " + wct);
                return false;
            }
            if (DEBUG) Slog.d(TAG, "Queue is non-empty, so queueing up " + wct);
            SyncCallback cb = new SyncCallback(wct);
            mQueue.add(cb);
            if (mQueue.size() == 1) {
                cb.send();
            }
        }
        return true;
    }

    /**
     * Runs a runnable in sync with sync transactions (ie. when the current in-flight transaction
     * returns. If there are no transactions in-flight, runnable executes immediately.
     */
    public void runInSync(TransactionRunnable runnable) {
        synchronized (mQueue) {
            if (DEBUG) Slog.d(TAG, "Run in sync. mInFlight=" + mInFlight);
            if (mInFlight != null) {
                mRunnables.add(runnable);
                return;
            }
        }
        SurfaceControl.Transaction t = mTransactionPool.acquire();
        runnable.runWithTransaction(t);
        t.apply();
        mTransactionPool.release(t);
    }

    // Synchronized on mQueue
    private void onTransactionReceived(@NonNull SurfaceControl.Transaction t) {
        if (DEBUG) Slog.d(TAG, "  Running " + mRunnables.size() + " sync runnables");
        final int n = mRunnables.size();
        for (int i = 0; i < n; ++i) {
            mRunnables.get(i).runWithTransaction(t);
        }
        // More runnables may have been added, so only remove the ones that ran.
        mRunnables.subList(0, n).clear();
    }

    /** Task to run with transaction. */
    public interface TransactionRunnable {
        /** Runs with transaction. */
        void runWithTransaction(SurfaceControl.Transaction t);
    }

    private class SyncCallback extends WindowContainerTransactionCallback {
        int mId = -1;
        final WindowContainerTransaction mWCT;
        final LegacyTransitions.LegacyTransition mLegacyTransition;

        SyncCallback(WindowContainerTransaction wct) {
            mWCT = wct;
            mLegacyTransition = null;
        }

        SyncCallback(LegacyTransitions.ILegacyTransition legacyTransition,
                @WindowManager.TransitionType int type, WindowContainerTransaction wct) {
            mWCT = wct;
            mLegacyTransition = new LegacyTransitions.LegacyTransition(type, legacyTransition);
        }

        // Must be sychronized on mQueue
        void send() {
            if (mInFlight == this) {
                // This was probably queued up and sent during a sync runnable of the last callback.
                // Don't queue it again.
                return;
            }
            if (mInFlight != null) {
                throw new IllegalStateException("Sync Transactions must be serialized. In Flight: "
                        + mInFlight.mId + " - " + mInFlight.mWCT);
            }
            mInFlight = this;
            if (DEBUG) Slog.d(TAG, "Sending sync transaction: " + mWCT);
            if (mLegacyTransition != null) {
                mId = new WindowOrganizer().startLegacyTransition(mLegacyTransition.getType(),
                        mLegacyTransition.getAdapter(), this, mWCT);
            } else {
                mId = new WindowOrganizer().applySyncTransaction(mWCT, this);
            }
            if (DEBUG) Slog.d(TAG, " Sent sync transaction. Got id=" + mId);
            mMainExecutor.executeDelayed(mOnReplyTimeout, REPLY_TIMEOUT);
        }

        @BinderThread
        @Override
        public void onTransactionReady(int id,
                @NonNull SurfaceControl.Transaction t) {
            mMainExecutor.execute(() -> {
                synchronized (mQueue) {
                    if (mId != id) {
                        Slog.e(TAG, "Got an unexpected onTransactionReady. Expected "
                                + mId + " but got " + id);
                        return;
                    }
                    mInFlight = null;
                    mMainExecutor.removeCallbacks(mOnReplyTimeout);
                    if (DEBUG) Slog.d(TAG, "onTransactionReady id=" + mId);
                    mQueue.remove(this);
                    onTransactionReceived(t);
                    if (mLegacyTransition != null) {
                        try {
                            mLegacyTransition.getSyncCallback().onTransactionReady(mId, t);
                        } catch (RemoteException e) {
                            Slog.e(TAG, "Error sending callback to legacy transition: " + mId, e);
                        }
                    } else {
                        t.apply();
                        t.close();
                    }
                    if (!mQueue.isEmpty()) {
                        mQueue.get(0).send();
                    }
                }
            });
        }
    }
}
