/*
 * Copyright (C) 2014 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.android.tv.settings.util;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.graphics.RectF;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

/**
 * Class used by an activity to perform animation of multiple TransitionImageViews
 * Usage:
 * - on activity create:
 *   TransitionImageAnimation animation = new TransitionImageAnimation(rootView);
 *   for_each TransitionImage of source
 *       animation.addTransitionSource(source);
 *   animation.startCancelTimer();
 * - When the activity loads all target images
 *   for_each TransitionImage of target
 *       animation.addTransitionTarget(target);
 *   animation.startTransition();
 */
public class TransitionImageAnimation {

    public static class Listener {

        public void onRemovedView(TransitionImage src, TransitionImage dst) {
        }

        public void onCancelled(TransitionImageAnimation animation) {
        }

        public void onFinished(TransitionImageAnimation animation) {
        }
    }

    private static long DEFAULT_TRANSITION_TIMEOUT_MS = 2000;
    private static long DEFAULT_CANCEL_TRANSITION_MS = 250;
    private static long DEFAULT_TRANSITION_DURATION_MS = 250;
    private static long DEFAULT_TRANSITION_START_DELAY_MS = 160;

    private Interpolator mInterpolator = new DecelerateInterpolator();
    private ViewGroup mRoot;
    private long mTransitionTimeoutMs = DEFAULT_TRANSITION_TIMEOUT_MS;
    private long mCancelTransitionMs = DEFAULT_CANCEL_TRANSITION_MS;
    private long mTransitionDurationMs = DEFAULT_TRANSITION_DURATION_MS;
    private long mTransitionStartDelayMs = DEFAULT_TRANSITION_START_DELAY_MS;
    private List<TransitionImageView> mTransitions = new ArrayList<TransitionImageView>();
    private Listener mListener;
    private Comparator<TransitionImage> mComparator = new TransitionImageMatcher();

    private static final int STATE_INITIAL = 0;
    private static final int STATE_WAIT_DST = 1;
    private static final int STATE_TRANSITION = 2;
    private static final int STATE_CANCELLED = 3;
    private static final int STATE_FINISHED = 4;
    private int mState;

    private boolean mListeningLayout;
    private static RectF sTmpRect1 = new RectF();
    private static RectF sTmpRect2 = new RectF();

    public TransitionImageAnimation(ViewGroup root) {
        mRoot = root;
        mState = STATE_INITIAL;
    }

    /**
     * Set listener for animation events
     */
    public TransitionImageAnimation listener(Listener listener) {
        mListener = listener;
        return this;
    }

    public Listener getListener() {
        return mListener;
    }

    /**
     * set comparator for matching src and dst ImageTransition
     */
    public TransitionImageAnimation comparator(Comparator<TransitionImage> comparator) {
        mComparator = comparator;
        return this;
    }

    public Comparator<TransitionImage> getComparator() {
        return mComparator;
    }

    /**
     * set interpolator used for animating the Transition
     */
    public TransitionImageAnimation interpolator(Interpolator interpolator) {
        mInterpolator = interpolator;
        return this;
    }

    public Interpolator getInterpolator() {
        return mInterpolator;
    }

    /**
     * set timeout in ms for {@link #startCancelTimer}
     */
    public TransitionImageAnimation timeoutMs(long timeoutMs) {
        mTransitionTimeoutMs = timeoutMs;
        return this;
    }

    public long getTimeoutMs() {
        return mTransitionTimeoutMs;
    }

    /**
     * set duration of fade out animation when cancel the transition
     */
    public TransitionImageAnimation cancelDurationMs(long ms) {
        mCancelTransitionMs = ms;
        return this;
    }

    public long getCancelDurationMs() {
        return mCancelTransitionMs;
    }

    /**
     * set start delay of transition animation
     */
    public TransitionImageAnimation transitionStartDelayMs(long delay) {
        mTransitionStartDelayMs = delay;
        return this;
    }

    public long getTransitionStartDelayMs() {
        return mTransitionStartDelayMs;
    }

    /**
     * set duration of transition animation
     */
    public TransitionImageAnimation transitionDurationMs(long duration) {
        mTransitionDurationMs = duration;
        return this;
    }

    public long getTransitionDurationMs() {
        return mTransitionDurationMs;
    }

    /**
     * add source transition and create initial view in root
     */
    public void addTransitionSource(TransitionImage src) {
        if (mState != STATE_INITIAL) {
            return;
        }
        TransitionImageView view = new TransitionImageView(mRoot.getContext());
        mRoot.addView(view);
        view.setSourceTransition(src);
        mTransitions.add(view);
        if (!mListeningLayout) {
            mListeningLayout = true;
            mRoot.addOnLayoutChangeListener(mInitializeClip);
        }
    }

