/* TrackerImpl.java

	Purpose:
		
	Description:
		
	History:
		Aug 24, 2011 7:31:14 PM, Created by henrichen

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

package org.zkoss.bind.tracker.impl;

import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.WeakHashMap;

import org.zkoss.bind.annotation.Immutable;
import org.zkoss.bind.impl.AllocUtil;
import org.zkoss.bind.impl.IndirectBinding;
import org.zkoss.bind.impl.WeakIdentityMap;
import org.zkoss.bind.sys.Binding;
import org.zkoss.bind.sys.ChildrenBinding;
import org.zkoss.bind.sys.FormBinding;
import org.zkoss.bind.sys.LoadBinding;
import org.zkoss.bind.sys.LoadChildrenBinding;
import org.zkoss.bind.sys.LoadPropertyBinding;
import org.zkoss.bind.sys.PropertyBinding;
import org.zkoss.bind.sys.ReferenceBinding;
import org.zkoss.bind.sys.tracker.Tracker;
import org.zkoss.bind.sys.tracker.TrackerNode;
import org.zkoss.bind.xel.zel.BindELContext;
import org.zkoss.lang.Strings;
import org.zkoss.util.IdentityHashSet;
import org.zkoss.zk.ui.Component;
import org.zkoss.zul.Combobox;
import org.zkoss.zul.Grid;
import org.zkoss.zul.ListModel;
import org.zkoss.zul.ListModelArray;
import org.zkoss.zul.ListModelList;
import org.zkoss.zul.Listbox;
import org.zkoss.zul.Radiogroup;
import org.zkoss.zul.Tabbox;

/**
 * Implementation of dependency tracking.
 * @author henrichen
 * @since 6.0.0
 */
public class TrackerImpl implements Tracker, Serializable {
	private static final long serialVersionUID = 1463169907348730644L;
	protected Map<Component, Map<Object, TrackerNode>> _compMap; //comp -> path -> head TrackerNode
	protected Map<Object, Set<TrackerNode>> _nullMap = new LinkedHashMap<Object, Set<TrackerNode>>(); //property -> Set of head TrackerNode that eval to null
	protected transient Map<Object, Set<TrackerNode>> _beanMap = new WeakIdentityMap<Object, Set<TrackerNode>>(); //bean -> Set of TrackerNode
	protected transient EqualBeansMap _equalBeansMap; //bean -> beans (use to manage equal beans)

	public TrackerImpl() {
		_equalBeansMap = newEqualBeansMap();
		_compMap = initCompMap();
	}

	// provide a way for sub-class to override it for some situation
	protected Map<Component, Map<Object, TrackerNode>> initCompMap() {
		return new LinkedHashMap<Component, Map<Object, TrackerNode>>();
	}

	protected EqualBeansMap newEqualBeansMap() {
		return new EqualBeansMap();
	}

	public void addTracking(Component comp, String[] series, Binding binding) {
		//Track only LoadBinding
		if (!(binding instanceof LoadBinding)) {
			return;
		}

		final TrackerNode node = getOrCreateTrackerNode(comp, series);

		//node is leaf of this series, add the binding to it
		node.addBinding(binding);
	}

	public void addDependsOn(Component srcComp, String[] srcSeries, Binding srcBinding, Component dependsOnComp,
			String[] dependsOnSeries) {
		//Track only LoadBinding
		if (!(srcBinding instanceof LoadBinding)) {
			return;
		}
		if (dependsOnComp == null) {
			dependsOnComp = srcComp; //share same component context for @DependsOn case
		}
		final TrackerNode dependsOnNode = getOrCreateTrackerNode(dependsOnComp, dependsOnSeries);
		//bug# 1: depends-on is not working in nested C->B->A when A changed
		final TrackerNode srcnode = getOrCreateTrackerNode(srcComp, srcSeries);
		dependsOnNode.addAssociate(srcnode);
	}

	protected TrackerNode getOrCreateTrackerNode(Component comp, String[] series) {
		Map<Object, TrackerNode> nodes = _compMap.get(comp);

		TrackerNode parentNode = null;
		for (String script : series) {
			TrackerNode node = null;
			if (parentNode == null) { //head node
				node = nodes == null ? null : nodes.get(script);
				if (node == null) {
					node = newTrackerNode(script);
					//ZK-2289
					Map<Object, TrackerNode> nodes0 = AllocUtil.inst.putMap(nodes, script, node);
					if (nodes != nodes0) { //Yes, use != instead of !equals()
						_compMap.put(comp, nodes0);
					}
				}
			} else {
				node = parentNode.getDependent(script);
				if (node == null) {
					node = newTrackerNode(script);
				}
				parentNode.addDependent(script, node);
			}
			parentNode = node;
		}
		return parentNode;
	}

