package net.simonvt.menudrawer;

import net.simonvt.menudrawer.compat.ActionBarHelper;

import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Interpolator;

public abstract class MenuDrawer extends ViewGroup {

    /**
     * Callback interface for changing state of the drawer.
     */
    public interface OnDrawerStateChangeListener {

        /**
         * Called when the drawer state changes.
         *
         * @param oldState The old drawer state.
         * @param newState The new drawer state.
         */
        void onDrawerStateChange(int oldState, int newState);

        /**
         * Called when the drawer slides.
         *
         * @param openRatio    Ratio for how open the menu is.
         * @param offsetPixels Current offset of the menu in pixels.
         */
        void onDrawerSlide(float openRatio, int offsetPixels);
    }

    /**
     * Callback that is invoked when the drawer is in the process of deciding whether it should intercept the touch
     * event. This lets the listener decide if the pointer is on a view that would disallow dragging of the drawer.
     * This is only called when the touch mode is {@link #TOUCH_MODE_FULLSCREEN}.
     */
    public interface OnInterceptMoveEventListener {

        /**
         * Called for each child the pointer i on when the drawer is deciding whether to intercept the touch event.
         *
         * @param v     View to test for draggability
         * @param delta Delta drag in pixels
         * @param x     X coordinate of the active touch point
         * @param y     Y coordinate of the active touch point
         * @return true if view is draggable by delta dx.
         */
        boolean isViewDraggable(View v, int delta, int x, int y);
    }

    public enum Type {
        /**
         * Positions the drawer behind the content.
         */
        BEHIND,

        /**
         * A static drawer that can not be dragged.
         */
        STATIC,

        /**
         * Positions the drawer on top of the content.
         */
        OVERLAY,
    }

    /**
     * Tag used when logging.
     */
    private static final String TAG = "MenuDrawer";

    /**
     * Indicates whether debug code should be enabled.
     */
    private static final boolean DEBUG = false;

    /**
     * The time between each frame when animating the drawer.
     */
    protected static final int ANIMATION_DELAY = 1000 / 60;

    /**
     * The default touch bezel size of the drawer in dp.
     */
    private static final int DEFAULT_DRAG_BEZEL_DP = 24;

    /**
     * The default drop shadow size in dp.
     */
    private static final int DEFAULT_DROP_SHADOW_DP = 6;

    /**
     * Drag mode for sliding only the content view.
     */
    public static final int MENU_DRAG_CONTENT = 0;

    /**
     * Drag mode for sliding the entire window.
     */
    public static final int MENU_DRAG_WINDOW = 1;

    /**
     * Disallow opening the drawer by dragging the screen.
     */
    public static final int TOUCH_MODE_NONE = 0;

    /**
     * Allow opening drawer only by dragging on the edge of the screen.
     */
    public static final int TOUCH_MODE_BEZEL = 1;

    /**
     * Allow opening drawer by dragging anywhere on the screen.
     */
    public static final int TOUCH_MODE_FULLSCREEN = 2;

    /**
     * Indicates that the drawer is currently closed.
     */
    public static final int STATE_CLOSED = 0;

    /**
     * Indicates that the drawer is currently closing.
     */
    public static final int STATE_CLOSING = 1;

    /**
     * Indicates that the drawer is currently being dragged by the user.
     */
    public static final int STATE_DRAGGING = 2;

    /**
     * Indicates that the drawer is currently opening.
     */
    public static final int STATE_OPENING = 4;

    /**
     * Indicates that the drawer is currently open.
     */
    public static final int STATE_OPEN = 8;

    /**
     * Indicates whether to use {@link View#setTranslationX(float)} when positioning views.
     */
    static final boolean USE_TRANSLATIONS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;

    /**
     * Time to animate the indicator to the new active view.
     */
    static final int INDICATOR_ANIM_DURATION = 800;

    /**
     * The maximum animation duration.
     */
    private static final int DEFAULT_ANIMATION_DURATION = 600;

    /**
     * Interpolator used when animating the drawer open/closed.
     */
    protected static final Interpolator SMOOTH_INTERPOLATOR = new SmoothInterpolator();

    /**
     * Interpolator used for stretching/retracting the active indicator.
     */
    protected static final Interpolator INDICATOR_INTERPOLATOR = new AccelerateInterpolator();

    /**
     * Drawable used as menu overlay.
     */
    protected Drawable mMenuOverlay;

    /**
     * Defines whether the drop shadow is enabled.
     */
    protected boolean mDropShadowEnabled;

    /**
     * The color of the drop shadow.
     */
    protected int mDropShadowColor;

    /**
     * Drawable used as content drop shadow onto the menu.
     */
    protected Drawable mDropShadowDrawable;

    /**
     * The size of the content drop shadow.
     */
    protected int mDropShadowSize;

    /**
     * Bitmap used to indicate the active view.
     */
    protected Bitmap mActiveIndicator;

    /**
     * The currently active view.
     */
    protected View mActiveView;

    /**
     * Position of the active view. This is compared to View#getTag(R.id.mdActiveViewPosition) when drawing the
     * indicator.
     */
    protected int mActivePosition;

    /**
     * Whether the indicator should be animated between positions.
     */
    private boolean mAllowIndicatorAnimation;

    /**
     * Used when reading the position of the active view.
     */
    protected final Rect mActiveRect = new Rect();

