/**
 * The MIT License (MIT)
 * <p/>
 * Copyright (c) 2016 Alessandro Crugnola
 * <p/>
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy,
 * modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
 * is furnished to do so, subject to the following conditions:
 * <p/>
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 * <p/>
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package it.sephiroth.android.library.bottomnavigation;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.IdRes;
import android.support.annotation.MenuRes;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.ViewCompat;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;

import com.readystatesoftware.systembartint.SystemBarTintManager;

import java.lang.ref.SoftReference;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

import it.sephiroth.android.library.bottonnavigation.R;

import static android.util.Log.INFO;
import static android.util.Log.VERBOSE;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static it.sephiroth.android.library.bottomnavigation.MiscUtils.log;

/**
 * Created by alessandro crugnola on 4/2/16.
 * BottomNavigation
 */
public class BottomNavigation extends FrameLayout implements OnItemClickListener {
    private static final String TAG = BottomNavigation.class.getSimpleName();

    @SuppressWarnings ("checkstyle:staticvariablename")
    public static boolean DEBUG = false;

    static final int PENDING_ACTION_NONE = 0x0;
    static final int PENDING_ACTION_EXPANDED = 0x1;
    static final int PENDING_ACTION_COLLAPSED = 0x2;
    static final int PENDING_ACTION_ANIMATE_ENABLED = 0x4;

    private static final String WIDGET_PACKAGE_NAME;

    static {
        final Package pkg = BottomNavigation.class.getPackage();
        WIDGET_PACKAGE_NAME = pkg != null ? pkg.getName() : null;
    }

    static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[]{BottomNavigation.class};

    /**
     * Current pending action (used inside the BottomBehavior instance)
     */
    private int mPendingAction = PENDING_ACTION_NONE;

    /**
     * This is the amount of space we have to cover in case there's a translucent navigation
     * enabled.
     */
    private int bottomInset;

    /**
     * This is the amount of space we have to cover in case there's a translucent status
     * enabled.
     */
    private int topInset;

    /**
     * This is the current view height. It does take into account the extra space
     * used in case we have to cover the navigation translucent area, and neither the shadow height.
     */
    private int defaultHeight;

    /**
     * Same as defaultHeight, but for tablet mode.
     */
    private int defaultWidth;

    /**
     * Shadow is created above the widget background. It simulates the
     * elevation.
     */
    private int shadowHeight;

    /**
     * Layout container used to create and manage the UI items.
     * It can be either Fixed or Shifting, based on the widget `mode`
     */
    private ItemsLayoutContainer itemsContainer;

    /**
     * This is where the color animation is happening
     */
    private View backgroundOverlay;

    /**
     * View used to show the press ripple overlay. I don't use the drawable in item view itself
     * because the ripple background will be clipped inside its bounds
     */
    private View rippleOverlay;

    /**
     * Toggle the ripple background animation on item press
     */
    private boolean enabledRippleBackground;

    /**
     * current menu
     */
    MenuParser.Menu menu;

    private MenuParser.Menu pendingMenu;

    /**
     * Default selected index.
     * After the items are populated changing this
     * won't have any effect
     */
    private int defaultSelectedIndex = 0;

    /**
     * View visible background color
     */
    private ColorDrawable backgroundDrawable;

    /**
     * Animation duration for the background color change
     */
    private long backgroundColorAnimation;

    /**
     * Optional typeface used for the items' text labels
     */
    SoftReference<Typeface> typeface;

    /**
     * Current BottomBehavior assigned from the CoordinatorLayout
     */
    private CoordinatorLayout.Behavior mBehavior;

    /**
     * Menu selection listener
     */
    private OnMenuItemSelectionListener listener;

    /**
     * Menu changed listener
     */
    private OnMenuChangedListener menuChangedListener;

    /**
     * The user defined layout_gravity
     */
    private int gravity;

    /**
     * View is attached
     */
    private boolean attached;

    private BadgeProvider badgeProvider;

    public BottomNavigation(final Context context) {
        this(context, null);
    }

    public BottomNavigation(final Context context, final AttributeSet attrs) {
        super(context, attrs);
        initialize(context, attrs, 0, 0);
    }

    public BottomNavigation(final Context context, final AttributeSet attrs, final int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initialize(context, attrs, defStyleAttr, 0);
    }