	//ZK-1989, sub-class could override this method to provide better tracker-node impl.
	protected TrackerNode newTrackerNode(Object script) {
		return new TrackerNodeImpl(AllocUtil.inst.processScript(script)); //ZK-2289
	}

	public void removeTrackings(Set<Component> comps) {
		final Set<TrackerNode> removed = new LinkedHashSet<TrackerNode>();
		for (Component comp : comps) {
			final Map<Object, TrackerNode> nodesMap = _compMap.remove(comp);
			if (nodesMap != null) {
				final Collection<TrackerNode> nodes = nodesMap.values();
				for (TrackerNode node : nodes) {
					removed.add(node);
					removed.addAll(node.getDependents());
				}
			}
		}
		if (removed.size() > 0) {
			removeAllFromBeanMap(removed);
			removeAllFromNullMap(removed);
		}
	}

	public void removeTrackings(Component comp) {
		final Map<Object, TrackerNode> nodesMap = _compMap.remove(comp);
		if (nodesMap != null) {
			final Set<TrackerNode> removed = new LinkedHashSet<TrackerNode>();
			final Collection<TrackerNode> nodes = nodesMap.values();
			for (TrackerNode node : nodes) {
				removed.add(node);
				removed.addAll(node.getDependents());
			}
			removeAllFromBeanMap(removed);
			removeAllFromNullMap(removed);
		}
	}

	//remove all specified nodes from the _nullMap 
	protected void removeAllFromNullMap(Set<TrackerNode> removed) {
		removeNodes(_nullMap.values(), removed);
	}

	private void getLoadBindingsPerProperty(Collection<TrackerNode> nodes, String prop,
			LinkedHashSet<LoadBinding> bindings, LinkedHashSet<LoadBinding> kidBindings, LinkedHashSet<Object> kidbases, Set<TrackerNode> visited) {
		if (".".equals(prop)) { //all base object
			for (TrackerNode node : nodes) {
				getLoadBindings0(node, bindings, kidBindings, kidbases, visited);
			}
		} else if ("*".equals(prop)) { //all binding properties of the base object
			for (TrackerNode node : nodes) {
				final Set<TrackerNode> kids = node.getDirectDependents();

				// refix for ZK-1787 test cases.
				for (LoadBinding binding : node.getLoadBindings()) {
					if (testBindingRendererCase(binding))
						bindings.add(binding);
				}

				getNodesLoadBindings(kids, bindings, kidBindings, kidbases, visited);
			}
		} else {
			boolean bracket = BindELContext.isBracket(prop);
			int index = bracket ? Integer.parseInt(prop.substring(1, prop.length() - 1)) : -1;
			for (TrackerNode node : nodes) {
				Set<TrackerNode> dependents = node.getDependents(prop);
				if (bracket && dependents.isEmpty()) {
					Object bean = node.getBean();
					if (bean instanceof List<?>) {
						kidbases.add(((List<?>) bean).get(index));
					} else if (bean instanceof ListModel<?>) {
						if (bean instanceof ListModelArray) {
							kidbases.add(((ListModelArray<Object>) bean).get(index));
						} else if (bean instanceof ListModelList<?>) {
							kidbases.add(((ListModelList<Object>) bean).get(index));
						}
					}
				} else {
					for (TrackerNode kid : dependents) {
						if (kid != null) {
							getLoadBindings0(kid, bindings, kidBindings, kidbases, visited);
						}
					}
				}
			}
		}
	}

	// refix for ZK-2552
	// we need to add an indirect binding here.
	private boolean testBindingRendererCase(LoadBinding binding) {
		if (binding instanceof LoadChildrenBinding)
			return true;
		if (binding instanceof LoadPropertyBinding) {
			LoadPropertyBinding lb = (LoadPropertyBinding) binding;
			if ("model".equals(lb.getFieldName())) {
				Component comp = lb.getComponent();
				if (comp instanceof Listbox || comp instanceof Grid || comp instanceof Tabbox
						|| comp instanceof Radiogroup || comp instanceof Combobox)
					return true;
			}
		}

		return false;
	}

