/**
 * 
 */
package org.zkoss.zk.ui.select;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import org.slf4j.Logger;

import org.zkoss.lang.Strings;
import org.zkoss.xel.VariableResolver;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Components;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Execution;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.Session;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WebApp;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.select.annotation.Listen;
import org.zkoss.zk.ui.select.annotation.Wire;
import org.zkoss.zk.ui.select.annotation.WireVariable;
import org.zkoss.zk.ui.select.impl.ComponentIterator;
import org.zkoss.zk.ui.select.impl.Reflections;
import org.zkoss.zk.ui.select.impl.Reflections.FieldRunner;
import org.zkoss.zk.ui.select.impl.Reflections.MethodRunner;

/**
 * A collection of selector related utilities. 
 * @since 6.0.0
 * @author simonpai
 */
public class Selectors {

	/**
	 * Returns an Iterable that iterates through all Components matched by the
	 * selector. 
	 * @param page the reference page for selector
	 * @param selector the selector string
	 * @return an Iterable of Component
	 */
	public static Iterable<Component> iterable(final Page page, final String selector) {
		return new Iterable<Component>() {
			public Iterator<Component> iterator() {
				return new ComponentIterator(page, selector);
			}
		};
	}

	/**
	 * Returns an Iterable that iterates through all Components matched by the
	 * selector. 
	 * @param root the reference component for selector
	 * @param selector the selector string
	 * @return an Iterable of Component
	 */
	public static Iterable<Component> iterable(final Component root, final String selector) {
		return new Iterable<Component>() {
			public Iterator<Component> iterator() {
				return new ComponentIterator(root, selector);
			}
		};
	}

	/**
	 * Returns a list of Components that match the selector.
	 * @param page the reference page for selector
	 * @param selector the selector string
	 * @return a List of Component
	 */
	public static List<Component> find(Page page, String selector) {
		return toList(iterable(page, selector));
	}

	/**
	 * Returns a list of Components that match the selector.
	 * @param root the reference component for selector
	 * @param selector the selector string
	 * @return a List of Component
	 */
	public static List<Component> find(Component root, String selector) {
		return toList(iterable(root, selector));
	}

	/*
	 * Returns the ith component that matches the selector
	 * @param page the reference page for selector
	 * @param selector the selector string
	 * @param index 1-based index (1 means the first component found)
	 * @return Component, null if not found
	 */
	/*
	public static Component find(Page page, String selector, int index) {
		return getIthItem(new ComponentIterator(page, selector), index);
	}
	*/

	/*
	 * Returns the ith component that matches the selector
	 * @param root root the reference component for selector
	 * @param selector selector the selector string
	 * @param index 1-based index (1 means the first component found)
	 * @return Component, null if not found
	 */
	/*
	public static Component find(Component root, String selector, int index) {
		return getIthItem(new ComponentIterator(root, selector), index);
	}
	*/

	/**
	 * Wire variables to controller, including XEL variables, implicit variables.
	 * @param component the reference component
	 * @param controller the controller object to be injected with variables
	 */
	public static void wireVariables(Component component, Object controller, List<VariableResolver> extraResolvers) {
		new Wirer(controller, false).wireVariables(new ComponentFunctor(component), extraResolvers);
	}

	/**
	 * Wire variables to controller, including XEL variables, implicit variables.
	 * @param page the reference page
	 * @param controller the controller object to be injected with variables
	 */
	public static void wireVariables(Page page, Object controller, List<VariableResolver> extraResolvers) {
		new Wirer(controller, false).wireVariables(new PageFunctor(page), extraResolvers);
	}

	/**
	 * Wire components to controller.
	 * @param component the reference component for selector
	 * @param controller the controller object to be injected with variables
	 * @param ignoreNonNull ignore wiring when the value of the field is a 
	 * Component (non-null) or a non-empty Collection.
	 */
	public static void wireComponents(Component component, Object controller, boolean ignoreNonNull) {
		new Wirer(controller, false).wireComponents(new ComponentFunctor(component), ignoreNonNull);
	}

	/**
	 * Wire components to controller.
	 * @param page the reference page for selector
	 * @param controller the controller object to be injected with variables
	 * @param ignoreNonNull ignore wiring when the value of the field is a 
	 * Component (non-null) or a non-empty Collection.
	 */
	public static void wireComponents(Page page, Object controller, boolean ignoreNonNull) {
		new Wirer(controller, false).wireComponents(new PageFunctor(page), ignoreNonNull);
	}

