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

import android.annotation.Nullable;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.text.TextUtils;
import android.view.View;

import androidx.annotation.IntDef;

import com.android.settingslib.Utils;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.phone.KeyguardIndicationTextView;
import com.android.systemui.util.ViewController;
import com.android.systemui.util.concurrency.DelayableExecutor;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Rotates through messages to show on the keyguard bottom area on the lock screen
 * NOTE: This controller should not be used on AoD to avoid waking up the AP too often.
 */
public class KeyguardIndicationRotateTextViewController extends
        ViewController<KeyguardIndicationTextView> implements Dumpable {
    public static String TAG = "KgIndicationRotatingCtrl";
    private static final long DEFAULT_INDICATION_SHOW_LENGTH = 3500; // milliseconds

    private final StatusBarStateController mStatusBarStateController;
    private final float mMaxAlpha;
    private final ColorStateList mInitialTextColorState;

    // Stores @IndicationType => KeyguardIndication messages
    private final Map<Integer, KeyguardIndication> mIndicationMessages = new HashMap<>();

    // Executor that will show the next message after a delay
    private final DelayableExecutor mExecutor;
    @Nullable private ShowNextIndication mShowNextIndicationRunnable;

    // List of indication types to show. The next indication to show is always at index 0
    private final List<Integer> mIndicationQueue = new LinkedList<>();
    private @IndicationType int mCurrIndicationType = INDICATION_TYPE_NONE;

    private boolean mIsDozing;

    public KeyguardIndicationRotateTextViewController(
            KeyguardIndicationTextView view,
            @Main DelayableExecutor executor,
            StatusBarStateController statusBarStateController
    ) {
        super(view);
        mMaxAlpha = view.getAlpha();
        mExecutor = executor;
        mInitialTextColorState = mView != null
                ? mView.getTextColors() : ColorStateList.valueOf(Color.WHITE);
        mStatusBarStateController = statusBarStateController;
        init();
    }

    @Override
    protected void onViewAttached() {
        mStatusBarStateController.addCallback(mStatusBarStateListener);
    }

    @Override
    protected void onViewDetached() {
        mStatusBarStateController.removeCallback(mStatusBarStateListener);
        cancelScheduledIndication();
    }

    /**
     * Update the indication type with the given String.
     * @param type of indication
     * @param newIndication message to associate with this indication type
     * @param showImmediately if true: shows this indication message immediately. Else, the text
     *                        associated with this type is updated and will show when its turn in
     *                        the IndicationQueue comes around.
     */
    public void updateIndication(@IndicationType int type, KeyguardIndication newIndication,
            boolean updateImmediately) {
        if (type == INDICATION_TYPE_NOW_PLAYING
                || type == INDICATION_TYPE_REVERSE_CHARGING) {
            // temporarily don't show here, instead use AmbientContainer b/181049781
            return;
        }
        final boolean hasPreviousIndication = mIndicationMessages.get(type) != null;
        final boolean hasNewIndication = newIndication != null;
        if (!hasNewIndication) {
            mIndicationMessages.remove(type);
            mIndicationQueue.removeIf(x -> x == type);
        } else {
            if (!hasPreviousIndication) {
                mIndicationQueue.add(type);
            }

            mIndicationMessages.put(type, newIndication);
        }

        if (mIsDozing) {
            return;
        }

        final boolean showNow = updateImmediately
                || mCurrIndicationType == INDICATION_TYPE_NONE
                || mCurrIndicationType == type;
        if (hasNewIndication) {
            if (showNow) {
                showIndication(type);
            } else if (!isNextIndicationScheduled()) {
                scheduleShowNextIndication();
            }
            return;
        }

        if (mCurrIndicationType == type
                && !hasNewIndication
                && updateImmediately) {
            if (mShowNextIndicationRunnable != null) {
                mShowNextIndicationRunnable.runImmediately();
            } else {
                showIndication(INDICATION_TYPE_NONE);
            }
        }
    }

    /**
     * Stop showing the following indication type.
     *
     * If the current indication is of this type, immediately stops showing the message.
     */
    public void hideIndication(@IndicationType int type) {
        if (!mIndicationMessages.containsKey(type)
                || TextUtils.isEmpty(mIndicationMessages.get(type).getMessage())) {
            return;
        }
        updateIndication(type, null, true);
    }

    /**
     * Show a transient message.
     * Transient messages:
     * - show immediately
     * - will continue to be in the rotation of messages shown until hideTransient is called.
     * - can be presented with an "error" color if isError is true
     */
    public void showTransient(CharSequence newIndication, boolean isError) {
        updateIndication(INDICATION_TYPE_TRANSIENT,
                new KeyguardIndication.Builder()
                        .setMessage(newIndication)
                        .setTextColor(isError
                                ? Utils.getColorError(getContext())
                                : mInitialTextColorState)
                        .build(),
                /* showImmediately */true);
    }

    /**
     * Hide a transient message immediately.
     */
    public void hideTransient() {
        hideIndication(INDICATION_TYPE_TRANSIENT);
    }

    /**
     * @return true if there are available indications to show
     */
    public boolean hasIndications() {
        return mIndicationMessages.keySet().size() > 0;
    }

    /**
     * Immediately show the passed indication type and schedule the next indication to show.
     * Will re-add this indication to be re-shown after all other indications have been
     * rotated through.
     */
    private void showIndication(@IndicationType int type) {
        cancelScheduledIndication();

        mCurrIndicationType = type;
        mIndicationQueue.removeIf(x -> x == type);
        if (mCurrIndicationType == INDICATION_TYPE_NONE) {
            mView.setVisibility(View.GONE);
        } else {
            mView.setVisibility(View.VISIBLE);
            mIndicationQueue.add(type); // re-add to show later
        }

        mView.switchIndication(mIndicationMessages.get(type));

        // only schedule next indication if there's more than just this indication in the queue
        if (mCurrIndicationType != INDICATION_TYPE_NONE && mIndicationQueue.size() > 1) {
            scheduleShowNextIndication();
        }
    }

    protected boolean isNextIndicationScheduled() {
        return mShowNextIndicationRunnable != null;
    }

    private void scheduleShowNextIndication() {
        cancelScheduledIndication();
        mShowNextIndicationRunnable = new ShowNextIndication(DEFAULT_INDICATION_SHOW_LENGTH);
    }

    private void cancelScheduledIndication() {
        if (mShowNextIndicationRunnable != null) {
            mShowNextIndicationRunnable.cancelDelayedExecution();
            mShowNextIndicationRunnable = null;
        }
    }

    private StatusBarStateController.StateListener mStatusBarStateListener =
            new StatusBarStateController.StateListener() {
                @Override
                public void onDozeAmountChanged(float linear, float eased) {
                    mView.setAlpha((1 - linear) * mMaxAlpha);
                }

                @Override
                public void onDozingChanged(boolean isDozing) {
                    if (isDozing == mIsDozing) return;
                    mIsDozing = isDozing;
                    if (mIsDozing) {
                        showIndication(INDICATION_TYPE_NONE);
                    } else if (mIndicationQueue.size() > 0) {
                        showIndication(mIndicationQueue.remove(0));
                    }
                }
            };

    /**
     * Shows the next indication in the IndicationQueue after an optional delay.
     * This wrapper has the ability to cancel itself (remove runnable from DelayableExecutor) or
     * immediately run itself (which also removes itself from the DelayableExecutor).
     */
    class ShowNextIndication {
        private final Runnable mShowIndicationRunnable;
        private Runnable mCancelDelayedRunnable;

        ShowNextIndication(long delay) {
            mShowIndicationRunnable = () -> {
                int type = mIndicationQueue.size() == 0
                        ? INDICATION_TYPE_NONE : mIndicationQueue.remove(0);
                showIndication(type);
            };
            mCancelDelayedRunnable = mExecutor.executeDelayed(mShowIndicationRunnable, delay);
        }

        public void runImmediately() {
            cancelDelayedExecution();
            mShowIndicationRunnable.run();
        }

        public void cancelDelayedExecution() {
            if (mCancelDelayedRunnable != null) {
                mCancelDelayedRunnable.run();
                mCancelDelayedRunnable = null;
            }
        }
    }

    @Override
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        pw.println("KeyguardIndicationRotatingTextViewController:");
        pw.println("    currentMessage=" + mView.getText());
        pw.println("    dozing:" + mIsDozing);
        pw.println("    queue:" + mIndicationQueue.toString());
        pw.println("    showNextIndicationRunnable:" + mShowNextIndicationRunnable);

        if (hasIndications()) {
            pw.println("    All messages:");
            for (int type : mIndicationMessages.keySet()) {
                pw.println("        type=" + type + " " + mIndicationMessages.get(type));
            }
        }
    }

    private static final int INDICATION_TYPE_NONE = -1;
    public static final int INDICATION_TYPE_OWNER_INFO = 0;
    public static final int INDICATION_TYPE_DISCLOSURE = 1;
    public static final int INDICATION_TYPE_LOGOUT = 2;
    public static final int INDICATION_TYPE_BATTERY = 3;
    public static final int INDICATION_TYPE_ALIGNMENT = 4;
    public static final int INDICATION_TYPE_TRANSIENT = 5;
    public static final int INDICATION_TYPE_TRUST = 6;
    public static final int INDICATION_TYPE_RESTING = 7;
    public static final int INDICATION_TYPE_USER_LOCKED = 8;
    public static final int INDICATION_TYPE_NOW_PLAYING = 9;
    public static final int INDICATION_TYPE_REVERSE_CHARGING = 10;

    @IntDef({
            INDICATION_TYPE_NONE,
            INDICATION_TYPE_DISCLOSURE,
            INDICATION_TYPE_OWNER_INFO,
            INDICATION_TYPE_LOGOUT,
            INDICATION_TYPE_BATTERY,
            INDICATION_TYPE_ALIGNMENT,
            INDICATION_TYPE_TRANSIENT,
            INDICATION_TYPE_TRUST,
            INDICATION_TYPE_RESTING,
            INDICATION_TYPE_USER_LOCKED,
            INDICATION_TYPE_NOW_PLAYING,
            INDICATION_TYPE_REVERSE_CHARGING,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface IndicationType{}
}