	public Set<LoadBinding> getLoadBindings(Object base, String prop) {
		final LinkedHashSet<LoadBinding> bindings = new LinkedHashSet<LoadBinding>();
		final LinkedHashSet<LoadBinding> kidBaseBindings = new LinkedHashSet<LoadBinding>();
		final Set<TrackerNode> visited = new LinkedHashSet<TrackerNode>();
		collectLoadBindings(base, prop, bindings, kidBaseBindings, visited);
		bindings.addAll(kidBaseBindings);
		return bindings;
	}

	public Set<LoadBinding> getDirectLoadBindings(Object base, String prop) {
		final LinkedHashSet<LoadBinding> bindings = new LinkedHashSet<LoadBinding>();
		final Set<TrackerNode> visited = new LinkedHashSet<TrackerNode>();
		collectLoadBindings(base, prop, bindings, null, visited);
		return bindings;
	}

	public Set<LoadBinding> getKidBaseLoadBindings(Object base, String prop) {
		final LinkedHashSet<LoadBinding> bindings = new LinkedHashSet<LoadBinding>();
		final LinkedHashSet<LoadBinding> kidBaseBindings = new LinkedHashSet<LoadBinding>();
		final Set<TrackerNode> visited = new LinkedHashSet<TrackerNode>();
		collectLoadBindings(base, prop, bindings, kidBaseBindings, visited);
		return kidBaseBindings;
	}

	protected Collection<TrackerNode> getAllTrackerNodes() {
		Set<TrackerNode> all = null;
		final Collection<Map<Object, TrackerNode>> nodesMaps = _compMap.values();
		if (nodesMaps != null && !nodesMaps.isEmpty()) {
			all = new LinkedHashSet<TrackerNode>();
			for (Map<Object, TrackerNode> nodesMap : nodesMaps) {
				final Collection<TrackerNode> nodes = nodesMap.values();
				if (nodes != null) {
					all.addAll(nodes);
				}
			}
		}
		return all;
	}

	private void collectLoadBindings(Object base, String prop, LinkedHashSet<LoadBinding> bindings,
			 LinkedHashSet<LoadBinding> kidBindings, Set<TrackerNode> visited) {
		final LinkedHashSet<Object> kidbases = new LinkedHashSet<Object>(); //collect kid as base bean
		if (base != null) {
			final Set<TrackerNode> nodes = getAllTrackerNodesByBean(base);
			if (nodes != null && !nodes.isEmpty()) {
				getLoadBindingsPerProperty(nodes, prop, bindings, kidBindings, kidbases, visited);
			}
		} else { //base == null)
			if ("*".equals(prop)) {
				for (Set<TrackerNode> basenodes : _nullMap.values()) {
					getNodesLoadBindings(basenodes, kidBindings, kidBindings, kidbases, visited);
				}
			} else {
				final Set<TrackerNode> basenodes = _nullMap.get(prop);
				getNodesLoadBindings(basenodes, bindings, kidBindings, kidbases, visited);
			}
		}
		if (kidBindings != null) {
			for (Object kidbase : kidbases) {
				collectLoadBindings(kidbase, "*", kidBindings, kidBindings, visited); //recursive, for kid base
			}
		}
	}

	protected TrackerNode getTrackerNodePerComponentScript(Object comp, Object script) {
		//locate head TrackerNodes of this component
		final Map<Object, TrackerNode> bindingNodes = _compMap.get(comp);
		return bindingNodes != null ? bindingNodes.get(script) : null;
	}

