package com.tumblr.backboard;

import android.annotation.SuppressLint;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Property;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import com.facebook.rebound.Spring;
import com.facebook.rebound.SpringListener;
import com.facebook.rebound.SpringSystem;
import com.tumblr.backboard.imitator.EventImitator;
import com.tumblr.backboard.imitator.Imitator;
import com.tumblr.backboard.imitator.MotionImitator;
import com.tumblr.backboard.performer.Performer;

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

/**
 * Coordinates the relationship between {@link com.tumblr.backboard.imitator.MotionImitator}s,
 * {@link com.facebook.rebound.Spring}s, and {@link com.tumblr.backboard.performer.Performer}s on a
 * single {@link android.view.View}.
 * <p>
 * This primarily exists to manage the {@link android.view.View.OnTouchListener} on the
 * {@link android.view.View}.
 * <p>
 * Created by ericleong on 5/20/14.
 */
public final class Actor {

	/**
	 * Distance in pixels that can be moved before a touch is no longer considered a "click".
	 */
	public static final int MAX_CLICK_DISTANCE = 10;

	/**
	 * Contains the imitators and listeners coupled to a single spring.
	 */
	public static final class Motion {
		@NonNull
		private final Spring spring;
		@NonNull
		private final EventImitator[] imitators;
		@NonNull
		private final Performer[] performers;
		@Nullable
		private final SpringListener[] springListeners;

		private Motion(@NonNull final Spring spring, @NonNull final EventImitator imitator, @NonNull final Performer[] performers,
		               @Nullable final SpringListener[] springListeners) {
			this(spring, new EventImitator[] { imitator }, performers, springListeners);
		}

		private Motion(@NonNull final Spring spring, @NonNull final Performer[] performers,
		               @Nullable final SpringListener[] springListeners) {
			this.imitators = new MotionImitator[0];
			this.performers = performers;
			this.spring = spring;
			this.springListeners = springListeners;
		}

		private Motion(@NonNull final Spring spring, @NonNull final EventImitator[] imitators, @NonNull final Performer[] performers,
		               @Nullable final SpringListener[] springListeners) {
			this.imitators = imitators;
			this.performers = performers;
			this.spring = spring;
			this.springListeners = springListeners;
		}

		@NonNull
		public Spring getSpring() {
			return spring;
		}

		@NonNull
		public EventImitator[] getImitators() {
			return imitators;
		}
	}

	@NonNull
	private final View mView;
	@NonNull
	private final List<Motion> mMotions;
	@NonNull
	private final MotionListener mMotionListener;
	@Nullable
	private final View.OnTouchListener mOnTouchListener;
	/**
	 * Allows the user to disable the motion listener.
	 */
	private boolean mMotionListenerEnabled;
	/**
	 * Prevent parent from intercepting touch events (useful when in lists).
	 */
	private boolean mRequestDisallowTouchEvent;

	private Actor(@NonNull final View view, @NonNull final List<Motion> motions,
	              @Nullable final View.OnTouchListener onTouchListener,
	              final boolean motionListenerEnabled, final boolean attachTouchListener,
	              final boolean requestDisallowTouchEvent) {
		mView = view;
		mMotions = motions;
		mOnTouchListener = onTouchListener;

		mMotionListener = new MotionListener();
		mMotionListenerEnabled = motionListenerEnabled;

		mRequestDisallowTouchEvent = requestDisallowTouchEvent;

		if (attachTouchListener) {
			view.setOnTouchListener(mMotionListener);
		}
	}

	@Nullable
	public View.OnTouchListener getOnTouchListener() {
		return mOnTouchListener;
	}

	@NonNull
	public View.OnTouchListener getMotionListener() {
		return mMotionListener;
	}

	@NonNull
	public View getView() {
		return mView;
	}

	@NonNull
	public List<Motion> getMotions() {
		return mMotions;
	}

	public boolean isTouchEnabled() {
		return mMotionListenerEnabled;
	}