    /**
     * Temporary {@link Rect} used for deciding whether the view should be invalidated so the indicator can be redrawn.
     */
    private final Rect mTempRect = new Rect();

    /**
     * The custom menu view set by the user.
     */
    private View mMenuView;

    /**
     * The parent of the menu view.
     */
    protected BuildLayerFrameLayout mMenuContainer;

    /**
     * The parent of the content view.
     */
    protected BuildLayerFrameLayout mContentContainer;

    /**
     * The size of the menu (width or height depending on the gravity).
     */
    protected int mMenuSize;

    /**
     * Indicates whether the menu is currently visible.
     */
    protected boolean mMenuVisible;

    /**
     * The drag mode of the drawer. Can be either {@link #MENU_DRAG_CONTENT} or {@link #MENU_DRAG_WINDOW}.
     */
    private int mDragMode = MENU_DRAG_CONTENT;

    /**
     * The current drawer state.
     *
     * @see #STATE_CLOSED
     * @see #STATE_CLOSING
     * @see #STATE_DRAGGING
     * @see #STATE_OPENING
     * @see #STATE_OPEN
     */
    protected int mDrawerState = STATE_CLOSED;

    /**
     * The touch bezel size of the drawer in px.
     */
    protected int mTouchBezelSize;

    /**
     * The touch area size of the drawer in px.
     */
    protected int mTouchSize;

    /**
     * Listener used to dispatch state change events.
     */
    private OnDrawerStateChangeListener mOnDrawerStateChangeListener;

    /**
     * Touch mode for the Drawer.
     * Possible values are {@link #TOUCH_MODE_NONE}, {@link #TOUCH_MODE_BEZEL} or {@link #TOUCH_MODE_FULLSCREEN}
     * Default: {@link #TOUCH_MODE_BEZEL}
     */
    protected int mTouchMode = TOUCH_MODE_BEZEL;

    /**
     * Indicates whether to use {@link View#LAYER_TYPE_HARDWARE} when animating the drawer.
     */
    protected boolean mHardwareLayersEnabled = true;

    /**
     * The Activity the drawer is attached to.
     */
    private Activity mActivity;

    /**
     * Scroller used when animating the indicator to a new position.
     */
    private FloatScroller mIndicatorScroller;

    /**
     * Runnable used when animating the indicator to a new position.
     */
    private Runnable mIndicatorRunnable = new Runnable() {
        @Override
        public void run() {
            animateIndicatorInvalidate();
        }
    };

    /**
     * The start position of the indicator when animating it to a new position.
     */
    protected int mIndicatorStartPos;

    /**
     * [0..1] value indicating the current progress of the animation.
     */
    protected float mIndicatorOffset;

    /**
     * Whether the indicator is currently animating.
     */
    protected boolean mIndicatorAnimating;

    /**
     * Bundle used to hold the drawers state.
     */
    protected Bundle mState;

    /**
     * The maximum duration of open/close animations.
     */
    protected int mMaxAnimationDuration = DEFAULT_ANIMATION_DURATION;

    /**
     * Callback that lets the listener override intercepting of touch events.
     */
    protected OnInterceptMoveEventListener mOnInterceptMoveEventListener;

    protected SlideDrawable mSlideDrawable;

    protected Drawable mThemeUpIndicator;

    protected boolean mDrawerIndicatorEnabled;

    private ActionBarHelper mActionBarHelper;

    private int mCurrentUpContentDesc;

    private int mDrawerOpenContentDesc;

    private int mDrawerClosedContentDesc;

    /**
     * The position of the drawer.
     */
    protected Position mPosition;

    private final Rect mIndicatorClipRect = new Rect();

    protected boolean mIsStatic;

    protected final Rect mDropShadowRect = new Rect();

    /**
     * Current offset.
     */
    protected float mOffsetPixels;

    /**
     * Whether an overlay should be drawn as the drawer is opened and closed.
     */
    protected boolean mDrawOverlay;

    /**
     * Attaches the MenuDrawer to the Activity.
     *
     * @param activity The activity that the MenuDrawer will be attached to.
     * @return The created MenuDrawer instance.
     */
    public static MenuDrawer attach(Activity activity) {
        return attach(activity, Type.BEHIND);
    }

    /**
     * Attaches the MenuDrawer to the Activity.
     *
     * @param activity The activity the menu drawer will be attached to.
     * @param type     The {@link Type} of the drawer.
     * @return The created MenuDrawer instance.
     */
    public static MenuDrawer attach(Activity activity, Type type) {
        return attach(activity, type, Position.LEFT);
    }

    /**
     * Attaches the MenuDrawer to the Activity.
     *
     * @param activity The activity the menu drawer will be attached to.
     * @param position Where to position the menu.
     * @return The created MenuDrawer instance.
     */
    public static MenuDrawer attach(Activity activity, Position position) {
        return attach(activity, Type.BEHIND, position);
    }

    /**
     * Attaches the MenuDrawer to the Activity.
     *
     * @param activity The activity the menu drawer will be attached to.
     * @param type     The {@link Type} of the drawer.
     * @param position Where to position the menu.
     * @return The created MenuDrawer instance.
     */
    public static MenuDrawer attach(Activity activity, Type type, Position position) {
        return attach(activity, type, position, MENU_DRAG_CONTENT);
    }

