package net.simonvt.menudrawer;

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.GradientDrawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;

public class OverlayDrawer extends DraggableDrawer {

    private static final String TAG = "OverlayDrawer";

    private int mPeekSize;

    private Runnable mRevealRunnable = new Runnable() {
        @Override
        public void run() {
            cancelContentTouch();
            int animateTo = 0;
            switch (getPosition()) {
                case RIGHT:
                case BOTTOM:
                    animateTo = -mPeekSize;
                    break;

                default:
                    animateTo = mPeekSize;
                    break;
            }
            animateOffsetTo(animateTo, 250);
        }
    };

    OverlayDrawer(Activity activity, int dragMode) {
        super(activity, dragMode);
    }

    public OverlayDrawer(Context context) {
        super(context);
    }

    public OverlayDrawer(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

    @Override
    protected void initDrawer(Context context, AttributeSet attrs, int defStyle) {
        super.initDrawer(context, attrs, defStyle);
        super.addView(mContentContainer, -1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        if (USE_TRANSLATIONS) {
            mContentContainer.setLayerType(View.LAYER_TYPE_NONE, null);
        }
        mContentContainer.setHardwareLayersEnabled(false);
        super.addView(mMenuContainer, -1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        mPeekSize = dpToPx(20);
    }

    @Override
    protected void drawOverlay(Canvas canvas) {
        final int width = getWidth();
        final int height = getHeight();
        final int offsetPixels = (int) mOffsetPixels;
        final float openRatio = Math.abs(mOffsetPixels) / mMenuSize;

        switch (getPosition()) {
            case LEFT:
                mMenuOverlay.setBounds(offsetPixels, 0, width, height);
                break;

            case RIGHT:
                mMenuOverlay.setBounds(0, 0, width + offsetPixels, height);
                break;

            case TOP:
                mMenuOverlay.setBounds(0, offsetPixels, width, height);
                break;

            case BOTTOM:
                mMenuOverlay.setBounds(0, 0, width, height + offsetPixels);
                break;
        }

        mMenuOverlay.setAlpha((int) (MAX_MENU_OVERLAY_ALPHA * openRatio));
        mMenuOverlay.draw(canvas);
    }

    @Override
    public void openMenu(boolean animate) {
        int animateTo = 0;
        switch (getPosition()) {
            case LEFT:
            case TOP:
                animateTo = mMenuSize;
                break;

            case RIGHT:
            case BOTTOM:
                animateTo = -mMenuSize;
                break;
        }

        animateOffsetTo(animateTo, 0, animate);
    }

    @Override
    public void closeMenu(boolean animate) {
        animateOffsetTo(0, 0, animate);
    }

    @Override
    protected void onOffsetPixelsChanged(int offsetPixels) {
        if (USE_TRANSLATIONS) {
            switch (getPosition()) {
                case LEFT:
                    mMenuContainer.setTranslationX(offsetPixels - mMenuSize);
                    break;

                case TOP:
                    mMenuContainer.setTranslationY(offsetPixels - mMenuSize);
                    break;

                case RIGHT:
                    mMenuContainer.setTranslationX(offsetPixels + mMenuSize);
                    break;

                case BOTTOM:
                    mMenuContainer.setTranslationY(offsetPixels + mMenuSize);
                    break;
            }
        } else {
            switch (getPosition()) {
                case TOP:
                    mMenuContainer.offsetTopAndBottom(offsetPixels - mMenuContainer.getBottom());
                    break;

                case BOTTOM:
                    mMenuContainer.offsetTopAndBottom(offsetPixels - (mMenuContainer.getTop() - getHeight()));
                    break;

                case LEFT:
                    mMenuContainer.offsetLeftAndRight(offsetPixels - mMenuContainer.getRight());
                    break;

                case RIGHT:
                    mMenuContainer.offsetLeftAndRight(offsetPixels - (mMenuContainer.getLeft() - getWidth()));
                    break;
            }
        }

        invalidate();
    }

    @Override
    protected void initPeekScroller() {
        switch (getPosition()) {
            case RIGHT:
            case BOTTOM: {
                final int dx = -mPeekSize;
                mPeekScroller.startScroll(0, 0, dx, 0, PEEK_DURATION);
                break;
            }

            default: {
                final int dx = mPeekSize;
                mPeekScroller.startScroll(0, 0, dx, 0, PEEK_DURATION);
                break;
            }
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        onOffsetPixelsChanged((int) mOffsetPixels);
    }

    @Override
    protected GradientDrawable.Orientation getDropShadowOrientation() {
        switch (getPosition()) {
            case TOP:
                return GradientDrawable.Orientation.TOP_BOTTOM;

            case RIGHT:
                return GradientDrawable.Orientation.RIGHT_LEFT;

            case BOTTOM:
                return GradientDrawable.Orientation.BOTTOM_TOP;

            default:
                return GradientDrawable.Orientation.LEFT_RIGHT;
        }
    }

    @Override
    protected void updateDropShadowRect() {
        final float openRatio = Math.abs(mOffsetPixels) / mMenuSize;
        final int dropShadowSize = (int) (mDropShadowSize * openRatio);

        switch (getPosition()) {
            case LEFT:
                mDropShadowRect.top = 0;
                mDropShadowRect.bottom = getHeight();
                mDropShadowRect.left = ViewHelper.getRight(mMenuContainer);
                mDropShadowRect.right = mDropShadowRect.left + dropShadowSize;
                break;

            case TOP:
                mDropShadowRect.left = 0;
                mDropShadowRect.right = getWidth();
                mDropShadowRect.top = ViewHelper.getBottom(mMenuContainer);
                mDropShadowRect.bottom = mDropShadowRect.top + dropShadowSize;
                break;

            case RIGHT:
                mDropShadowRect.top = 0;
                mDropShadowRect.bottom = getHeight();
                mDropShadowRect.right = ViewHelper.getLeft(mMenuContainer);
                mDropShadowRect.left = mDropShadowRect.right - dropShadowSize;
                break;

            case BOTTOM:
                mDropShadowRect.left = 0;
                mDropShadowRect.right = getWidth();
                mDropShadowRect.bottom = ViewHelper.getTop(mMenuContainer);
                mDropShadowRect.top = mDropShadowRect.bottom - dropShadowSize;
                break;
        }
    }

    @Override
    protected void startLayerTranslation() {
        if (USE_TRANSLATIONS && mHardwareLayersEnabled && !mLayerTypeHardware) {
            mLayerTypeHardware = true;
            mMenuContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
        }
    }

    @Override
    protected void stopLayerTranslation() {
        if (mLayerTypeHardware) {
            mLayerTypeHardware = false;
            mMenuContainer.setLayerType(View.LAYER_TYPE_NONE, null);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int width = r - l;
        final int height = b - t;

        mContentContainer.layout(0, 0, width, height);

        if (USE_TRANSLATIONS) {
            switch (getPosition()) {
                case LEFT:
                    mMenuContainer.layout(0, 0, mMenuSize, height);
                    break;

                case RIGHT:
                    mMenuContainer.layout(width - mMenuSize, 0, width, height);
                    break;

                case TOP:
                    mMenuContainer.layout(0, 0, width, mMenuSize);
                    break;

                case BOTTOM:
                    mMenuContainer.layout(0, height - mMenuSize, width, height);
                    break;
            }

        } else {
            final int offsetPixels = (int) mOffsetPixels;
            final int menuSize = mMenuSize;

            switch (getPosition()) {
                case LEFT:
                    mMenuContainer.layout(-menuSize + offsetPixels, 0, offsetPixels, height);
                    break;

                case RIGHT:
                    mMenuContainer.layout(width + offsetPixels, 0, width + menuSize + offsetPixels, height);
                    break;

                case TOP:
                    mMenuContainer.layout(0, -menuSize + offsetPixels, width, offsetPixels);
                    break;

                case BOTTOM:
                    mMenuContainer.layout(0, height + offsetPixels, width, height + menuSize + offsetPixels);
                    break;
            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) {
            throw new IllegalStateException("Must measure with an exact size");
        }

        final int width = MeasureSpec.getSize(widthMeasureSpec);
        final int height = MeasureSpec.getSize(heightMeasureSpec);

        if (mOffsetPixels == -1) openMenu(false);

        int menuWidthMeasureSpec;
        int menuHeightMeasureSpec;
        switch (getPosition()) {
            case TOP:
            case BOTTOM:
                menuWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, width);
                menuHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, 0, mMenuSize);
                break;

            default:
                // LEFT/RIGHT
                menuWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, mMenuSize);
                menuHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, height);
        }
        mMenuContainer.measure(menuWidthMeasureSpec, menuHeightMeasureSpec);

        final int contentWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, width);
        final int contentHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, height);
        mContentContainer.measure(contentWidthMeasureSpec, contentHeightMeasureSpec);

        setMeasuredDimension(width, height);

        updateTouchAreaSize();
    }

