/*
 * Copyright 2014 Soichiro Kashima
 *
 * 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 org.goodev.droidddle.widget;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;

/**
 * A layout that delegates interception of touch motion events.
 */
public class TouchInterceptionFrameLayout extends FrameLayout {

    private boolean mIntercepting;
    private boolean mDownMotionEventPended;
    private boolean mBeganFromDownMotionEvent;
    private float mInitialY;
    private MotionEvent mPendingDownMotionEvent;
    private TouchInterceptionListener mTouchInterceptionListener;

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

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

    public TouchInterceptionFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

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

    public void setScrollInterceptionListener(TouchInterceptionListener listener) {
        mTouchInterceptionListener = listener;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (mTouchInterceptionListener == null) {
            return super.onInterceptTouchEvent(ev);
        }
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                mInitialY = ev.getY();
                mPendingDownMotionEvent = MotionEvent.obtainNoHistory(ev);
                mBeganFromDownMotionEvent = true;
                mDownMotionEventPended = true;
                mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, false, 0);
                return mIntercepting;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (mTouchInterceptionListener != null) {
            switch (ev.getActionMasked()) {
                case MotionEvent.ACTION_DOWN:
                    mTouchInterceptionListener.onDownMotionEvent(ev);
                    return true;
                case MotionEvent.ACTION_MOVE:
                    float diffY = ev.getY() - mInitialY;
                    mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, true, diffY);
                    if (mIntercepting) {
                        if (!mBeganFromDownMotionEvent) {
                            mBeganFromDownMotionEvent = true;

                            // Layout didn't receive ACTION_DOWN motion event,
                            // so generate down motion event with current position.
                            MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);
                            event.setLocation(ev.getX(), ev.getY());
                            mInitialY = ev.getY();
                            diffY = 0;
                            mTouchInterceptionListener.onDownMotionEvent(event);
                        }
                        mTouchInterceptionListener.onMoveMotionEvent(ev, diffY);

                        // If next mIntercepting become false,
                        // then we should generate fake ACTION_DOWN event.
                        // Therefore we set pending flag to true as if this is a down motion event.
                        mDownMotionEventPended = true;
                    } else {
                        final boolean downMotionEventPended = mDownMotionEventPended;
                        if (mDownMotionEventPended) {
                            mDownMotionEventPended = false;
                        }

                        // We want to dispatch a down motion event and this ev event to
                        // child views, but calling dispatchTouchEvent() causes StackOverflowError.
                        // Therefore we do it manually.
                        for (int i = getChildCount() - 1; 0 <= i; i--) {
                            View childView = getChildAt(i);
                            if (childView != null) {
                                Rect childRect = new Rect();
                                childView.getHitRect(childRect);
                                if (!childRect.contains((int) ev.getX(), (int) ev.getY())) {
                                    continue;
                                }
                                boolean consumed = false;
                                if (downMotionEventPended) {
                                    // Update location to prevent the point jumping
                                    consumed = childView.onTouchEvent(mPendingDownMotionEvent);
                                }
                                consumed |= childView.onTouchEvent(ev);
                                if (consumed) {
                                    break;
                                }
                            }
                        }

                        // If next mIntercepting become true,
                        // then we should generate fake ACTION_DOWN event.
                        // Therefore we set beganFromDownMotionEvent flag to false
                        // as if we haven't received a down motion event.
                        mBeganFromDownMotionEvent = false;
                    }
                    return true;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    mBeganFromDownMotionEvent = false;
                    if (mIntercepting) {
                        mTouchInterceptionListener.onUpOrCancelMotionEvent(ev);
                    } else {
                        for (int i = 0; i < getChildCount(); i++) {
                            View childView = getChildAt(i);
                            if (childView != null) {
                                if (mDownMotionEventPended) {
                                    mDownMotionEventPended = false;
                                    childView.onTouchEvent(mPendingDownMotionEvent);
                                }
                                childView.onTouchEvent(ev);
                            }
                        }
                    }
                    return true;
            }
        }
        return super.onTouchEvent(ev);
    }

    /**
     * Callbacks for TouchInterceptionFrameLayout.
     */
    public interface TouchInterceptionListener {
        /**
         * Determines whether the layout should intercept this event.
         *
         * @param ev     motion event
         * @param moving true if this event is ACTION_MOVE type
         * @param diffY  difference between previous Y and current Y, if moving is true
         * @return true if the layout should intercept
         */
        boolean shouldInterceptTouchEvent(MotionEvent ev, boolean moving, float diffY);

        /**
         * Called if the down motion event is intercepted by this layout.
         *
         * @param ev motion event
         */
        void onDownMotionEvent(MotionEvent ev);

        /**
         * Called if the move motion event is intercepted by this layout.
         *
         * @param ev    motion event
         * @param diffY difference between previous Y and current Y
         */
        void onMoveMotionEvent(MotionEvent ev, float diffY);

        /**
         * Called if the up (or cancel) motion event is intercepted by this layout.
         *
         * @param ev motion event
         */
        void onUpOrCancelMotionEvent(MotionEvent ev);
    }
}