	public void tieValue(Object comp, Object base, Object script, Object propName, Object value, Object basePath) {
		if (value instanceof IndirectBinding) {
			value = ((IndirectBinding) value).getValue(null);
		}
		if (base == null) { //track from component
			final TrackerNode node = getTrackerNodePerComponentScript(comp, script);
			//ZK-877: NPE in a save only binding
			//No corresponding LoadBinding with the head script in the specified component. 
			if (node != null) {
				if (value != null) {
					addBeanMap(node, value, basePath);
				} else {
					removeAllBeanMap(node); //dependent nodes shall be null, too. Remove them from _beanMap 
					addNullMap(node); //head TrackerNode evaluate to null
				}
			}
		} else {

			final Set<TrackerNode> baseNodes = getAllTrackerNodesByBean(base);
			if (baseNodes != null) { //FormBinding will keep base nodes only (so no associated dependent nodes)
				final Set<TrackerNode> propNodes = new LinkedHashSet<TrackerNode>(); //normal nodes; i.e. a base + property node. e.g. vm.selectedPerson
				for (TrackerNode baseNode : baseNodes) {
					for (TrackerNode node : baseNode.getDependents(script)) {
						if (node == null) { //FormBinding will keep base nodes only (so no associated dependent nodes)
							continue;
						}

						// ZK-2552: we need to ignore if the target component in reference binding
						// is different from the given component, because they may bind with a same
						// data object with the same EL expression.
						boolean ignore = false;
						for (ReferenceBinding refBinding : node.getReferenceBindings()) {
							if (refBinding.getComponent() == comp) {
								ignore = false;
								break;
							}
							ignore = true;
						}
						// don't need to update with a wrong target component.
						if (ignore)
							continue;

						if (BindELContext.isBracket((String) script)) {
							if (!baseNode.isPropNameNodeMapped(node)) {
								baseNode.tieProperty(propName, node);
								propNodes.add(node);
							} else {
								// ZK-5043, tie property if the propName is text, not variable
								if (script instanceof String) {
									String expr = ((String) script).substring(
											1, ((String) script).length() - 1);
									if (!Strings.isEmpty(expr) && expr.charAt(0) == '\'' && expr.charAt(expr.length() - 1) == '\'') {
										baseNode.tieProperty(propName, node);
										propNodes.add(node);
									}
								}

							}
						} else {
							propNodes.add(node);
						}
					}
				}

				if (value != null) {
					for (TrackerNode node : propNodes) { //normal nodes
						addBeanMap(node, value, basePath);
					}
				} else { //value == null
					for (TrackerNode node : propNodes) { //normal nodes
						removeAllBeanMap(node); //dependent nodes shall be null, too. Remove them from _beanMap
					}
				}
			}
		}
	}

	//add node into the _beanMap
	protected void addBeanMap(TrackerNode node, Object value, Object basePath) {
		//add node into _beanMap
		if (!testEqualsBean(node.getBean(), value)) {
			//try to remove from the _beanMap
			removeBeanMap(node);

			//add into _beanMap
			if (value.getClass().getAnnotation(Immutable.class) == null) {
				Set<TrackerNode> nodes = _beanMap.get(value);
				//ZK-2289
				final Set<TrackerNode> nodes0 = AllocUtil.inst.addLinkedHashSet(nodes, node);
				if (nodes == null) {
					_equalBeansMap.put(value);
				}
				if (nodes != nodes0) { // yes, !=; not !equals
					_beanMap.put(value, nodes0);
				}
				//only when value is not a primitive that we shall store it
				node.setBean(value);
			}
		}

		//maybe a head node, try remove it from the nullMap
		removeNullMap(node);
	}

	//add head node into the _nullMap
	private void addNullMap(TrackerNode node) {
		//add node into _nullMap
		final Object propName = node.getFieldScript();
		final Set<TrackerNode> nodes = _nullMap.get(propName);
		//ZK-2289
		final Set<TrackerNode> nodes0 = AllocUtil.inst.addLinkedHashSet(nodes, node);
		if (nodes != nodes0) { //Yes, user != instead of !equals()
			_nullMap.put(propName, nodes0);
		}

		//remove node from the _beanMap
		removeBeanMap(node);
	}

	//remove head node from the _nullMap
	private void removeNullMap(TrackerNode node) {
		final Object propName = node.getFieldScript();
		final Set<TrackerNode> nodes = _nullMap.get(propName);
		if (nodes != null) {
			nodes.remove(node);
			if (nodes.isEmpty()) {
				_nullMap.remove(propName);
			}
		}
	}

	//remove this node and all its dependent nodes from _beanMap
	private void removeAllBeanMap(TrackerNode node) {
		removeBeanMap(node);
		//all dependent node shall be removed, too.
		final Set<TrackerNode> kidnodes = node.getDependents();
		for (TrackerNode kid : kidnodes) {
			removeBeanMap(kid);
		}
	}

	//remove node from the _beanMap
	protected void removeBeanMap(TrackerNode node) {
		final Object value = node.getBean();
		if (value != null) {
			node.setBean(null);
			final Set<TrackerNode> nodes = _beanMap.get(value);
			if (nodes != null) {
				nodes.remove(node); //remove this node from the _beanMap
				if (nodes.isEmpty()) {
					_equalBeansMap.remove(value); //sync the equalBeanMap 
					_beanMap.remove(value);
				}
			}
		}
	}