    /**
     * kick off the timer for cancel transition
     */
    public void startCancelTimer() {
        if (mState != STATE_INITIAL) {
            return;
        }
        mRoot.postDelayed(mCancelTransitionRunnable, mTransitionTimeoutMs);
        mState = STATE_WAIT_DST;
    }

    private Runnable mCancelTransitionRunnable = new Runnable() {

        @Override
        public void run() {
            cancelTransition();
        }

    };

    private void setProgress(float progress) {
        // draw from last child (top most in z-order)
        int lastIndex = mTransitions.size() - 1;
        for (int i = lastIndex; i >= 0; i--) {
            TransitionImageView view = mTransitions.get(i);
            view.setProgress(progress);
            sTmpRect2.left = 0;
            sTmpRect2.top = 0;
            sTmpRect2.right = view.getWidth();
            sTmpRect2.bottom = view.getHeight();
            WindowLocationUtil.getLocationsInWindow(view, sTmpRect2);
            if (i == lastIndex) {
                view.clearExcludeClipRect();
                sTmpRect1.set(sTmpRect2);
            } else {
                view.setExcludeClipRect(sTmpRect1);
                // FIXME: this assumes 3rd image will be clipped by "1st union 2nd",
                // applies to certain situation such as images are stacked in one row
                sTmpRect1.union(sTmpRect2);
            }
            view.invalidate();
        }
    }

    private View.OnLayoutChangeListener mInitializeClip = new View.OnLayoutChangeListener() {
        @Override
        public void onLayoutChange(View v, int left, int top, int right, int bottom,
                int oldLeft, int oldTop, int oldRight, int oldBottom) {
            v.removeOnLayoutChangeListener(this);
            mListeningLayout = false;
            // set initial clipping for all views
            setProgress(0f);
        }
    };

    /**
     * start transition
     */
    public void startTransition() {
        if (mState != STATE_WAIT_DST && mState != STATE_INITIAL) {
            return;
        }
        for (int i = mTransitions.size() - 1; i >= 0; i--) {
            TransitionImageView view = mTransitions.get(i);
            if (view.getDestTransition() == null) {
                cancelTransition(view);
                mTransitions.remove(i);
            }
        }
        if (mTransitions.size() == 0) {
            mState = STATE_CANCELLED;
            if (mListener != null) {
                mListener.onCancelled(this);
            }
            return;
        }
        ValueAnimator v = ValueAnimator.ofFloat(0f, 1f);
        v.setInterpolator(mInterpolator);
        v.setDuration(mTransitionDurationMs);
        v.setStartDelay(mTransitionStartDelayMs);
        v.addUpdateListener(new AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float progress = animation.getAnimatedFraction();
                setProgress(progress);
            }
        });
        v.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                for (int i = 0, count = mTransitions.size(); i < count; i++) {
                    final TransitionImageView view = mTransitions.get(i);
                    if (mListener != null) {
                        mListener.onRemovedView(view.getSourceTransition(),
                                view.getDestTransition());
                    }
                    mRoot.removeView(view);
                }
                mTransitions.clear();
                mState = STATE_FINISHED;
                if (mListener != null) {
                    mListener.onFinished(TransitionImageAnimation.this);
                }
            }
        });
        v.start();
        mState = STATE_TRANSITION;
    }

    private void cancelTransition(final View iv) {
        iv.animate().alpha(0f).setDuration(mCancelTransitionMs).
            setListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator arg0) {
                mRoot.removeView(iv);
            }
        }).start();
    }

    /**
     * Cancel the transition before it starts, no effect if it already starts
     */
    public void cancelTransition() {
        if (mState != STATE_WAIT_DST && mState != STATE_INITIAL) {
            return;
        }
        int count = mTransitions.size();
        if (count > 0) {
            for (int i = 0; i < count; i++) {
                cancelTransition(mTransitions.get(i));
            }
            mTransitions.clear();
        }
        mState = STATE_CANCELLED;
        if (mListener != null) {
            mListener.onCancelled(this);
        }
    }

    /**
     * find a matching source and relate it with destination
     */
    public boolean addTransitionTarget(TransitionImage dst) {
        if (mState != STATE_WAIT_DST && mState != STATE_INITIAL) {
            return false;
        }
        for (int i = 0, count = mTransitions.size(); i < count; i++) {
            TransitionImageView view = mTransitions.get(i);
            if (mComparator.compare(view.getSourceTransition(), dst) == 0) {
                view.setDestTransition(dst);
                return true;
            }
        }
        return false;
    }
}