    @TargetApi (Build.VERSION_CODES.LOLLIPOP)
    public BottomNavigation(final Context context, final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initialize(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        log(TAG, INFO, "onSaveInstanceState");
        Parcelable parcelable = super.onSaveInstanceState();
        SavedState savedState = new SavedState(parcelable);

        if (null == menu) {
            savedState.selectedIndex = 0;
        } else {
            // savedState.selectedIndex = Math.max(0, Math.min(getSelectedIndex(), menu.getItemsCount() - 1));
            savedState.selectedIndex = getSelectedIndex();
        }

        if (null != badgeProvider) {
            savedState.badgeBundle = badgeProvider.save();
        }

        return savedState;
    }

    @Override
    protected void onRestoreInstanceState(final Parcelable state) {
        log(TAG, INFO, "onRestoreInstanceState");
        SavedState savedState = (SavedState) state;
        super.onRestoreInstanceState(savedState.getSuperState());

        defaultSelectedIndex = savedState.selectedIndex;
        log(TAG, Log.DEBUG, "defaultSelectedIndex: %d", defaultSelectedIndex);

        if (null != badgeProvider && null != savedState.badgeBundle) {
            badgeProvider.restore(savedState.badgeBundle);
        }
    }

    public BadgeProvider getBadgeProvider() {
        return badgeProvider;
    }

    private void initialize(final Context context, final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) {
        typeface = new SoftReference<>(Typeface.DEFAULT);

        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.BottomNavigation, defStyleAttr, defStyleRes);
        final int menuResId = array.getResourceId(R.styleable.BottomNavigation_bbn_entries, 0);
        pendingMenu = MenuParser.inflateMenu(context, menuResId);
        badgeProvider = parseBadgeProvider(this, context, array.getString(R.styleable.BottomNavigation_bbn_badgeProvider));
        array.recycle();

        backgroundColorAnimation = getResources().getInteger(R.integer.bbn_background_animation_duration);
        defaultSelectedIndex = 0;

        defaultHeight = getResources().getDimensionPixelSize(R.dimen.bbn_bottom_navigation_height);
        defaultWidth = getResources().getDimensionPixelSize(R.dimen.bbn_bottom_navigation_width);
        shadowHeight = getResources().getDimensionPixelOffset(R.dimen.bbn_top_shadow_height);

        // check if the bottom navigation is translucent
        if (!isInEditMode()) {
            final Activity activity = MiscUtils.getActivity(context);
            if (null != activity) {
                final SystemBarTintManager systembarTint = new SystemBarTintManager(activity);
                if (MiscUtils.hasTranslucentNavigation(activity)
                    && systembarTint.getConfig().isNavigationAtBottom()
                    && systembarTint.getConfig().hasNavigtionBar()) {
                    bottomInset = systembarTint.getConfig().getNavigationBarHeight();
                } else {
                    bottomInset = 0;
                }
                topInset = systembarTint.getConfig().getStatusBarHeight();
            }
        }

        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
        backgroundOverlay = new View(getContext());
        backgroundOverlay.setLayoutParams(params);
        addView(backgroundOverlay);

        final Drawable drawable = ContextCompat.getDrawable(getContext(), R.drawable.bbn_ripple_selector);
        drawable.mutate();
        MiscUtils.setDrawableColor(drawable, Color.WHITE);

        rippleOverlay = new View(getContext());
        rippleOverlay.setLayoutParams(new LayoutParams(MATCH_PARENT, MATCH_PARENT));
        rippleOverlay.setBackground(drawable);
        rippleOverlay.setClickable(false);
        rippleOverlay.setFocusable(false);
        rippleOverlay.setFocusableInTouchMode(false);
        addView(rippleOverlay);
    }

    int getPendingAction() {
        return mPendingAction;
    }

    void resetPendingAction() {
        mPendingAction = PENDING_ACTION_NONE;
    }

    @Override
    public void setLayoutParams(final ViewGroup.LayoutParams params) {
        log(TAG, INFO, "setLayoutParams: %s", params);
        super.setLayoutParams(params);
    }

    private boolean isTablet(final int gravity) {
        return MiscUtils.isGravitiyLeft(gravity) || MiscUtils.isGravityRight(gravity);
    }

    @SuppressWarnings ("unused")
    public void setSelectedIndex(final int position, final boolean animate) {
        if (null != itemsContainer) {
            setSelectedItemInternal(
                itemsContainer, ((ViewGroup) itemsContainer).getChildAt(position), position, animate, false);
        } else {
            defaultSelectedIndex = position;
        }
    }