	private void getNodesLoadBindings(Set<TrackerNode> basenodes, LinkedHashSet<LoadBinding> bindings,
				LinkedHashSet<LoadBinding> kidBindings, LinkedHashSet<Object> kidbases, Set<TrackerNode> visited) {
		if (basenodes != null) {
			for (TrackerNode node : basenodes) {
				if (node != null) {
					getLoadBindings0(node, bindings, kidBindings, kidbases, visited);
				}
			}
		}
	}

	private void getLoadBindings0(TrackerNode node, LinkedHashSet<LoadBinding> bindings, LinkedHashSet<LoadBinding> kidBindings,
		  Set<Object> kidbases, Set<TrackerNode> visited) {
		if (visited.contains(node)) { //already visited
			return;
		}
		visited.add(node);

		bindings.addAll(node.getLoadBindings());
		final Set<ReferenceBinding> refBindings = node.getReferenceBindings();
		bindings.addAll(refBindings);
		for (ReferenceBinding refBinding : refBindings) {
			refBinding.invalidateCache();
			//ZK-950: The expression reference doesn't update while change the instant of the reference
			//Have to load bindings that refer this ReferenceBinding as well
			collectLoadBindings(refBinding, ".", bindings, kidBindings, visited); //recursive
		}

		//bug #1: depends-on is not working in nested C->B->A when A changed
		for (TrackerNode associate : node.getAssociates()) {
			getLoadBindings0(associate, bindings, kidBindings, kidbases, visited); //recursive
		}

		final Object kidbase = node.getBean();
		if (kidbases != null && kidbase != null) {
			kidbases.add(kidbase);
		} else if (kidBindings != null) {
			//check dependents
			final Set<TrackerNode> nodes = node.getDirectDependents();
			for (TrackerNode kid : nodes) {
				getLoadBindings0(kid, kidBindings, kidBindings, null, visited); //recursive
			}
		}
	}

	//given base and postfix, found the associated TrackerNode. 
	@SuppressWarnings("unused")
	private Set<TrackerNode> getNodes(Object base, String postfix) {
		Set<TrackerNode> nodes = getAllTrackerNodesByBean(base);
		String[] props = postfix.split("\\.");
		for (String prop : props) {
			nodes = getDependents(nodes, prop);
		}
		return nodes;
	}

	//get dependents of a group of TrackerNodes.
	private Set<TrackerNode> getDependents(Set<TrackerNode> parentnodes, String prop) {
		final Set<TrackerNode> kidnodes = new LinkedHashSet<TrackerNode>();
		for (TrackerNode node : parentnodes) {
			for (TrackerNode kid : node.getDependents(prop)) {
				if (kid != null) {
					kidnodes.add(kid);
				}
			}
		}
		return kidnodes;
	}

	//remove all specified nodes from the _beanMap 
	protected void removeAllFromBeanMap(Collection<TrackerNode> removed) {
		final Collection<Entry<Object, Set<TrackerNode>>> nodesets = _beanMap.entrySet();
		for (final Iterator<Entry<Object, Set<TrackerNode>>> it = nodesets.iterator(); it.hasNext();) {
			final Entry<Object, Set<TrackerNode>> nodeset = it.next();
			final Object bean = nodeset.getKey();
			nodeset.getValue().removeAll(removed);
			if (nodeset.getValue().isEmpty()) {
				it.remove();
				_equalBeansMap.remove(bean);
			}
		}
	}

	private void removeNodes(Collection<Set<TrackerNode>> nodesets, Collection<TrackerNode> removed) {
		for (final Iterator<Set<TrackerNode>> it = nodesets.iterator(); it.hasNext();) {
			final Set<TrackerNode> nodeset = it.next();
			nodeset.removeAll(removed);
			if (nodeset.isEmpty()) {
				it.remove();
			}
		}
	}

	private Set<TrackerNode> getAllTrackerNodesByBean(Object bean) {
		final Set<TrackerNode> results = new LinkedHashSet<TrackerNode>();
		getAllTrackerNodesByBean0(bean, results);
		return results;
	}