	/**
	 * Rewire the variables on session activation
	 * @since 7.0.7
	 */
	public static void rewireVariablesOnActivate(Component component, Object controller,
			List<VariableResolver> extraResolvers) {
		// called when activated
		new Wirer(controller, true).wireVariables(new ComponentFunctor(component), extraResolvers);
	}

	/**
	 * Rewire the components on session activation
	 * @since 7.0.7
	 */
	public static void rewireComponentsOnActivate(Component component, Object controller) {
		// called when activated
		new Wirer(controller, true).wireComponents(new ComponentFunctor(component), false);
	}

	/**
	 * Add event listeners to components based on the controller.
	 * @param component the reference component for selector 
	 * @param controller the controller of event listening methods
	 */
	public static void wireEventListeners(final Component component, final Object controller) {
		wireEventListeners0(component, controller, false);
	}

	private static void wireEventListeners0(final Component component, final Object controller, final boolean rewire) {
		Reflections.forMethods(controller.getClass(), Listen.class, new MethodRunner<Listen>() {
			public void onMethod(Class<?> clazz, Method method, Listen anno) {
				// check method signature
				if ((method.getModifiers() & Modifier.STATIC) != 0)
					throw new UiException("Cannot add forward to static method: " + method.getName());
				// method should have 0 or 1 parameter
				if (method.getParameterTypes().length > 1)
					throw new UiException(
							"Event handler method should have " + "at most one parameter: " + method.getName());
				for (String[] strs : splitListenAnnotationValues(anno.value())) {
					String name = strs[0];
					if (name == null)
						name = "onClick";
					// http://tracker.zkoss.org/browse/ZK-2582
					int prio = 0;
					int idx = name.indexOf('(');
					if (idx > 0) {
 						int li = name.indexOf(')');
						prio = Integer.parseInt(name.substring(idx + 1, li));
						name = name.substring(0, idx);
					}
					Iterable<Component> iter = iterable(component, strs[1]);
					// no forwarding, just add to event listener
					Set<Component> rewired = rewire ? new HashSet<Component>() : null;
					for (Component c : iter) {
						if (rewired != null && !rewired.contains(c)) {
							rewired.add(c);
							c.removeAttribute(EVT_LIS);
							Iterable<EventListener<? extends Event>> listeners = c.getEventListeners(name);
							if (listeners != null) {
								for (EventListener<? extends Event> listener : listeners)
									if (listener instanceof ComposerEventListener)
										c.removeEventListener(name, listener);
							}
						}
						Set<String> set = getEvtLisSet(c, EVT_LIS);
						String mhash = name + "#" + method.toString();
						if (set.contains(mhash))
							continue;
						c.addEventListener(prio, name, new ComposerEventListener(method, controller));
						set.add(mhash);
					}
				}
			}
		});
	}

	/*package*/ static void rewireEventListeners(final Component component, final Object controller) {
		wireEventListeners0(component, controller, true);
	}

	private static final String EVT_LIS = "_SELECTOR_COMPOSER_EVENT_LISTENERS";

	@SuppressWarnings("unchecked")
	private static Set<String> getEvtLisSet(Component comp, String name) {
		Object obj = comp.getAttribute(name);
		if (obj != null)
			return (Set<String>) obj;
		Set<String> set = new HashSet<String>();
		comp.setAttribute(name, set);
		return set;
	}

	/** Creates a list of instances of {@link VariableResolver} based
	 * on the annotation of the given class.
	 * If no such annotation is found, an empty list is returned.
	 * @param cls the class to look for the annotation.
	 * @param untilClass the class to stop the searching.
	 * By default, it will look for the annotation of the super class if not found.
	 * Ignored if null.
	 */
	public static List<VariableResolver> newVariableResolvers(Class<?> cls, Class<?> untilClass) {
		final List<VariableResolver> resolvers = new ArrayList<VariableResolver>();
		while (cls != null && cls != untilClass) {
			final org.zkoss.zk.ui.select.annotation.VariableResolver anno = cls
					.getAnnotation(org.zkoss.zk.ui.select.annotation.VariableResolver.class);
			if (anno != null)
				for (Class<? extends VariableResolver> rc : anno.value()) {
					try {
						resolvers.add(rc.getConstructor().newInstance());
					} catch (Exception e) {
						throw UiException.Aide.wrap(e);
					}
				}
			cls = cls.getSuperclass();
		}
		return resolvers;
	}

