// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.ui.widget;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ProgressBar;

import org.chromium.base.ResettersForTesting;
import org.chromium.ui.interpolators.Interpolators;

import java.util.ArrayList;
import java.util.List;

/**
 * A {@link ProgressBar} that understands the hiding/showing policy defined in Material Design.
 */
public class LoadingView extends ProgressBar {
    private static final int LOADING_ANIMATION_DELAY_MS = 500;
    private static final int MINIMUM_ANIMATION_SHOW_TIME_MS = 500;

    /**
     * A observer interface that will be notified when the progress bar is hidden.
     */
    public interface Observer {
        /**
         * Notify the listener a call to {@link #showLoadingUI()} is complete and loading view
         * is VISIBLE.
         */
        void onShowLoadingUIComplete();

        /**
         * Notify the listener a call to {@link #hideLoadingUI()} is complete and loading view is
         * GONE.
         */
        void onHideLoadingUIComplete();
    }

    private long mStartTime = -1;
    private static boolean sDisableAnimationForTest;

    private final List<Observer> mObservers = new ArrayList<>();

    private final Runnable mDelayedShow = new Runnable() {
        @Override
        public void run() {
            if (!mShouldShow) return;
            mStartTime = SystemClock.elapsedRealtime();
            setVisibility(View.VISIBLE);
            setAlpha(1.0f);

            for (Observer observer : mObservers) {
                observer.onShowLoadingUIComplete();
            }
        }
    };

    /**
     * Tracks whether the View should be displayed when {@link #mDelayedShow} is run.  Android
     * doesn't always cancel a Runnable when requested, meaning that the View could be hidden before
     * it even has a chance to be shown.
     */
    private boolean mShouldShow;

    // Material loading design spec requires us to show progress spinner at least 500ms, so we need
    // this delayed runnable to implement that.
    private final Runnable mDelayedHide = new Runnable() {
        @Override
        public void run() {
            if (sDisableAnimationForTest) {
                onHideLoadingFinished();
                return;
            }

            animate()
                    .alpha(0.0f)
                    .setInterpolator(Interpolators.FAST_OUT_SLOW_IN_INTERPOLATOR)
                    .setListener(new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            onHideLoadingFinished();
                        }
                    });
        }
    };

    /**
     * Constructor for creating the view programatically.
     */
    public LoadingView(Context context) {
        super(context);
    }

    /**
     * Constructor for inflating from XML.
     */
    public LoadingView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * Show loading UI. It shows the loading animation 500ms after.
     */
    public void showLoadingUI() {
        removeCallbacks(mDelayedShow);
        removeCallbacks(mDelayedHide);
        mShouldShow = true;

        setVisibility(GONE);
        postDelayed(mDelayedShow, LOADING_ANIMATION_DELAY_MS);
    }

    /**
     * Hide loading UI. If progress bar is not shown, it disappears immediately. If so, it smoothly
     * fades out.
     */
    public void hideLoadingUI() {
        removeCallbacks(mDelayedShow);
        removeCallbacks(mDelayedHide);
        mShouldShow = false;

        if (getVisibility() == VISIBLE) {
            postDelayed(mDelayedHide,
                    Math.max(0,
                            mStartTime + MINIMUM_ANIMATION_SHOW_TIME_MS
                                    - SystemClock.elapsedRealtime()));
        } else {
            onHideLoadingFinished();
        }
    }

    /**
     * Remove all callbacks when this view is no longer needed.
     */
    public void destroy() {
        removeCallbacks(mDelayedShow);
        removeCallbacks(mDelayedHide);
        mObservers.clear();
    }

    /**
     * Add the listener that will be notified when the spinner is completely hidden with {@link
     * #hideLoadingUI()}.
     * @param listener {@link Observer} that will be notified when the spinner is
     *         completely hidden with {@link #hideLoadingUI()}.
     */
    public void addObserver(Observer listener) {
        mObservers.add(listener);
    }

    private void onHideLoadingFinished() {
        setVisibility(GONE);
        for (Observer observer : mObservers) {
            observer.onHideLoadingUIComplete();
        }
    }

    /**
     * Set disable the fading animation during {@link #hideLoadingUI()}.
     * This function is added as a work around for disable animation during unit tests.
     * @param disableAnimation Whether the fading animation should be disabled during {@link
     *         #hideLoadingUI()}.
     */
    public static void setDisableAnimationForTest(boolean disableAnimation) {
        sDisableAnimationForTest = disableAnimation;
        ResettersForTesting.register(() -> sDisableAnimationForTest = false);
    }

    /**
     * Check if the Loading View Observer is empty or not.
     * @return If the observers is empty then return true.
     */
    public boolean isObserverListEmpty() {
        return mObservers.isEmpty();
    }
}
