/*
 * Copyright (C) 2015 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.messaging.ui;

import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Handler;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.View.OnAttachStateChangeListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewPropertyAnimator;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.WindowManager;
import android.widget.PopupWindow;
import android.widget.PopupWindow.OnDismissListener;

import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.ui.SnackBar.Placement;
import com.android.messaging.ui.SnackBar.SnackBarListener;
import com.android.messaging.util.AccessibilityUtil;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.TextUtil;
import com.android.messaging.util.UiUtils;
import com.google.common.base.Joiner;

import java.util.List;

public class SnackBarManager {

    private static SnackBarManager sInstance;

    public static SnackBarManager get() {
        if (sInstance == null) {
            synchronized (SnackBarManager.class) {
                if (sInstance == null) {
                    sInstance = new SnackBarManager();
                }
            }
        }
        return sInstance;
    }

    private final Runnable mDismissRunnable = new Runnable() {
        @Override
        public void run() {
            dismiss();
        }
    };

    private final OnTouchListener mDismissOnTouchListener = new OnTouchListener() {
        @Override
        public boolean onTouch(final View view, final MotionEvent event) {
            // Dismiss the {@link SnackBar} but don't consume the event.
            dismiss();
            return false;
        }
    };

    private final SnackBarListener mDismissOnUserTapListener = new SnackBarListener() {
        @Override
        public void onActionClick() {
            dismiss();
        }
    };

    private final OnAttachStateChangeListener mAttachStateChangeListener =
            new OnAttachStateChangeListener() {
                @Override
                public void onViewDetachedFromWindow(View v) {
                    // Dismiss the PopupWindow and clear SnackBarManager state.
                    mHideHandler.removeCallbacks(mDismissRunnable);
                    mPopupWindow.dismiss();

                    mCurrentSnackBar = null;
                    mNextSnackBar = null;
                    mIsCurrentlyDismissing = false;
                }

                @Override
                public void onViewAttachedToWindow(View v) {}
            };

    private final int mTranslationDurationMs;
    private final Handler mHideHandler;

    private SnackBar mCurrentSnackBar;
    private SnackBar mLatestSnackBar;
    private SnackBar mNextSnackBar;
    private boolean mIsCurrentlyDismissing;
    private PopupWindow mPopupWindow;

    private SnackBarManager() {
        mTranslationDurationMs = Factory.get().getApplicationContext().getResources().getInteger(
                R.integer.snackbar_translation_duration_ms);
        mHideHandler = new Handler();
    }

    public SnackBar getLatestSnackBar() {
        return mLatestSnackBar;
    }

    public SnackBar.Builder newBuilder(final View parentView) {
        return new SnackBar.Builder(this, parentView);
    }

    /**
     * The given snackBar is not guaranteed to be shown. If the previous snackBar is animating away,
     * and another snackBar is requested to show after this one, this snackBar will be skipped.
     */
    public void show(final SnackBar snackBar) {
        Assert.notNull(snackBar);

        if (mCurrentSnackBar != null) {
            LogUtil.d(LogUtil.BUGLE_TAG, "Showing snack bar, but currentSnackBar was not null.");

            // Dismiss the current snack bar. That will cause the next snack bar to be shown on
            // completion.
            mNextSnackBar = snackBar;
            mLatestSnackBar = snackBar;
            dismiss();
            return;
        }

        mCurrentSnackBar = snackBar;
        mLatestSnackBar = snackBar;

        // We want to know when either button was tapped so we can dismiss.
        snackBar.setListener(mDismissOnUserTapListener);

        // Cancel previous dismisses & set dismiss for the delay time.
        mHideHandler.removeCallbacks(mDismissRunnable);
        mHideHandler.postDelayed(mDismissRunnable, snackBar.getDuration());

        snackBar.setEnabled(false);

        // For some reason, the addView function does not respect layoutParams.
        // We need to explicitly set it first here.
        final View rootView = snackBar.getRootView();

        if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) {
            LogUtil.d(LogUtil.BUGLE_TAG, "Showing snack bar: " + snackBar);
        }
        // Measure the snack bar root view so we know how much to translate by.
        measureSnackBar(snackBar);
        mPopupWindow = new PopupWindow(snackBar.getContext());
        mPopupWindow.setWidth(LayoutParams.MATCH_PARENT);
        mPopupWindow.setHeight(LayoutParams.WRAP_CONTENT);
        mPopupWindow.setBackgroundDrawable(null);
        mPopupWindow.setContentView(rootView);
        final Placement placement = snackBar.getPlacement();
        if (placement == null) {
            mPopupWindow.showAtLocation(
                    snackBar.getParentView(), Gravity.BOTTOM | Gravity.START,
                    0, getScreenBottomOffset(snackBar));
        } else {
            final View anchorView = placement.getAnchorView();

            // You'd expect PopupWindow.showAsDropDown to ensure the popup moves with the anchor
            // view, which it does for scrolling, but not layout changes, so we have to manually
            // update while the snackbar is showing
            final OnGlobalLayoutListener listener = new OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    mPopupWindow.update(anchorView, 0, getRelativeOffset(snackBar),
                            anchorView.getWidth(), LayoutParams.WRAP_CONTENT);
                }
            };
            anchorView.getViewTreeObserver().addOnGlobalLayoutListener(listener);
            mPopupWindow.setOnDismissListener(new OnDismissListener() {
                @Override
                public void onDismiss() {
                    anchorView.getViewTreeObserver().removeOnGlobalLayoutListener(listener);
                }
            });
            mPopupWindow.showAsDropDown(anchorView, 0, getRelativeOffset(snackBar));
        }

        snackBar.getParentView().addOnAttachStateChangeListener(mAttachStateChangeListener);

        // Animate the toast bar into view.
        placeSnackBarOffScreen(snackBar);
        animateSnackBarOnScreen(snackBar).withEndAction(new Runnable() {
            @Override
            public void run() {
                mCurrentSnackBar.setEnabled(true);
                makeCurrentSnackBarDismissibleOnTouch();
                // Fire an accessibility event as needed
                String snackBarText = snackBar.getMessageText();
                if (!TextUtils.isEmpty(snackBarText) &&
                        TextUtils.getTrimmedLength(snackBarText) > 0) {
                    snackBarText = snackBarText.trim();
                    final String snackBarActionText = snackBar.getActionLabel();
                    if (!TextUtil.isAllWhitespace(snackBarActionText)) {
                        snackBarText = Joiner.on(", ").join(snackBarText, snackBarActionText);
                    }
                    AccessibilityUtil.announceForAccessibilityCompat(snackBar.getSnackBarView(),
                            null /*accessibilityManager*/, snackBarText);
                }
            }
        });

        // Animate any interaction views out of the way.
        animateInteractionsOnShow(snackBar);
    }

    /**
     * Dismisses the current toast that is showing. If there is a toast waiting to be shown, that
     * toast will be shown when the current one has been dismissed.
     */
    public void dismiss() {
        mHideHandler.removeCallbacks(mDismissRunnable);

        if (mCurrentSnackBar == null || mIsCurrentlyDismissing) {
            return;
        }

        final SnackBar snackBar = mCurrentSnackBar;

        LogUtil.d(LogUtil.BUGLE_TAG, "Dismissing snack bar.");
        mIsCurrentlyDismissing = true;

        snackBar.setEnabled(false);

        // Animate the toast bar down.
        final View rootView = snackBar.getRootView();
        animateSnackBarOffScreen(snackBar).withEndAction(new Runnable() {
            @Override
            public void run() {
                rootView.setVisibility(View.GONE);
                try {
                    mPopupWindow.dismiss();
                } catch (IllegalArgumentException e) {
                    // PopupWindow.dismiss() will fire an IllegalArgumentException if the activity
                    // has already ended while we were animating
                }
                snackBar.getParentView()
                        .removeOnAttachStateChangeListener(mAttachStateChangeListener);

                mCurrentSnackBar = null;
                mIsCurrentlyDismissing = false;

                // Show the next toast if one is waiting.
                if (mNextSnackBar != null) {
                    final SnackBar localNextSnackBar = mNextSnackBar;
                    mNextSnackBar = null;
                    show(localNextSnackBar);
                }
            }
        });

        // Animate any interaction views back.
        animateInteractionsOnDismiss(snackBar);
    }

    private void makeCurrentSnackBarDismissibleOnTouch() {
        // Set touching on the entire view, the {@link SnackBar} itself, as
        // well as the button's dismiss the toast.
        mCurrentSnackBar.getRootView().setOnTouchListener(mDismissOnTouchListener);
        mCurrentSnackBar.getSnackBarView().setOnTouchListener(mDismissOnTouchListener);
    }

    private void measureSnackBar(final SnackBar snackBar) {
        final View rootView = snackBar.getRootView();
        final Point displaySize = new Point();
        getWindowManager(snackBar.getContext()).getDefaultDisplay().getSize(displaySize);
        final int widthSpec = ViewGroup.getChildMeasureSpec(
                MeasureSpec.makeMeasureSpec(displaySize.x, MeasureSpec.EXACTLY),
                0, LayoutParams.MATCH_PARENT);
        final int heightSpec = ViewGroup.getChildMeasureSpec(
                MeasureSpec.makeMeasureSpec(displaySize.y, MeasureSpec.EXACTLY),
                0, LayoutParams.WRAP_CONTENT);
        rootView.measure(widthSpec, heightSpec);
    }

    private void placeSnackBarOffScreen(final SnackBar snackBar) {
        final View rootView = snackBar.getRootView();
        final View snackBarView = snackBar.getSnackBarView();
        snackBarView.setTranslationY(rootView.getMeasuredHeight());
    }

    private ViewPropertyAnimator animateSnackBarOnScreen(final SnackBar snackBar) {
        final View snackBarView = snackBar.getSnackBarView();
        return normalizeAnimator(snackBarView.animate()).translationX(0).translationY(0);
    }

    private ViewPropertyAnimator animateSnackBarOffScreen(final SnackBar snackBar) {
        final View rootView = snackBar.getRootView();
        final View snackBarView = snackBar.getSnackBarView();
        return normalizeAnimator(snackBarView.animate()).translationY(rootView.getHeight());
    }

    private void animateInteractionsOnShow(final SnackBar snackBar) {
        final List<SnackBarInteraction> interactions = snackBar.getInteractions();
        for (final SnackBarInteraction interaction : interactions) {
            if (interaction != null) {
                final ViewPropertyAnimator animator = interaction.animateOnSnackBarShow(snackBar);
                if (animator != null) {
                    normalizeAnimator(animator);
                }
            }
        }
    }

    private void animateInteractionsOnDismiss(final SnackBar snackBar) {
        final List<SnackBarInteraction> interactions = snackBar.getInteractions();
        for (final SnackBarInteraction interaction : interactions) {
            if (interaction != null) {
                final ViewPropertyAnimator animator =
                        interaction.animateOnSnackBarDismiss(snackBar);
                if (animator != null) {
                    normalizeAnimator(animator);
                }
            }
        }
    }

    private ViewPropertyAnimator normalizeAnimator(final ViewPropertyAnimator animator) {
        return animator
                .setInterpolator(UiUtils.DEFAULT_INTERPOLATOR)
                .setDuration(mTranslationDurationMs);
    }

    private WindowManager getWindowManager(final Context context) {
        return (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    }

    /**
     * Get the offset from the bottom of the screen where the snack bar should be placed.
     */
    private int getScreenBottomOffset(final SnackBar snackBar) {
        final WindowManager windowManager = getWindowManager(snackBar.getContext());
        final DisplayMetrics displayMetrics = new DisplayMetrics();
        if (OsUtil.isAtLeastL()) {
            windowManager.getDefaultDisplay().getRealMetrics(displayMetrics);
        } else {
            windowManager.getDefaultDisplay().getMetrics(displayMetrics);
        }
        final int screenHeight = displayMetrics.heightPixels;

        if (OsUtil.isAtLeastL()) {
            // In L, the navigation bar is included in the space for the popup window, so we have to
            // offset by the size of the navigation bar
            final Rect displayRect = new Rect();
            snackBar.getParentView().getRootView().getWindowVisibleDisplayFrame(displayRect);
            return screenHeight - displayRect.bottom;
        }

        return 0;
    }

    private int getRelativeOffset(final SnackBar snackBar) {
        final Placement placement = snackBar.getPlacement();
        Assert.notNull(placement);
        final View anchorView = placement.getAnchorView();
        if (placement.getAnchorAbove()) {
            return -snackBar.getRootView().getMeasuredHeight() - anchorView.getHeight();
        } else {
            // Use the default dropdown positioning
            return 0;
        }
    }
}