	public void setTouchEnabled(final boolean enabled) {
		this.mMotionListenerEnabled = enabled;
	}

	/**
	 * Removes all spring listeners controlled by this {@link Actor}.
	 */
	public void removeAllListeners() {
		for (Motion motion : mMotions) {
			for (Performer performer : motion.performers) {
				motion.spring.removeListener(performer);
			}

			if (motion.springListeners != null) {
				for (SpringListener listener : motion.springListeners) {
					motion.spring.removeListener(listener);
				}
			}
		}
	}

	/**
	 * Adds all spring listeners back.
	 */
	public void addAllListeners() {
		for (Motion motion : mMotions) {
			for (Performer performer : motion.performers) {
				motion.spring.addListener(performer);
			}

			if (motion.springListeners != null) {
				for (SpringListener listener : motion.springListeners) {
					motion.spring.addListener(listener);
				}
			}
		}
	}

	/**
	 * Implements the builder pattern for {@link Actor}.
	 */
	public static class Builder {

		@NonNull
		private final View mView;
		@NonNull
		private final List<Motion> mMotions = new ArrayList<Motion>();
		@Nullable
		private View.OnTouchListener mOnTouchListener;
		@NonNull
		private final SpringSystem mSpringSystem;
		private boolean mMotionListenerEnabled = true;
		private boolean mAttachMotionListener = true;
		private boolean mRequestDisallowTouchEvent;
		private boolean mAttachSpringListeners = true;

		/**
		 * Animates the given view with the default {@link com.facebook.rebound.SpringConfig} and
		 * automatically creates a {@link com.facebook.rebound.SpringSystem}.
		 *
		 * @param springSystem
		 * 		the spring system to use
		 * @param view
		 * 		the view to animate
		 */
		public Builder(@NonNull final SpringSystem springSystem, @NonNull final View view) {
			mView = view;
			mSpringSystem = springSystem;
		}

		/**
		 * @param onTouchListener
		 * 		a touch listener to pass touch events to
		 * @return this builder for chaining
		 */
		@NonNull
		public Builder onTouchListener(final View.OnTouchListener onTouchListener) {
			mOnTouchListener = onTouchListener;
			return this;
		}

		/**
		 * Uses the default {@link com.facebook.rebound.SpringConfig} to animate the view.
		 *
		 * @param properties
		 * 		the event fields to imitate and the view properties to animate.
		 * @return this builder for chaining
		 */
		@NonNull
		public Builder addTranslateMotion(final MotionProperty... properties) {
			return addMotion(mSpringSystem.createSpring(), properties);
		}

		/**
		 * Uses the default {@link com.facebook.rebound.SpringConfig} to animate the view.
		 *
		 * @param property
		 * 		the event field to imitate and the view property to animate.
		 * @param listener
		 * 		a listener to call
		 * @return this builder for chaining
		 */
		@NonNull
		public Builder addTranslateMotion(final MotionProperty property, final SpringListener listener) {
			return addMotion(mSpringSystem.createSpring(), Imitator.TRACK_ABSOLUTE,
					Imitator.FOLLOW_EXACT, new MotionProperty[] { property },
					new SpringListener[] { listener });
		}

		/**
		 * Uses the default {@link com.facebook.rebound.SpringConfig} to animate the view.
		 *
		 * @param trackStrategy
		 * 		the tracking behavior
		 * @param followStrategy
		 * 		the follow behavior
		 * @param properties
		 * 		the event fields to imitate and the view properties to animate.
		 * @return this builder for chaining
		 */
		@NonNull
		public Builder addTranslateMotion(final int trackStrategy, final int followStrategy,
		                                  final MotionProperty... properties) {
			return addMotion(mSpringSystem.createSpring(), trackStrategy, followStrategy,
					properties);
		}