    @SuppressWarnings ("unused")
    public int getSelectedIndex() {
        if (null != itemsContainer) {
            return itemsContainer.getSelectedIndex();
        }
        return -1;
    }

    @SuppressWarnings ("unused")
    public void setExpanded(boolean expanded, boolean animate) {
        log(TAG, INFO, "setExpanded(%b, %b)", expanded, animate);
        mPendingAction = (expanded ? PENDING_ACTION_EXPANDED : PENDING_ACTION_COLLAPSED)
            | (animate ? PENDING_ACTION_ANIMATE_ENABLED : 0);
        requestLayout();
    }

    public boolean isExpanded() {
        if (null != mBehavior && mBehavior instanceof BottomBehavior) {
            return ((BottomBehavior) mBehavior).isExpanded();
        }
        return false;
    }

    public void setOnMenuItemClickListener(final OnMenuItemSelectionListener listener) {
        this.listener = listener;
    }

    public void setOnMenuChangedListener(final OnMenuChangedListener listener) {
        this.menuChangedListener = listener;
    }

    /**
     * Inflate a menu resource into this navigation component
     *
     * @param menuResId the menu resource id
     */
    public void inflateMenu(@MenuRes final int menuResId) {
        defaultSelectedIndex = 0;
        if (isAttachedToWindow()) {
            setItems(MenuParser.inflateMenu(getContext(), menuResId));
            pendingMenu = null;
        } else {
            pendingMenu = MenuParser.inflateMenu(getContext(), menuResId);
        }
    }

    /**
     * Returns the current menu items count
     *
     * @return number of items in the current menu
     */
    public int getMenuItemCount() {
        if (null != menu) {
            return menu.getItemsCount();
        }
        return 0;
    }

    /**
     * Returns the id of the item at the specified position
     *
     * @param position the position inside the menu
     * @return the item ID
     */
    @IdRes
    public int getMenuItemId(final int position) {
        if (null != menu) {
            return menu.getItemAt(position).getId();
        }
        return 0;
    }

    public void setMenuItemEnabled(final int index, final boolean enabled) {
        log(TAG, INFO, "setMenuItemEnabled(%d, %b)", index, enabled);
        if (null != menu) {
            menu.getItemAt(index).setEnabled(enabled);
            if (null != itemsContainer) {
                itemsContainer.setItemEnabled(index, enabled);
            }
        }
    }

    public boolean getMenuItemEnabled(final int index) {
        if (null != menu) {
            return menu.getItemAt(index).isEnabled();
        }
        // menu has not been parsed yet
        return false;
    }

    public String getMenuItemTitle(final int index) {
        if (null != menu) {
            return menu.getItemAt(index).getTitle();
        }
        // menu has not been parsed yet
        return null;
    }

