package dreamers.graphics;

import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;

import java.lang.ref.WeakReference;

public class TouchTracker implements View.OnTouchListener {

    RippleDrawable mHotspotDrawable;

    PerformClick mPerformClick;
    CheckForTap mPendingCheckForTap;
    CheckForLongPress mPendingCheckForLongPress;
    UnsetPressedState mUnsetPressedState;

    boolean mHasPerformedLongPress;

    int mTouchSlop;
    boolean mPrePressed;
    boolean mInsideScrollContainer;

    public TouchTracker(RippleDrawable hotspot) {
        mHotspotDrawable = hotspot;

        mTouchSlop = -1;
    }

    public void setInsideScrollContainer(boolean inside) {
        mInsideScrollContainer = inside;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();

        if (v.isClickable() || v.isLongClickable()) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    if ((mPrePressed && pointInView(v, x, y, mTouchSlop)) || v.isPressed()) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (v.isFocusable() && v.isFocusableInTouchMode() && !v.isFocused()) {
                            focusTaken = v.requestFocus();
                        }

                        if (mPrePressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(v, true, x, y);
                        }

                        if (!mHasPerformedLongPress) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback(v);

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick(v);
                                }
                                if (!v.post(mPerformClick)) {
                                    v.performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState(v);
                        }

                        if (mPrePressed) {
                            v.postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!v.post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback(v);

                    }
                    break;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (mInsideScrollContainer) {

                        mPrePressed = true;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap(v);
                        }

                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        v.postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());

                    } else {
                        setPressed(v, true, x, y);
                        checkForLongClick(v, 0);
                    }
                    break;

                case MotionEvent.ACTION_MOVE:
                    mHotspotDrawable.setHotspot(x, y);

                    if (mTouchSlop == -1) {
                        mTouchSlop = ViewConfiguration.get(v.getContext()).getScaledTouchSlop();
                    }

                    // Be lenient about moving outside of buttons
                    if (!pointInView(v, x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback(v);
                        if (v.isPressed()) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback(v);

                            v.setPressed(false);
                        }
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    v.setPressed(false);
                    removeTapCallback(v);
                    removeLongPressCallback(v);
                    break;
            }
            return true;
        }

        return false;
    }

    private void checkForLongClick(View target, int delayOffset) {
        if (target.isLongClickable()) {
            mHasPerformedLongPress = false;

            if (mPendingCheckForLongPress == null) {
                mPendingCheckForLongPress = new CheckForLongPress(target);
            }
            target.postDelayed(mPendingCheckForLongPress,
                    ViewConfiguration.getLongPressTimeout() - delayOffset);
        }
    }

    void setPressed(View target, boolean pressed, float x, float y) {
        target.setPressed(pressed);
        mHotspotDrawable.setHotspot(x, y);
    }

    /**
     * Utility method to determine whether the given point, in local coordinates,
     * is inside the view, where the area of the view is expanded by the slop factor.
     * This method is called while processing touch-move events to determine if the event
     * is still within the view.
     *
     * @hide
     */
    public boolean pointInView(View target, float localX, float localY, float slop) {
        return localX >= -slop && localY >= -slop && localX < ((target.getRight() - target.getLeft()) + slop) &&
                localY < ((target.getBottom() - target.getTop()) + slop);
    }


    void removeTapCallback(View target) {
        if (mPendingCheckForTap != null) {
            target.removeCallbacks(mPendingCheckForTap);
        }
    }

    void removeLongPressCallback(View target) {
        if (mPendingCheckForLongPress != null) {
            target.removeCallbacks(mPendingCheckForLongPress);
        }
    }

    private final static class PerformClick implements Runnable {

        WeakReference<View> target;

        private PerformClick(View target) {
            this.target = new WeakReference<>(target);
        }

        @Override
        public void run() {
            if (target.get() != null) {
                target.get().performClick();
            }
        }
    }


    final class CheckForTap implements Runnable {

        View target;
        float x, y;

        CheckForTap(View target) {
            this.target = target;
        }

        @Override
        public void run() {
            mPrePressed = true;
            setPressed(target, true, x, y);
            checkForLongClick(target, ViewConfiguration.getTapTimeout());
        }
    }

    private final class CheckForLongPress implements Runnable {

        View target;

        private CheckForLongPress(View target) {
            this.target = target;
        }

        @Override
        public void run() {
            if (target.isPressed() && (target.getParent() != null)) {
                if (target.performLongClick()) {
                    mHasPerformedLongPress = true;
                    mPrePressed = false;
                }
            }
        }
    }

    private static final class UnsetPressedState implements Runnable {

        View target;

        private UnsetPressedState(View target) {
            this.target = target;
        }

        @Override
        public void run() {
            target.setPressed(false);
        }
    }

}
