package com.goka.flickableview;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.SystemClock;
import android.support.v4.animation.AnimatorCompatHelper;
import android.support.v4.animation.AnimatorListenerCompat;
import android.support.v4.animation.AnimatorUpdateListenerCompat;
import android.support.v4.animation.ValueAnimatorCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPropertyAnimatorListenerAdapter;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;

/**
 * Created by katsuyagoto on 15/07/04.
 */
public class FlickableImageView extends ImageViewTouchBase {

    public static final String TAG = FlickableImageView.class.getSimpleName();

    public enum Direction {
        UP, DOWN;

        public static Direction decide(float value) {
            if (value >= 0) {
                return UP;
            } else {
                return DOWN;
            }
        }
    }

    private static final int SNAPBACK_ANIMATION_TIME = 500;

    private static final int DISMISS_ANIMATION_TIME = 400;

    private static final long MIN_FLING_DELTA_TIME = 150;

    private long mPointerUpTime;

    private float mScaleFactor;

    protected int mTouchSlop;

    protected int mDoubleTapDirection;

    private float mPreviousPositionY;

    protected ScaleGestureDetector mScaleDetector;

    protected GestureDetector mGestureDetector;

    protected GestureDetector.OnGestureListener mGestureListener;

    protected ScaleGestureDetector.OnScaleGestureListener mScaleListener;

    protected boolean mDoubleTapEnabled = true;

    protected boolean mScaleEnabled = true;

    protected boolean mScrollEnabled = true;

    private boolean mDragging = false;

    private boolean mScaling = false;

    private OnFlickableImageViewDoubleTapListener mDoubleTapListener;

    private OnFlickableImageViewSingleTapListener mSingleTapListener;

    private OnFlickableImageViewFlickListener mOnFlickListener;

    private OnFlickableImageViewDraggingListener mOnDraggingListener;

    private OnFlickableImageViewZoomListener mOnZoomListener;

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

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

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

    @Override
    protected void init(Context context, AttributeSet attrs, int defStyle) {
        super.init(context, attrs, defStyle);
        mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
        mGestureListener = getGestureListener();
        mScaleListener = getScaleListener();

        mScaleDetector = new ScaleGestureDetector(getContext(), mScaleListener);
        mGestureDetector = new GestureDetector(getContext(), mGestureListener, null, true);
        mDoubleTapDirection = 1;
        setQuickScaleEnabled(false);
    }

    @TargetApi(19)
    public void setQuickScaleEnabled(boolean value) {
        if (Build.VERSION.SDK_INT >= 19) {
            mScaleDetector.setQuickScaleEnabled(value);
        }
    }

    @TargetApi(19)
    @SuppressWarnings("unused")
    public boolean getQuickScaleEnabled() {
        if (Build.VERSION.SDK_INT >= 19) {
            return mScaleDetector.isQuickScaleEnabled();
        }
        return false;
    }

    public void setOnDoubleTapListener(OnFlickableImageViewDoubleTapListener listener) {
        mDoubleTapListener = listener;
    }

    public void setOnSingleTapListener(OnFlickableImageViewSingleTapListener listener) {
        mSingleTapListener = listener;
    }

    public void setOnFlickListener(OnFlickableImageViewFlickListener onFlickListener) {
        this.mOnFlickListener = onFlickListener;
    }

    public void setOnDraggingListener(OnFlickableImageViewDraggingListener onDraggingListener) {
        this.mOnDraggingListener = onDraggingListener;
    }

    public void setOnZoomListener(OnFlickableImageViewZoomListener onZoomListener) {
        this.mOnZoomListener = onZoomListener;
    }

    public void setDoubleTapEnabled(boolean value) {
        mDoubleTapEnabled = value;
    }

    public void setScaleEnabled(boolean value) {
        mScaleEnabled = value;
    }

    public void setScrollEnabled(boolean value) {
        mScrollEnabled = value;
    }

    public boolean getDoubleTapEnabled() {
        return mDoubleTapEnabled;
    }

    protected GestureDetector.OnGestureListener getGestureListener() {
        return new GestureListener();
    }

    protected ScaleGestureDetector.OnScaleGestureListener getScaleListener() {
        return new ScaleListener();
    }

    @Override
    protected void setBaseImageDrawable(final Drawable drawable, final Matrix initialMatrix, float minZoom,
            float maxZoom) {
        super.setBaseImageDrawable(drawable, initialMatrix, minZoom, maxZoom);
    }

    @Override
    protected void onLayoutChanged(final int left, final int top, final int right, final int bottom) {
        super.onLayoutChanged(left, top, right, bottom);
        LogUtil.V(TAG, "min: " + getMinScale() + ", max: " + getMaxScale() + ", result: "
                + (getMaxScale() - getMinScale()) / 2f);
        mScaleFactor = ((getMaxScale() - getMinScale()) / 2f) + 0.5f;
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (getBitmapChanged()) {
            return false;
        }

        final int action = event.getActionMasked();

        if (action == MotionEvent.ACTION_POINTER_UP) {
            mPointerUpTime = event.getEventTime();
        }

        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
            LogUtil.D(TAG, "SnapBack");
            snapBack();
        }