    private boolean isContentTouch(int x, int y) {
        boolean contentTouch = false;

        switch (getPosition()) {
            case LEFT:
                contentTouch = ViewHelper.getRight(mMenuContainer) < x;
                break;

            case RIGHT:
                contentTouch = ViewHelper.getLeft(mMenuContainer) > x;
                break;

            case TOP:
                contentTouch = ViewHelper.getBottom(mMenuContainer) < y;
                break;

            case BOTTOM:
                contentTouch = ViewHelper.getTop(mMenuContainer) > y;
                break;
        }

        return contentTouch;
    }

    protected boolean onDownAllowDrag(int x, int y) {
        switch (getPosition()) {
            case LEFT:
                return (!mMenuVisible && mInitialMotionX <= mTouchSize)
                        || (mMenuVisible && mInitialMotionX <= mOffsetPixels);

            case RIGHT:
                final int width = getWidth();
                final int initialMotionX = (int) mInitialMotionX;

                return (!mMenuVisible && initialMotionX >= width - mTouchSize)
                        || (mMenuVisible && initialMotionX >= width + mOffsetPixels);

            case TOP:
                return (!mMenuVisible && mInitialMotionY <= mTouchSize)
                        || (mMenuVisible && mInitialMotionY <= mOffsetPixels);

            case BOTTOM:
                final int height = getHeight();
                return (!mMenuVisible && mInitialMotionY >= height - mTouchSize)
                        || (mMenuVisible && mInitialMotionY >= height + mOffsetPixels);
        }

        return false;
    }