	private void getAllTrackerNodesByBean0(Object bean, Set<TrackerNode> results) {
		final Set<Object> beans = _equalBeansMap.getEqualBeans(bean); //return a set of equal beans
		final Set<TrackerNode> nodes = new LinkedHashSet<TrackerNode>();
		for (Object obj : beans) {
			Set<TrackerNode> beanNodes = _beanMap.get(obj);
			if (beanNodes != null) { //zk-1185, _beanMap could contains no such entry, and returned null.
				nodes.addAll(beanNodes);
			}
		}
		results.addAll(nodes);
		getAllTrackerNodesByBeanNodes(nodes, results);
	}

	//ZK-950: The expression reference doesn't update while change the instant of the reference
	//Check if the passed in bean nodes contains ReferenceBindings; have to collect those
	//nodes that refers those ReferenceBindings as well
	private void getAllTrackerNodesByBeanNodes(Set<TrackerNode> nodes, Set<TrackerNode> results) {
		for (TrackerNode node : nodes) {
			final Set<ReferenceBinding> refBindings = node.getReferenceBindings();
			for (ReferenceBinding refBinding : refBindings) {
				getAllTrackerNodesByBean0(refBinding, results); //recursive
			}
		}
	}

	//Returns equal beans with the given bean in an IdentityHashSet() 
	public Set<Object> getEqualBeans(Object bean) {
		return _equalBeansMap.getEqualBeans(bean); //return a set of equal beans
	}

	private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
		s.defaultReadObject();