    @Override
    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if (MiscUtils.isGravityBottom(gravity)) {
            final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            final int widthSize = MeasureSpec.getSize(widthMeasureSpec);

            if (widthMode == MeasureSpec.AT_MOST) {
                throw new IllegalArgumentException("layout_width must be equal to `match_parent`");
            }
            setMeasuredDimension(widthSize, defaultHeight + bottomInset + shadowHeight);

        } else if (MiscUtils.isGravitiyLeft(gravity) || MiscUtils.isGravityRight(gravity)) {
            final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

            if (heightMode == MeasureSpec.AT_MOST) {
                throw new IllegalArgumentException("layout_height must be equal to `match_parent`");
            }
            setMeasuredDimension(defaultWidth, heightSize);
        } else {
            throw new IllegalArgumentException("invalid layout_gravity. Only one start, end, left, right or bottom is allowed");
        }
    }

    @SuppressWarnings ("unused")
    public int getNavigationHeight() {
        return defaultHeight;
    }

    @SuppressWarnings ("unused")
    public int getNavigationWidth() {
        return defaultWidth;
    }

    public int getBottomInset() {
        return bottomInset;
    }

    public int getShadowHeight() {
        return shadowHeight;
    }

    @Override
    protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
        log(TAG, INFO, "onSizeChanged(%d, %d)", w, h);
        super.onSizeChanged(w, h, oldw, oldh);
        MarginLayoutParams marginLayoutParams = (MarginLayoutParams) getLayoutParams();
        marginLayoutParams.bottomMargin = -bottomInset;
    }

    public boolean isAttachedToWindow() {
        if (Build.VERSION.SDK_INT >= 19) {
            return super.isAttachedToWindow();
        }
        return attached;
    }

    @Override
    protected void onAttachedToWindow() {
        log(TAG, INFO, "onAttachedToWindow");
        super.onAttachedToWindow();
        attached = true;

        ViewGroup.LayoutParams params = getLayoutParams();
        final CoordinatorLayout.LayoutParams layoutParams;
        if (CoordinatorLayout.LayoutParams.class.isInstance(params)) {
            layoutParams = (CoordinatorLayout.LayoutParams) params;
            this.gravity = GravityCompat.getAbsoluteGravity(layoutParams.gravity, ViewCompat.getLayoutDirection(this));
        } else {
            layoutParams = null;
            // TODO: check the gravity in other viewparent types
            this.gravity = Gravity.BOTTOM;
        }

        initializeUI(gravity);

        if (null != pendingMenu) {
            setItems(pendingMenu);
            pendingMenu = null;
        }

        if (null == mBehavior) {
            if (null != layoutParams) {
                mBehavior = layoutParams.getBehavior();

                if (isInEditMode()) {
                    return;
                }

                if (BottomBehavior.class.isInstance(mBehavior)) {
                    ((BottomBehavior) mBehavior).setLayoutValues(defaultHeight, bottomInset);
                } else if (TabletBehavior.class.isInstance(mBehavior)) {
                    final Activity activity = MiscUtils.getActivity(getContext());
                    boolean translucentStatus = MiscUtils.hasTranslucentStatusBar(activity);
                    ((TabletBehavior) mBehavior).setLayoutValues(defaultWidth, topInset, translucentStatus);
                }
            }
        }
    }

    public CoordinatorLayout.Behavior getBehavior() {
        if (null == mBehavior) {
            if (CoordinatorLayout.LayoutParams.class.isInstance(getLayoutParams())) {
                return ((CoordinatorLayout.LayoutParams) getLayoutParams()).getBehavior();
            }
        }
        return mBehavior;
    }

    private void setItems(MenuParser.Menu menu) {
        log(TAG, INFO, "setItems: %s", menu);

        this.menu = menu;

        if (null != menu) {
            if (menu.getItemsCount() < 3 || menu.getItemsCount() > 5) {
                throw new IllegalArgumentException("BottomNavigation expects 3 to 5 items. " + menu.getItemsCount() + " found");
            }

            enabledRippleBackground = !menu.getItemAt(0).hasColor() || menu.isTablet();

            menu.setTabletMode(isTablet(gravity));

            initializeBackgroundColor(menu);
            initializeContainer(menu);
            initializeItems(menu);

            if (null != menuChangedListener) {
                menuChangedListener.onMenuChanged(this);
            }
        }

        requestLayout();
    }

    private void initializeUI(final int gravity) {
        log(TAG, INFO, "initializeUI(%d)", gravity);
        final LayerDrawable layerDrawable;

        final boolean tablet = isTablet(gravity);
        final int elevation = getResources().getDimensionPixelSize(!tablet ? R.dimen.bbn_elevation : R.dimen.bbn_elevation_tablet);
        final int bgResId = !tablet ? R.drawable.bbn_background
            : (MiscUtils.isGravityRight(gravity) ? R.drawable.bbn_background_tablet_right : R.drawable.bbn_background_tablet_left);
        final int paddingBottom = !tablet ? shadowHeight : 0;

        // View elevation
        ViewCompat.setElevation(this, elevation);

        // Main background
        layerDrawable = (LayerDrawable) ContextCompat.getDrawable(getContext(), bgResId);
        layerDrawable.mutate();
        backgroundDrawable = (ColorDrawable) layerDrawable.findDrawableByLayerId(R.id.bbn_background);
        setBackground(layerDrawable);

        // Padding bottom
        setPadding(0, paddingBottom, 0, 0);
    }

    private void initializeBackgroundColor(final MenuParser.Menu menu) {
        log(TAG, INFO, "initializeBackgroundColor");

        final int color = menu.getBackground();
        log(TAG, VERBOSE, "background: %x", color);
        backgroundDrawable.setColor(color);
    }

    private void initializeContainer(final MenuParser.Menu menu) {
        log(TAG, INFO, "initializeContainer");
        if (null != itemsContainer) {

            // remove the layout listener
            log(TAG, VERBOSE, "remove listener from: %s", itemsContainer);
            ((ViewGroup) itemsContainer).removeOnLayoutChangeListener(mLayoutChangedListener);

            if (menu.isTablet() && !TabletLayout.class.isInstance(itemsContainer)) {
                removeView((View) itemsContainer);
                itemsContainer = null;
            } else if ((menu.isShifting() && !ShiftingLayout.class.isInstance(itemsContainer))
                || (!menu.isShifting() && !FixedLayout.class.isInstance(itemsContainer))) {
                removeView((View) itemsContainer);
                itemsContainer = null;
            } else {
                itemsContainer.removeAll();
            }
        }

        if (null == itemsContainer) {
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                menu.isTablet() ? defaultWidth : MATCH_PARENT,
                menu.isTablet() ? MATCH_PARENT : defaultHeight
            );

            if (menu.isTablet()) {
                itemsContainer = new TabletLayout(getContext());
            } else if (menu.isShifting()) {
                itemsContainer = new ShiftingLayout(getContext());
            } else {
                itemsContainer = new FixedLayout(getContext());
            }

            // force the layout manager ID
            ((View) itemsContainer).setId(R.id.bbn_layoutManager);
            itemsContainer.setLayoutParams(params);
            addView((View) itemsContainer);
        }

        // add the layout listener
        log(TAG, VERBOSE, "attach listener to: %s", itemsContainer);
        ((ViewGroup) itemsContainer).addOnLayoutChangeListener(mLayoutChangedListener);
    }

    private void initializeItems(final MenuParser.Menu menu) {
        log(TAG, INFO, "initializeItems(%d)", defaultSelectedIndex);

        itemsContainer.setSelectedIndex(defaultSelectedIndex, false);
        itemsContainer.populate(menu);
        itemsContainer.setOnItemClickListener(this);

        if (defaultSelectedIndex > -1 && menu.getItemAt(defaultSelectedIndex).hasColor()) {
            backgroundDrawable.setColor(menu.getItemAt(defaultSelectedIndex).getColor());
        }

        MiscUtils.setDrawableColor(rippleOverlay.getBackground(), menu.getRippleColor());
    }

    /**
     * Checks if the menu item at the passed index is available and enabled
     */
    private boolean isMenuItemEnabled(MenuParser.Menu menu, final int index) {
        if (menu.getItemsCount() > index) {
            return menu.getItemAt(index).isEnabled();
        }
        return false;
    }

    private int findFirstSelectedIndex(MenuParser.Menu menu) {
        for (int i = 0; i < menu.getItemsCount(); i++) {
            if (menu.getItemAt(i).isEnabled()) {
                return i;
            }
        }
        return -1;
    }

    private MyLayoutChangedListener mLayoutChangedListener = new MyLayoutChangedListener();

    class MyLayoutChangedListener implements OnLayoutChangeListener {
        public BottomNavigationItemViewAbstract view;
        private final Rect outRect = new Rect();

        @Override
        public void onLayoutChange(
            final View unused,
            final int left,
            final int top,
            final int right,
            final int bottom,
            final int oldLeft,
            final int oldTop,
            final int oldRight,
            final int oldBottom) {
            if (null == view) {
                return;
            }

            view.getHitRect(outRect);
            log(TAG, VERBOSE, "rect: %s", outRect);

            final int centerX = rippleOverlay.getWidth() / 2;
            final int centerY = rippleOverlay.getHeight() / 2;
            rippleOverlay.setTranslationX(outRect.centerX() - centerX);
            rippleOverlay.setTranslationY(outRect.centerY() - centerY);
        }

        public void forceLayout(final View v) {
            view = (BottomNavigationItemViewAbstract) v;
            onLayoutChange(view, view.getLeft(), view.getTop(), view.getRight(), view.getBottom(), 0, 0, 0, 0);
        }
    }

    @Override
    public void onItemPressed(final ItemsLayoutContainer parent, final View view, final boolean pressed) {
        if (Build.VERSION.SDK_INT < 21) {
            return;
        }

        if (!pressed) {
            if (enabledRippleBackground) {
                rippleOverlay.setPressed(false);
            }
            rippleOverlay.setHovered(false);
            return;
        }

        mLayoutChangedListener.forceLayout(view);
        rippleOverlay.setHovered(true);

        if (enabledRippleBackground) {
            rippleOverlay.setPressed(true);
        }
    }

    @Override
    public void onItemClick(final ItemsLayoutContainer parent, final View view, final int index, boolean animate) {
        log(TAG, INFO, "onItemClick: %d", index);
        setSelectedItemInternal(parent, view, index, animate, true);
        mLayoutChangedListener.forceLayout(view);
    }

    private void setSelectedItemInternal(
        final ItemsLayoutContainer layoutContainer,
        final View view, final int index,
        final boolean animate,
        final boolean fromUser) {

        final BottomNavigationItem item;
        if (index > -1 && index < menu.getItemsCount()) {
            item = menu.getItemAt(index);
        } else {
            item = null;
        }

        if (layoutContainer.getSelectedIndex() != index) {
            layoutContainer.setSelectedIndex(index, animate);

            if ((null != item && item.hasColor()) && !menu.isTablet()) {
                if (animate) {
                    MiscUtils.animate(
                        this,
                        view,
                        backgroundOverlay,
                        backgroundDrawable,
                        item.getColor(),
                        backgroundColorAnimation
                    );
                } else {
                    MiscUtils.switchColor(
                        this,
                        view,
                        backgroundOverlay,
                        backgroundDrawable,
                        item.getColor()
                    );
                }
            }

            if (null != listener) {
                listener.onMenuItemSelect(null != item ? item.getId() : -1, index, fromUser);
            }

        } else {
            if (null != listener) {
                listener.onMenuItemReselect(null != item ? item.getId() : -1, index, fromUser);
            }
        }
    }

    public void setDefaultTypeface(final Typeface typeface) {
        this.typeface = new SoftReference<>(typeface);
    }

    public void setDefaultSelectedIndex(final int defaultSelectedIndex) {
        this.defaultSelectedIndex = defaultSelectedIndex;
    }

    public void invalidateBadge(final int itemId) {
        log(TAG, INFO, "invalidateBadge: %d", itemId);
        if (null != itemsContainer) {
            final BottomNavigationItemViewAbstract viewAbstract =
                (BottomNavigationItemViewAbstract) itemsContainer.findViewById(itemId);
            if (null != viewAbstract) {
                viewAbstract.invalidateBadge();
            }
        }
    }

    static final ThreadLocal<Map<String, Constructor<BadgeProvider>>> S_CONSTRUCTORS = new ThreadLocal<>();

    static BadgeProvider parseBadgeProvider(final BottomNavigation navigation, final Context context, final String name) {
        log(TAG, INFO, "parseBadgeProvider: %s", name);

        if (TextUtils.isEmpty(name)) {
            return new BadgeProvider(navigation);
        }

        final String fullName;
        if (name.startsWith(".")) {
            fullName = context.getPackageName() + name;
        } else if (name.indexOf('.') >= 0) {
            fullName = name;
        } else {
            // Assume stock behavior in this package (if we have one)
            fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
                ? (WIDGET_PACKAGE_NAME + '.' + name)
                : name;
        }

        try {
            Map<String, Constructor<BadgeProvider>> constructors = S_CONSTRUCTORS.get();
            if (constructors == null) {
                constructors = new HashMap<>();
                S_CONSTRUCTORS.set(constructors);
            }
            Constructor<BadgeProvider> c = constructors.get(fullName);
            if (c == null) {
                final Class<BadgeProvider> clazz = (Class<BadgeProvider>) Class.forName(fullName, true, context.getClassLoader());
                c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
                c.setAccessible(true);
                constructors.put(fullName, c);
            }
            return c.newInstance(navigation);
        } catch (Exception e) {
            throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
        }
    }

    public interface OnMenuItemSelectionListener {
        void onMenuItemSelect(@IdRes final int itemId, final int position, final boolean fromUser);

        void onMenuItemReselect(@IdRes final int itemId, final int position, final boolean fromUser);
    }

    public interface OnMenuChangedListener {
        void onMenuChanged(BottomNavigation parent);
    }

    static class SavedState extends BaseSavedState {
        int selectedIndex;
        Bundle badgeBundle;

        public SavedState(Parcel in) {
            super(in);
            selectedIndex = in.readInt();
            badgeBundle = in.readBundle();
        }

        public SavedState(final Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(final Parcel out, final int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(selectedIndex);
            out.writeBundle(badgeBundle);
        }

        @Override
        public int describeContents() {
            return super.describeContents();
        }

        public static final Parcelable.Creator<SavedState> CREATOR
            = new Parcelable.Creator<SavedState>() {
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }
}