/*
 * Copyright (C) 2016 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.systemui.statusbar.notification.collection.legacy;

import android.os.Handler;
import android.os.SystemClock;
import android.view.View;

import androidx.collection.ArraySet;

import com.android.systemui.Dumpable;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.notification.NotificationEntryListener;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.dagger.NotificationsModule;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;

/**
 * A manager that ensures that notifications are visually stable. It will suppress reorderings
 * and reorder at the right time when they are out of view.
 */
public class VisualStabilityManager implements OnHeadsUpChangedListener, Dumpable {

    private static final long TEMPORARY_REORDERING_ALLOWED_DURATION = 1000;

    private final ArrayList<Callback> mReorderingAllowedCallbacks = new ArrayList<>();
    private final ArraySet<Callback> mPersistentReorderingCallbacks = new ArraySet<>();
    private final ArrayList<Callback> mGroupChangesAllowedCallbacks = new ArrayList<>();
    private final ArraySet<Callback> mPersistentGroupCallbacks = new ArraySet<>();
    private final Handler mHandler;

    private boolean mPanelExpanded;
    private boolean mScreenOn;
    private boolean mReorderingAllowed;
    private boolean mGroupChangedAllowed;
    private boolean mIsTemporaryReorderingAllowed;
    private long mTemporaryReorderingStart;
    private VisibilityLocationProvider mVisibilityLocationProvider;
    private ArraySet<View> mAllowedReorderViews = new ArraySet<>();
    private ArraySet<NotificationEntry> mLowPriorityReorderingViews = new ArraySet<>();
    private ArraySet<View> mAddedChildren = new ArraySet<>();
    private boolean mPulsing;

