/*
 * 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 androidx.window.extensions.embedding;

import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE;
import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_NONE;

import android.os.IBinder;
import android.window.TaskFragmentOrganizer;
import android.window.TaskFragmentOrganizer.TaskFragmentTransitionType;
import android.window.WindowContainerTransaction;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.internal.annotations.VisibleForTesting;

/**
 * Responsible for managing the current {@link WindowContainerTransaction} as a response to device
 * state changes and app interactions.
 *
 * A typical use flow:
 * 1. Call {@link #startNewTransaction} to start tracking the changes.
 * 2. Use {@link TransactionRecord#setOriginType(int)} (int)} to record the type of operation that
 *    will start a new transition on system server.
 * 3. Use {@link #getCurrentTransactionRecord()} to get current {@link TransactionRecord} for
 *    changes.
 * 4. Call {@link TransactionRecord#apply(boolean)} to request the system server to apply changes in
 *    the current {@link WindowContainerTransaction}, or call {@link TransactionRecord#abort()} to
 *    dispose the current one.
 *
 * Note:
 * There should be only one transaction at a time. The caller should not call
 * {@link #startNewTransaction} again before calling {@link TransactionRecord#apply(boolean)} or
 * {@link TransactionRecord#abort()} to the previous transaction.
 */
class TransactionManager {

    @NonNull
    private final TaskFragmentOrganizer mOrganizer;

    @Nullable
    private TransactionRecord mCurrentTransaction;

    TransactionManager(@NonNull TaskFragmentOrganizer organizer) {
        mOrganizer = organizer;
    }

    @NonNull
    TransactionRecord startNewTransaction() {
        return startNewTransaction(null /* taskFragmentTransactionToken */);
    }

    /**
     * Starts tracking the changes in a new {@link WindowContainerTransaction}. Caller can call
     * {@link #getCurrentTransactionRecord()} later to continue adding change to the current
     * transaction until {@link TransactionRecord#apply(boolean)} or
     * {@link TransactionRecord#abort()} is called.
     * @param taskFragmentTransactionToken  {@link android.window.TaskFragmentTransaction
     *                                      #getTransactionToken()} if this is a response to a
     *                                      {@link android.window.TaskFragmentTransaction}.
     */
    @NonNull
    TransactionRecord startNewTransaction(@Nullable IBinder taskFragmentTransactionToken) {
        if (mCurrentTransaction != null) {
            final TransactionRecord lastTransaction = mCurrentTransaction;
            mCurrentTransaction = null;
            throw new IllegalStateException(
                    "The previous transaction:" + lastTransaction + " has not been applied or "
                            + "aborted.");
        }
        mCurrentTransaction = new TransactionRecord(taskFragmentTransactionToken);
        return mCurrentTransaction;
    }

    /**
     * Gets the current {@link TransactionRecord} started from {@link #startNewTransaction}.
     */
    @NonNull
    TransactionRecord getCurrentTransactionRecord() {
        if (mCurrentTransaction == null) {
            throw new IllegalStateException("startNewTransaction() is not invoked before calling"
                    + " getCurrentTransactionRecord().");
        }
        return mCurrentTransaction;
    }

    /** The current transaction. The manager should only handle one transaction at a time. */
    class TransactionRecord {
        /**
         * {@link WindowContainerTransaction} containing the current change.
         * @see #startNewTransaction(IBinder)
         * @see #apply (boolean)
         */
        @NonNull
        private final WindowContainerTransaction mTransaction = new WindowContainerTransaction();

        /**
         * If the current transaction is a response to a
         * {@link android.window.TaskFragmentTransaction}, this is the
         * {@link android.window.TaskFragmentTransaction#getTransactionToken()}.
         * @see #startNewTransaction(IBinder)
         */
        @Nullable
        private final IBinder mTaskFragmentTransactionToken;

        /**
         * To track of the origin type of the current {@link #mTransaction}. When
         * {@link #apply (boolean)} to start a new transition, this is the type to request.
         * @see #setOriginType(int)
         * @see #getTransactionTransitionType()
         */
        @TaskFragmentTransitionType
        private int mOriginType = TASK_FRAGMENT_TRANSIT_NONE;

        TransactionRecord(@Nullable IBinder taskFragmentTransactionToken) {
            mTaskFragmentTransactionToken = taskFragmentTransactionToken;
        }

        @NonNull
        WindowContainerTransaction getTransaction() {
            ensureCurrentTransaction();
            return mTransaction;
        }

        /**
         * Sets the {@link TaskFragmentTransitionType} that triggers this transaction. If there are
         * multiple calls, only the first call will be respected as the "origin" type.
         */
        void setOriginType(@TaskFragmentTransitionType int type) {
            ensureCurrentTransaction();
            if (mOriginType != TASK_FRAGMENT_TRANSIT_NONE) {
                // Skip if the origin type has already been set.
                return;
            }
            mOriginType = type;
        }

        /**
         * Requests the system server to apply the current transaction started from
         * {@link #startNewTransaction}.
         * @param shouldApplyIndependently  If {@code true}, the {@link #mCurrentTransaction} will
         *                                  request a new transition, which will be queued until the
         *                                  sync engine is free if there is any other active sync.
         *                                  If {@code false}, the {@link #startNewTransaction} will
         *                                  be directly applied to the active sync.
         */
        void apply(boolean shouldApplyIndependently) {
            ensureCurrentTransaction();
            if (mTaskFragmentTransactionToken != null) {
                // If this is a response to a TaskFragmentTransaction.
                mOrganizer.onTransactionHandled(mTaskFragmentTransactionToken, mTransaction,
                        getTransactionTransitionType(), shouldApplyIndependently);
            } else {
                mOrganizer.applyTransaction(mTransaction, getTransactionTransitionType(),
                        shouldApplyIndependently);
            }
            dispose();
        }

        /** Called when there is no need to {@link #apply(boolean)} the current transaction. */
        void abort() {
            ensureCurrentTransaction();
            dispose();
        }

        private void dispose() {
            TransactionManager.this.mCurrentTransaction = null;
        }

        private void ensureCurrentTransaction() {
            if (TransactionManager.this.mCurrentTransaction != this) {
                throw new IllegalStateException(
                        "This transaction has already been apply() or abort().");
            }
        }

        /**
         * Gets the {@link TaskFragmentTransitionType} that we will request transition with for the
         * current {@link WindowContainerTransaction}.
         */
        @VisibleForTesting
        @TaskFragmentTransitionType
        int getTransactionTransitionType() {
            // Use TASK_FRAGMENT_TRANSIT_CHANGE as default if there is not opening/closing window.
            return mOriginType != TASK_FRAGMENT_TRANSIT_NONE
                    ? mOriginType
                    : TASK_FRAGMENT_TRANSIT_CHANGE;
        }

        @Override
        @NonNull
        public String toString() {
            return TransactionRecord.class.getSimpleName() + "{"
                    + "token=" + mTaskFragmentTransactionToken
                    + ", type=" + getTransactionTransitionType()
                    + ", transaction=" + mTransaction
                    + "}";
        }
    }
}
