/* BindELContext.java

	Purpose:
		
	Description:
		
	History:
		Aug 10, 2011 4:52:27 PM, Created by henrichen

Copyright (C) 2011 Potix Corporation. All Rights Reserved.
*/

package org.zkoss.bind.xel.zel;

import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.zkoss.bind.BindContext;
import org.zkoss.bind.Binder;
import org.zkoss.bind.Property;
import org.zkoss.bind.annotation.DependsOn;
import org.zkoss.bind.annotation.Immutable;
import org.zkoss.bind.annotation.NotifyChange;
import org.zkoss.bind.annotation.NotifyChangeDisabled;
import org.zkoss.bind.annotation.SmartNotifyChange;
import org.zkoss.bind.impl.AllocUtil;
import org.zkoss.bind.impl.BindContextUtil;
import org.zkoss.bind.impl.BinderImpl;
import org.zkoss.bind.impl.LoadChildrenBindingImpl;
import org.zkoss.bind.impl.LoadFormBindingImpl;
import org.zkoss.bind.impl.LoadPropertyBindingImpl;
import org.zkoss.bind.impl.PropertyImpl;
import org.zkoss.bind.init.ViewModelAnnotationResolvers;
import org.zkoss.bind.sys.BindEvaluatorX;
import org.zkoss.bind.sys.Binding;
import org.zkoss.lang.Primitives;
import org.zkoss.lang.reflect.Fields;
import org.zkoss.xel.ExpressionX;
import org.zkoss.xel.ValueReference;
import org.zkoss.xel.XelContext;
import org.zkoss.xel.zel.XelELContext;
import org.zkoss.zel.ELResolver;
import org.zkoss.zel.impl.parser.AstBracketSuffix;
import org.zkoss.zel.impl.parser.AstDotSuffix;
import org.zkoss.zel.impl.parser.AstMethodParameters;
import org.zkoss.zel.impl.parser.AstValue;
import org.zkoss.zel.impl.parser.Node;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.UiException;

/**
 * ELContext for Binding.
 * @author henrichen
 * @since 6.0.0
 */
public class BindELContext extends XelELContext {
	public BindELContext(XelContext xelc) {
		super(xelc);
	}

	protected ELResolver newELResolver(XelContext xelc) {
		return new BindELResolver(xelc);
	}

	public Binding getBinding() {
		return (Binding) getXelContext().getAttribute(BinderImpl.BINDING); //see BindEvaluatorXImpl#newXelContext()
	}

	public BindContext getBindContext() {
		return (BindContext) getXelContext().getAttribute(BinderImpl.BINDCTX); //see BindEvaluatorXImpl#newXelContext()
	}

	public boolean ignoreTracker() {
		return getBinding() == null || Boolean.TRUE.equals(getXelContext().getAttribute(BinderImpl.IGNORE_TRACKER)); //see BindEvaluatorXImpl#newXelContext()
	}

	public Object getAttribute(String name) {
		return getXelContext().getAttribute(name); //see BindEvaluatorXImpl#newXelContext()
	}

	public Object setAttribute(String name, Object value) {
		return getXelContext().setAttribute(name, value);
	}

	/**
	 * Removes the attribute, if any.
	 * @since 8.0.0
	 */
	public Object removeAttribute(String name) {
		return getXelContext().removeAttribute(name);
	}

	private static final String TMPBASE = "$TMPBASE$";

	public static Property prepareProperty(Object base, String prop, Object value, BindContext ctx) {
		if (ctx != null && prop.indexOf('[') >= 0) { //handle properties that containing [] indirect reference
			final Binder binder = ctx.getBinder();
			final Component comp = ctx.getComponent();
			Object old = null;
			try {
				old = comp.setAttribute(TMPBASE, base);
				final BindEvaluatorX eval = binder.getEvaluatorX();
				final BindContext bctx = BindContextUtil.newBindContext(binder, null, false, null, comp, null);
				final String expression = TMPBASE + (prop.startsWith("[") ? prop : ("." + prop));
				final ExpressionX exprX = eval.parseExpressionX(bctx, expression, Object.class);
				final String propTrim = prop.trim();
				final ValueReference valref = eval.getValueReference(bctx, comp, exprX);
				if (valref == null) {
					throw new UiException(
							"value reference not found by expression [" + exprX.getExpressionString() + "]");
				}
				base = valref.getBase();
				prop = propTrim.endsWith("]") ? "[" + valref.getProperty() + "]" : "" + valref.getProperty();
			} finally {
				comp.setAttribute(TMPBASE, old);
			}
		}
		return new PropertyImpl(base, prop, value);
	}