	// helper //
	private static String[][] splitListenAnnotationValues(String str) {
		List<String[]> result = new ArrayList<String[]>();
		int len = str.length();
		boolean inSqBracket = false;
		boolean inQuote = false;
		boolean escaped = false;
		String evtName = null;
		int i = 0;

		for (int j = 0; j < len; j++) {
			char c = str.charAt(j);

			if (!escaped)
				switch (c) {
				case '[':
					inSqBracket = true;
					break;
				case ']':
					inSqBracket = false;
					break;
				case '"':
				case '\'':
					inQuote = !inQuote;
					break;
				case '=':
					if (inSqBracket || inQuote)
						break;
					if (evtName != null)
						throw new UiException("Illegal value of @Listen: " + str);
					evtName = str.substring(i, j).trim();
					// check event name: onX
					if (evtName.length() < 3 || !evtName.startsWith("on") || !Character.isUpperCase(evtName.charAt(2)))
						throw new UiException("Illegal value of @Listen: " + str);
					i = j + 1;
					break;
				case ';':
					if (inQuote)
						break;
					String target = str.substring(i, j).trim();
					// check selector string: nonempty
					if (target.length() == 0)
						throw new UiException("Illegal value of @Listen: " + str);
					result.add(new String[] { evtName, target });
					i = j + 1;
					evtName = null;
					break;
				default:
					// do nothing
				}

			escaped = !escaped && c == '\\';
		}

		// flush last chunk if any
		if (i < len) {
			String last = str.substring(i).trim();
			if (last.length() > 0)
				result.add(new String[] { evtName, last });
		}
		return result.toArray(new String[0][0]);
	}

	private static <T> List<T> toList(Iterable<T> iterable) {
		List<T> result = new ArrayList<T>();
		for (T t : iterable)
			result.add(t);
		return result;
	}

	// helper: auto wire //
	private static class Wirer {

		private final Object _controller;
		private final boolean _rewire;

		private Wirer(Object controller, final boolean rewire) {
			_controller = controller;
			_rewire = rewire;
		}

		private void wireComponents(final PsdoCompFunctor functor, final boolean ignoreNonNull) {
			final Class<?> ctrlClass = _controller.getClass();
			// wire to fields
			Reflections.forFields(ctrlClass, Wire.class, new FieldRunner<Wire>() {
				public void onField(Class<?> clazz, Field field, Wire anno) {
					if ((field.getModifiers() & Modifier.STATIC) != 0)
						throw new UiException("Cannot wire variable to " + "static field: " + field.getName());

					if (_rewire && !anno.rewireOnActivate())
						return; // skipped, not rewired

					if (ignoreNonNull) {
						// if not null && not collection, skip
						Object value = Reflections.getFieldValue(_controller, field);
						if (value != null && (!(value instanceof Collection<?>) || !((Collection<?>) value).isEmpty()))
							return;
					}

					String selector = anno.value();
					if (!Strings.isEmpty(selector))
						injectComponent(field, functor.iterable(selector));

					else {
						// no selector value, wire implicit object by naming convention
						Component value = getComponentByName(functor, field.getName(), field.getType());
						if (value != null)
							Reflections.setFieldValue(_controller, field, value);
					}

				}
			});
			// wire by methods
			Reflections.forMethods(ctrlClass, Wire.class, new MethodRunner<Wire>() {
				public void onMethod(Class<?> clazz, Method method, Wire anno) {
					// check method signature
					String name = method.getName();
					if ((method.getModifiers() & Modifier.STATIC) != 0)
						throw new UiException("Cannot wire component by static method: " + name);
					Class<?>[] paramTypes = method.getParameterTypes();
					if (paramTypes.length != 1)
						throw new UiException("Setter method should have only" + " one parameter: " + name);

					if (_rewire && !anno.rewireOnActivate())
						return; // skipped, not rewired

					String selector = anno.value();
					// check selector string: nonempty
					if (!Strings.isEmpty(selector))
						injectComponent(method, functor.iterable(selector));

					else {
						Component value = getComponentByName(functor, desetterize(method.getName()), paramTypes[0]);
						Reflections.invokeMethod(method, this, value);
					}
				}
			});
		}