        mScaleDetector.onTouchEvent(event);

        if (!mScaleDetector.isInProgress()) {
            mGestureDetector.onTouchEvent(event);
        }

        switch (action) {
            case MotionEvent.ACTION_UP:
                return onUp(event);
        }
        return true;
    }

    protected float onDoubleTapPost(float scale, final float maxZoom, final float minScale) {
        if ((scale + mScaleFactor) <= maxZoom) {
            return scale + mScaleFactor;
        } else {
            return minScale;
        }
    }

    public boolean onSingleTapConfirmed(MotionEvent e) {
        return true;
    }

    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {

        LogUtil.I(TAG, "onScroll");

        if (!canScroll()) {
            if (mOnDraggingListener != null) {
                if (!mDragging) {
                    mOnDraggingListener.onStartDrag();
                    mDragging = true;
                }
            }

            mPreviousPositionY = e1.getY();
            float currY = e2.getY();

            float deltaY = currY - mPreviousPositionY;
            ViewCompat.setTranslationY(this, ViewCompat.getTranslationY(this) + deltaY);
            return true;
        }

        mUserScaled = true;
        scrollBy(-distanceX, -distanceY);
        invalidate();
        return true;
    }

    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        LogUtil.I(TAG, "onFling");

        if (!canScroll()) {
            LogUtil.I(TAG, "Flicker");

            View target = FlickableImageView.this;
            float translationY = target.getHeight() * 2;

            switch (Direction.decide(-ViewCompat.getTranslationY(this))) {
                case UP:
                    if (mOnFlickListener != null) {
                        mOnFlickListener.onStartFlick();
                    }
                    ViewCompat.animate(target)
                            .translationY(-translationY)
                            .setDuration(DISMISS_ANIMATION_TIME)
                            .setListener(new ViewPropertyAnimatorListenerAdapter() {
                                @Override
                                public void onAnimationEnd(View view) {
                                    if (mOnFlickListener != null) {
                                        mOnFlickListener.onFinishFlick();
                                    }
                                }
                            });
                    break;
                case DOWN:
                    if (mOnFlickListener != null) {
                        mOnFlickListener.onStartFlick();
                    }
                    ViewCompat.animate(target)
                            .translationY(translationY)
                            .setDuration(DISMISS_ANIMATION_TIME)
                            .setListener(new ViewPropertyAnimatorListenerAdapter() {
                                @Override
                                public void onAnimationEnd(View view) {
                                    if (mOnFlickListener != null) {
                                        mOnFlickListener.onFinishFlick();
                                    }
                                }
                            });
                    break;
            }
            return true;
        }

        if (Math.abs(velocityX) > (mMinFlingVelocity * 4) || Math.abs(velocityY) > (mMinFlingVelocity * 4)) {
            LogUtil.V(TAG, "velocity: " + velocityY);
            LogUtil.V(TAG, "diff: " + (e2.getY() - e1.getY()));

            final float scale = Math.min(Math.max(2f, getScale() / 2), 3.f);

            float scaledDistanceX = ((velocityX) / mMaxFlingVelocity) * (getWidth() * scale);
            float scaledDistanceY = ((velocityY) / mMaxFlingVelocity) * (getHeight() * scale);

            LogUtil.V(TAG, "scale: " + getScale() + ", scale_final: " + scale);
            LogUtil.V(TAG, "scaledDistanceX: " + scaledDistanceX);
            LogUtil.V(TAG, "scaledDistanceY: " + scaledDistanceY);

            mUserScaled = true;

            double total = Math.sqrt(Math.pow(scaledDistanceX, 2) + Math.pow(scaledDistanceY, 2));

            scrollBy(scaledDistanceX, scaledDistanceY, (long) Math.min(Math.max(300, total / 5), 800));

            postInvalidate();
            return true;
        }
        return false;
    }

    public boolean onDown(MotionEvent e) {
        if (getBitmapChanged()) {
            return false;
        }
        return true;
    }

    public boolean onUp(MotionEvent e) {
        if (getBitmapChanged()) {
            return false;
        }
        float minScale = getMinScale();
        if (getScale() < minScale) {
            zoomTo(minScale, 50);
            if (mOnZoomListener != null) {
                if (mScaling) {
                    mOnZoomListener.onBackFromMinScale();
                    mScaling = false;
                }
            }
        }
        return true;
    }

    public boolean onSingleTapUp(MotionEvent e) {
        if (getBitmapChanged()) {
            return false;
        }
        return true;
    }

    public boolean canScroll() {
        if (getScale() > 1) {
            return true;
        }
        RectF bitmapRect = getBitmapRect();
        return !mViewPort.contains(bitmapRect);
    }

    public class GestureListener extends GestureDetector.SimpleOnGestureListener {

        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {

            if (null != mSingleTapListener) {
                mSingleTapListener.onSingleTapConfirmed();
            }

            return FlickableImageView.this.onSingleTapConfirmed(e);
        }

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            LogUtil.I(TAG, "onDoubleTap. double tap enabled? " + mDoubleTapEnabled);

            if (mDoubleTapEnabled) {
                if (Build.VERSION.SDK_INT >= 19) {
                    if (mScaleDetector.isQuickScaleEnabled()) {
                        return true;
                    }
                }

                mUserScaled = true;

                float minScale = getMinScale();
                float scale = getScale();
                float targetScale;
                targetScale = onDoubleTapPost(scale, getMaxScale(), minScale);
                targetScale = Math.min(getMaxScale(), Math.max(targetScale, minScale));
                zoomTo(targetScale, e.getX(), e.getY(), mDefaultAnimationDuration);
            }

            if (null != mDoubleTapListener) {
                mDoubleTapListener.onDoubleTap();
            }

            return super.onDoubleTap(e);
        }

        @Override
        public void onLongPress(MotionEvent e) {
            if (isLongClickable()) {
                if (!mScaleDetector.isInProgress()) {
                    setPressed(true);
                    performLongClick();
                }
            }
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            if (!mScrollEnabled) {
                return false;
            }
            if (e1 == null || e2 == null) {
                return false;
            }
            if (e1.getPointerCount() > 1 || e2.getPointerCount() > 1) {
                return false;
            }
            if (mScaleDetector.isInProgress()) {
                return false;
            }
            return FlickableImageView.this.onScroll(e1, e2, distanceX, distanceY);
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            if (!mScrollEnabled) {
                return false;
            }
            if (e1 == null || e2 == null) {
                return false;
            }
            if (e1.getPointerCount() > 1 || e2.getPointerCount() > 1) {
                return false;
            }
            if (mScaleDetector.isInProgress()) {
                return false;
            }

            final long delta = (SystemClock.uptimeMillis() - mPointerUpTime);
            if (delta > MIN_FLING_DELTA_TIME) {
                return FlickableImageView.this.onFling(e1, e2, velocityX, velocityY);
            } else {
                return false;
            }
        }

        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return FlickableImageView.this.onSingleTapUp(e);
        }

        @Override
        public boolean onDown(MotionEvent e) {
            LogUtil.I(TAG, "onDown");

            stopAllAnimations();

            return FlickableImageView.this.onDown(e);
        }
    }

    public class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {

        protected boolean mScaled = false;

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            float span = detector.getCurrentSpan() - detector.getPreviousSpan();
            float targetScale = getScale() * detector.getScaleFactor();

            if (mScaleEnabled) {
                if (mOnZoomListener != null) {
                    if (!mScaling) {
                        mOnZoomListener.onStartZoom();
                        mScaling = true;
                    }
                }

                if (mScaled && span != 0) {
                    mUserScaled = true;
                    targetScale = Math.min(getMaxScale(), Math.max(targetScale, getMinScale() - 0.1f));
                    zoomTo(targetScale, detector.getFocusX(), detector.getFocusY());
                    mDoubleTapDirection = 1;
                    invalidate();
                    return true;
                }

                if (!mScaled) {
                    mScaled = true;
                }
            }
            return true;
        }

    }

    private void snapBack() {
        final float currentY = ViewCompat.getY(this);

        ValueAnimatorCompat animatorCompat = AnimatorCompatHelper.emptyValueAnimator();
        animatorCompat.setDuration(SNAPBACK_ANIMATION_TIME);
        final Interpolator interpolator = new DecelerateInterpolator();
        animatorCompat.addUpdateListener(new AnimatorUpdateListenerCompat() {
            @Override
            public void onAnimationUpdate(ValueAnimatorCompat animation) {
                float fraction = interpolator.getInterpolation(animation.getAnimatedFraction());
                float interpolatedValue = currentY - (currentY * fraction);
                ViewCompat.setTranslationY(FlickableImageView.this, interpolatedValue);
            }
        });
        animatorCompat.addListener(new AnimatorListenerCompat() {
            @Override
            public void onAnimationStart(ValueAnimatorCompat animation) {

            }

            @Override
            public void onAnimationEnd(ValueAnimatorCompat animation) {
                if (mOnDraggingListener != null) {
                    if (mDragging) {
                        mOnDraggingListener.onCancelDrag();
                        mDragging = false;
                    }
                }
            }

            @Override
            public void onAnimationCancel(ValueAnimatorCompat animation) {

            }

            @Override
            public void onAnimationRepeat(ValueAnimatorCompat animation) {

            }
        });
        animatorCompat.start();
    }


    public interface OnFlickableImageViewDoubleTapListener {

        void onDoubleTap();
    }

    public interface OnFlickableImageViewSingleTapListener {

        void onSingleTapConfirmed();
    }

    public interface OnFlickableImageViewFlickListener {

        void onStartFlick();

        void onFinishFlick();
    }

    public interface OnFlickableImageViewDraggingListener {

        void onStartDrag();

        void onCancelDrag();
    }

    public interface OnFlickableImageViewZoomListener {

        void onStartZoom();

        void onBackFromMinScale();

    }

}
