package com.wmbest.widget;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.*;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.animation.Interpolator;
import android.view.animation.BounceInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.OvershootInterpolator;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;

import com.nineoldandroids.animation.*;
import com.nineoldandroids.view.ViewHelper;
import com.nineoldandroids.view.ViewPropertyAnimator;

public class SwipeableLayout extends FrameLayout {
    protected static final String TAG = SwipeableLayout.class.getSimpleName();

    protected int mWidth;
    protected View mBackChild;
    protected View mFrontChild;
    protected View mTabChild;
    protected ViewGroup mFrontContainer;
    protected ViewGroup mBackContainer;

    protected Bitmap mFrontCache;
    protected Bitmap mBackCache;

    private int mFrontId = -1;
    private int mBackId  = -1;
    private int mTabId   = -1;

    protected int mFrontNaturalHeight;
    protected int mBackNaturalHeight;

    protected boolean mIsOpen = false;
    protected boolean mIsAnimating = false;
    private boolean mSwipeable = true;

    protected Direction mDirection = Direction.LEFT;
    protected MotionGrabber mMotionGrabber;

    protected int mSwipeThresh;
    protected int mFrameGravity;
    protected int mGrabSize;
    protected int mPeekSize;

    private boolean mFromBind;
    private boolean mFromIntercept;

    private static String ISOPEN = "isopen";
    private static String FRONTNAT = "frontheight";
    private static String BACKNAT = "backheight";

    private OnOpenListener mListener;

    public SwipeableLayout(Context context, AttributeSet attrs) {
        super(context, attrs, 0);

        mWidth = context.getResources().getDisplayMetrics().widthPixels;

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SwipeableLayout); 
         
        mFrontId = a.getResourceId(R.styleable.SwipeableLayout_frontView, -1);
        mBackId = a.getResourceId(R.styleable.SwipeableLayout_backView, -1);
        mTabId = a.getResourceId(R.styleable.SwipeableLayout_tabView, -1);

        mPeekSize = a.getDimensionPixelSize(R.styleable.SwipeableLayout_peekSize, 48);
        mGrabSize = a.getDimensionPixelSize(R.styleable.SwipeableLayout_grabSize, 48);
        mSwipeThresh = a.getDimensionPixelSize(R.styleable.SwipeableLayout_swipeThreshold, 20);
        mFrameGravity = a.getInt(R.styleable.SwipeableLayout_android_layout_gravity, 0);

        int dir = a.getInt(R.styleable.SwipeableLayout_direction, 0);
        mDirection = Direction.values()[dir];
        updateGrabber();

        a.recycle();

