package com.mobify.astro.plugins.webviewplugin;

import android.content.Context;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.Animation;
import android.widget.FrameLayout;

import com.mobify.astro.animations.FadeInAnimation;
import com.mobify.astro.animations.FadeOutAnimation;
import com.mobify.astro.plugins.loaders.LoaderPlugin;

public class LoadingContainerView extends FrameLayout {
    /**
     * A placeholder loader class to use before another loader has been set
     */
    protected class PlaceholderLoader implements LoaderPlugin {
        View view;
        public PlaceholderLoader(Context context) {
            view = new View(context);
            view.setVisibility(View.INVISIBLE);
        }

        public View getView() {
            return view;
        }

        public void start() {}

        public void stop() {}

        public void setUrl(String url) {}
    }

    private final int CHILD_CONTAINER_ANIMATION_DURATION = 500;
    private final int LOADING_SPINNER_ANIMATION_DURATION = 175;

    private LayoutParams _centeredLayoutParams;
    private FrameLayout _childContainerView;

    protected LoaderPlugin _loaderPlugin;
    private boolean loaderEnabled = true;

    private FadeOutAnimation _childContainerAnimation;
    private FadeInAnimation _loadingSpinnerAnimation;

    protected View nextView = null;
    protected boolean cancelChildContainerAnimationListenerTick = false;

    protected enum State {
        CHILD_CONTAINER_FADING_OUT, LOADING_SPINNER_FADING_IN, LOADING, SHOWING
    }
    protected State state = State.SHOWING;

    //region setters
    public void setLoaderPlugin(LoaderPlugin plugin) {
        View oldLoaderView = _loaderPlugin.getView();
        _loaderPlugin = plugin;
        removeView(oldLoaderView);

        ViewParent pluginParent = _loaderPlugin.getView().getParent();
        // Short-circuit if this is already the parent of the loaderPlugin to add
        if (this == pluginParent) {
            return;
        }

        // This is necessary since the LoaderPlugin may still be present in another view
        // and we are reusing it here (it has to be removed before we can add it to another view).
        if (pluginParent != null) {
            ((ViewGroup) pluginParent).removeView(_loaderPlugin.getView());
        }

        addView(_loaderPlugin.getView(), getCenteredLayoutParams());
    }

    protected void addLoaderViewToContainer() {
        ViewParent pluginParent = _loaderPlugin.getView().getParent();

        // Short-circuit if this is already the parent of the loaderPlugin to add
        if (this == pluginParent) {
            return;
        }

        // This is necessary since the LoaderPlugin may still be present in another view
        // and we are reusing it here (it has to be removed before we can add it to another view).
        if (pluginParent != null) {
            ((ViewGroup) pluginParent).removeView(_loaderPlugin.getView());
        }

        addView(_loaderPlugin.getView(), getCenteredLayoutParams());
    }

    //endregion

    //region Lazy getters

    protected LayoutParams getCenteredLayoutParams() {
        if (_centeredLayoutParams != null) {
            return _centeredLayoutParams;
        }

        _centeredLayoutParams = new LayoutParams(
                LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT
        );
        _centeredLayoutParams.gravity = Gravity.CENTER;
        return _centeredLayoutParams;
    }

    protected FrameLayout getChildContainer() {
        if (_childContainerView != null) {
            return _childContainerView;
        }

        _childContainerView = new FrameLayout(getContext());
        return _childContainerView;
    }

    protected View getLoaderView() {
        return _loaderPlugin.getView();
    }

    protected Animation getChildContainerAnimation() {
        if (_childContainerAnimation != null) {
            return _childContainerAnimation;
        }

        _childContainerAnimation = new FadeOutAnimation(
                CHILD_CONTAINER_ANIMATION_DURATION, new Runnable() {
            @Override
            public void run() {
                if (!cancelChildContainerAnimationListenerTick) {
                    enterLoadingSpinnerFadingInState();
                }
                cancelChildContainerAnimationListenerTick = false;
            }
        });
        return _childContainerAnimation;
    }

    protected Animation getLoadingSpinnerAnimation() {
        if (_loadingSpinnerAnimation != null) {
            return _loadingSpinnerAnimation;
        }

        _loadingSpinnerAnimation = new FadeInAnimation(
                LOADING_SPINNER_ANIMATION_DURATION, new Runnable() {
            @Override
            public void run() {
                enterLoadingState();
            }
        });
        return _loadingSpinnerAnimation;
    }