    protected boolean onMoveAllowDrag(int x, int y, float dx, float dy) {
        if (mMenuVisible && mTouchMode == TOUCH_MODE_FULLSCREEN) {
            return true;
        }

        switch (getPosition()) {
            case LEFT:
                return (!mMenuVisible && mInitialMotionX <= mTouchSize && (dx > 0)) // Drawer closed
                        || (mMenuVisible && x <= mOffsetPixels) // Drawer open
                        || (Math.abs(mOffsetPixels) <= mPeekSize && mMenuVisible); // Drawer revealed

            case RIGHT:
                final int width = getWidth();
                return (!mMenuVisible && mInitialMotionX >= width - mTouchSize && (dx < 0))
                        || (mMenuVisible && x >= width - mOffsetPixels)
                        || (Math.abs(mOffsetPixels) <= mPeekSize && mMenuVisible);

            case TOP:
                return (!mMenuVisible && mInitialMotionY <= mTouchSize && (dy > 0))
                        || (mMenuVisible && x <= mOffsetPixels)
                        || (Math.abs(mOffsetPixels) <= mPeekSize && mMenuVisible);

            case BOTTOM:
                final int height = getHeight();
                return (!mMenuVisible && mInitialMotionY >= height - mTouchSize && (dy < 0))
                        || (mMenuVisible && x >= height - mOffsetPixels)
                        || (Math.abs(mOffsetPixels) <= mPeekSize && mMenuVisible);
        }

        return false;
    }

    protected void onMoveEvent(float dx, float dy) {
        switch (getPosition()) {
            case LEFT:
                setOffsetPixels(Math.min(Math.max(mOffsetPixels + dx, 0), mMenuSize));
                break;

            case RIGHT:
                setOffsetPixels(Math.max(Math.min(mOffsetPixels + dx, 0), -mMenuSize));
                break;

            case TOP:
                setOffsetPixels(Math.min(Math.max(mOffsetPixels + dy, 0), mMenuSize));
                break;

            case BOTTOM:
                setOffsetPixels(Math.max(Math.min(mOffsetPixels + dy, 0), -mMenuSize));
                break;
        }
    }

