package de.cypressworks.animationlistview;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import android.content.Context;
import android.database.DataSetObserver;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnPreDrawListener;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.OvershootInterpolator;
import android.widget.BaseAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;

import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.Animator.AnimatorListener;
import com.nineoldandroids.animation.AnimatorListenerAdapter;
import com.nineoldandroids.animation.AnimatorSet;
import com.nineoldandroids.animation.ObjectAnimator;
import com.nineoldandroids.view.ViewHelper;
import com.nineoldandroids.view.animation.AnimatorProxy;

@SuppressWarnings({ "rawtypes", "unchecked" })
public class AnimationListView extends ListView {

	public static interface Manipulator<T extends ListAdapter> {
		void manipulate(T adapter);
	}

	private static class AdapterWrapper extends BaseAdapter {
		private final ListAdapter adapter;
		private boolean mayNotify = true;

		private final DataSetObserver observer = new DataSetObserver() {
			@Override
			public void onChanged() {
				if (mayNotify) {
					notifyDataSetChanged();
				}
			}

			@Override
			public void onInvalidated() {
				notifyDataSetInvalidated();
			};
		};

		public AdapterWrapper(final ListAdapter adapter) {
			this.adapter = adapter;

			adapter.registerDataSetObserver(observer);
		}

		public void setMayNotify(final boolean mayNotify) {
			this.mayNotify = mayNotify;
		}

		@Override
		public int getCount() {
			return adapter.getCount();
		}

		@Override
		public Object getItem(final int position) {
			return adapter.getItem(position);
		}

		@Override
		public long getItemId(final int position) {
			return adapter.getItemId(position);
		}

		@Override
		public boolean hasStableIds() {
			return adapter.hasStableIds();
		}

		@Override
		public View getView(final int position, final View convertView, final ViewGroup parent) {
			return adapter.getView(position, convertView, parent);
		}

	}

	protected static final int MAX_ANIM_DURATION = 900;
	protected static final int MIN_ANIM_DURATION = 500;
	protected static final int ALPHA_ANIM_DURATION = 300;

	protected final Map<Long, Float> yMap = new HashMap<Long, Float>();
	protected final Map<Long, Integer> positionMap = new HashMap<Long, Integer>();
	protected final Collection<Long> beforeVisible = new HashSet<>();
	protected final Collection<Long> afterVisible = new HashSet<>();

	private final List<Manipulator> pendingManipulations = new ArrayList<>();
	private final List<Manipulator> pendingManipulationsWithoutAnimation = new ArrayList<>();

	private boolean animating = false;
	private AdapterWrapper adapter;
	private float animationDurationFactor = 1f;
	private Interpolator translateInterpolater = new OvershootInterpolator(1.1f);

	public AnimationListView(final Context context, final AttributeSet attrs, final int defStyle) {
		super(context, attrs, defStyle);
		init();
	}

	public AnimationListView(final Context context, final AttributeSet attrs) {
		super(context, attrs);
		init();
	}

	public AnimationListView(final Context context) {
		super(context);
		init();
	}

	private void init() {
	}

	public void setAnimationDurationFactor(final float animationDurationFactor) {
		this.animationDurationFactor = animationDurationFactor;
	}

	public float getAnimationDurationFactor() {
		return animationDurationFactor;
	}

	public void setInterpolater(final Interpolator translateInterpolater) {
		this.translateInterpolater = translateInterpolater;
	}

	public Interpolator getInterpolater() {
		return translateInterpolater;
	}

	@Override
	public void setAdapter(final ListAdapter adapter) {
		this.adapter = new AdapterWrapper(adapter);
		super.setAdapter(this.adapter);
	}

	public <T extends ListAdapter> void manipulate(final Manipulator<T> manipulator) {
		if (!animating) {
			prepareAnimation();

			manipulator.manipulate((T) adapter.adapter);

			doAnimation();
		} else {
			pendingManipulations.add(manipulator);
		}
	}

	public <T extends ListAdapter> void manipulateWithoutAnimation(final Manipulator<T> manipulator) {
		if (!animating) {
			manipulator.manipulate((T) adapter.adapter);
			adapter.notifyDataSetChanged();
		} else {
			pendingManipulationsWithoutAnimation.add(manipulator);
		}
	}

