/*
 * Copyright (C) 2013 Masahiko Adachi(http://www.adamrocker.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.navdrawer;

import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.Scroller;

/**
 * <p>This class enables to add a NavDrawer simply.</p>
 * <p>How to use:</p>
 * <p>After calling setContentView method in onCreate method, call 2 methods bellow.</p>
 * <ul>
 *     <li>- New SimpleNavDrawer instance</li>
 *     <li>- Set a layout file which will be set in the NavDrawer.</li>
 * </ul> 
 * <pre class="prettyprint">
 * public void onCreate(Bundle data) {
 *     super.onCreate(data);
 *     setContentView(R.layout.main);
 *     SimpleNavDrawer nav = new SimpleNavDrawer(this);
 *     nav.setLeftBehindContentView(R.layout.manu);
 * }
 * </pre>
 * @author Masahiko Adachi
  */
public class SimpleSideDrawer extends FrameLayout {
    private final Window mWindow;
    private final ViewGroup mAboveView;
    private final BehindLinearLayout mBehindView;
    private final LinearLayout mLeftBehindBase;
    private final LinearLayout mRightBehindBase;
    private final View mOverlay;
    
    private Scroller mScroller;
    private View mLeftBehindView;//menu of left-behind will be set
    private View mRightBehindView;//menu of right-behind will be set
    private Rect mLeftPaddingRect;
    private Rect mRightPaddingRect;
    private int mDurationLeft;
    private int mDurationRight;
    private int mLeftBehindViewWidth;
    private int mRightBehindViewWidth;
    
    private abstract class DragAction {
        private float mLastMotionX = 0f;
        private boolean mOpening = false;
        private boolean mDraggable = false;
        abstract public boolean onTouchEvent(MotionEvent event); 
    }