    /**
     * Attaches the MenuDrawer to the Activity.
     *
     * @param activity The activity the menu drawer will be attached to.
     * @param type     The {@link Type} of the drawer.
     * @param position Where to position the menu.
     * @param dragMode The drag mode of the drawer. Can be either {@link MenuDrawer#MENU_DRAG_CONTENT}
     *                 or {@link MenuDrawer#MENU_DRAG_WINDOW}.
     * @return The created MenuDrawer instance.
     */
    public static MenuDrawer attach(Activity activity, Type type, Position position, int dragMode) {
        MenuDrawer menuDrawer = createMenuDrawer(activity, dragMode, position, type);
        menuDrawer.setId(R.id.md__drawer);

        switch (dragMode) {
            case MenuDrawer.MENU_DRAG_CONTENT:
                attachToContent(activity, menuDrawer);
                break;

            case MenuDrawer.MENU_DRAG_WINDOW:
                attachToDecor(activity, menuDrawer);
                break;

            default:
                throw new RuntimeException("Unknown menu mode: " + dragMode);
        }

        return menuDrawer;
    }

    /**
     * Constructs the appropriate MenuDrawer based on the position.
     */
    private static MenuDrawer createMenuDrawer(Activity activity, int dragMode, Position position, Type type) {
        MenuDrawer drawer;

        if (type == Type.STATIC) {
            drawer = new StaticDrawer(activity);

        } else if (type == Type.OVERLAY) {
            drawer = new OverlayDrawer(activity, dragMode);
            if (position == Position.LEFT) {
                drawer.setupUpIndicator(activity);
            }

        } else {
            drawer = new SlidingDrawer(activity, dragMode);
            if (position == Position.LEFT) {
                drawer.setupUpIndicator(activity);
            }
        }

        drawer.mDragMode = dragMode;
        drawer.mPosition = position;

        return drawer;
    }