		/**
		 * Uses the default {@link com.facebook.rebound.SpringConfig} to animate the view.
		 *
		 * @param trackStrategy
		 * 		the tracking behavior
		 * @param followStrategy
		 * 		the follow behavior
		 * @param restValue
		 * 		the rest value of the spring
		 * @param properties
		 * 		the event fields to imitate and the view properties to animate.
		 * @return this builder for chaining
		 */
		@NonNull
		public Builder addTranslateMotion(final int trackStrategy, final int followStrategy,
		                                  final int restValue,
		                                  final MotionProperty... properties) {
			return addMotion(mSpringSystem.createSpring(), trackStrategy, followStrategy,
					restValue, properties);
		}

		/**
		 * @param spring
		 * 		the underlying {@link com.facebook.rebound.Spring}.
		 * @param properties
		 * 		the event fields to imitate and the view properties to animate.
		 * @return this builder for chaining
		 */
		@NonNull
		public Builder addMotion(@NonNull final Spring spring, final MotionProperty... properties) {
			return addMotion(spring, Imitator.TRACK_ABSOLUTE, Imitator.FOLLOW_EXACT, properties);
		}

		/**
		 * @param spring
		 * 		the underlying {@link com.facebook.rebound.Spring}.
		 * @param trackStrategy
		 * 		the tracking behavior
		 * @param followStrategy
		 * 		the follow behavior
		 * @param properties
		 * 		the event fields to imitate and the view properties to animate.
		 * @return this builder for chaining
		 */
		@NonNull
		public Builder addMotion(@NonNull final Spring spring, final int trackStrategy, final int followStrategy,
		                         @NonNull final MotionProperty... properties) {

			mMotions.add(createMotionFromProperties(spring, properties, null, trackStrategy, followStrategy, 0));

			return this;
		}

		/**
		 * @param spring
		 * 		the underlying {@link com.facebook.rebound.Spring}.
		 * @param trackStrategy
		 * 		the tracking behavior
		 * @param followStrategy
		 * 		the follow behavior
		 * @param restValue
		 * 		the rest value
		 * @param properties
		 * 		the event fields to imitate and the view properties to animate.
		 * @return this builder for chaining
		 */
		@NonNull
		public Builder addMotion(@NonNull final Spring spring, final int trackStrategy, final int followStrategy,
		                         final int restValue, @NonNull final MotionProperty... properties) {

			mMotions.add(
					createMotionFromProperties(spring, properties, null, trackStrategy, followStrategy, restValue));

			return this;
		}

		/**
		 * @param spring
		 * 		the underlying {@link com.facebook.rebound.Spring}.
		 * @param trackStrategy
		 * 		the tracking behavior
		 * @param followStrategy
		 * 		the follow behavior
		 * @param restValue
		 * 		the rest value
		 * @param property
		 * 		the event fields to imitate and the view property to animate.
		 * @param springListener
		 * 		a spring listener to attach to the spring
		 * @return this builder for chaining
		 */
		@NonNull
		public Builder addMotion(@NonNull final Spring spring, final int trackStrategy, final int followStrategy,
		                         final int restValue, final MotionProperty property, @Nullable final SpringListener springListener) {

			mMotions.add(
					createMotionFromProperties(spring, new MotionProperty[] { property },
							new SpringListener[] { springListener }, trackStrategy, followStrategy, restValue));

			return this;
		}

		/**
		 * @param spring
		 * 		the underlying {@link com.facebook.rebound.Spring}.
		 * @param trackStrategy
		 * 		the tracking behavior
		 * @param followStrategy
		 * 		the follow behavior
		 * @param properties
		 * 		the event fields to imitate and the view properties to animate.
		 * @param springListeners
		 * 		an array of spring listeners to attach to the spring
		 * @return this builder for chaining
		 */
		@NonNull
		public Builder addMotion(@NonNull final Spring spring, final int trackStrategy, final int followStrategy,
		                         @NonNull final MotionProperty[] properties, final SpringListener[] springListeners) {

			mMotions.add(
					createMotionFromProperties(spring, properties, springListeners, trackStrategy, followStrategy, 0));

			return this;
		}

