/*
 * 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.systemui.toast;

import static android.content.res.Configuration.ORIENTATION_PORTRAIT;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.INotificationManager;
import android.app.ITransientNotificationCallback;
import android.content.Context;
import android.content.res.Configuration;
import android.hardware.display.DisplayManager;
import android.os.IBinder;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.util.Log;
import android.view.Display;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.IAccessibilityManager;
import android.widget.ToastPresenter;

import androidx.annotation.VisibleForTesting;

import com.android.systemui.CoreStartable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.CommandQueue;

import java.util.Objects;

import javax.inject.Inject;

/**
 * Controls display of text toasts.
 */
@SysUISingleton
public class ToastUI implements CoreStartable, CommandQueue.Callbacks {
    // values from NotificationManagerService#LONG_DELAY and NotificationManagerService#SHORT_DELAY
    private static final int TOAST_LONG_TIME = 3500; // 3.5 seconds
    private static final int TOAST_SHORT_TIME = 2000; // 2 seconds

    private static final String TAG = "ToastUI";

    private final Context mContext;
    private final CommandQueue mCommandQueue;
    private final INotificationManager mNotificationManager;
    private final IAccessibilityManager mIAccessibilityManager;
    private final AccessibilityManager mAccessibilityManager;
    private final ToastFactory mToastFactory;
    private final ToastLogger mToastLogger;
    @Nullable private ToastPresenter mPresenter;
    @Nullable private ITransientNotificationCallback mCallback;
    @VisibleForTesting ToastOutAnimatorListener mToastOutAnimatorListener;

    @VisibleForTesting SystemUIToast mToast;
    private int mOrientation = ORIENTATION_PORTRAIT;

    @Inject
    public ToastUI(
            Context context,
            CommandQueue commandQueue,
            ToastFactory toastFactory,
            ToastLogger toastLogger) {
        this(context, commandQueue,
                INotificationManager.Stub.asInterface(
                        ServiceManager.getService(Context.NOTIFICATION_SERVICE)),
                IAccessibilityManager.Stub.asInterface(
                        ServiceManager.getService(Context.ACCESSIBILITY_SERVICE)),
                toastFactory,
                toastLogger);
    }

    @VisibleForTesting
    ToastUI(Context context, CommandQueue commandQueue, INotificationManager notificationManager,
            @Nullable IAccessibilityManager accessibilityManager,
            ToastFactory toastFactory, ToastLogger toastLogger
    ) {
        mContext = context;
        mCommandQueue = commandQueue;
        mNotificationManager = notificationManager;
        mIAccessibilityManager = accessibilityManager;
        mToastFactory = toastFactory;
        mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
        mToastLogger = toastLogger;
    }

    @Override
    public void start() {
        mCommandQueue.addCallback(this);
    }

    @Override
    @MainThread
    public void showToast(int uid, String packageName, IBinder token, CharSequence text,
            IBinder windowToken, int duration, @Nullable ITransientNotificationCallback callback,
            int displayId) {
        Runnable showToastRunnable = () -> {
            UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
            Context context = mContext.createContextAsUser(userHandle, 0);

            DisplayManager mDisplayManager = mContext.getSystemService(DisplayManager.class);
            Display display = mDisplayManager.getDisplay(displayId);
            if (display == null) {
                // Display for which this toast was scheduled for is no longer available.
                mToastLogger.logOnSkipToastForInvalidDisplay(packageName, token.toString(),
                        displayId);
                return;
            }
            Context displayContext = context.createDisplayContext(display);
            mToast = mToastFactory.createToast(mContext /* sysuiContext */, text, packageName,
                    userHandle.getIdentifier(), mOrientation);

            if (mToast.getInAnimation() != null) {
                mToast.getInAnimation().start();
            }

            mCallback = callback;
            mPresenter = new ToastPresenter(displayContext, mIAccessibilityManager,
                    mNotificationManager, packageName);
            // Set as trusted overlay so touches can pass through toasts
            mPresenter.getLayoutParams().setTrustedOverlay();
            mToastLogger.logOnShowToast(uid, packageName, text.toString(), token.toString());
            mPresenter.show(mToast.getView(), token, windowToken, duration, mToast.getGravity(),
                    mToast.getXOffset(), mToast.getYOffset(), mToast.getHorizontalMargin(),
                    mToast.getVerticalMargin(), mCallback, mToast.hasCustomAnimation());
        };

        if (mToastOutAnimatorListener != null) {
            // if we're currently animating out a toast, show new toast after prev toast is hidden
            mToastOutAnimatorListener.setShowNextToastRunnable(showToastRunnable);
        } else if (mPresenter != null) {
            // if there's a toast already showing that we haven't tried hiding yet, hide it and
            // then show the next toast after its hidden animation is done
            hideCurrentToast(showToastRunnable);
        } else {
            // else, show this next toast immediately
            showToastRunnable.run();
        }
    }

    @Override
    @MainThread
    public void hideToast(String packageName, IBinder token) {
        if (mPresenter == null || !Objects.equals(mPresenter.getPackageName(), packageName)
                || !Objects.equals(mPresenter.getToken(), token)) {
            Log.w(TAG, "Attempt to hide non-current toast from package " + packageName);
            return;
        }
        mToastLogger.logOnHideToast(packageName, token.toString());
        hideCurrentToast(null);
    }

    @MainThread
    private void hideCurrentToast(Runnable runnable) {
        if (mToast.getOutAnimation() != null) {
            Animator animator = mToast.getOutAnimation();
            mToastOutAnimatorListener = new ToastOutAnimatorListener(mPresenter, mCallback,
                    runnable, animator);
            animator.addListener(mToastOutAnimatorListener);
            animator.start();
        } else {
            mPresenter.hide(mCallback);
            if (runnable != null) {
                runnable.run();
            }
        }
        mToast = null;
        mPresenter = null;
        mCallback = null;
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        if (newConfig.orientation != mOrientation) {
            mOrientation = newConfig.orientation;
            if (mToast != null) {
                mToastLogger.logOrientationChange(mToast.mText.toString(),
                        mOrientation == ORIENTATION_PORTRAIT);
                mToast.onOrientationChange(mOrientation);
                mPresenter.updateLayoutParams(
                        mToast.getXOffset(),
                        mToast.getYOffset(),
                        mToast.getHorizontalMargin(),
                        mToast.getVerticalMargin(),
                        mToast.getGravity());
            }
        }
    }

    /**
     * Once the out animation for a toast is finished, start showing the next toast.
     */
    class ToastOutAnimatorListener extends AnimatorListenerAdapter {
        final ToastPresenter mPrevPresenter;
        final ITransientNotificationCallback mPrevCallback;
        @Nullable Runnable mShowNextToastRunnable;
        @NonNull private final Animator mAnimator;

        ToastOutAnimatorListener(
                @NonNull ToastPresenter presenter,
                @NonNull ITransientNotificationCallback callback,
                @Nullable Runnable runnable,
                @NonNull Animator animator) {
            mPrevPresenter = presenter;
            mPrevCallback = callback;
            mShowNextToastRunnable = runnable;
            mAnimator = animator;
        }

        void setShowNextToastRunnable(Runnable runnable) {
            mShowNextToastRunnable = runnable;
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            mPrevPresenter.hide(mPrevCallback);
            if (mShowNextToastRunnable != null) {
                mShowNextToastRunnable.run();
            }
            mAnimator.removeListener(this);
            mShowNextToastRunnable = null;
            mToastOutAnimatorListener = null;
        }
    }
}