    /**
     * Attaches the menu drawer to the content view.
     */
    private static void attachToContent(Activity activity, MenuDrawer menuDrawer) {
        /**
         * Do not call mActivity#setContentView.
         * E.g. if using with a ListActivity, Activity#setContentView is overridden and dispatched to
         * MenuDrawer#setContentView, which then again would call Activity#setContentView.
         */
        ViewGroup content = (ViewGroup) activity.findViewById(android.R.id.content);
        content.removeAllViews();
        content.addView(menuDrawer, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

    /**
     * Attaches the menu drawer to the window.
     */
    private static void attachToDecor(Activity activity, MenuDrawer menuDrawer) {
        ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
        ViewGroup decorChild = (ViewGroup) decorView.getChildAt(0);

        decorView.removeAllViews();
        decorView.addView(menuDrawer, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);

        menuDrawer.mContentContainer.addView(decorChild, decorChild.getLayoutParams());
    }

    MenuDrawer(Activity activity, int dragMode) {
        this(activity);

        mActivity = activity;
        mDragMode = dragMode;
    }

    public MenuDrawer(Context context) {
        this(context, null);
    }

    public MenuDrawer(Context context, AttributeSet attrs) {
        this(context, attrs, R.attr.menuDrawerStyle);
    }

    public MenuDrawer(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initDrawer(context, attrs, defStyle);
    }

    protected void initDrawer(Context context, AttributeSet attrs, int defStyle) {
        setWillNotDraw(false);
        setFocusable(false);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MenuDrawer, R.attr.menuDrawerStyle,
                R.style.Widget_MenuDrawer);

        final Drawable contentBackground = a.getDrawable(R.styleable.MenuDrawer_mdContentBackground);
        final Drawable menuBackground = a.getDrawable(R.styleable.MenuDrawer_mdMenuBackground);

        mMenuSize = a.getDimensionPixelSize(R.styleable.MenuDrawer_mdMenuSize, dpToPx(240));

        final int indicatorResId = a.getResourceId(R.styleable.MenuDrawer_mdActiveIndicator, 0);
        if (indicatorResId != 0) {
            mActiveIndicator = BitmapFactory.decodeResource(getResources(), indicatorResId);
        }

        mDropShadowEnabled = a.getBoolean(R.styleable.MenuDrawer_mdDropShadowEnabled, true);

        mDropShadowDrawable = a.getDrawable(R.styleable.MenuDrawer_mdDropShadow);

        if (mDropShadowDrawable == null) {
            mDropShadowColor = a.getColor(R.styleable.MenuDrawer_mdDropShadowColor, 0xFF000000);
        }

        mDropShadowSize = a.getDimensionPixelSize(R.styleable.MenuDrawer_mdDropShadowSize,
                dpToPx(DEFAULT_DROP_SHADOW_DP));

        mTouchBezelSize = a.getDimensionPixelSize(R.styleable.MenuDrawer_mdTouchBezelSize,
                dpToPx(DEFAULT_DRAG_BEZEL_DP));

        mAllowIndicatorAnimation = a.getBoolean(R.styleable.MenuDrawer_mdAllowIndicatorAnimation, false);

        mMaxAnimationDuration = a.getInt(R.styleable.MenuDrawer_mdMaxAnimationDuration, DEFAULT_ANIMATION_DURATION);

        final int slideDrawableResId = a.getResourceId(R.styleable.MenuDrawer_mdSlideDrawable, -1);
        if (slideDrawableResId != -1) {
            setSlideDrawable(slideDrawableResId);
        }

        mDrawerOpenContentDesc = a.getResourceId(R.styleable.MenuDrawer_mdDrawerOpenUpContentDescription, 0);
        mDrawerClosedContentDesc = a.getResourceId(R.styleable.MenuDrawer_mdDrawerClosedUpContentDescription, 0);

        mDrawOverlay = a.getBoolean(R.styleable.MenuDrawer_mdDrawOverlay, true);

        final int position = a.getInt(R.styleable.MenuDrawer_mdPosition, 0);
        mPosition = Position.fromValue(position);

        a.recycle();

        mMenuContainer = new NoClickThroughFrameLayout(context);
        mMenuContainer.setId(R.id.md__menu);
        mMenuContainer.setBackgroundDrawable(menuBackground);

        mContentContainer = new NoClickThroughFrameLayout(context);
        mContentContainer.setId(R.id.md__content);
        mContentContainer.setBackgroundDrawable(contentBackground);

        mMenuOverlay = new ColorDrawable(0xFF000000);

        mIndicatorScroller = new FloatScroller(SMOOTH_INTERPOLATOR);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        View menu = findViewById(R.id.mdMenu);
        if (menu != null) {
            removeView(menu);
            setMenuView(menu);
        }

        View content = findViewById(R.id.mdContent);
        if (content != null) {
            removeView(content);
            setContentView(content);
        }

        if (getChildCount() > 2) {
            throw new IllegalStateException(
                    "Menu and content view added in xml must have id's @id/mdMenu and @id/mdContent");
        }
    }

    protected int dpToPx(int dp) {
        return (int) (getResources().getDisplayMetrics().density * dp + 0.5f);
    }

    protected boolean isViewDescendant(View v) {
        ViewParent parent = v.getParent();
        while (parent != null) {
            if (parent == this) {
                return true;
            }

            parent = parent.getParent();
        }

        return false;
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        getViewTreeObserver().addOnScrollChangedListener(mScrollListener);
    }

    @Override
    protected void onDetachedFromWindow() {
        getViewTreeObserver().removeOnScrollChangedListener(mScrollListener);
        super.onDetachedFromWindow();
    }

    private boolean shouldDrawIndicator() {
        return mActiveView != null && mActiveIndicator != null && isViewDescendant(mActiveView);
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        final int offsetPixels = (int) mOffsetPixels;

        if (mDrawOverlay && offsetPixels != 0) {
            drawOverlay(canvas);
        }
        if (mDropShadowEnabled && (offsetPixels != 0 || mIsStatic)) {
            drawDropShadow(canvas);
        }
        if (shouldDrawIndicator() && (offsetPixels != 0 || mIsStatic)) {
            drawIndicator(canvas);
        }
    }

    protected abstract void drawOverlay(Canvas canvas);

    private void drawDropShadow(Canvas canvas) {
        // Can't pass the position to the constructor, so wait with loading the drawable until the drop shadow is
        // actually drawn.
        if (mDropShadowDrawable == null) {
            setDropShadowColor(mDropShadowColor);
        }

        updateDropShadowRect();
        mDropShadowDrawable.setBounds(mDropShadowRect);
        mDropShadowDrawable.draw(canvas);
    }

    protected void updateDropShadowRect() {
        // This updates the rect for the static and sliding drawer. The overlay drawer has its own implementation.
        switch (mPosition) {
            case LEFT:
                mDropShadowRect.top = 0;
                mDropShadowRect.bottom = getHeight();
                mDropShadowRect.right = ViewHelper.getLeft(mContentContainer);
                mDropShadowRect.left = mDropShadowRect.right - mDropShadowSize;
                break;

            case TOP:
                mDropShadowRect.left = 0;
                mDropShadowRect.right = getWidth();
                mDropShadowRect.bottom = ViewHelper.getTop(mContentContainer);
                mDropShadowRect.top = mDropShadowRect.bottom - mDropShadowSize;
                break;

            case RIGHT:
                mDropShadowRect.top = 0;
                mDropShadowRect.bottom = getHeight();
                mDropShadowRect.left = ViewHelper.getRight(mContentContainer);
                mDropShadowRect.right = mDropShadowRect.left + mDropShadowSize;
                break;

            case BOTTOM:
                mDropShadowRect.left = 0;
                mDropShadowRect.right = getWidth();
                mDropShadowRect.top = ViewHelper.getBottom(mContentContainer);
                mDropShadowRect.bottom = mDropShadowRect.top + mDropShadowSize;
                break;
        }
    }

    private void drawIndicator(Canvas canvas) {
        Integer position = (Integer) mActiveView.getTag(R.id.mdActiveViewPosition);
        final int pos = position == null ? 0 : position;
        if (pos == mActivePosition) {
            updateIndicatorClipRect();
            canvas.save();
            canvas.clipRect(mIndicatorClipRect);

            int drawLeft = 0;
            int drawTop = 0;
            switch (mPosition) {
                case LEFT:
                case TOP:
                    drawLeft = mIndicatorClipRect.left;
                    drawTop = mIndicatorClipRect.top;
                    break;

                case RIGHT:
                    drawLeft = mIndicatorClipRect.right - mActiveIndicator.getWidth();
                    drawTop = mIndicatorClipRect.top;
                    break;

                case BOTTOM:
                    drawLeft = mIndicatorClipRect.left;
                    drawTop = mIndicatorClipRect.bottom - mActiveIndicator.getHeight();
            }

            canvas.drawBitmap(mActiveIndicator, drawLeft, drawTop, null);
            canvas.restore();
        }
    }

    /**
     * Update the {@link Rect} where the indicator is drawn.
     */
    protected void updateIndicatorClipRect() {
        mActiveView.getDrawingRect(mActiveRect);
        offsetDescendantRectToMyCoords(mActiveView, mActiveRect);

        final float openRatio = mIsStatic ? 1.0f : Math.abs(mOffsetPixels) / mMenuSize;

        final float interpolatedRatio = 1.f - INDICATOR_INTERPOLATOR.getInterpolation((1.f - openRatio));

        final int indicatorWidth = mActiveIndicator.getWidth();
        final int indicatorHeight = mActiveIndicator.getHeight();

        final int interpolatedWidth = (int) (indicatorWidth * interpolatedRatio);
        final int interpolatedHeight = (int) (indicatorHeight * interpolatedRatio);

        final int startPos = mIndicatorStartPos;

        int left = 0;
        int top = 0;
        int right = 0;
        int bottom = 0;

        switch (mPosition) {
            case LEFT:
            case RIGHT:
                final int finalTop = mActiveRect.top + ((mActiveRect.height() - indicatorHeight) / 2);
                if (mIndicatorAnimating) {
                    top = (int) (startPos + ((finalTop - startPos) * mIndicatorOffset));
                } else {
                    top = finalTop;
                }
                bottom = top + indicatorHeight;
                break;

            case TOP:
            case BOTTOM:
                final int finalLeft = mActiveRect.left + ((mActiveRect.width() - indicatorWidth) / 2);
                if (mIndicatorAnimating) {
                    left = (int) (startPos + ((finalLeft - startPos) * mIndicatorOffset));
                } else {
                    left = finalLeft;
                }
                right = left + indicatorWidth;
                break;
        }

        switch (mPosition) {
            case LEFT: {
                right = ViewHelper.getLeft(mContentContainer);
                left = right - interpolatedWidth;
                break;
            }

            case TOP: {
                bottom = ViewHelper.getTop(mContentContainer);
                top = bottom - interpolatedHeight;
                break;
            }

            case RIGHT: {
                left = ViewHelper.getRight(mContentContainer);
                right = left + interpolatedWidth;
                break;
            }

            case BOTTOM: {
                top = ViewHelper.getBottom(mContentContainer);
                bottom = top + interpolatedHeight;
                break;
            }
        }

        mIndicatorClipRect.left = left;
        mIndicatorClipRect.top = top;
        mIndicatorClipRect.right = right;
        mIndicatorClipRect.bottom = bottom;
    }

    /**
     * Toggles the menu open and close with animation.
     */
    public void toggleMenu() {
        toggleMenu(true);
    }

    /**
     * Toggles the menu open and close.
     *
     * @param animate Whether open/close should be animated.
     */
    public abstract void toggleMenu(boolean animate);

    /**
     * Animates the menu open.
     */
    public void openMenu() {
        openMenu(true);
    }

    /**
     * Opens the menu.
     *
     * @param animate Whether open/close should be animated.
     */
    public abstract void openMenu(boolean animate);

    /**
     * Animates the menu closed.
     */
    public void closeMenu() {
        closeMenu(true);
    }

    /**
     * Closes the menu.
     *
     * @param animate Whether open/close should be animated.
     */
    public abstract void closeMenu(boolean animate);

    /**
     * Indicates whether the menu is currently visible.
     *
     * @return True if the menu is open, false otherwise.
     */
    public abstract boolean isMenuVisible();

    /**
     * Set the size of the menu drawer when open.
     *
     * @param size The size of the menu.
     */
    public abstract void setMenuSize(int size);

    /**
     * Returns the size of the menu.
     *
     * @return The size of the menu.
     */
    public int getMenuSize() {
        return mMenuSize;
    }

    /**
     * Set the active view.
     * If the mdActiveIndicator attribute is set, this View will have the indicator drawn next to it.
     *
     * @param v The active view.
     */
    public void setActiveView(View v) {
        setActiveView(v, 0);
    }

    /**
     * Set the active view.
     * If the mdActiveIndicator attribute is set, this View will have the indicator drawn next to it.
     *
     * @param v        The active view.
     * @param position Optional position, usually used with ListView. v.setTag(R.id.mdActiveViewPosition, position)
     *                 must be called first.
     */
    public void setActiveView(View v, int position) {
        final View oldView = mActiveView;
        mActiveView = v;
        mActivePosition = position;

        if (mAllowIndicatorAnimation && oldView != null) {
            startAnimatingIndicator();
        }

        invalidate();
    }

    /**
     * Sets whether the indicator should be animated between active views.
     *
     * @param animate Whether the indicator should be animated between active views.
     */
    public void setAllowIndicatorAnimation(boolean animate) {
        if (animate != mAllowIndicatorAnimation) {
            mAllowIndicatorAnimation = animate;
            completeAnimatingIndicator();
        }
    }

    /**
     * Indicates whether the indicator should be animated between active views.
     *
     * @return Whether the indicator should be animated between active views.
     */
    public boolean getAllowIndicatorAnimation() {
        return mAllowIndicatorAnimation;
    }

    /**
     * Scroll listener that checks whether the active view has moved before the drawer is invalidated.
     */
    private ViewTreeObserver.OnScrollChangedListener mScrollListener = new ViewTreeObserver.OnScrollChangedListener() {
        @Override
        public void onScrollChanged() {
            if (mActiveView != null && isViewDescendant(mActiveView)) {
                mActiveView.getDrawingRect(mTempRect);
                offsetDescendantRectToMyCoords(mActiveView, mTempRect);
                if (mTempRect.left != mActiveRect.left || mTempRect.top != mActiveRect.top
                        || mTempRect.right != mActiveRect.right || mTempRect.bottom != mActiveRect.bottom) {
                    invalidate();
                }
            }
        }
    };

    /**
     * Starts animating the indicator to a new position.
     */
    private void startAnimatingIndicator() {
        mIndicatorStartPos = getIndicatorStartPos();
        mIndicatorAnimating = true;
        mIndicatorScroller.startScroll(0.0f, 1.0f, INDICATOR_ANIM_DURATION);

        animateIndicatorInvalidate();
    }

    /**
     * Returns the start position of the indicator.
     *
     * @return The start position of the indicator.
     */
    private int getIndicatorStartPos() {
        switch (mPosition) {
            case TOP:
                return mIndicatorClipRect.left;
            case RIGHT:
                return mIndicatorClipRect.top;
            case BOTTOM:
                return mIndicatorClipRect.left;
            default:
                return mIndicatorClipRect.top;
        }
    }

    /**
     * Compute the touch area based on the touch mode.
     */
    protected void updateTouchAreaSize() {
        if (mTouchMode == TOUCH_MODE_BEZEL) {
            mTouchSize = mTouchBezelSize;
        } else if (mTouchMode == TOUCH_MODE_FULLSCREEN) {
            mTouchSize = getMeasuredWidth();
        } else {
            mTouchSize = 0;
        }
    }

    /**
     * Callback when each frame in the indicator animation should be drawn.
     */
    private void animateIndicatorInvalidate() {
        if (mIndicatorScroller.computeScrollOffset()) {
            mIndicatorOffset = mIndicatorScroller.getCurr();
            invalidate();

            if (!mIndicatorScroller.isFinished()) {
                postOnAnimation(mIndicatorRunnable);
                return;
            }
        }

        completeAnimatingIndicator();
    }

    /**
     * Called when the indicator animation has completed.
     */
    private void completeAnimatingIndicator() {
        mIndicatorOffset = 1.0f;
        mIndicatorAnimating = false;
        invalidate();
    }

    /**
     * Enables or disables offsetting the menu when dragging the drawer.
     *
     * @param offsetMenu True to offset the menu, false otherwise.
     */
    public abstract void setOffsetMenuEnabled(boolean offsetMenu);

    /**
     * Indicates whether the menu is being offset when dragging the drawer.
     *
     * @return True if the menu is being offset, false otherwise.
     */
    public abstract boolean getOffsetMenuEnabled();

    /**
     * Get the current state of the drawer.
     *
     * @return The state of the drawer.
     */
    public int getDrawerState() {
        return mDrawerState;
    }

    /**
     * Register a callback to be invoked when the drawer state changes.
     *
     * @param listener The callback that will run.
     */
    public void setOnDrawerStateChangeListener(OnDrawerStateChangeListener listener) {
        mOnDrawerStateChangeListener = listener;
    }

    /**
     * Register a callback that will be invoked when the drawer is about to intercept touch events.
     *
     * @param listener The callback that will be invoked.
     */
    public void setOnInterceptMoveEventListener(OnInterceptMoveEventListener listener) {
        mOnInterceptMoveEventListener = listener;
    }

    /**
     * Defines whether the drop shadow is enabled.
     *
     * @param enabled Whether the drop shadow is enabled.
     */
    public void setDropShadowEnabled(boolean enabled) {
        mDropShadowEnabled = enabled;
        invalidate();
    }

    protected GradientDrawable.Orientation getDropShadowOrientation() {
        // Gets the orientation for the static and sliding drawer. The overlay drawer provides its own implementation.
        switch (mPosition) {
            case TOP:
                return GradientDrawable.Orientation.BOTTOM_TOP;

            case RIGHT:
                return GradientDrawable.Orientation.LEFT_RIGHT;

            case BOTTOM:
                return GradientDrawable.Orientation.TOP_BOTTOM;

            default:
                return GradientDrawable.Orientation.RIGHT_LEFT;
        }
    }

    /**
     * Sets the color of the drop shadow.
     *
     * @param color The color of the drop shadow.
     */
    public void setDropShadowColor(int color) {
        GradientDrawable.Orientation orientation = getDropShadowOrientation();

        final int endColor = color & 0x00FFFFFF;
        mDropShadowDrawable = new GradientDrawable(orientation,
                new int[] {
                        color,
                        endColor,
                });
        invalidate();
    }

    /**
     * Sets the drawable of the drop shadow.
     *
     * @param drawable The drawable of the drop shadow.
     */
    public void setDropShadow(Drawable drawable) {
        mDropShadowDrawable = drawable;
        invalidate();
    }

    /**
     * Sets the drawable of the drop shadow.
     *
     * @param resId The resource identifier of the the drawable.
     */
    public void setDropShadow(int resId) {
        setDropShadow(getResources().getDrawable(resId));
    }

    /**
     * Returns the drawable of the drop shadow.
     */
    public Drawable getDropShadow() {
        return mDropShadowDrawable;
    }

    /**
     * Sets the size of the drop shadow.
     *
     * @param size The size of the drop shadow in px.
     */
    public void setDropShadowSize(int size) {
        mDropShadowSize = size;
        invalidate();
    }

    /**
     * Animates the drawer slightly open until the user opens the drawer.
     */
    public abstract void peekDrawer();

    /**
     * Animates the drawer slightly open. If delay is larger than 0, this happens until the user opens the drawer.
     *
     * @param delay The delay (in milliseconds) between each run of the animation. If 0, this animation is only run
     *              once.
     */
    public abstract void peekDrawer(long delay);

    /**
     * Animates the drawer slightly open. If delay is larger than 0, this happens until the user opens the drawer.
     *
     * @param startDelay The delay (in milliseconds) until the animation is first run.
     * @param delay      The delay (in milliseconds) between each run of the animation. If 0, this animation is only run
     *                   once.
     */
    public abstract void peekDrawer(long startDelay, long delay);

    /**
     * Enables or disables the user of {@link View#LAYER_TYPE_HARDWARE} when animations views.
     *
     * @param enabled Whether hardware layers are enabled.
     */
    public abstract void setHardwareLayerEnabled(boolean enabled);

    /**
     * Sets the maximum duration of open/close animations.
     *
     * @param duration The maximum duration in milliseconds.
     */
    public void setMaxAnimationDuration(int duration) {
        mMaxAnimationDuration = duration;
    }

    /**
     * Sets whether an overlay should be drawn when sliding the drawer.
     *
     * @param drawOverlay Whether an overlay should be drawn when sliding the drawer.
     */
    public void setDrawOverlay(boolean drawOverlay) {
        mDrawOverlay = drawOverlay;
    }

    /**
     * Gets whether an overlay is drawn when sliding the drawer.
     *
     * @return Whether an overlay is drawn when sliding the drawer.
     */
    public boolean getDrawOverlay() {
        return mDrawOverlay;
    }

    protected void updateUpContentDescription() {
        final int upContentDesc = isMenuVisible() ? mDrawerOpenContentDesc : mDrawerClosedContentDesc;
        if (mDrawerIndicatorEnabled && mActionBarHelper != null && upContentDesc != mCurrentUpContentDesc) {
            mCurrentUpContentDesc = upContentDesc;
            mActionBarHelper.setActionBarDescription(upContentDesc);
        }
    }

    /**
     * Sets the drawable used as the drawer indicator.
     *
     * @param drawable The drawable used as the drawer indicator.
     */
    public void setSlideDrawable(int drawableRes) {
        setSlideDrawable(getResources().getDrawable(drawableRes));
    }

    /**
     * Sets the drawable used as the drawer indicator.
     *
     * @param drawable The drawable used as the drawer indicator.
     */
    public void setSlideDrawable(Drawable drawable) {
        mSlideDrawable = new SlideDrawable(drawable);

        if (mActionBarHelper != null) {
            mActionBarHelper.setDisplayShowHomeAsUpEnabled(true);

            if (mDrawerIndicatorEnabled) {
                mActionBarHelper.setActionBarUpIndicator(mSlideDrawable,
                        isMenuVisible() ? mDrawerOpenContentDesc : mDrawerClosedContentDesc);
            }
        }
    }

    /**
     * Sets up the drawer indicator. It cna then be shown with {@link #setDrawerIndicatorEnabled(boolean)}.
     *
     * @param activity The activity the drawer is attached to.
     */
    public void setupUpIndicator(Activity activity) {
        if (mActionBarHelper == null) {
            mActionBarHelper = new ActionBarHelper(activity);
            mThemeUpIndicator = mActionBarHelper.getThemeUpIndicator();

            if (mDrawerIndicatorEnabled) {
                mActionBarHelper.setActionBarUpIndicator(mSlideDrawable,
                        isMenuVisible() ? mDrawerOpenContentDesc : mDrawerClosedContentDesc);
            }
        }
    }

    /**
     * Sets whether the drawer indicator should be enabled. {@link #setupUpIndicator(android.app.Activity)} must be
     * called first.
     *
     * @param enabled Whether the drawer indicator should enabled.
     */
    public void setDrawerIndicatorEnabled(boolean enabled) {
        if (mActionBarHelper == null) {
            throw new IllegalStateException("setupUpIndicator(Activity) has not been called");
        }

        mDrawerIndicatorEnabled = enabled;
        if (enabled) {
            mActionBarHelper.setActionBarUpIndicator(mSlideDrawable,
                    isMenuVisible() ? mDrawerOpenContentDesc : mDrawerClosedContentDesc);
        } else {
            mActionBarHelper.setActionBarUpIndicator(mThemeUpIndicator, 0);
        }
    }

    /**
     * Indicates whether the drawer indicator is currently enabled.
     *
     * @return Whether the drawer indicator is enabled.
     */
    public boolean isDrawerIndicatorEnabled() {
        return mDrawerIndicatorEnabled;
    }

    /**
     * Returns the ViewGroup used as a parent for the menu view.
     *
     * @return The menu view's parent.
     */
    public ViewGroup getMenuContainer() {
        return mMenuContainer;
    }

    /**
     * Returns the ViewGroup used as a parent for the content view.
     *
     * @return The content view's parent.
     */
    public ViewGroup getContentContainer() {
        if (mDragMode == MENU_DRAG_CONTENT) {
            return mContentContainer;
        } else {
            return (ViewGroup) findViewById(android.R.id.content);
        }
    }

    /**
     * Set the menu view from a layout resource.
     *
     * @param layoutResId Resource ID to be inflated.
     */
    public void setMenuView(int layoutResId) {
        mMenuContainer.removeAllViews();
        mMenuView = LayoutInflater.from(getContext()).inflate(layoutResId, mMenuContainer, false);
        mMenuContainer.addView(mMenuView);
    }

    /**
     * Set the menu view to an explicit view.
     *
     * @param view The menu view.
     */
    public void setMenuView(View view) {
        setMenuView(view, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
    }

    /**
     * Set the menu view to an explicit view.
     *
     * @param view   The menu view.
     * @param params Layout parameters for the view.
     */
    public void setMenuView(View view, LayoutParams params) {
        mMenuView = view;
        mMenuContainer.removeAllViews();
        mMenuContainer.addView(view, params);
    }

    /**
     * Returns the menu view.
     *
     * @return The menu view.
     */
    public View getMenuView() {
        return mMenuView;
    }

    /**
     * Set the content from a layout resource.
     *
     * @param layoutResId Resource ID to be inflated.
     */
    public void setContentView(int layoutResId) {
        switch (mDragMode) {
            case MenuDrawer.MENU_DRAG_CONTENT:
                mContentContainer.removeAllViews();
                LayoutInflater.from(getContext()).inflate(layoutResId, mContentContainer, true);
                break;

            case MenuDrawer.MENU_DRAG_WINDOW:
                mActivity.setContentView(layoutResId);
                break;
        }
    }

    /**
     * Set the content to an explicit view.
     *
     * @param view The desired content to display.
     */
    public void setContentView(View view) {
        setContentView(view, new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
    }

    /**
     * Set the content to an explicit view.
     *
     * @param view   The desired content to display.
     * @param params Layout parameters for the view.
     */
    public void setContentView(View view, LayoutParams params) {
        switch (mDragMode) {
            case MenuDrawer.MENU_DRAG_CONTENT:
                mContentContainer.removeAllViews();
                mContentContainer.addView(view, params);
                break;

            case MenuDrawer.MENU_DRAG_WINDOW:
                mActivity.setContentView(view, params);
                break;
        }
    }

    protected void setDrawerState(int state) {
        if (state != mDrawerState) {
            final int oldState = mDrawerState;
            mDrawerState = state;
            if (mOnDrawerStateChangeListener != null) mOnDrawerStateChangeListener.onDrawerStateChange(oldState, state);
            if (DEBUG) logDrawerState(state);
        }
    }

    protected void logDrawerState(int state) {
        switch (state) {
            case STATE_CLOSED:
                Log.d(TAG, "[DrawerState] STATE_CLOSED");
                break;

            case STATE_CLOSING:
                Log.d(TAG, "[DrawerState] STATE_CLOSING");
                break;

            case STATE_DRAGGING:
                Log.d(TAG, "[DrawerState] STATE_DRAGGING");
                break;

            case STATE_OPENING:
                Log.d(TAG, "[DrawerState] STATE_OPENING");
                break;

            case STATE_OPEN:
                Log.d(TAG, "[DrawerState] STATE_OPEN");
                break;

            default:
                Log.d(TAG, "[DrawerState] Unknown: " + state);
        }
    }

    /**
     * Returns the touch mode.
     */
    public abstract int getTouchMode();

    /**
     * Sets the drawer touch mode. Possible values are {@link #TOUCH_MODE_NONE}, {@link #TOUCH_MODE_BEZEL} or
     * {@link #TOUCH_MODE_FULLSCREEN}.
     *
     * @param mode The touch mode.
     */
    public abstract void setTouchMode(int mode);

    /**
     * Sets the size of the touch bezel.
     *
     * @param size The touch bezel size in px.
     */
    public abstract void setTouchBezelSize(int size);

    /**
     * Returns the size of the touch bezel in px.
     */
    public abstract int getTouchBezelSize();

    @Override
    public void postOnAnimation(Runnable action) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            super.postOnAnimation(action);
        } else {
            postDelayed(action, ANIMATION_DELAY);
        }
    }

    @Override
    protected boolean fitSystemWindows(Rect insets) {
        if (mDragMode == MENU_DRAG_WINDOW) {
            mMenuContainer.setPadding(0, insets.top, 0, 0);
        }
        return super.fitSystemWindows(insets);
    }

    protected void dispatchOnDrawerSlide(float openRatio, int offsetPixels) {
        if (mOnDrawerStateChangeListener != null) {
            mOnDrawerStateChangeListener.onDrawerSlide(openRatio, offsetPixels);
        }
    }

    /**
     * Saves the state of the drawer.
     *
     * @return Returns a Parcelable containing the drawer state.
     */
    public final Parcelable saveState() {
        if (mState == null) mState = new Bundle();
        saveState(mState);
        return mState;
    }

    void saveState(Bundle state) {
        // State saving isn't required for subclasses.
    }

    /**
     * Restores the state of the drawer.
     *
     * @param in A parcelable containing the drawer state.
     */
    public void restoreState(Parcelable in) {
        mState = (Bundle) in;
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        SavedState state = new SavedState(superState);

        if (mState == null) mState = new Bundle();
        saveState(mState);

        state.mState = mState;
        return state;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        SavedState savedState = (SavedState) state;
        super.onRestoreInstanceState(savedState.getSuperState());

        restoreState(savedState.mState);
    }

    static class SavedState extends BaseSavedState {

        Bundle mState;

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

        public SavedState(Parcel in) {
            super(in);
            mState = in.readBundle();
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeBundle(mState);
        }

        @SuppressWarnings("UnusedDeclaration")
        public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

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