		/**
		 * Uses a default {@link com.facebook.rebound.SpringConfig}.
		 *
		 * @param eventImitator
		 * 		maps an event to a {@link com.facebook.rebound.Spring}
		 * @param viewProperties
		 * 		the {@link android.view.View} property to animate
		 * @return the builder for chaining
		 */
		@NonNull
		public Builder addMotion(@NonNull final EventImitator eventImitator,
		                         @NonNull final Property<View, Float>... viewProperties) {
			final Performer[] performers = new Performer[viewProperties.length];

			for (int i = 0; i < viewProperties.length; i++) {
				performers[i] = new Performer(viewProperties[i]);
			}

			return addMotion(mSpringSystem.createSpring(), eventImitator, performers);
		}

		/**
		 * Uses a default {@link com.facebook.rebound.SpringConfig}.
		 *
		 * @param eventImitator
		 * 		maps an event to a {@link com.facebook.rebound.Spring}
		 * @param performers
		 * 		map the {@link com.facebook.rebound.Spring} to a
		 * 		{@link android.view.View}
		 * @return the builder for chaining
		 */
		@NonNull
		public Builder addMotion(@NonNull final EventImitator eventImitator, final Performer... performers) {
			return addMotion(mSpringSystem.createSpring(), eventImitator, performers);
		}

		/**
		 * @param spring
		 * 		the underlying {@link com.facebook.rebound.Spring}.
		 * @param eventImitator
		 * 		maps an event to a {@link com.facebook.rebound.Spring}
		 * @param performers
		 * 		map the {@link com.facebook.rebound.Spring} to a
		 * 		{@link android.view.View}
		 * @return the builder for chaining
		 */
		@NonNull
		public Builder addMotion(@NonNull final Spring spring, @NonNull final EventImitator eventImitator,
		                         @NonNull final Performer... performers) {

			final Motion motion = new Motion(spring, eventImitator, performers, null);

			// connect actors
			motion.imitators[0].setSpring(motion.spring);

			for (Performer performer : motion.performers) {
				performer.setTarget(mView);
			}

			mMotions.add(motion);

			return this;
		}

		/**
		 * @param spring
		 * 		the underlying {@link com.facebook.rebound.Spring}.
		 * @param eventImitator
		 * 		maps an event to a {@link com.facebook.rebound.Spring}
		 * @param performers
		 * 		map the {@link com.facebook.rebound.Spring} to a
		 * 		{@link android.view.View}
		 * @param springListeners
		 * 		additional listeners to attach
		 * @return the builder for chaining
		 */
		@NonNull
		public Builder addMotion(@NonNull final Spring spring, @NonNull final EventImitator eventImitator,
		                         @NonNull final Performer[] performers, final SpringListener[] springListeners) {

			// create struct
			final Motion motion = new Motion(spring, eventImitator, performers, springListeners);

			// connect actors
			motion.imitators[0].setSpring(motion.spring);

			for (Performer performer : motion.performers) {
				performer.setTarget(mView);
			}

			mMotions.add(motion);

			return this;
		}

		/**
		 * @param motionImitator
		 * 		maps an event to a {@link com.facebook.rebound.Spring}
		 * @param viewProperty
		 * 		the {@link android.view.View} property to animate
		 * @param springListener
		 * 		additional listener to attach
		 * @return the builder for chaining
		 */
		@NonNull
		public Builder addMotion(@NonNull final MotionImitator motionImitator,
		                         @NonNull final Property<View, Float> viewProperty,
		                         final SpringListener springListener) {

			return addMotion(mSpringSystem.createSpring(), motionImitator,
					new Performer[] { new Performer(viewProperty) },
					new SpringListener[] { springListener });
		}

		/**
		 * @return flag to tell the attached {@link android.view.View.OnTouchListener} to call
		 * {@link android.view.ViewParent#requestDisallowInterceptTouchEvent(boolean)} with
		 * <code>true</code>.
		 */
		@NonNull
		public Builder requestDisallowTouchEvent() {
			mRequestDisallowTouchEvent = true;
			return this;
		}

