/*
 * Copyright (c) 2005-2010 Trident Kirill Grouchnikov. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of Trident Kirill Grouchnikov nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.pushingpixels.trident;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import org.pushingpixels.trident.interpolator.KeyFrames;
import org.pushingpixels.trident.interpolator.PropertyInterpolator;

public class TimelinePropertyBuilder<T> {

	/**
	 * Defines how to set a property.
	 */
	public static interface PropertySetter<T> {
		public void set(Object obj, String fieldName, T value);
	}

	/**
	 * Defines how to get a property.
	 */
	public static interface PropertyGetter<T> {
		public T get(Object obj, String fieldName);
	}

	/**
	 * Defines how to access a property.
	 */
	public static interface PropertyAccessor<T> extends PropertyGetter<T>,
			PropertySetter<T> {
	}

	/**
	 * Default property setter.
	 */
	public static class DefaultPropertySetter<T> implements PropertySetter<T> {
		private Method setterMethod;

		public DefaultPropertySetter(Object obj, String fieldName) {
			setterMethod = getSetter(obj, fieldName);
		}

		@Override
        public void set(Object obj, String fieldName, T value) {
			try {
				setterMethod.invoke(obj, value);
			} catch (Throwable t) {
				throw new RuntimeException(
						"Unable to set the value of the field '" + fieldName
								+ "'", t);
			}
		}
	}

	/**
	 * Default property getter.
	 */
	public static class DefaultPropertyGetter<T> implements PropertyGetter<T> {
		private Method getterMethod;

		public DefaultPropertyGetter(Object obj, String fieldName) {
			getterMethod = getGetter(obj, fieldName);
		}

		@Override
        public T get(Object obj, String fieldName) {
			try {
				return (T) getterMethod.invoke(obj);
			} catch (Throwable t) {
				throw new RuntimeException(
						"Unable to get the value of the field '" + fieldName
								+ "'", t);
			}
		}
	}

	private Object target; // may be null
	private final String propertyName; // required
	private T from; // optional
	private boolean isFromCurrent;
	private T to; // must be optional because of KeyFrames
	private PropertyInterpolator<T> interpolator; // optional
	private PropertyGetter<T> getter; // optional
	private PropertySetter<T> setter; // optional
	private KeyFrames<T> keyFrames; // optional

	TimelinePropertyBuilder(String propertyName) {
		this.propertyName = propertyName;
		this.isFromCurrent = false;
	}

	public TimelinePropertyBuilder<T> from(T startValue) {
		if (this.from != null) {
			throw new IllegalArgumentException("from() can only be called once");
		}
		if (this.isFromCurrent) {
			throw new IllegalArgumentException(
					"from() cannot be called after fromCurrent()");
		}
		if (this.keyFrames != null) {
			throw new IllegalArgumentException(
					"from() cannot be called after goingThrough()");
		}
		this.from = startValue;
		return this;
	}

	public TimelinePropertyBuilder<T> fromCurrent() {
		if (this.isFromCurrent) {
			throw new IllegalArgumentException(
					"fromCurrent() can only be called once");
		}
		if (this.from != null) {
			throw new IllegalArgumentException(
					"fromCurrent() cannot be called after from()");
		}
		if (this.keyFrames != null) {
			throw new IllegalArgumentException(
					"fromCurrent() cannot be called after goingThrough()");
		}
		this.isFromCurrent = true;
		return this;
	}

	public TimelinePropertyBuilder<T> to(T endValue) {
		if (this.to != null) {
			throw new IllegalArgumentException("to() can only be called once");
		}
		if (this.keyFrames != null) {
			throw new IllegalArgumentException(
					"to() cannot be called after goingThrough()");
		}
		this.to = endValue;
		return this;
	}

	public TimelinePropertyBuilder<T> on(Object object) {
		this.target = object;
		return this;
	}

	public TimelinePropertyBuilder<T> interpolatedWith(
			PropertyInterpolator<T> pInterpolator) {
		if (this.interpolator != null) {
			throw new IllegalArgumentException(
					"interpolateWith() can only be called once");
		}
		this.interpolator = pInterpolator;
		return this;
	}

	public TimelinePropertyBuilder<T> setWith(PropertySetter<T> pSetter) {
		if (this.setter != null) {
			throw new IllegalArgumentException(
					"setWith() can only be called once");
		}
		this.setter = pSetter;
		return this;
	}

	public TimelinePropertyBuilder<T> getWith(PropertyGetter<T> pGetter) {
		if (this.getter != null) {
			throw new IllegalArgumentException(
					"getWith() can only be called once");
		}
		this.getter = pGetter;
		return this;
	}

	public TimelinePropertyBuilder<T> accessWith(PropertyAccessor<T> pAccessor) {
		if (this.setter != null || this.getter != null) {
			throw new IllegalArgumentException(
					"accessWith() can only be called once");
		}
		this.setter = pAccessor;
		this.getter = pAccessor;
		return this;
	}

	public TimelinePropertyBuilder<T> goingThrough(KeyFrames<T> keyFrames) {
		if (this.keyFrames != null) {
			throw new IllegalArgumentException(
					"goingThrough() can only be called once");
		}
		if (this.isFromCurrent) {
			throw new IllegalArgumentException(
					"goingThrough() cannot be called after fromCurrent()");
		}
		if (this.from != null) {
			throw new IllegalArgumentException(
					"goingThrough() cannot be called after from()");
		}
		if (this.to != null) {
			throw new IllegalArgumentException(
					"goingThrough() cannot be called after to()");
		}
		this.keyFrames = keyFrames;
		return this;
	}

	AbstractFieldInfo getFieldInfo(Timeline timeline) {
		if (this.target == null) {
			this.target = timeline.mainObject;
		}

		if (this.keyFrames != null) {
			return new KeyFramesFieldInfo(this.target, this.propertyName,
					this.keyFrames, this.setter);
		}

		if (this.isFromCurrent) {
			if (this.interpolator == null) {
				this.interpolator = TridentConfig.getInstance()
						.getPropertyInterpolator(this.to);

				if (this.interpolator == null) {
					throw new IllegalArgumentException(
							"No interpolator found for "
									+ this.to.getClass().getName());
				}
			}
			return new GenericFieldInfoTo(this.target, this.propertyName,
					this.to, this.interpolator, this.getter, this.setter);
		}

		if (this.interpolator == null) {
			this.interpolator = TridentConfig.getInstance()
					.getPropertyInterpolator(this.from, this.to);

			if (this.interpolator == null) {
				throw new IllegalArgumentException("No interpolator found for "
						+ this.from.getClass().getName() + ":"
						+ this.to.getClass().getName());
			}
		}
		return new GenericFieldInfo(this.target, this.propertyName, this.from,
				this.to, this.interpolator, this.setter);
	}

	abstract class AbstractFieldInfo<T> {
		protected Object object;

		protected String fieldName;

		protected PropertyGetter getter;
		protected PropertySetter setter;

		protected T from;

		protected T to;

		AbstractFieldInfo(Object obj, String fieldName,
				PropertyGetter<T> pGetter, PropertySetter<T> pSetter) {
			this.object = obj;
			this.fieldName = fieldName;

			this.getter = pGetter;
			this.setter = pSetter;
		}

		void setValues(T from, T to) {
			this.from = from;
			this.to = to;
		}

		abstract void onStart();

		abstract void updateFieldValue(float timelinePosition);
	}

	private static <T> PropertyGetter<T> getPropertyGetter(Object obj,
			String fieldName, PropertyGetter<T> pGetter) {
		if (pGetter != null)
			return pGetter;
		return new DefaultPropertyGetter(obj, fieldName);
	}

	private static <T> PropertySetter<T> getPropertySetter(Object obj,
			String fieldName, PropertySetter<T> pSetter) {
		if (pSetter != null)
			return pSetter;
		return new DefaultPropertySetter(obj, fieldName);
	}

	private class GenericFieldInfoTo extends AbstractFieldInfo<Object> {
		private PropertyInterpolator propertyInterpolator;

		private Object to;

		GenericFieldInfoTo(Object obj, String fieldName, Object to,
				PropertyInterpolator propertyInterpolator,
				PropertyGetter propertyGetter, PropertySetter propertySetter) {
			super(obj, fieldName, getPropertyGetter(obj, fieldName,
					propertyGetter), getPropertySetter(obj, fieldName,
					propertySetter));
			this.propertyInterpolator = propertyInterpolator;
			this.to = to;
		}

		@Override
		void onStart() {
			this.from = getter.get(object, fieldName);
		}

		@Override
		void updateFieldValue(float timelinePosition) {
			try {
				Object value = this.propertyInterpolator.interpolate(from, to,
						timelinePosition);
				this.setter.set(this.object, this.fieldName, value);
			} catch (Throwable exc) {
				System.err.println("Exception occurred in updating field '"
						+ this.fieldName + "' of object "
						+ this.object.getClass().getCanonicalName()
						+ " at timeline position " + timelinePosition);
				exc.printStackTrace();
			}
		}
	}

	private class GenericFieldInfo extends AbstractFieldInfo<Object> {
		private PropertyInterpolator propertyInterpolator;

		GenericFieldInfo(Object obj, String fieldName, Object from, Object to,
				PropertyInterpolator propertyInterpolator,
				PropertySetter propertySetter) {
			super(obj, fieldName, null, getPropertySetter(obj, fieldName,
					propertySetter));
			this.propertyInterpolator = propertyInterpolator;
			this.setValues(from, to);
		}

		@Override
		void onStart() {
		}

		@Override
		void updateFieldValue(float timelinePosition) {
			try {
				Object value = this.propertyInterpolator.interpolate(from, to,
						timelinePosition);
				this.setter.set(this.object, this.fieldName, value);
			} catch (Throwable exc) {
				System.err.println("Exception occurred in updating field '"
						+ this.fieldName + "' of object "
						+ this.object.getClass().getCanonicalName()
						+ " at timeline position " + timelinePosition);
				exc.printStackTrace();
			}
		}
	}

	private class KeyFramesFieldInfo extends AbstractFieldInfo<Object> {
		KeyFrames keyFrames;

		KeyFramesFieldInfo(Object obj, String fieldName, KeyFrames keyFrames,
				PropertySetter propertySetter) {
			super(obj, fieldName, null, getPropertySetter(obj, fieldName,
					propertySetter));
			this.keyFrames = keyFrames;
		}

		@Override
		void onStart() {
		}

		@Override
		void updateFieldValue(float timelinePosition) {
			if (this.setter != null) {
				try {
					Object value = this.keyFrames.getValue(timelinePosition);
					this.setter.set(this.object, this.fieldName, value);
				} catch (Throwable exc) {
					exc.printStackTrace();
				}
			}
		}
	}

	private static Method getSetter(Object object, String propertyName) {
		String setterMethodName = "set"
				+ Character.toUpperCase(propertyName.charAt(0))
				+ propertyName.substring(1);
		Class oClazz = object.getClass();
		while (oClazz != null) {
			for (Method m : oClazz.getMethods()) {
				if (setterMethodName.equals(m.getName())
						&& (m.getParameterTypes().length == 1)
						&& (m.getReturnType() == Void.TYPE)
						&& (!Modifier.isStatic(m.getModifiers()))) {
					return m;
				}
			}
			oClazz = oClazz.getSuperclass();
		}
		return null;
	}

	private static Method getGetter(Object object, String propertyName) {
		String getterMethodName = "get"
				+ Character.toUpperCase(propertyName.charAt(0))
				+ propertyName.substring(1);
		Class oClazz = object.getClass();
		while (oClazz != null) {
			for (Method m : oClazz.getMethods()) {
				if (getterMethodName.equals(m.getName())
						&& (m.getParameterTypes().length == 0)
						&& (!Modifier.isStatic(m.getModifiers()))) {
					return m;
				}
			}
			oClazz = oClazz.getSuperclass();
		}
		return null;
	}
}