    private DragAction mLeftDragAction = new DragAction() {
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            int action = ev.getAction() & MotionEvent.ACTION_MASK;
            switch (action) {
            case MotionEvent.ACTION_DOWN:
            {
                float x = ev.getX();
                mLeftDragAction.mLastMotionX = x;
                mLeftDragAction.mDraggable = mAboveView.getScrollX() != 0;
                break;
            }
            case MotionEvent.ACTION_UP:
            {
                if (mLeftDragAction.mDraggable) {
                    int currentX = mAboveView.getScrollX();
                    int diffX;
                    if (mLeftDragAction.mOpening) {
                        diffX = -(mLeftBehindViewWidth + currentX);
                    } else {
                        diffX = -currentX;
                    }
                    mScroller.startScroll(currentX, 0, diffX, 0, mDurationLeft);
                    invalidate();
                }
                break;
            }
            case MotionEvent.ACTION_MOVE:
                if (!mLeftDragAction.mDraggable) {
                    return false;
                }
            
                float newX = ev.getX();
                float diffX = -(newX - mLeftDragAction.mLastMotionX);
                int x = mAboveView.getScrollX();
                mLeftDragAction.mOpening = mLeftDragAction.mLastMotionX < newX;
                mLeftDragAction.mLastMotionX = newX;
                float nextX = x + diffX;
                if (0 < nextX) {
                    mAboveView.scrollTo(0, 0);
                } else {
                    if (nextX < -mLeftBehindViewWidth) {
                        mAboveView.scrollTo(-mLeftBehindViewWidth, 0);
                    } else {
                        mAboveView.scrollBy((int) diffX, 0);
                    }
                }
                break;
            }
            return false;
        }
    };
    
    private DragAction mRightDragAction = new DragAction() {
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            int action = ev.getAction() & MotionEvent.ACTION_MASK;
            switch (action) {
            case MotionEvent.ACTION_DOWN:
            {
                float x = ev.getX();
                mRightDragAction.mLastMotionX = x;
                mRightDragAction.mDraggable = mAboveView.getScrollX() != 0;
                break;
            }
            case MotionEvent.ACTION_UP:
            {
                if (mRightDragAction.mDraggable) {
                    int currentX = mAboveView.getScrollX();
                    int diffX;
                    if (mRightDragAction.mOpening) {
                        diffX = mRightBehindViewWidth - currentX;
                    } else {
                        diffX = -currentX;
                    }
                    mScroller.startScroll(currentX, 0, diffX, 0, mDurationRight);
                    invalidate();
                }
                break;
            }
            case MotionEvent.ACTION_MOVE:
                if (!mRightDragAction.mDraggable) {
                    return false;
                }
            
                float newX = ev.getX();
                float diffX = -(newX - mRightDragAction.mLastMotionX);
                int x = mAboveView.getScrollX();
                mRightDragAction.mOpening = newX < mRightDragAction.mLastMotionX;
                mRightDragAction.mLastMotionX = newX;
                float nextX = x + diffX;
                if (nextX < 0) {
                    mAboveView.scrollTo(0, 0);
                } else {
                    if (nextX < mRightBehindViewWidth) {
                        mAboveView.scrollBy((int) diffX, 0);
                    } else {
                        mAboveView.scrollTo(mRightBehindViewWidth, 0);
                    }
                }
                break;
            }
            return false;
        }
    };
    
    /**
     * <p>The default Interpolator of drawer animation is DecelerateInterpolator(9.9).</p>
     * <p>The default animation duration is 230msec.</p>
     * @see SimpleSideDrawer#SimpleSideDrawer(Activity, Interpolator, int);
     * @param act
     */
    public SimpleSideDrawer(Activity act) {
        this(act, new DecelerateInterpolator(0.9f), 180);
    }
    
    public SimpleSideDrawer(Activity act, Interpolator ip, int duration) {
        super(act.getApplicationContext());
        final Context context = act.getApplicationContext();
        mDurationLeft = duration;
        mDurationRight = duration;
        mWindow = act.getWindow();
        mScroller = new Scroller(context, ip);
        
        final int fp = ViewGroup.LayoutParams.MATCH_PARENT;
        final int wp = ViewGroup.LayoutParams.WRAP_CONTENT;
        //behind
        mBehindView = new BehindLinearLayout(context);
        mBehindView.setLayoutParams(new LinearLayout.LayoutParams(fp, fp));
        mBehindView.setOrientation(LinearLayout.HORIZONTAL);
        //left-behind base
        mLeftBehindBase = new BehindLinearLayout(context);
        mBehindView.addView(mLeftBehindBase, new LinearLayout.LayoutParams(wp, fp));
        //behind adjusting view
        mBehindView.addView(new View(context), new LinearLayout.LayoutParams(0, fp, 1));
        //right-behind base
        mRightBehindBase = new BehindLinearLayout(context);
        mBehindView.addView(mRightBehindBase, new LinearLayout.LayoutParams(wp, fp));

        addView(mBehindView);
        
        //above
        mAboveView = new FrameLayout(context);
        mAboveView.setLayoutParams(new FrameLayout.LayoutParams(fp, fp));
        //overlay is used for controlling drag action, slid to close/open.
        mOverlay = new OverlayView(getContext());
        mOverlay.setLayoutParams(new FrameLayout.LayoutParams(fp, fp, Gravity.BOTTOM));
        mOverlay.setEnabled(true);
        mOverlay.setVisibility(View.GONE);
        mOverlay.setOnClickListener(new OnClickListener() {
            @Override public void onClick(View v) {
                if ( mLeftBehindBase.getVisibility() != View.GONE ) {
                    closeLeftSide();
                } else if ( mRightBehindBase.getVisibility() != View.GONE ){
                    closeRightSide();
                }
            }
        });
        
        ViewGroup decor = (ViewGroup) mWindow.getDecorView();
        ViewGroup above = (ViewGroup) decor.getChildAt(0);//including actionbar
        decor.removeView(above);
        //noinspection deprecation(API16から非推奨になった無視)
        above.setBackgroundDrawable(decor.getBackground());
        mAboveView.addView(above);
        mAboveView.addView(mOverlay);
        decor.addView(this);
        
        addView(mAboveView);
    }
    
    /**
     * <p>Get the left behind view</p>
     * @return The view which you set. Return null if you did not set the view.
     */
    public View getLeftBehindView() {
        return mLeftBehindBase.getChildAt(0);
    }
    
    /**
     * <p>Get the right behind view</p>
     * @return The view which you set. Return null if you did not set the view.
     */
    public View getRightBehindView() {
        return mRightBehindBase.getChildAt(0);
    }
    
    /**
     * <p>Set the behind view layout.</p>
     * <p>Call this method after setting the main content view by calling setContentView().</p>
     * @param leftBehindLayout <p><b>leftBehindLayout</b>: The layout id, under the res/layout directory, which is displayed left side.</p>
     * @return The view which will be created from the layout id.
     * @deprecated You should use setLeftBehindContentView()
     */
    public View setBehindContentView(int leftBehindLayout) {
        return setLeftBehindContentView(leftBehindLayout);
    }
    
    /**
     * <p>Set the left behind view layout.</p>
     * <p>Call this method after setting the main content view by calling setContentView().</p>
     * @param leftBehindLayout <p><b>leftBehindLayout</b>: The layout id, under the res/layout directory, which is displayed left side.</p>
     * @return The view which will be created from the layout id.
     */
    public View setLeftBehindContentView(int leftBehindLayout) {
        final View content = ((LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(leftBehindLayout, mLeftBehindBase);
        mLeftPaddingRect = new Rect(content.getPaddingLeft(), content.getPaddingTop(), content.getPaddingRight(), content.getPaddingBottom());
        mLeftBehindView = content;
        return content;
    }

    /**
     * <p>Set the right behind view layout.</p>
     * <p>Call this method after setting the main content view by calling setContentView().</p>
     * @param rightBehindLayout <p><b>leftBehindLayout</b>: The layout id, under the res/layout directory, which is displayed left side.</p>
     * @return The view which will be created from the layout id.
     */
    public View setRightBehindContentView(int rightBehindLayout) {
        final View content = ((LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(rightBehindLayout, mRightBehindBase);
        mRightPaddingRect = new Rect(content.getPaddingLeft(), content.getPaddingTop(), content.getPaddingRight(), content.getPaddingBottom());
        mRightBehindView = content;
        return content;
    }

    /**
     * Change the side scroll interpolator
     * @param ip Interpolator object
     */
    public void setScrollInterpolator(Interpolator ip) {
        mScroller = new Scroller(getContext(), ip);
    }
    
    /**
     * Change the duration time of scrolling
     * @param msec The duration time should be milli-second
     * @deprecated You should use setAnimationDurationLeft()
     */
    public void setAnimationDuration(int msec) {
        setAnimationDurationLeft(msec);
    }
    
    /**
     * Set the duration time of left sliding animation. 
     * @param msec The duration time should be milli-second ( default = 180 )
     */
    public void setAnimationDurationLeft(int msec) {
        mDurationLeft = msec;
    }
    
    /**
     * Set the duration time of ringht sliding animation. 
     * @param msec The duration time should be milli-second ( default = 180 )
     */
    public void setAnimationDurationRight(int msec) {
        mDurationRight = msec;
    }
    
    /**
     * Close the behind view by swiping left the front view.
     * @deprecated You should use closeLeftSide()
     */
    public void close() {
        closeLeftSide();
    }
    
    /**
     * Close the left-side behind view
     */
    public void closeLeftSide() {
        int curX = -mLeftBehindViewWidth;//mAboveView.getScrollX();
        mScroller.startScroll(curX, 0, -curX, 0, mDurationLeft);
        invalidate();
    }
    
    /**
     * Close the right-side behide view 
     */
    public void closeRightSide() {
        int curX = mRightBehindViewWidth;//mAboveView.getScrollX();
        mScroller.startScroll(curX, 0, -curX, 0, mDurationRight);
        invalidate();
    }
    
    /**
     * Open the behind view by swiping the front view right
     * @deprecated You should use openLeftSide()
     */
    public void open() {
        openLeftSide();
    }
    
    /**
     * Open the left behind view by swiping the front view right
     */
    public void openLeftSide() {
        mLeftBehindBase.setVisibility( View.VISIBLE );
        mRightBehindBase.setVisibility( View.GONE );
            
        int curX = mAboveView.getScrollX();
        mScroller.startScroll(curX, 0, -mLeftBehindViewWidth, 0, mDurationLeft);
        invalidate();
    }
    
    public void openRightSide() {
        mRightBehindBase.setVisibility( View.VISIBLE );
        mLeftBehindBase.setVisibility( View.GONE );
        
        int curX = mAboveView.getScrollX();
        mScroller.startScroll(curX, 0, mRightBehindViewWidth, 0, mDurationRight);
        invalidate();
    }
    
    /**
     * If the behind view is opened, close it. If the behind view is closed, open it.
     * @deprecated You should use toggleLeftDrawer()
     */
    public void toggleDrawer() {
        toggleLeftDrawer();
    }
    
    /**
     * If the left behind view is opened, close it. If the left behind view is closed, open it.
     */
    public void toggleLeftDrawer() {
        if (isClosed()) {
            openLeftSide();
        } else {
            closeLeftSide();
        }
    }
    
    /**
     * If the right behind view is opened, close it. If the left behind view is closed, open it.
     */
    public void toggleRightDrawer() {
        if (isClosed()) {
            openRightSide();
        } else {
            closeRightSide();
        }
    }

    /**
     * Check the current status of the behind view
     * @return
     */
    public boolean isClosed() {
        return mAboveView != null && mAboveView.getScrollX() == 0;
    }
    
    private boolean isLeftSideOpened() {
        return mLeftBehindBase.getVisibility() == View.VISIBLE && mRightBehindBase.getVisibility() == View.GONE;
    }
    
    private boolean isRightSideOpened() {
        return mRightBehindBase.getVisibility() == View.VISIBLE && mLeftBehindBase.getVisibility() == View.GONE;
    }
    
    /**
     * Need to adjust the behind view height
     * {@hide}
     */
    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mLeftBehindViewWidth = mLeftBehindBase.getMeasuredWidth();
        mRightBehindViewWidth = mRightBehindBase.getMeasuredWidth();

        //adjust the behind display area
        ViewGroup decor = (ViewGroup) mWindow.getDecorView();
        Rect rect = new Rect();
        decor.getWindowVisibleDisplayFrame(rect);
        mBehindView.fitDisplay(rect);
    }
    
    /**
     * Side scroll animation
     * {@hide}
     */
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            mAboveView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        } else {
            if (mAboveView.getScrollX() == 0) {
                mOverlay.setVisibility(View.GONE);
                mLeftBehindBase.setVisibility(View.GONE);
                mRightBehindBase.setVisibility(View.GONE);
            } else {
                mOverlay.setVisibility(View.VISIBLE);
            }
        }
    }
    
    /**
     * {@hide}
     */
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (isLeftSideOpened()) {
            return mLeftDragAction.onTouchEvent(ev);
        } else if (isRightSideOpened()) {
            return mRightDragAction.onTouchEvent(ev);
        } else {
            return true;
        }
    }

    private class BehindLinearLayout extends LinearLayout {

        public BehindLinearLayout(Context context) {
            super(context);
        }
        
        /**
         * Adjust the behind view
         * @param rect The display area
         */
        public void fitDisplay(Rect rect) {
            mBehindView.setPadding(rect.left, rect.top, 0, 0);
            requestLayout();
        }
    }
    
    /**
     * Overlay view only when the behind menu is appeared.
     * This view control scrolling the above view  
     * @author Masahiko Adachi
     */
    private class OverlayView extends View {
        private static final float CLICK_RANGE = 3;
        private float mDownX;
        private float mDownY;
        private OnClickListener mClickListener;
        public OverlayView(Context context) {
            super(context);
        }
        
        @Override
        public void setOnClickListener(OnClickListener listener) {
            mClickListener = listener;
            super.setOnClickListener(listener);
        }
        
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            ev.setLocation(ev.getX() - mAboveView.getScrollX(), 0);
            SimpleSideDrawer.this.onTouchEvent(ev);
            int action = ev.getAction() & MotionEvent.ACTION_MASK;
                float x = ev.getX();
                float y = ev.getY();
            if (action == MotionEvent.ACTION_DOWN) {
                mDownX = x;
                mDownY = y;
            } else if (action == MotionEvent.ACTION_UP) {
                if (mClickListener != null) {
                    if (Math.abs(mDownX - x) < CLICK_RANGE && Math.abs(mDownY - y) < CLICK_RANGE) {
                        mClickListener.onClick(this);
                    }
                }
            }
            return true;
        }
    }
}