	private void manipulatePending() {

		if (!pendingManipulationsWithoutAnimation.isEmpty()) {
			animating = true;
			for (final Manipulator manipulator : pendingManipulationsWithoutAnimation) {
				manipulator.manipulate(adapter.adapter);
			}
			pendingManipulationsWithoutAnimation.clear();
			adapter.notifyDataSetChanged();

			post(new Runnable() {

				@Override
				public void run() {
					animating = false;
					manipulatePending();
				}
			});
		} else {

			if (pendingManipulations.isEmpty()) {
				return;
			}

			prepareAnimation();

			for (final Manipulator manipulator : pendingManipulations) {
				manipulator.manipulate(adapter.adapter);
			}
			pendingManipulations.clear();

			doAnimation();
		}
	}

	private void prepareAnimation() {
		yMap.clear();
		positionMap.clear();
		beforeVisible.clear();
		afterVisible.clear();

		adapter.setMayNotify(false);

		final int childCount = getChildCount();

		final int firstVisiblePosition = getFirstVisiblePosition();

		for (int i = 0; i < childCount; i++) {
			final View child = getChildAt(i);
			final long id = adapter.getItemId(firstVisiblePosition + i);

			yMap.put(id, ViewHelper.getY(child));
			positionMap.put(id, firstVisiblePosition + i);
		}

		for (int i = 0; i < firstVisiblePosition; i++) {
			final long id = adapter.getItemId(i);
			beforeVisible.add(id);
		}

		final int count = adapter.getCount();

		for (int i = firstVisiblePosition + childCount; i < count; i++) {
			final long id = adapter.getItemId(i);
			afterVisible.add(id);
		}

	}