		private void wireVariables(final PsdoCompFunctor functor, final List<VariableResolver> resolvers) {
			Class<?> ctrlClass = _controller.getClass();
			// wire to fields
			Reflections.forFields(ctrlClass, WireVariable.class, new FieldRunner<WireVariable>() {
				public void onField(Class<?> clazz, Field field, WireVariable anno) {
					if ((field.getModifiers() & Modifier.STATIC) != 0)
						throw new UiException("Cannot wire variable to " + "static field: " + field.getName());

					if (_rewire && !anno.rewireOnActivate() && !isSessionOrWebApp(field.getType()))
						return; // skipped, not rewired

					String name = anno.value();
					if (Strings.isEmpty(name))
						name = guessImplicitObjectName(field.getType());

					if (Strings.isEmpty(name))
						name = field.getName();

					Object value = getObjectByName(functor, name, field.getType(), resolvers);
					if (value != null)
						Reflections.setFieldValue(_controller, field, value);
				}
			});
			// wire by methods
			Reflections.forMethods(ctrlClass, WireVariable.class, new MethodRunner<WireVariable>() {
				public void onMethod(Class<?> clazz, Method method, WireVariable anno) {
					// check method signature
					String mname = method.getName();
					if ((method.getModifiers() & Modifier.STATIC) != 0)
						throw new UiException("Cannot wire variable by static" + " method: " + mname);
					Class<?>[] paramTypes = method.getParameterTypes();
					if (paramTypes.length != 1)
						throw new UiException("Setter method should have" + " exactly one parameter: " + mname);

					if (_rewire && !anno.rewireOnActivate() && !isSessionOrWebApp(paramTypes[0]))
						return; // skipped, not rewired

					String name = anno.value();
					if (Strings.isEmpty(name))
						name = guessImplicitObjectName(paramTypes[0]);

					if (Strings.isEmpty(name))
						name = desetterize(method.getName());

					Object value = getObjectByName(functor, name, paramTypes[0], resolvers);
					Reflections.invokeMethod(method, _controller, value);
				}
			});
		}

		private void injectComponent(Method method, Iterable<Component> iter) {
			injectComponent(new MethodFunctor(method), iter);
		}

		private void injectComponent(Field field, Iterable<Component> iter) {
			injectComponent(new FieldFunctor(field), iter);
		}

		@SuppressWarnings("unchecked")
		private void injectComponent(InjectionFunctor injector, Iterable<Component> comps) {
			Class<?> type = injector.getType();
			boolean isField = injector instanceof FieldFunctor;
			// Array
			if (type.isArray()) {
				injector.inject(_controller, generateArray(type.getComponentType(), comps));
				return;
			}
			// Collection
			if (Collection.class.isAssignableFrom(type)) {

				Collection collection = null;
				if (isField) {
					Field field = ((FieldFunctor) injector).getField();
					try {
						collection = (Collection) field.get(_controller);
					} catch (Exception e) {
						throw new IllegalStateException(
								"Field " + field + " not accessible or not declared by" + _controller);
					}
				}

				// try to give an instance if null 
				if (collection == null) {
					collection = getCollectionInstanceIfPossible(type);
					if (collection == null)
						throw new UiException("Cannot initiate collection for " + (isField ? "field" : "method") + ": "
								+ injector.getName() + " on " + _controller);
					if (isField)
						injector.inject(_controller, collection);
				}

				// try add to collection
				collection.clear();
				for (Component c : comps)
					if (Reflections.isAppendableToCollection(injector.getGenericType(), c))
						collection.add(c);
				if (!isField)
					injector.inject(_controller, collection);
				return;
			}
			// set to field once or invoke method once
			for (Component c : comps) {
				if (!type.isInstance(c))
					continue;
				injector.inject(_controller, c);
				return;
			}
			injector.inject(_controller, null); // no match, inject null
		}

		private Component getComponentByName(PsdoCompFunctor functor, String name, Class<?> type) {
			Component result = functor.getFellowIfAny(name);
			return isValidValue(result, type) ? result : null;
		}

		private Object getObjectByName(PsdoCompFunctor functor, String name, Class<?> type,
				List<VariableResolver> resolvers) {

			Object result = functor.getXelVariable(name);
			if (isValidValue(result, type))
				return result;

			if (resolvers != null)
				for (VariableResolver resv : resolvers) {
					result = resv.resolveVariable(name);
					if (isValidValue(result, type))
						return result;
				}

			result = functor.getImplicit(name);
			return isValidValue(result, type) ? result : null;
		}

		private interface InjectionFunctor {
			public void inject(Object obj, Object value);

			public String getName();

			public Class<?> getType();

			public Type getGenericType();
		}

		private class FieldFunctor implements InjectionFunctor {
			private final Field _field;

			private FieldFunctor(Field field) {
				_field = field;
			}

			public void inject(Object obj, Object value) {
				Reflections.setFieldValue(obj, _field, value);
			}

			public String getName() {
				return _field.getName();
			}

			public Class<?> getType() {
				return _field.getType();
			}

			public Type getGenericType() {
				return _field.getGenericType();
			}

			public Field getField() {
				return _field;
			}
		}

		private class MethodFunctor implements InjectionFunctor {
			private final Method _method;

			private MethodFunctor(Method method) {
				_method = method;
			}

			public void inject(Object obj, Object value) {
				Reflections.invokeMethod(_method, obj, value);
			}