    //endregion

    public LoadingContainerView(Context context) {
        super(context);

        // put in placeholder on initialization
        _loaderPlugin = new PlaceholderLoader(context);
        addView(_loaderPlugin.getView(), getCenteredLayoutParams());

        addView(getChildContainer());
    }

    //region States

    /**
     * To ensure UI consistency, states should only transition linearly according to the state
     * machine's flow:
     *
     *     CHILD_CONTAINER_FADING_OUT -> LOADING_SPINNER_FADING_IN -> LOADING -> SHOWING
     */

    protected void enterChildContainerFadingOutState() {
        state = State.CHILD_CONTAINER_FADING_OUT;
        getChildContainer().startAnimation(getChildContainerAnimation());
    }

    protected void enterLoadingSpinnerFadingInState() {
        state = State.LOADING_SPINNER_FADING_IN;
        getChildContainer().setVisibility(View.INVISIBLE);

        // We ensure that the loader is attached to the container
        // in the event that it was previously removed.
        addLoaderViewToContainer();
        _loaderPlugin.getView().startAnimation(getLoadingSpinnerAnimation());

        // Now that its container is hidden, swap child views if requested.
        setNextChildView();
    }

    protected void enterLoadingState() {
        state = State.LOADING;

        if (loaderEnabled) {
            _loaderPlugin.getView().setVisibility(View.VISIBLE);
            _loaderPlugin.start();
        }
    }

    protected void enterShowingState() {
        state = State.SHOWING;

        // So you might be thinking.. hey, I can just call `setNextChildView()` here and
        // eliminate about fifty lines of code from this class. You could - BUT then you'd be
        // introducing render jank as the child view will layout as its being shown.

        _loaderPlugin.getView().setVisibility(View.INVISIBLE);
        _loaderPlugin.stop();

        getChildContainer().setVisibility(View.VISIBLE);
    }

    //endregion

    //region Protected methods

    protected void setNextChildView() {
        if (nextView != null) {
            setChildView(nextView);
            nextView = null;
        }
    }

    protected void clearAnimations() {
        Animation a = getChildContainerAnimation();
        if (a.hasStarted() && !a.hasEnded()) {
            // Ignore the animation's `onAnimationEnd` callback for one tick as it will get called
            // when we clear the animation. The loading spinner animation doesn't listen on
            // `onAnimationEnd` so we can safely just clear it.
            cancelChildContainerAnimationListenerTick = true;
        }

        getChildContainer().clearAnimation();
        _loaderPlugin.getView().clearAnimation();
    }

    //endregion

    //region Public methods

    public void clearChildView() {
        setChildView(null);
    }

    public void setChildView(View view) {
        getChildContainer().removeAllViews();
        if (view != null) {
            if (view.getParent() != null) {
                ((ViewGroup) view.getParent()).removeView(view);
            }
            getChildContainer().addView(view);
        }
    }

    public void transitionToLoadingWithView(View view, String url) {
        _loaderPlugin.setUrl(url);
        switch (state) {
            // We can't swap the current view yet because it's fading out.
            // Queue up the view as the next view to be shown.
            case CHILD_CONTAINER_FADING_OUT:
                nextView = view;
                break;

            // The child view is hidden, so we can swap in the new view.
            case LOADING_SPINNER_FADING_IN:
            case LOADING:
                setChildView(view);
                break;

            // Set the view as the next view to be shown once the current view fades out.
            case SHOWING:
                nextView = view;
                transitionToLoading();
                break;
        }
    }

    public void transitionToLoading() {
        if (state == State.SHOWING) {
            enterChildContainerFadingOutState();
        }
    }

    public void transitionToShowingWithView(View view) {
        clearAnimations();
        setChildView(view);

        if (state != State.SHOWING) {
            enterShowingState();
        }
    }

    public void transitionToShowing() {
        clearAnimations();

        if (state == State.CHILD_CONTAINER_FADING_OUT) {
            setNextChildView();
        }

        if (state != State.SHOWING) {
            enterShowingState();
        }
    }

    public void setLoaderEnabled(boolean loaderEnabled) {
        this.loaderEnabled = loaderEnabled;
    }

    @Override
    public boolean hasOverlappingRendering() {
        return false;
    }

    public void notifySubtreeAccessibilityStateChangedIfNeeded() {
        // Function is necessary to solve an issue with Android
        // Android thinks the function doesn't exist but it does in ViewGroup
    }
    //endregion
}