		_beanMap = new WeakIdentityMap<Object, Set<TrackerNode>>(); //bean -> Set of TrackerNode
		_equalBeansMap = new EqualBeansMap(); //bean -> beans (use to manage equal beans)
	}

	protected static boolean testEqualsBean(Object nodeBean, Object bean) {
		if (nodeBean == bean)
			return true;
		if (nodeBean instanceof WeakReference) {
			nodeBean = ((WeakReference) nodeBean).get();
		}
		return bean.equals(nodeBean);
	}

	protected static class EqualBeansMap {
		private transient WeakHashMap<Object, EqualBeans> _innerMap = new WeakHashMap<Object, EqualBeans>(); //bean -> EqualBeans
		private transient WeakIdentityMap<Object, EqualBeans> _identityMap = new WeakIdentityMap<Object, EqualBeans>(); //bean -> EqualBeans

		//bug #ZK-678: NotifyChange on Map is not work
		private void syncInnerMap(EqualBeans equalBeans, Object bean) {
			//hashCode of bean has changed, must reset
			boolean found = false;
			final WeakHashMap<Object, EqualBeans> newMap = new WeakHashMap<Object, EqualBeans>(_innerMap.size());
			//ZK-781. Copy one by one to reset _innerMap
			for (Iterator<Entry<Object, EqualBeans>> it = _innerMap.entrySet().iterator(); it.hasNext();) {
				final Entry<Object, EqualBeans> entry = it.next();
				final EqualBeans beans = entry.getValue();
				if (equalBeans.equals(beans)) { //found
					found = true;
					continue;
				}
				newMap.put(entry.getKey(), entry.getValue());
			}
			if (found) {
				_innerMap = newMap;
				//reput equalBeans (item inside might not equal to each other any more)
				for (Object b : equalBeans.getBeans()) {
					_identityMap.remove(b);
					put(b); //recursive
				}
			}
		}

		public void put(Object bean) {
			EqualBeans equalBeans = _innerMap.get(bean);
			if (equalBeans == null) { //hashcode might changed
				equalBeans = _identityMap.remove(bean);
				if (equalBeans != null) { //hashcode is changed
					syncInnerMap(equalBeans, bean);
					return;
				} else { //a new bean
					equalBeans = new EqualBeans(bean);
					_innerMap.put(bean, equalBeans);
				}
			} else {
				equalBeans.put(bean);
			}
			_identityMap.put(bean, equalBeans);
		}

		public void remove(Object bean) {
			EqualBeans equalBeans = _innerMap.remove(bean);
			if (equalBeans != null) {
				_identityMap.remove(bean);
				removeFromEqualBeansAndReput(equalBeans, bean);
			} else { //hashcode might changed
				equalBeans = _identityMap.remove(bean);
				if (equalBeans != null) { //hashcode is changed
					//hashCode of bean has changed, must reset
					boolean found = false;
					final WeakHashMap<Object, EqualBeans> newMap = new WeakHashMap<Object, EqualBeans>(
							_innerMap.size());
					//ZK-781. Copy one by one to reset _innerMap
					for (Iterator<Entry<Object, EqualBeans>> it = _innerMap.entrySet().iterator(); it.hasNext();) {
						final Entry<Object, EqualBeans> entry = it.next();
						final EqualBeans beans = entry.getValue();
						if (equalBeans.equals(beans)) { //found
							found = true;
							continue;
						}
						newMap.put(entry.getKey(), entry.getValue());
					}
					if (found) {
						_innerMap = newMap;
						removeFromEqualBeansAndReput(equalBeans, bean); //remove from EqualBeans
					}
				}
			}
		}

		private void removeFromEqualBeansAndReput(EqualBeans equalBeans, Object bean) {
			final Object proxy = equalBeans.remove(bean);
			if (!equalBeans.isEmpty()) {
				_innerMap.put(proxy, equalBeans); //reput into _innerMap with new Proxy
			}
		}

		public Set<Object> getEqualBeans(Object bean) {
			boolean doSync = false;
			EqualBeans equalBeans;
			if (bean instanceof Collection) {
				equalBeans = _identityMap.get(bean);
				EqualBeans innerBean = _innerMap.get(bean);
				if (equalBeans == null) {
					equalBeans = innerBean;
				} else if (innerBean == null) {
					_identityMap.remove(bean);
					doSync = true;
				}
			} else {
				equalBeans = _innerMap.get(bean);
				if (equalBeans == null) { //hashcode might changed
					equalBeans = _identityMap.remove(bean);
					if (equalBeans != null) { //hashcode is changed
						doSync = true;
					}
				}
			}
			if (doSync) {
				syncInnerMap(equalBeans, bean);
				put(bean);
				equalBeans = _identityMap.get(bean);
			}
			return equalBeans == null ? Collections.emptySet() : equalBeans.getBeans();
		}

		public int size() {
			return _innerMap.size();
		}

		private Set<Entry<Object, EqualBeans>> entrySet() {
			return _innerMap.entrySet();
		}
	}

	private static class EqualBeans {
		private transient WeakReference<Object> _proxy; //surrogate object as the key for the _beanSet
		private transient WeakIdentityMap<Object, Boolean> _beanSet; //different instance of beans equal to each other

		public EqualBeans(Object proxy) {
			_proxy = new WeakReference<Object>(proxy);
			_beanSet = new WeakIdentityMap<Object, Boolean>(2);
			_beanSet.put(proxy, Boolean.TRUE);
		}

		public void put(Object value) {
			_beanSet.put(value, Boolean.TRUE);
		}

		public Set<Object> getBeans() {
			return _beanSet != null ? new IdentityHashSet<Object>(_beanSet.keySet()) : Collections.emptySet();
		}

		//return proxy bean(could be migrated or not)
		public Object remove(Object value) {
			_beanSet.remove(value);
			if (_beanSet.isEmpty()) {
				_beanSet = null;
			} else if (System.identityHashCode(_proxy.get()) == System.identityHashCode(value)) {
				//proxy deleted, must migrate proxy
				for (final Iterator<Object> it = _beanSet.keySet().iterator(); it.hasNext();) {
					final Object obj = it.next();
					if (obj != null) {
						_proxy = new WeakReference<Object>(obj); //migrate
						break;
					} else {
						it.remove();
					}
				}
			}
			return _proxy.get();
		}

		public boolean isEmpty() {
			return _beanSet == null || _beanSet.isEmpty();
		}
	}

	//------ debug dump ------//
	public void dumpLess() {
		dumpCompMap(false);
		dumpBeanMap(false);
		dumpNullMap(false);
		dumpEqualBeansMap();
	}

	public void dump() {
		dumpCompMap(true);
		dumpBeanMap(true);
		dumpNullMap(true);
		dumpEqualBeansMap();
	}

	private void dumpBeanMap(boolean dumpNodes) {
		System.out.println("******* _beanMap: *********");
		System.out.println("******* size: " + _beanMap.size());
		for (Object bean : _beanMap.keySet()) {
			System.out.println("bean:" + bean + "------------");
			if (dumpNodes) {
				Set<TrackerNode> nodes = _beanMap.get(bean);
				if (nodes != null) {
					for (TrackerNode node : nodes) {
						dumpNodeTree(node, 4);
					}
				} else {
					System.out.println("NO TrackerNode bound to this bean.");
				}
			}
		}
	}

	private void dumpCompMap(boolean dumpNodes) {
		System.out.println("******* _compMap: *********");
		System.out.println("******* size: " + _compMap.size());
		for (Component comp : _compMap.keySet()) {
			System.out.println("comp:" + comp + "------------");
			if (dumpNodes) {
				Map<Object, TrackerNode> nodes = _compMap.get(comp);
				for (Entry<Object, TrackerNode> entry : nodes.entrySet()) {
					System.out.println("----field:" + entry.getKey() + "");
					dumpNodeTree(entry.getValue(), 4);
				}
			}
		}
	}

	private void dumpNullMap(boolean dumpNodes) {
		System.out.println("******* _nullMap: *********");
		System.out.println("******* size: " + _nullMap.size());
		for (Object field : _nullMap.keySet()) {
			System.out.println("field:" + field + "------");
			if (dumpNodes) {
				Set<TrackerNode> nodes = _nullMap.get(field);
				for (TrackerNode node : nodes) {
					dumpNodeTree(node, 4);
				}
			}
		}
	}

	private void dumpEqualBeansMap() {
		System.out.println("******* _equalBeansMap: *********");
		System.out.println("******* size: " + _equalBeansMap.size());

		for (Entry<Object, EqualBeans> entry : _equalBeansMap.entrySet()) {
			System.out.print("proxy:[" + entry.getKey());
			System.out.print("], val:[" + entry.getValue().getBeans());
			System.out.println("]----");
		}
	}

	private void dumpNodeTree(TrackerNode node, int indent) {
		dumpNode(node, indent);
		for (TrackerNode kid : node.getDirectDependents()) {
			dumpNodeTree(kid, indent + 4);
		}
	}

	private void dumpNode(TrackerNode node, int spaces) {
		System.out.println(dumpSpace(spaces) + node.getFieldScript() + ":" + node.getBean());
		dumpBindings(node, spaces);
		dumpPropNameMapping(node, spaces);
		dumpAssociate(node, spaces);
	}

	private void dumpNode0(TrackerNode node, int spaces) {
		System.out.println(dumpSpace(spaces) + node.getFieldScript() + ":" + node.getBean());
		dumpBindings(node, spaces);
		dumpPropNameMapping(node, spaces);
	}

	private void dumpAssociate(TrackerNode node, int spaces) {
		if (node.getAssociates().isEmpty())
			return; //don't dump if empty
		System.out.println(dumpSpace(spaces) + "[dependents:");
		for (TrackerNode dependent : node.getAssociates()) {
			dumpNode0(dependent, spaces + 4); //avoid recursive
		}
		System.out.println(dumpSpace(spaces) + "]");
	}

	private void dumpBindings(TrackerNode node, int spaces) {
		if (node.getBindings().isEmpty())
			return; //don't dump if empty
		System.out.println(dumpSpace(spaces) + "[bindings:");
		for (Binding binding : node.getBindings()) {
			dumpBinding(binding, spaces + 4);
		}
		System.out.println(dumpSpace(spaces) + "]");
	}

	private void dumpBinding(Binding binding, int spaces) {
		if (binding instanceof PropertyBinding) {
			System.out.println(dumpSpace(spaces) + ((PropertyBinding) binding).getPropertyString() + ":" + binding);
		} else if (binding instanceof FormBinding) {
			System.out.println(dumpSpace(spaces) + ((FormBinding) binding).getPropertyString() + ":" + binding);
		} else if (binding instanceof ChildrenBinding) {
			System.out.println(dumpSpace(spaces) + ((ChildrenBinding) binding).getPropertyString() + ":" + binding);
		} else if (binding instanceof ReferenceBinding) {
			System.out.println(dumpSpace(spaces) + ((ReferenceBinding) binding).getPropertyString() + ":" + binding);
		} else {
			System.out.println(dumpSpace(spaces) + ":" + binding);
		}
	}

	private void dumpPropNameMapping(TrackerNode node, int spaces) {
		if (node.getPropNameMapping().size() == 0)
			return; //don't dump if empty
		System.out.println(dumpSpace(spaces) + "[propertys:");
		for (Entry<Object, ?> entry : node.getPropNameMapping().entrySet()) {
			dumpEntry(entry, spaces + 4);
		}
		System.out.println(dumpSpace(spaces) + "]");
	}

	private void dumpEntry(Entry<Object, ?> entry, int spaces) {
		System.out.println(dumpSpace(spaces) + entry.getKey() + "=" + entry.getValue());
	}

	private String dumpSpace(int space) {
		char[] spaces = new char[space];
		Arrays.fill(spaces, ' ');
		return new String(spaces);
	}
}