			public String getName() {
				return _method.getName();
			}

			public Class<?> getType() {
				return _method.getParameterTypes()[0];
			}

			public Type getGenericType() {
				return _method.getGenericParameterTypes()[0];
			}
		}

	}

	private static String guessImplicitObjectName(Class<?> cls) {
		if (Execution.class.equals(cls))
			return "execution";
		if (Page.class.equals(cls))
			return "page";
		if (Desktop.class.equals(cls))
			return "desktop";
		if (Session.class.equals(cls))
			return "session";
		if (WebApp.class.equals(cls))
			return "application";
		if (Logger.class.equals(cls))
			return "log";
		return null;
	}

	private static boolean isSessionOrWebApp(Class<?> cls) {
		return Session.class.equals(cls) || WebApp.class.equals(cls);
	}

	private static boolean isValidValue(Object value, Class<?> clazz) {
		return value != null && clazz.isAssignableFrom(value.getClass());
	}

	private static String desetterize(String name) {
		if (name.length() < 4 || !name.startsWith("set") || Character.isLowerCase(name.charAt(3)))
			throw new UiException("Expecting method name in form setXxx: " + name);
		return Character.toLowerCase(name.charAt(3)) + name.substring(4);
	}

	@SuppressWarnings("unchecked")
	private static Collection getCollectionInstanceIfPossible(Class<?> clazz) {
		if (clazz.isAssignableFrom(ArrayList.class))
			return new ArrayList();
		if (clazz.isAssignableFrom(HashSet.class))
			return new HashSet();
		if (clazz.isAssignableFrom(TreeSet.class))
			return new TreeSet();
		try {
			return (Collection) clazz.getConstructor().newInstance();
		} catch (Exception e) {
			// ignore
		}
		return null;
	}

	@SuppressWarnings("unchecked")
	private static <T> T[] generateArray(Class<T> clazz, Iterable<Component> comps) {
		// add to a temporary ArrayList then set to Array
		ArrayList<T> list = new ArrayList<T>();
		for (Component c : comps)
			if (clazz.isAssignableFrom(c.getClass()))
				list.add((T) c);
		return list.toArray((T[]) Array.newInstance(clazz, 0));
	}

	// Cannot be serialized
	public static class ComposerEventListener implements EventListener<Event> {

		private final Method _ctrlMethod;
		private final Object _ctrl;

		public ComposerEventListener(Method method, Object controller) {
			_ctrlMethod = method;
			_ctrl = controller;
		}

		public void onEvent(Event event) throws Exception {
			if (_ctrlMethod.getParameterTypes().length == 0)
				_ctrlMethod.invoke(_ctrl);
			else
				_ctrlMethod.invoke(_ctrl, event);
		}
	}

	// helper: functor //
	private interface PsdoCompFunctor {
		public Iterable<Component> iterable(String selector);

		public Object getImplicit(String name);

		public Object getAttribute(String name);

		public Object getXelVariable(String name);

		public Component getFellowIfAny(String name);
	}

	private static class PageFunctor implements PsdoCompFunctor {
		private final Page _page;

		private PageFunctor(Page page) {
			_page = page;
		}

		public Iterable<Component> iterable(String selector) {
			return Selectors.iterable(_page, selector);
		}

		public Object getImplicit(String name) {
			return Components.getImplicit(_page, name);
		}

		public Object getXelVariable(String name) {
			return _page.getXelVariable(null, null, name, true);
		}

		public Object getAttribute(String name) {
			return _page.getAttribute(name, true);
		}

		public Component getFellowIfAny(String name) {
			return _page.getFellowIfAny(name);
		}
	}

	private static class ComponentFunctor implements PsdoCompFunctor {
		private final Component _comp;

		private ComponentFunctor(Component comp) {
			_comp = comp;
		}

		public Iterable<Component> iterable(String selector) {
			return Selectors.iterable(_comp, selector);
		}

		public Object getImplicit(String name) {
			return Components.getImplicit(_comp, name);
		}

		public Object getXelVariable(String name) {
			return getPage().getXelVariable(null, null, name, true);
		}

		public Object getAttribute(String name) {
			return _comp.getAttribute(name, true);
		}

		public Component getFellowIfAny(String name) {
			return _comp.getFellowIfAny(name);
		}

		private Page getPage() {
			return Components.getCurrentPage(_comp);
		}
	}

	/*
	private static <T> T getIthItem(Iterator<T> iter, int index){
		// shift (index - 1) times
		for(int i = 1; i < index; i++) {
			if(!iter.hasNext())
				return null;
			iter.next();
		}
		return iter.hasNext() ? iter.next() : null;
	}
	*/

}