    protected void onUpEvent(int x, int y) {
        final int offsetPixels = (int) mOffsetPixels;

        switch (getPosition()) {
            case LEFT: {
                if (mIsDragging) {
                    mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
                    final int initialVelocity = (int) getXVelocity(mVelocityTracker);
                    mLastMotionX = x;
                    animateOffsetTo(initialVelocity > 0 ? mMenuSize : 0, initialVelocity, true);

                    // Close the menu when content is clicked while the menu is visible.
                } else if (mMenuVisible) {
                    closeMenu();
                }
                break;
            }

            case TOP: {
                if (mIsDragging) {
                    mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
                    final int initialVelocity = (int) getYVelocity(mVelocityTracker);
                    mLastMotionY = y;
                    animateOffsetTo(initialVelocity > 0 ? mMenuSize : 0, initialVelocity, true);

                    // Close the menu when content is clicked while the menu is visible.
                } else if (mMenuVisible) {
                    closeMenu();
                }
                break;
            }

            case RIGHT: {
                final int width = getWidth();

                if (mIsDragging) {
                    mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
                    final int initialVelocity = (int) getXVelocity(mVelocityTracker);
                    mLastMotionX = x;
                    animateOffsetTo(initialVelocity > 0 ? 0 : -mMenuSize, initialVelocity, true);

                    // Close the menu when content is clicked while the menu is visible.
                } else if (mMenuVisible) {
                    closeMenu();
                }
                break;
            }

            case BOTTOM: {
                if (mIsDragging) {
                    mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
                    final int initialVelocity = (int) getYVelocity(mVelocityTracker);
                    mLastMotionY = y;
                    animateOffsetTo(initialVelocity < 0 ? -mMenuSize : 0, initialVelocity, true);

                    // Close the menu when content is clicked while the menu is visible.
                } else if (mMenuVisible) {
                    closeMenu();
                }
                break;
            }
        }
    }

    protected boolean checkTouchSlop(float dx, float dy) {
        switch (getPosition()) {
            case TOP:
            case BOTTOM:
                return Math.abs(dy) > mTouchSlop && Math.abs(dy) > Math.abs(dx);

            default:
                return Math.abs(dx) > mTouchSlop && Math.abs(dx) > Math.abs(dy);
        }
    }

    @Override
    protected void stopAnimation() {
        super.stopAnimation();
        removeCallbacks(mRevealRunnable);
    }

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction() & MotionEvent.ACTION_MASK;

        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
            removeCallbacks(mRevealRunnable);
            mActivePointerId = INVALID_POINTER;
            mIsDragging = false;
            if (mVelocityTracker != null) {
                mVelocityTracker.recycle();
                mVelocityTracker = null;
            }

            if (Math.abs(mOffsetPixels) > mMenuSize / 2) {
                openMenu();
            } else {
                closeMenu();
            }