	//check method annotation and collect NotifyChange annotation
	public static Set<Property> getNotifys(Method m, Object base, String prop, Object value, BindContext ctx) {
		//TODO, Dennis, do we really need to pass value here?
		final Set<Property> notifys = new LinkedHashSet<Property>();
		if (m == null)
			return notifys;

		final NotifyChange annt = ViewModelAnnotationResolvers.getAnnotation(m, NotifyChange.class);
		//ZK-687 @NotifyChange should be doing automatically. 
		final NotifyChangeDisabled anntdis = ViewModelAnnotationResolvers.getAnnotation(m, NotifyChangeDisabled.class);

		final SmartNotifyChange sannt = ViewModelAnnotationResolvers.getAnnotation(m, SmartNotifyChange.class);

		if (annt != null && anntdis != null) {
			throw new UiException(
					"don't use " + NotifyChange.class + " with " + NotifyChangeDisabled.class + ", choose only one");
		}
		if (sannt != null && anntdis != null) {
			throw new UiException("don't use " + SmartNotifyChange.class + " with " + NotifyChangeDisabled.class
					+ ", choose only one");
		}

		if (annt != null) {
			//if has annotation, use annotated value or prop (if no value in annotation)
			String[] notifies = annt.value();
			if (notifies.length > 0) {
				for (String notify : notifies) {
					final Property propx = prepareProperty(base, notify, value, ctx);
					notifys.add(propx);
				}
			} else if (prop != null) { //property is null in doExecute case
				notifys.add(new PropertyImpl(base, prop, value));
			}
		} else if (anntdis == null && prop != null) {
			notifys.add(new PropertyImpl(base, prop, value));
		}

		if (sannt != null) {
			//if has annotation, use annotated value or prop (if no value in annotation)
			String[] notifies = sannt.value();
			if (notifies.length > 0) {
				for (String notify : notifies) {
					Property propx = null;
					try {
						if (!("*".equals(notify) || ".".equals(notify)))
							propx = prepareProperty(base, notify, Fields.get(base, notify), ctx);
						else
							propx = prepareProperty(base, notify, value, ctx);
					} catch (NoSuchMethodException e) {
						propx = prepareProperty(base, notify, value, ctx); // eat the exception
					}
					if (propx != null)
						notifys.add(propx);
				}
			} else if (prop != null) { //property is null in doExecute case
				notifys.add(new PropertyImpl(base, prop, value));
			}
		}

		return notifys;
	}
	
	@SuppressWarnings("unchecked")
	public static Set<Property> getNotifys(BindContext ctx) {
		return (Set<Property>) ctx.getAttribute(BinderImpl.NOTIFYS);
	}

	public static void addNotifys(Object base, String prop, Object value, BindContext ctx) {
		final Set<Property> properties = new HashSet<Property>(3);
		properties.add(new PropertyImpl(base, prop, value));
		addNotifys(properties, ctx);
	}

	public static void addNotifys(Method m, Object base, String prop, Object value, BindContext ctx) {
		final Set<Property> props = getNotifys(m, base, prop, value, ctx);
		addNotifys(props, ctx);
	}

	//utility method to add notifys to BindContext
	public static void addNotifys(Set<Property> props, BindContext ctx) {
		if (ctx == null) {
			return;
		}
		Set<Property> notifys = getNotifys(ctx);
		if (notifys == null) {
			notifys = new LinkedHashSet<Property>();
			ctx.setAttribute(BinderImpl.NOTIFYS, notifys);
		}
		notifys.addAll(props);
	}