	private void doAnimation() {
		setEnabled(false);
		animating = true;

		final float durationUnit = (float) MAX_ANIM_DURATION / getHeight();

		animatePreLayout(durationUnit, new AnimatorListenerAdapter() {
			@Override
			public void onAnimationEnd(final Animator animation) {
				adapter.notifyDataSetChanged();

				getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {

					@Override
					public boolean onPreDraw() {
						getViewTreeObserver().removeOnPreDrawListener(this);

						animatePostLayout(durationUnit);

						return true;
					}

				});
			}

		});

	}

	/**
	 * Animate items that are deleted entirely and items that move out of
	 * bounds.
	 */
	private void animatePreLayout(final float durationUnit, final AnimatorListener listener) {
		final AnimatorSet animatorSet = new AnimatorSet();

		final int firstVisiblePosition = getFirstVisiblePosition();
		final int childCount = getChildCount();

		for (final Iterator<Entry<Long, Float>> iter = yMap.entrySet().iterator(); iter.hasNext();) {
			final Map.Entry<Long, Float> entry = iter.next();

			final long id = entry.getKey();
			final int oldPos = positionMap.get(id);
			final View child = getChildAt(oldPos - firstVisiblePosition);
			final int newPos = getPositionForId(id);

			// fade out items that disappear
			if (newPos == -1) {
				final ObjectAnimator anim = animateAlpha(child, false);
				animatorSet.play(anim);

				iter.remove();
				positionMap.remove(id);
				continue;
			}

			// translate items that move out of bounds
			if (newPos < firstVisiblePosition || newPos > firstVisiblePosition + childCount) {
				final float offset;

				if (newPos < firstVisiblePosition) {
					offset = -getHeight();
				} else {
					offset = getHeight();
				}

				final AnimatorProxy proxy = AnimatorProxy.wrap(child);
				final ObjectAnimator anim = ObjectAnimator.ofFloat(proxy, "translationY", 0f,
						offset);

				final int finalDuration = getDuration(0, getHeight() / 2, durationUnit);

				anim.setInterpolator(new AccelerateInterpolator());
				anim.setDuration((long) (finalDuration * animationDurationFactor));

				animatorSet.addListener(new AnimatorListenerAdapter() {
					@Override
					public void onAnimationEnd(final Animator animation) {
						child.post(new Runnable() {

							@Override
							public void run() {
								proxy.setTranslationY(0f);
							}
						});
					}
				});
				animatorSet.play(anim);

				iter.remove();
				positionMap.remove(id);
				continue;
			}
		}

		if (!animatorSet.getChildAnimations().isEmpty()) {
			animatorSet.addListener(listener);
			animatorSet.start();
		} else {
			listener.onAnimationEnd(animatorSet);
		}
	}

	/**
	 * Animate items that just appeared and items that move within the screen.
	 */
	private void animatePostLayout(final float durationUnit) {

		final AnimatorSet animatorSet = new AnimatorSet();

		for (int i = 0; i < getChildCount(); i++) {
			final View child = getChildAt(i);
			final long id = getItemIdAtPosition(getFirstVisiblePosition() + i);

			ObjectAnimator anim = null;

			ViewHelper.setAlpha(child, 1f);

			if (yMap.containsKey(id)) {
				// moved within visible area

				// log("Moved within visible area id: " + id);
				final float oldY = yMap.remove(id);
				final float newY = ViewHelper.getY(child);

				if (oldY != newY) {
					anim = animateY(child, oldY, newY, durationUnit);
				}

			} else {
				// moved into visible area or new

				if (beforeVisible.contains(id)) {
					// moved from top
					final float newY = ViewHelper.getY(child);
					final float oldY = -child.getHeight();

					anim = animateY(child, oldY, newY, durationUnit);
				} else if (afterVisible.contains(id)) {
					// moved from bottom
					final float newY = ViewHelper.getY(child);
					final float oldY = getHeight();

					anim = animateY(child, oldY, newY, durationUnit);
				} else {
					// entirely new
					ViewHelper.setAlpha(child, 0f);

					anim = animateAlpha(child, true);
					anim.setStartDelay(MIN_ANIM_DURATION);
				}

			}

			if (anim != null) {
				animatorSet.play(anim);
			}

		}

		animatorSet.addListener(new AnimatorListenerAdapter() {
			@Override
			public void onAnimationEnd(final Animator animation) {
				finishAnimation();
			};
		});

		animatorSet.start();
	}

	private void finishAnimation() {
		adapter.setMayNotify(true);
		animating = false;

		setEnabled(true);

		manipulatePending();
	}

	protected int getPositionForId(final long id) {
		for (int i = 0; i < adapter.getCount(); i++) {
			if (adapter.getItemId(i) == id) {
				return i;
			}
		}

		return -1;
	}

	protected ObjectAnimator animateAlpha(final View view, final boolean fadeIn) {
		final ObjectAnimator anim = ObjectAnimator.ofFloat(view, "alpha", fadeIn ? new float[] {
				0f, 1f } : new float[] { 1f, 0f });

		anim.setDuration((long) (ALPHA_ANIM_DURATION * animationDurationFactor));

		return anim;

	}

	protected ObjectAnimator animateY(final View view, final float oldY, final float newY,
			final float durationUnit) {
		final int duration = getDuration(oldY, newY, durationUnit);

		final ObjectAnimator anim = ObjectAnimator.ofFloat(AnimatorProxy.wrap(view),
				"translationY", oldY - newY, 0);

		final int finalDuration = Math
				.min(Math.max(duration, MIN_ANIM_DURATION), MAX_ANIM_DURATION);

		anim.setDuration((long) (finalDuration * animationDurationFactor));
		anim.setInterpolator(translateInterpolater);

		return anim;
	}

	protected static int getDuration(final float oldY, final float newY, final float durationUnit) {
		final float distance = newY - oldY;
		final int duration = (int) (Math.abs(distance) * durationUnit);
		return duration;
	}

	protected boolean isPositionVisible(final int position) {
		final int firstVisiblePosition = getFirstVisiblePosition();

		if (position < firstVisiblePosition) {
			return false;
		}

		final int childCount = getChildCount();

		if (position > firstVisiblePosition + childCount) {
			return false;
		}

		return true;
	}

	protected void log(final String msg) {
		if (BuildConfig.DEBUG) {
			Log.d(getClass().getSimpleName(), msg);
		}
	}

}