    /**
     * Injected constructor. See {@link NotificationsModule}.
     */
    public VisualStabilityManager(
            NotificationEntryManager notificationEntryManager,
            @Main Handler handler,
            StatusBarStateController statusBarStateController,
            WakefulnessLifecycle wakefulnessLifecycle) {

        mHandler = handler;

        if (notificationEntryManager != null) {
            notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
                @Override
                public void onPreEntryUpdated(NotificationEntry entry) {
                    final boolean ambientStateHasChanged =
                            entry.isAmbient() != entry.getRow().isLowPriority();
                    if (ambientStateHasChanged) {
                        // note: entries are removed in onReorderingFinished
                        mLowPriorityReorderingViews.add(entry);
                    }
                }
            });
        }

        if (statusBarStateController != null) {
            setPulsing(statusBarStateController.isPulsing());
            statusBarStateController.addCallback(new StatusBarStateController.StateListener() {
                @Override
                public void onPulsingChanged(boolean pulsing) {
                    setPulsing(pulsing);
                }

                @Override
                public void onExpandedChanged(boolean expanded) {
                    setPanelExpanded(expanded);
                }
            });
        }

        if (wakefulnessLifecycle != null) {
            wakefulnessLifecycle.addObserver(mWakefulnessObserver);
        }
    }

    /**
     * Add a callback to invoke when reordering is allowed again.
     *
     * @param callback the callback to add
     * @param persistent {@code true} if this callback should this callback be persisted, otherwise
     *                               it will be removed after a single invocation
     */
    public void addReorderingAllowedCallback(Callback callback, boolean persistent) {
        if (persistent) {
            mPersistentReorderingCallbacks.add(callback);
        }
        if (mReorderingAllowedCallbacks.contains(callback)) {
            return;
        }
        mReorderingAllowedCallbacks.add(callback);
    }

    /**
     * Add a callback to invoke when group changes are allowed again.
     *
     * @param callback the callback to add
     * @param persistent {@code true} if this callback should this callback be persisted, otherwise
     *                               it will be removed after a single invocation
     */
    public void addGroupChangesAllowedCallback(Callback callback, boolean persistent) {
        if (persistent) {
            mPersistentGroupCallbacks.add(callback);
        }
        if (mGroupChangesAllowedCallbacks.contains(callback)) {
            return;
        }
        mGroupChangesAllowedCallbacks.add(callback);
    }

    /**
     * @param screenOn whether the screen is on
     */
    private void setScreenOn(boolean screenOn) {
        mScreenOn = screenOn;
        updateAllowedStates();
    }

    /**
     * Set the panel to be expanded.
     */
    private void setPanelExpanded(boolean expanded) {
        mPanelExpanded = expanded;
        updateAllowedStates();
    }

    /**
     * @param pulsing whether we are currently pulsing for ambient display.
     */
    private void setPulsing(boolean pulsing) {
        if (mPulsing == pulsing) {
            return;
        }
        mPulsing = pulsing;
        updateAllowedStates();
    }

    private void updateAllowedStates() {
        boolean reorderingAllowed =
                (!mScreenOn || !mPanelExpanded || mIsTemporaryReorderingAllowed) && !mPulsing;
        boolean changedToTrue = reorderingAllowed && !mReorderingAllowed;
        mReorderingAllowed = reorderingAllowed;
        if (changedToTrue) {
            notifyChangeAllowed(mReorderingAllowedCallbacks, mPersistentReorderingCallbacks);
        }
        boolean groupChangesAllowed = (!mScreenOn || !mPanelExpanded) && !mPulsing;
        changedToTrue = groupChangesAllowed && !mGroupChangedAllowed;
        mGroupChangedAllowed = groupChangesAllowed;
        if (changedToTrue) {
            notifyChangeAllowed(mGroupChangesAllowedCallbacks, mPersistentGroupCallbacks);
        }
    }

    private void notifyChangeAllowed(ArrayList<Callback> callbacks,
            ArraySet<Callback> persistentCallbacks) {
        for (int i = 0; i < callbacks.size(); i++) {
            Callback callback = callbacks.get(i);
            callback.onChangeAllowed();
            if (!persistentCallbacks.contains(callback)) {
                callbacks.remove(callback);
                i--;
            }
        }
    }

    /**
     * @return whether reordering is currently allowed in general.
     */
    public boolean isReorderingAllowed() {
        return mReorderingAllowed;
    }

    /**
     * @return whether changes in the grouping should be allowed right now.
     */
    public boolean areGroupChangesAllowed() {
        return mGroupChangedAllowed;
    }

    /**
     * @return whether a specific notification is allowed to reorder. Certain notifications are
     * allowed to reorder even if {@link #isReorderingAllowed()} returns false, like newly added
     * notifications or heads-up notifications that are out of view.
     */
    public boolean canReorderNotification(ExpandableNotificationRow row) {
        if (mReorderingAllowed) {
            return true;
        }
        if (mAddedChildren.contains(row)) {
            return true;
        }
        if (mLowPriorityReorderingViews.contains(row.getEntry())) {
            return true;
        }
        if (mAllowedReorderViews.contains(row)
                && !mVisibilityLocationProvider.isInVisibleLocation(row.getEntry())) {
            return true;
        }
        return false;
    }

    public void setVisibilityLocationProvider(
            VisibilityLocationProvider visibilityLocationProvider) {
        mVisibilityLocationProvider = visibilityLocationProvider;
    }

    /**
     * Notifications have been reordered, so reset all the allowed list of views that are allowed
     * to reorder.
     */
    public void onReorderingFinished() {
        mAllowedReorderViews.clear();
        mAddedChildren.clear();
        mLowPriorityReorderingViews.clear();
    }

    @Override
    public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) {
        if (isHeadsUp) {
            // Heads up notifications should in general be allowed to reorder if they are out of
            // view and stay at the current location if they aren't.
            mAllowedReorderViews.add(entry.getRow());
        }
    }

    /**
     * Temporarily allows reordering of the entire shade for a period of 1000ms. Subsequent calls
     * to this method will extend the timer.
     */
    public void temporarilyAllowReordering() {
        mHandler.removeCallbacks(mOnTemporaryReorderingExpired);
        mHandler.postDelayed(mOnTemporaryReorderingExpired, TEMPORARY_REORDERING_ALLOWED_DURATION);
        if (!mIsTemporaryReorderingAllowed) {
            mTemporaryReorderingStart = SystemClock.elapsedRealtime();
        }
        mIsTemporaryReorderingAllowed = true;
        updateAllowedStates();
    }

    private final Runnable mOnTemporaryReorderingExpired = () -> {
        mIsTemporaryReorderingAllowed = false;
        updateAllowedStates();
    };

    /**
     * Notify the visual stability manager that a new view was added and should be allowed to
     * reorder next time.
     */
    public void notifyViewAddition(View view) {
        mAddedChildren.add(view);
    }

    @Override
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        pw.println("VisualStabilityManager state:");
        pw.print("  mIsTemporaryReorderingAllowed="); pw.println(mIsTemporaryReorderingAllowed);
        pw.print("  mTemporaryReorderingStart="); pw.println(mTemporaryReorderingStart);

        long now = SystemClock.elapsedRealtime();
        pw.print("    Temporary reordering window has been open for ");
        pw.print(now - (mIsTemporaryReorderingAllowed ? mTemporaryReorderingStart : now));
        pw.println("ms");

        pw.println();
    }

    final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() {
        @Override
        public void onFinishedGoingToSleep() {
            setScreenOn(false);
        }

        @Override
        public void onStartedWakingUp() {
            setScreenOn(true);
        }
    };


    /**
     * See {@link Callback#onChangeAllowed()}
     */
    public interface Callback {

        /**
         * Called when changing is allowed again.
         */
        void onChangeAllowed();
    }
}