	@SuppressWarnings("unchecked")
	private static Set<Property> getValidates(BindContext ctx) {
		return (Set<Property>) ctx.getAttribute(BinderImpl.VALIDATES);
	}

	//utility method to add validates to BindContext
	@SuppressWarnings("unused")
	private static void addValidates(Set<Property> props, BindContext ctx) {
		if (ctx == null) {
			return;
		}
		Set<Property> validates = getValidates(ctx);
		if (validates == null) {
			validates = new LinkedHashSet<Property>();
			ctx.setAttribute(BinderImpl.VALIDATES, validates);
		}
		validates.addAll(props);
	}

	public static String toNodeString(Node next, StringBuilder path) {
		if (next instanceof AstBracketSuffix) {
			final String bracketString = toNodeString(next.jjtGetChild(0), new StringBuilder()); //recursive
			path.append("[").append(bracketString).append("]");
		} else if (next instanceof AstValue) {
			for (int j = 0, len = next.jjtGetNumChildren(); j < len; ++j) {
				final Node kid = next.jjtGetChild(j);
				toNodeString(kid, path); //recursive
			}
		} else if (next instanceof AstDotSuffix) {
			path.append(".").append(next.getImage());
		} else if (next instanceof AstMethodParameters) {
			StringBuilder subPath = new StringBuilder();
			for (int j = 0, len = next.jjtGetNumChildren(); j < len; ++j) {
				if (j > 0)
					subPath.append(',');
				final Node kid = next.jjtGetChild(j);
				toNodeString(kid, subPath); //recursive
			}
			path.append("(").append(subPath).append(")");
		} else {
			path.append(next.getImage());
		}
		return path.toString();
	}

	public static String toNodeString(Node next, StringBuffer path) {
		if (next instanceof AstBracketSuffix) {
			final String bracketString = toNodeString(next.jjtGetChild(0), new StringBuffer()); //recursive
			path.append("[").append(bracketString).append("]");
		} else if (next instanceof AstValue) {
			for (int j = 0, len = next.jjtGetNumChildren(); j < len; ++j) {
				final Node kid = next.jjtGetChild(j);
				toNodeString(kid, path); //recursive
			}
		} else if (next instanceof AstDotSuffix) {
			path.append(".").append(next.getImage());
		} else if (next instanceof AstMethodParameters) {
			StringBuilder subPath = new StringBuilder();
			for (int j = 0, len = next.jjtGetNumChildren(); j < len; ++j) {
				if (j > 0)
					subPath.append(',');
				final Node kid = next.jjtGetChild(j);
				toNodeString(kid, subPath); //recursive
			}
			path.append("(").append(subPath).append(")");
		} else {
			path.append(next.getImage());
		}
		return path.toString();
	}

	public static boolean isBracket(String script) {
		return script.startsWith("[") && script.endsWith("]");
	}

	public static String appendFields(String prefix, String field) {
		return prefix + (isBracket(field) ? "" : '.') + field;
	}

	//check method annotation and collect NotifyChange annotation
	public static void addDependsOnTrackings(Method m, String basepath, List<String> srcpath, Binding binding,
			BindContext ctx) {
		final DependsOn annt = ViewModelAnnotationResolvers.getAnnotation(m, DependsOn.class);
		if (annt != null) {
			String[] props = annt.value();
			if (props.length > 0) {
				if (binding instanceof LoadPropertyBindingImpl) {
					((LoadPropertyBindingImpl) binding).addDependsOnTrackings(srcpath, basepath, props);
				} else if (binding instanceof LoadFormBindingImpl) {
					((LoadFormBindingImpl) binding).addDependsOnTrackings(srcpath, basepath, props);
				} else if (binding instanceof LoadChildrenBindingImpl) {
					((LoadChildrenBindingImpl) binding).addDependsOnTrackings(srcpath, basepath, props);
				}
			}
		}
	}

	public static String pathToString(List<String> path) {
		final StringBuffer sb = new StringBuffer();
		for (String prop : path) {
			sb.append(prop);
		}
		return sb.toString();
	}