		/**
		 * A flag to tell this {@link Actor} not to attach the touch listener to the view.
		 *
		 * @return the builder for chaining
		 */
		@NonNull
		public Builder dontAttachMotionListener() {
			mAttachMotionListener = false;
			return this;
		}

		/**
		 * A flag to tell this builder not to attach the spring listeners to the spring.
		 * They can be added with {@link Actor#addAllListeners()}.
		 *
		 * @return the builder for chaining
		 */
		@NonNull
		public Builder dontAttachSpringListeners() {
			mAttachSpringListeners = false;
			return this;
		}

		/**
		 * Creations a new motion object.
		 *
		 * @param spring
		 * 		the spring to use
		 * @param motionProperties
		 * 		the properties of the event to track
		 * @param springListeners
		 * 		additional spring listeners to add
		 * @param trackStrategy
		 * 		the tracking strategy
		 * @param followStrategy
		 * 		the follow strategy
		 * @param restValue
		 * 		the spring rest value
		 * @return a motion object
		 */
		@Nullable
		private Motion createMotionFromProperties(@NonNull final Spring spring,
		                                          @NonNull final MotionProperty[] motionProperties,
		                                          @Nullable final SpringListener[] springListeners,
		                                          final int trackStrategy, final int followStrategy,
		                                          final int restValue) {

			final MotionImitator[] motionImitators = new MotionImitator[motionProperties.length];
			final Performer[] performers = new Performer[motionProperties.length];

			for (int i = 0; i < motionProperties.length; i++) {

				final MotionProperty property = motionProperties[i];

				motionImitators[i] = new MotionImitator(spring, property, restValue, trackStrategy, followStrategy);
				performers[i] = new Performer(mView, property.getViewProperty());
			}

			return new Motion(spring, motionImitators, performers, springListeners);
		}

		/**
		 * @return Builds the {@link Actor}.
		 */
		@NonNull
		public Actor build() {
			// make connections

			final Actor actor = new Actor(mView, mMotions, mOnTouchListener, mMotionListenerEnabled, mAttachMotionListener,
					mRequestDisallowTouchEvent);

			if (mAttachSpringListeners) {
				actor.addAllListeners();
			}

			return actor;
		}
	}

	private class MotionListener implements View.OnTouchListener {
		@Override
		@SuppressLint("ClickableViewAccessibility")
		public boolean onTouch(@NonNull final View v, @NonNull final MotionEvent event) {

			final boolean retVal;

			if (!mMotionListenerEnabled || mMotions.isEmpty()) {

				if (mOnTouchListener != null) {
					retVal = mOnTouchListener.onTouch(v, event);
				} else {
					retVal = false;
				}

				return retVal;
			}

			for (Motion motion : mMotions) {
				for (EventImitator imitator : motion.imitators) {
					imitator.imitate(v, event);
				}
			}

			if (mOnTouchListener != null) {
				retVal = mOnTouchListener.onTouch(v, event);
			} else {
				retVal = true;
			}

			if (mRequestDisallowTouchEvent) {
				// prevents parent from scrolling or otherwise stealing touch events
				v.getParent().requestDisallowInterceptTouchEvent(true);
			}

			if (v.isClickable()) {
				if (event.getEventTime() - event.getDownTime()
						> ViewConfiguration.getLongPressTimeout()) {
					v.setPressed(false);

					return true;
				}

				if (event.getHistorySize() > 0) {
					final float deltaX = event.getHistoricalX(event.getHistorySize() - 1) - event.getX();
					final float deltaY = event.getHistoricalY(event.getHistorySize() - 1) - event.getY();

					// if user has moved too far, it is no longer a click
					final boolean removeClickState = Math.pow(deltaX, 2) + Math.pow(deltaY, 2)
							> Math.pow(MAX_CLICK_DISTANCE, 2);

					v.setPressed(!removeClickState);

					return removeClickState;
				} else {
					return false;
				}
			}

			return retVal;
		}
	}
}