/*
* Copyright 2013 The Android Open Source Project
*
* 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.example.android.batchstepsensor.cardstream;

import android.animation.Animator;
import android.animation.LayoutTransition;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.LinearLayout;
import android.widget.ScrollView;

import com.example.android.common.logger.Log;
import com.example.android.batchstepsensor.R;

import java.util.ArrayList;

/**
 * A Layout that contains a stream of card views.
 */
public class CardStreamLinearLayout extends LinearLayout {

    public static final int ANIMATION_SPEED_SLOW = 1001;
    public static final int ANIMATION_SPEED_NORMAL = 1002;
    public static final int ANIMATION_SPEED_FAST = 1003;

    private static final String TAG = "CardStreamLinearLayout";
    private final ArrayList<View> mFixedViewList = new ArrayList<View>();
    private final Rect mChildRect = new Rect();
    private CardStreamAnimator mAnimators;
    private OnDissmissListener mDismissListener = null;
    private boolean mLayouted = false;
    private boolean mSwiping = false;
    private String mFirstVisibleCardTag = null;
    private boolean mShowInitialAnimation = false;

    /**
     * Handle touch events to fade/move dragged items as they are swiped out
     */
    private OnTouchListener mTouchListener = new OnTouchListener() {

        private float mDownX;
        private float mDownY;

        @Override
        public boolean onTouch(final View v, MotionEvent event) {

            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    mDownX = event.getX();
                    mDownY = event.getY();
                    break;
                case MotionEvent.ACTION_CANCEL:
                    resetAnimatedView(v);
                    mSwiping = false;
                    mDownX = 0.f;
                    mDownY = 0.f;
                    break;
                case MotionEvent.ACTION_MOVE: {

                    float x = event.getX() + v.getTranslationX();
                    float y = event.getY() + v.getTranslationY();

                    mDownX = mDownX == 0.f ? x : mDownX;
                    mDownY = mDownY == 0.f ? x : mDownY;

                    float deltaX = x - mDownX;
                    float deltaY = y - mDownY;

                    if (!mSwiping && isSwiping(deltaX, deltaY)) {
                        mSwiping = true;
                        v.getParent().requestDisallowInterceptTouchEvent(true);
                    } else {
                        swipeView(v, deltaX, deltaY);
                    }
                }
                break;
                case MotionEvent.ACTION_UP: {
                    // User let go - figure out whether to animate the view out, or back into place
                    if (mSwiping) {
                        float x = event.getX() + v.getTranslationX();
                        float y = event.getY() + v.getTranslationY();

                        float deltaX = x - mDownX;
                        float deltaY = y - mDownX;
                        float deltaXAbs = Math.abs(deltaX);

                        // User let go - figure out whether to animate the view out, or back into place
                        boolean remove = deltaXAbs > v.getWidth() / 4 && !isFixedView(v);
                        if( remove )
                            handleViewSwipingOut(v, deltaX, deltaY);
                        else
                            handleViewSwipingIn(v, deltaX, deltaY);
                    }
                    mDownX = 0.f;
                    mDownY = 0.f;
                    mSwiping = false;
                }
                break;
                default:
                    return false;
            }
            return false;
        }
    };
    private int mSwipeSlop = -1;
    /**
     * Handle end-transition animation event of each child and launch a following animation.
     */
    private LayoutTransition.TransitionListener mTransitionListener
            = new LayoutTransition.TransitionListener() {

        @Override
        public void startTransition(LayoutTransition transition, ViewGroup container, View
                view, int transitionType) {
            Log.d(TAG, "Start LayoutTransition animation:" + transitionType);
        }

        @Override
        public void endTransition(LayoutTransition transition, ViewGroup container,
                                  final View view, int transitionType) {

            Log.d(TAG, "End LayoutTransition animation:" + transitionType);
            if (transitionType == LayoutTransition.APPEARING) {
                final View area = view.findViewById(R.id.card_actionarea);
                if (area != null) {
                    runShowActionAreaAnimation(container, area);
                }
            }
        }
    };
    /**
     * Handle a hierarchy change event
     * when a new child is added, scroll to bottom and hide action area..
     */
    private OnHierarchyChangeListener mOnHierarchyChangeListener
            = new OnHierarchyChangeListener() {
        @Override
        public void onChildViewAdded(final View parent, final View child) {

            Log.d(TAG, "child is added: " + child);

            ViewParent scrollView = parent.getParent();
            if (scrollView != null && scrollView instanceof ScrollView) {
                ((ScrollView) scrollView).fullScroll(FOCUS_DOWN);
            }

            if (getLayoutTransition() != null) {
                View view = child.findViewById(R.id.card_actionarea);
                if (view != null)
                    view.setAlpha(0.f);
            }
        }

        @Override
        public void onChildViewRemoved(View parent, View child) {
            Log.d(TAG, "child is removed: " + child);
            mFixedViewList.remove(child);
        }
    };
    private int mLastDownX;

    public CardStreamLinearLayout(Context context) {
        super(context);
        initialize(null, 0);
    }

    public CardStreamLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        initialize(attrs, 0);
    }

    @SuppressLint("NewApi")
    public CardStreamLinearLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initialize(attrs, defStyle);
    }

    /**
     * add a card view w/ canDismiss flag.
     *
     * @param cardView   a card view
     * @param canDismiss flag to indicate this card is dismissible or not.
     */
    public void addCard(View cardView, boolean canDismiss) {
        if (cardView.getParent() == null) {
            initCard(cardView, canDismiss);

            ViewGroup.LayoutParams param = cardView.getLayoutParams();
            if(param == null)
                param = generateDefaultLayoutParams();

            super.addView(cardView, -1, param);
        }
    }

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        if (child.getParent() == null) {
            initCard(child, true);
            super.addView(child, index, params);
        }
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        Log.d(TAG, "onLayout: " + changed);

        if( changed && !mLayouted ){
            mLayouted = true;

            ObjectAnimator animator;
            LayoutTransition layoutTransition = new LayoutTransition();

            animator = mAnimators.getDisappearingAnimator(getContext());
            layoutTransition.setAnimator(LayoutTransition.DISAPPEARING, animator);

            animator = mAnimators.getAppearingAnimator(getContext());
            layoutTransition.setAnimator(LayoutTransition.APPEARING, animator);

            layoutTransition.addTransitionListener(mTransitionListener);

            if( animator != null )
                layoutTransition.setDuration(animator.getDuration());

            setLayoutTransition(layoutTransition);

            if( mShowInitialAnimation )
                runInitialAnimations();

            if (mFirstVisibleCardTag != null) {
                scrollToCard(mFirstVisibleCardTag);
                mFirstVisibleCardTag = null;
            }
        }
    }

    /**
     * Check whether a user moved enough distance to start a swipe action or not.
     *
     * @param deltaX
     * @param deltaY
     * @return true if a user is swiping.
     */
    protected boolean isSwiping(float deltaX, float deltaY) {

        if (mSwipeSlop < 0) {
            //get swipping slop from ViewConfiguration;
            mSwipeSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
        }

        boolean swipping = false;
        float absDeltaX = Math.abs(deltaX);

        if( absDeltaX > mSwipeSlop )
            return true;

        return swipping;
    }

    /**
     * Swipe a view by moving distance
     *
     * @param child a target view
     * @param deltaX x moving distance by x-axis.
     * @param deltaY y moving distance by y-axis.
     */
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    protected void swipeView(View child, float deltaX, float deltaY) {
        if (isFixedView(child)){
            deltaX = deltaX / 4;
        }

        float deltaXAbs = Math.abs(deltaX);
        float fractionCovered = deltaXAbs / (float) child.getWidth();

        child.setTranslationX(deltaX);
        child.setAlpha(1.f - fractionCovered);

        if (deltaX > 0)
            child.setRotationY(-15.f * fractionCovered);
        else
            child.setRotationY(15.f * fractionCovered);
    }

    protected void notifyOnDismissEvent( View child ){
        if( child == null || mDismissListener == null )
            return;

        mDismissListener.onDismiss((String) child.getTag());
    }

    /**
     * get the tag of the first visible child in this layout
     *
     * @return tag of the first visible child or null
     */
    public String getFirstVisibleCardTag() {

        final int count = getChildCount();

        if (count == 0)
            return null;

        for (int index = 0; index < count; ++index) {
            //check the position of each view.
            View child = getChildAt(index);
            if (child.getGlobalVisibleRect(mChildRect) == true)
                return (String) child.getTag();
        }

        return null;
    }

    /**
     * Set the first visible card of this linear layout.
     *
     * @param tag tag of a card which should already added to this layout.
     */
    public void setFirstVisibleCard(String tag) {
        if (tag == null)
            return; //do nothing.

        if (mLayouted) {
            scrollToCard(tag);
        } else {
            //keep the tag for next use.
            mFirstVisibleCardTag = tag;
        }
    }

    /**
     * If this flag is set,
     * after finishing initial onLayout event, an initial animation which is defined in DefaultCardStreamAnimator is launched.
     */
    public void triggerShowInitialAnimation(){
        mShowInitialAnimation = true;
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public void setCardStreamAnimator( CardStreamAnimator animators ){

        if( animators == null )
            mAnimators = new CardStreamAnimator.EmptyAnimator();
        else
            mAnimators = animators;

        LayoutTransition layoutTransition = getLayoutTransition();

        if( layoutTransition != null ){
            layoutTransition.setAnimator( LayoutTransition.APPEARING,
                    mAnimators.getAppearingAnimator(getContext()) );
            layoutTransition.setAnimator( LayoutTransition.DISAPPEARING,
                    mAnimators.getDisappearingAnimator(getContext()) );
        }
    }

    /**
     * set a OnDismissListener which called when user dismiss a card.
     *
     * @param listener
     */
    public void setOnDismissListener(OnDissmissListener listener) {
        mDismissListener = listener;
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    private void initialize(AttributeSet attrs, int defStyle) {

        float speedFactor = 1.f;

        if (attrs != null) {
            TypedArray a = getContext().obtainStyledAttributes(attrs,
                    R.styleable.CardStream, defStyle, 0);

            if( a != null ){
                int speedType = a.getInt(R.styleable.CardStream_animationDuration, 1001);
                switch (speedType){
                    case ANIMATION_SPEED_FAST:
                        speedFactor = 0.5f;
                        break;
                    case ANIMATION_SPEED_NORMAL:
                        speedFactor = 1.f;
                        break;
                    case ANIMATION_SPEED_SLOW:
                        speedFactor = 2.f;
                        break;
                }

                String animatorName = a.getString(R.styleable.CardStream_animators);

                try {
                    if( animatorName != null )
                        mAnimators = (CardStreamAnimator) getClass().getClassLoader()
                                .loadClass(animatorName).newInstance();
                } catch (Exception e) {
                    Log.e(TAG, "Fail to load animator:" + animatorName, e);
                } finally {
                    if(mAnimators == null)
                        mAnimators = new DefaultCardStreamAnimator();
                }
                a.recycle();
            }
        }

        mAnimators.setSpeedFactor(speedFactor);
        mSwipeSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
        setOnHierarchyChangeListener(mOnHierarchyChangeListener);
    }

    private void initCard(View cardView, boolean canDismiss) {
        resetAnimatedView(cardView);
        cardView.setOnTouchListener(mTouchListener);
        if (!canDismiss)
            mFixedViewList.add(cardView);
    }

    private boolean isFixedView(View v) {
        return mFixedViewList.contains(v);
    }

    private void resetAnimatedView(View child) {
        child.setAlpha(1.f);
        child.setTranslationX(0.f);
        child.setTranslationY(0.f);
        child.setRotation(0.f);
        child.setRotationY(0.f);
        child.setRotationX(0.f);
        child.setScaleX(1.f);
        child.setScaleY(1.f);
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    private void runInitialAnimations() {
        if( mAnimators == null )
            return;

        final int count = getChildCount();

        for (int index = 0; index < count; ++index) {
            final View child = getChildAt(index);
            ObjectAnimator animator =  mAnimators.getInitalAnimator(getContext());
            if( animator != null ){
                animator.setTarget(child);
                animator.start();
            }
        }
    }

    private void runShowActionAreaAnimation(View parent, View area) {
        area.setPivotY(0.f);
        area.setPivotX(parent.getWidth() / 2.f);

        area.setAlpha(0.5f);
        area.setRotationX(-90.f);
        area.animate().rotationX(0.f).alpha(1.f).setDuration(400);
    }

    private void handleViewSwipingOut(final View child, float deltaX, float deltaY) {
        ObjectAnimator animator = mAnimators.getSwipeOutAnimator(child, deltaX, deltaY);
        if( animator != null ){
            animator.addListener(new EndAnimationWrapper() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    removeView(child);
                    notifyOnDismissEvent(child);
                }
            });
        } else {
            removeView(child);
            notifyOnDismissEvent(child);
        }

        if( animator != null ){
            animator.setTarget(child);
            animator.start();
        }
    }

    private void handleViewSwipingIn(final View child, float deltaX, float deltaY) {
        ObjectAnimator animator = mAnimators.getSwipeInAnimator(child, deltaX, deltaY);
        if( animator != null ){
            animator.addListener(new EndAnimationWrapper() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    child.setTranslationY(0.f);
                    child.setTranslationX(0.f);
                }
            });
        } else {
            child.setTranslationY(0.f);
            child.setTranslationX(0.f);
        }

        if( animator != null ){
            animator.setTarget(child);
            animator.start();
        }
    }

    private void scrollToCard(String tag) {


        final int count = getChildCount();
        for (int index = 0; index < count; ++index) {
            View child = getChildAt(index);

            if (tag.equals(child.getTag())) {

                ViewParent parent = getParent();
                if( parent != null && parent instanceof ScrollView ){
                    ((ScrollView)parent).smoothScrollTo(
                            0, child.getTop() - getPaddingTop() - child.getPaddingTop());
                }
                return;
            }
        }
    }

    public interface OnDissmissListener {
        public void onDismiss(String tag);
    }

    /**
     * Empty default AnimationListener
     */
    private abstract class EndAnimationWrapper implements Animator.AnimatorListener {

        @Override
        public void onAnimationStart(Animator animation) {
        }

        @Override
        public void onAnimationCancel(Animator animation) {
        }

        @Override
        public void onAnimationRepeat(Animator animation) {
        }
    }//end of inner class
}