	/**
	 * Prepare the dependsOn nodes
	 * @param srcBinding associated binding of the source dependent field; e.g. <label id="full" value="@bind(vm.fullname)"/>
	 * @param srcPath the source dependent field name series in list. e.g. "vm", "fullname" for "vm.fullname". 
	 * @param dependsOnBasepath the base path for the depends-on field; e.g. the "vm" of the "vm.firstname"
	 * @param dependsOnProp the property name of the depends-on field; e.g. the "firstname" of the "vm.firstname"
	 */
	public static void addDependsOnTracking(Binding srcBinding, List<String> srcPath, String dependsOnBasepath,
			String dependsOnProp) {
		final String dependsOnPath = BindELContext.appendFields(dependsOnBasepath, dependsOnProp);
		final Component srcComp = srcBinding.getComponent();
		addDependsOnTracking(srcBinding, srcPath, srcComp, dependsOnPath, srcComp); //source component shared as DependsOnComp 
	}

	/**
	 * Prepare the dependsOn nodes
	 * @param srcBinding the binding with the source dependent field; e.g. <label id="full" value="@load(vm.fullname)"/>
	 * @param srcPath the source dependent field name series in list; e.g. ["vm", "fullname"] for "vm.fullname".
	 * @param srcComp the source component associated with the binding; e.g. <label>
	 * @param dependsOnPath the depends-on property name series; e.g. "vm.firstname" 
	 * @param dependsOnComp the depends-on component associated with the depends-on property name series binding; e.g. "vm.firstname"
	 */
	public static void addDependsOnTracking(Binding srcBinding, List<String> srcPath, Component srcComp,
			String dependsOnPath, Component dependsOnComp) {
		final Binder binder = srcBinding.getBinder();
		final BindEvaluatorX eval = binder.getEvaluatorX();

		//parse depends on series
		BindContext ctxparse = BindContextUtil.newBindContext(binder, srcBinding, false, null, srcComp, null);
		ctxparse.setAttribute(BinderImpl.SRCPATH, srcPath);
		ctxparse.setAttribute(BinderImpl.DEPENDS_ON_COMP, dependsOnComp);
		ExpressionX expr = eval.parseExpressionX(ctxparse, dependsOnPath, Object.class); //prepare the tracking and association

		//bean association
		BindContext ctx = BindContextUtil.newBindContext(binder, srcBinding, false, null, srcComp, null);
		eval.getValue(ctx, srcComp, expr); //will call tieValue() and recursive back via BindELResolver
	}

	/** Returns whether the specified Object is an immutable object */
	public static boolean isImmutable(Object value) {
		//null is deemed as primitive
		if (value == null) {
			return true;
		}
		final Class<? extends Object> cls = value.getClass();
		return cls.isPrimitive() //value is primitive 
				|| Primitives.toPrimitive(cls) != null //or a wrapper
				|| value instanceof String //or a String
				|| isAnnotatedImmutable(cls) //or an Immutable
				|| cls.isEnum(); // value is enum class
	}

	public static String getModelName(Component comp) {
		return (String) AllocUtil.inst.processScript(BinderImpl.MODEL + comp.getUuid());
	}

	public static void addModel(Component comp, Object model) {
		final String name = getModelName(comp);
		comp.setAttribute(name, model);
	}

	public static Object removeModel(Component comp) {
		final String name = getModelName(comp);
		return comp.removeAttribute(name);
	}

	private static boolean isAnnotatedImmutable(Class<? extends Object> cls) {
		return cls.getAnnotation(Immutable.class) != null;
	}

	public void putContext(@SuppressWarnings("rawtypes") Class key, Object contextObject) {

		// Bug fixed for ZK-2787
		if (contextObject instanceof Method && key == Method.class) {
			BindContext bindCtx = (BindContext) getXelContext().getAttribute(BinderImpl.BINDCTX);
			if (bindCtx != null) {
				bindCtx.setAttribute(String.valueOf(key), ((Method) contextObject).getDeclaredAnnotations());
			}
		}
		super.putContext(key, contextObject);
	}
}