        if (mFrontId == -1 || mBackId == -1) {
            throw new RuntimeException("SwipableLayout requires a frontView and backView attribute to be set");
        }

    }

    public View getFrontView() {
        return mFrontContainer;
    }

    public View getBackView() {
        return mBackContainer;
    }

    public void saveInstanceState(Bundle outState){
        outState.putBoolean(ISOPEN, mIsOpen);
        outState.putInt(FRONTNAT, mFrontNaturalHeight);
        outState.putInt(BACKNAT, mBackNaturalHeight);
    }

    public void restoreInstanceState(final Bundle saveState){
        if(saveState != null){
            post(new Runnable() {
                @Override
                public void run() {
                    mFrontNaturalHeight = saveState.getInt(FRONTNAT);
                    mBackNaturalHeight = saveState.getInt(BACKNAT);
                    if (saveState.getBoolean(ISOPEN)) {
                        open(false);
                    } else {
                        close(false);
                    }
                }
            });
        }
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        mBackChild =  findViewById(mBackId);
        mFrontChild = findViewById(mFrontId);

        if (mTabId != -1) {
            mTabChild = findViewById(mTabId);
        }

        mFrontContainer = wrapChild(mFrontChild);
        mBackContainer = wrapChild(mBackChild);

        mBackContainer.setVisibility(View.GONE);
        mFrontContainer.getLayoutParams().height  = LayoutParams.WRAP_CONTENT;
        mFrontContainer.setVisibility(View.VISIBLE);
    }

    public void clearDrawingCache() {
        if (!mIsOpen && mFrontCache != null) {
            mFrontCache.recycle();
            mFrontCache = null;
        }

        if (mBackCache != null) {
            mBackCache.recycle();
            mBackCache = null;
        }
    }

    private ViewGroup wrapChild(View aChild) {
        int index = indexOfChild(aChild);
        FrameLayout f = new FrameLayout(getContext());
        removeView(aChild);

        f.setBackgroundDrawable(aChild.getBackground());
        aChild.setBackgroundDrawable(null);

        LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
        addView(f, index, params);
        f.addView(aChild, 0, aChild.getLayoutParams());

        return f;
    }

    public void setSwipeable(boolean aIsSwipeable) {
        mSwipeable = aIsSwipeable;
    }

    public void setOpen(boolean isOpen) {
        mIsOpen = isOpen;

        if (mTabChild != null) {
            mTabChild.setVisibility(mIsOpen ? View.GONE : View.VISIBLE);
        }

        float margin = mPeekSize - mWidth;
        if (mDirection == Direction.RIGHT) {
            margin = -margin;
        }

        if (mIsOpen) {
            ViewHelper.setX(mFrontContainer, margin);
            mBackContainer.setVisibility(View.VISIBLE);
        } else {
            ViewHelper.setX(mFrontContainer, 0);
            mBackContainer.setVisibility(View.GONE);
            mFrontContainer.getLayoutParams().height = LayoutParams.WRAP_CONTENT;
            resetForContainer(mFrontContainer, mFrontChild);
        }
    }

    public void open(boolean animate) {
        mBackContainer.setVisibility(View.VISIBLE);

        int time = animate ? 300 : 0;

        float x = ViewHelper.getX(mFrontContainer);
        float margin = mPeekSize - mWidth;
        if (mDirection == Direction.RIGHT) {
            margin = -margin;
        }
        int height = (getLayoutParams().height < 0) ? mBackNaturalHeight : getLayoutParams().height;
        AnimatorSet set = new AnimatorSet();
        set.playTogether(
            ObjectAnimator.ofFloat(mFrontContainer, "x", x, margin),
            getHeightAnimator(height, mBackNaturalHeight)
        );
        set.setDuration(time);
        set.setInterpolator(new OvershootInterpolator());
        set.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator aAnimator) {
                mIsAnimating = false;
                mIsOpen = true;
                updateOpenState();
                getLayoutParams().height = LayoutParams.WRAP_CONTENT;
                resetForContainer(mBackContainer, mBackChild);
            }
        });
        set.start();
    }

    public void close(boolean animate) {
        int time = animate ? 300 : 0;

        int height = (getLayoutParams().height < 0) ? mFrontNaturalHeight : getLayoutParams().height;

        float x = ViewHelper.getX(mFrontContainer);
        AnimatorSet set = new AnimatorSet();
        set.playTogether(
            ObjectAnimator.ofFloat(mFrontContainer, "x", x, 0f),
            getHeightAnimator(height, mFrontNaturalHeight)
        );
        set.setDuration(time);
        set.setInterpolator(new SlamInterpolator());
        set.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator aAnimator) {
                mIsAnimating = false;
                mIsOpen = false;
                updateOpenState();

                resetForContainer(mBackContainer, mBackChild);
                mBackContainer.setVisibility(View.GONE);
                if (mTabChild != null) {
                    mTabChild.setVisibility(View.VISIBLE);
                }
                resetForContainer(mFrontContainer, mFrontChild);
                getLayoutParams().height = LayoutParams.WRAP_CONTENT;
            }
        });
        set.start();
    }
    
    private Animator getHeightAnimator(int start, int finish) {
        mIsAnimating = true;
        ValueAnimator va = ValueAnimator.ofInt(start, finish);
        va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            public void onAnimationUpdate(ValueAnimator animation) {
                Integer value = (Integer) animation.getAnimatedValue();

                int diff = Math.abs(getLayoutParams().height - value);
                if (diff > 0) {
                    getLayoutParams().height = value;
                    mFrontContainer.getLayoutParams().height = value;
                    mBackContainer.getLayoutParams().height = value;
                    requestLayout();
                }
            }
        });
        return va;
    }

    private void updateGrabber() {
        if (mDirection == Direction.LEFT) {
            mMotionGrabber = new LeftMotionGrabber();
        } else {
            mMotionGrabber = new RightMotionGrabber();
        }
    }

    public void setOnOpenListener(OnOpenListener aListener) {
        mListener = aListener;
    }

    protected void updateOpenState() {
        if (mListener != null) {
            mListener.onOpen(mIsOpen);
        }
    }

    public boolean isOpen() {
        return mIsOpen;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        if (!mIsAnimating) {
            mBackContainer.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED);
            mBackNaturalHeight = mBackContainer.getMeasuredHeight();

            if (mIsOpen) {
                mFrontContainer.getLayoutParams().height=mBackNaturalHeight;
            } else {
                mFrontContainer.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED);
                mFrontNaturalHeight = mFrontContainer.getMeasuredHeight();
            }
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mWidth = getMeasuredWidth();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent aEvent) {

        if(!mSwipeable) return false;

        int action = aEvent.getActionMasked();
        final int x = (int) aEvent.getRawX();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mMotionGrabber.setTouchDown(x);
                if (!mIsAnimating)
                    clearDrawingCache();
                break;
            case MotionEvent.ACTION_MOVE:
                mFromIntercept = mMotionGrabber.handleInterceptMove(x) || super.onInterceptTouchEvent(aEvent);
                return mFromIntercept;
        }

        return super.onInterceptTouchEvent(aEvent);

    }


    @Override
    public boolean onTouchEvent(MotionEvent aEvent) {

        if(!mSwipeable) return false;

        int action = aEvent.getActionMasked();
        final int x = (int) aEvent.getRawX();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mMotionGrabber.setTouchDown(x);
                mFromIntercept = false;
                return true;
            case MotionEvent.ACTION_MOVE:
                if (!mFromIntercept && mMotionGrabber.handleInterceptMove(x)) mFromIntercept = true;
                if (mFromIntercept) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                    if (mTabChild != null && mTabChild.isShown()) {
                        mTabChild.setVisibility(View.GONE);
                    }
                    mMotionGrabber.handleTouchMove(x);
                    return true;
                }
                return false;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if (mFromIntercept) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                    mFromIntercept = false;
                    handleTouchUp(x);
                    return true;
                }
                return false;
        }
        return super.onTouchEvent(aEvent);

    }

    private void fixContainerContents() {
        mFrontCache = fixForContainer(mFrontContainer, mFrontCache);
        mBackCache = fixForContainer(mBackContainer, mBackCache);
    }

    private Bitmap fixForContainer(ViewGroup aContainer, Bitmap aCache) {
        if(aContainer.getChildAt(0).getVisibility() == View.GONE) return null;

        if (aCache == null) {
            aCache = generateCache(aContainer);
        }

        ImageView cache = new ImageView(getContext());
        cache.setScaleType(ImageView.ScaleType.MATRIX);
        cache.setImageBitmap(aCache);
        cache.setTag("CACHE");

        aContainer.removeViewAt(0);

        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
        params.gravity = mFrameGravity;
        aContainer.addView(cache, 0, params);

        return aCache;
    }

    private Bitmap generateCache(ViewGroup aContainer) {
        View view = aContainer.getChildAt(0);
        int width = 0;
        if (view.getMeasuredWidth() <= 0 || view.getMeasuredHeight() <= 0) {
            int parentSpec = MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED);
            aContainer.measure(parentSpec, parentSpec);
        }
        width = view.getMeasuredWidth();

        int spec = MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED);
        int widthspec = MeasureSpec.makeMeasureSpec(width,MeasureSpec.EXACTLY);
        view.measure(widthspec,spec);
        view.layout(0, 0, width, view.getMeasuredHeight());

        Bitmap b = Bitmap.createBitmap(width, view.getHeight(), 
                Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(b);
        view.draw(canvas);

        return b;
    }

    private void resetForContainer(ViewGroup aContainer, View aChild) {
        if (aContainer.getChildAt(0).getTag() == null) return;

        aContainer.removeViewAt(0);
        aContainer.addView(aChild, 0, aChild.getLayoutParams());
        aContainer.getLayoutParams().height = LayoutParams.WRAP_CONTENT;
        aContainer.requestLayout();
    }

    private void handleTouchUp(int aX) {
        mMotionGrabber.handleRelease(aX);
    }

    public interface OnOpenListener {
        public void onOpen(boolean aOpen);
    }

    public static enum Direction {
        LEFT, RIGHT;
    }

    public abstract class MotionGrabber {
        protected int mTouchDownX;
        protected int mDeltaHeight;

        public void setTouchDown(int aTouchDown) {
            mTouchDownX = aTouchDown;
        }
        
        public int getTouchDown() {
            return mTouchDownX;
        }

        public abstract boolean handleInterceptMove(int aX);
        public abstract int getMargin(int aDelta);
        public abstract boolean shouldOpen(int aX);

        public void handleTouchMove(int aX) {
            int deltaX = aX - mTouchDownX;

            int startHeight = mIsOpen ?  mBackNaturalHeight : mFrontNaturalHeight;
            int margin = getMargin(deltaX);

            float ratio = (float) Math.abs(deltaX) / mWidth;
            int height = startHeight - (int)(ratio * mDeltaHeight);

            mIsAnimating = true;
            getLayoutParams().height = height;
            mFrontContainer.getLayoutParams().height = height;
            mBackContainer.getLayoutParams().height = height;
            requestLayout();

            AnimatorSet set = new AnimatorSet();
            set.playTogether(
                ObjectAnimator.ofFloat(mFrontContainer, "x", margin, margin)
            );
            set.start();

            if (!mBackContainer.isShown()) {
                mBackContainer.setVisibility(View.VISIBLE);
                mBackChild.setVisibility(View.VISIBLE);
            } else {
                fixContainerContents();
            }

        }

        public void handleRelease(int aX) {
            if (shouldOpen(aX)) {
                open(true);
            } else {
                close(true);
            }
        }
    }

    public class LeftMotionGrabber extends MotionGrabber {
        @Override
        public boolean handleInterceptMove(int aX) {
            int deltaX = aX - mTouchDownX;
            boolean isIntercept = false;
            if (Math.abs(deltaX) > mSwipeThresh) {
                if(mIsOpen){
                    isIntercept = mTouchDownX < mGrabSize;
                    mDeltaHeight = mBackNaturalHeight - mFrontNaturalHeight;
                }else{
                    isIntercept = deltaX < 0;
                    mDeltaHeight = mFrontNaturalHeight - mBackNaturalHeight;
                }

            }
            return isIntercept;
        }

        @Override
        public int getMargin(int aDeltaX) {
            int startHeight;
            int margin;
            if (mIsOpen) {
                if (aDeltaX >= 0) {
                    margin = ((mPeekSize - mWidth) + aDeltaX);
                } else {
                    margin = mPeekSize - mWidth;
                    aDeltaX = 0;
                }
            }
            else {
                if (aDeltaX <= 0)
                    margin = aDeltaX;
                else {
                    margin = 0;
                    aDeltaX = 0;
                }
            }
            return margin;
        }

        @Override
        public boolean shouldOpen(int aX) { 
            return (aX <= mWidth/2); 
        }
    }

    public class RightMotionGrabber extends MotionGrabber {
        @Override
        public boolean handleInterceptMove(int aX) {
            int deltaX = aX - mTouchDownX;
            boolean isIntercept = false;
            if (Math.abs(deltaX) > mSwipeThresh) {
                if (mIsOpen) {
                    isIntercept = mTouchDownX > (mWidth - mGrabSize);
                    mDeltaHeight = mBackNaturalHeight - mFrontNaturalHeight;
                } else {
                    isIntercept = deltaX > 0;
                    mDeltaHeight = mFrontNaturalHeight - mBackNaturalHeight;
                }

            }
            return isIntercept;
        }

        @Override
        public int getMargin(int aDeltaX) {
            int margin;
            if (mIsOpen) {
                if (aDeltaX <= 0)
                    margin = mWidth + aDeltaX;
                else {
                    margin = mWidth - mPeekSize;
                    aDeltaX = 0;
                }
            }
            else {
                if (aDeltaX >= 0)
                    margin = aDeltaX;
                else {
                    margin = 0;
                    aDeltaX = 0;
                }
            }
            return margin;
        }

        @Override
        public boolean shouldOpen(int aX) { 
            return (aX >= mWidth/2); 
        }
    }

    public class SlamInterpolator implements Interpolator {
        float mTension = 2.0f;
        public float getInterpolation(float t) {
            // _o(t) = t * t * ((tension + 1) * t + tension)
            // o(t) = _o(t - 1) + 1
            t -= 1.0f;
            float val = t * t * ((mTension + 1) * t + mTension) + 1.0f;
            if (val > 1f) {
                val += 1.5 * (1f - val);
            }
            return val;
        }
    }

}