            return false;
        }

        if (action == MotionEvent.ACTION_DOWN && mMenuVisible && isCloseEnough()) {
            setOffsetPixels(0);
            stopAnimation();
            endPeek();
            setDrawerState(STATE_CLOSED);
            mIsDragging = false;
        }

        // Always intercept events over the content while menu is visible.
        if (mMenuVisible) {
            int index = 0;
            if (mActivePointerId != INVALID_POINTER) {
                index = ev.findPointerIndex(mActivePointerId);
                index = index == -1 ? 0 : index;
            }

            final int x = (int) ev.getX(index);
            final int y = (int) ev.getY(index);
            if (isContentTouch(x, y)) {
                return true;
            }
        }

        if (!mMenuVisible && !mIsDragging && mTouchMode == TOUCH_MODE_NONE) {
            return false;
        }

        if (action != MotionEvent.ACTION_DOWN && mIsDragging) {
            return true;
        }

        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                mLastMotionX = mInitialMotionX = ev.getX();
                mLastMotionY = mInitialMotionY = ev.getY();
                final boolean allowDrag = onDownAllowDrag((int) mLastMotionX, (int) mLastMotionY);
                mActivePointerId = ev.getPointerId(0);

                if (allowDrag) {
                    setDrawerState(mMenuVisible ? STATE_OPEN : STATE_CLOSED);
                    stopAnimation();
                    endPeek();

                    if (!mMenuVisible && mInitialMotionX <= mPeekSize) {
                        postDelayed(mRevealRunnable, 160);
                    }

                    mIsDragging = false;
                }
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                final int activePointerId = mActivePointerId;
                if (activePointerId == INVALID_POINTER) {
                    // If we don't have a valid id, the touch down wasn't on content.
                    break;
                }

                final int pointerIndex = ev.findPointerIndex(activePointerId);
                if (pointerIndex == -1) {
                    mIsDragging = false;
                    mActivePointerId = INVALID_POINTER;
                    endDrag();
                    closeMenu(true);
                    return false;
                }

                final float x = ev.getX(pointerIndex);
                final float dx = x - mLastMotionX;
                final float y = ev.getY(pointerIndex);
                final float dy = y - mLastMotionY;

                if (Math.abs(dx) >= mTouchSlop || Math.abs(dy) >= mTouchSlop) {
                    removeCallbacks(mRevealRunnable);
                    endPeek();
                }

                if (checkTouchSlop(dx, dy)) {
                    if (mOnInterceptMoveEventListener != null && (mTouchMode == TOUCH_MODE_FULLSCREEN || mMenuVisible)
                            && canChildrenScroll((int) dx, (int) dy, (int) x, (int) y)) {
                        endDrag(); // Release the velocity tracker
                        requestDisallowInterceptTouchEvent(true);
                        return false;
                    }

                    final boolean allowDrag = onMoveAllowDrag((int) x, (int) y, dx, dy);

                    if (allowDrag) {
                        endPeek();
                        stopAnimation();
                        setDrawerState(STATE_DRAGGING);
                        mIsDragging = true;
                        mLastMotionX = x;
                        mLastMotionY = y;
                    }
                }
                break;
            }

            case MotionEvent.ACTION_POINTER_UP:
                onPointerUp(ev);
                mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId));
                mLastMotionY = ev.getY(ev.findPointerIndex(mActivePointerId));
                break;
        }

        if (mVelocityTracker == null) mVelocityTracker = VelocityTracker.obtain();
        mVelocityTracker.addMovement(ev);

        return mIsDragging;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (!mMenuVisible && !mIsDragging && mTouchMode == TOUCH_MODE_NONE) {
            return false;
        }
        final int action = ev.getAction() & MotionEvent.ACTION_MASK;

        if (mVelocityTracker == null) mVelocityTracker = VelocityTracker.obtain();
        mVelocityTracker.addMovement(ev);

        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                mLastMotionX = mInitialMotionX = ev.getX();
                mLastMotionY = mInitialMotionY = ev.getY();
                final boolean allowDrag = onDownAllowDrag((int) mLastMotionX, (int) mLastMotionY);

                mActivePointerId = ev.getPointerId(0);

                if (allowDrag) {
                    stopAnimation();
                    endPeek();

                    if (!mMenuVisible && mLastMotionX <= mPeekSize) {
                        postDelayed(mRevealRunnable, 160);
                    }

                    startLayerTranslation();
                }
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                final int pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex == -1) {
                    mIsDragging = false;
                    mActivePointerId = INVALID_POINTER;
                    endDrag();
                    closeMenu(true);
                    return false;
                }

                if (!mIsDragging) {
                    final float x = ev.getX(pointerIndex);
                    final float dx = x - mLastMotionX;
                    final float y = ev.getY(pointerIndex);
                    final float dy = y - mLastMotionY;

                    if (checkTouchSlop(dx, dy)) {
                        final boolean allowDrag = onMoveAllowDrag((int) x, (int) y, dx, dy);

                        if (allowDrag) {
                            endPeek();
                            stopAnimation();
                            setDrawerState(STATE_DRAGGING);
                            mIsDragging = true;
                            mLastMotionX = x;
                            mLastMotionY = y;
                        } else {
                            mInitialMotionX = x;
                            mInitialMotionY = y;
                        }
                    }
                }

                if (mIsDragging) {
                    startLayerTranslation();

                    final float x = ev.getX(pointerIndex);
                    final float dx = x - mLastMotionX;
                    final float y = ev.getY(pointerIndex);
                    final float dy = y - mLastMotionY;

                    mLastMotionX = x;
                    mLastMotionY = y;
                    onMoveEvent(dx, dy);
                }
                break;
            }

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP: {
                removeCallbacks(mRevealRunnable);
                int index = ev.findPointerIndex(mActivePointerId);
                index = index == -1 ? 0 : index;
                final int x = (int) ev.getX(index);
                final int y = (int) ev.getY(index);
                onUpEvent(x, y);
                mActivePointerId = INVALID_POINTER;
                mIsDragging = false;
                break;
            }

            case MotionEvent.ACTION_POINTER_DOWN:
                final int index = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                mLastMotionX = ev.getX(index);
                mLastMotionY = ev.getY(index);
                mActivePointerId = ev.getPointerId(index);
                break;

            case MotionEvent.ACTION_POINTER_UP:
                onPointerUp(ev);
                mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId));
                mLastMotionY = ev.getY(ev.findPointerIndex(mActivePointerId));
                break;
        }

        return true;
    }

    private void onPointerUp(MotionEvent ev) {
        final int pointerIndex = ev.getActionIndex();
        final int pointerId = ev.getPointerId(pointerIndex);
        if (pointerId == mActivePointerId) {
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            mLastMotionX = ev.getX(newPointerIndex);
            mActivePointerId = ev.getPointerId(newPointerIndex);
            if (mVelocityTracker != null) {
                mVelocityTracker.clear();
            }
        }
    }
}
