package org.jivesoftware.openfire.group;

import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;

import org.xmpp.packet.JID;

/**
 * This list specifies additional methods that understand groups among 
 * the items in the list.
 * 
 * @author Tom Evans
 */
public class ConcurrentGroupList<T> extends CopyOnWriteArrayList<T> implements GroupAwareList<T> {

	private static final long serialVersionUID = 7735849143650412115L;

	// This set is used to optimize group operations within this list.
	// We only populate this set when it's needed to dereference the
	// groups in the base list, but once it exists we keep it in sync
	// via the various add/remove operations.
	// NOTE: added volatile keyword for double-check idiom (lazy instantiation)
	private volatile transient Set<String> knownGroupNamesInList;
	
	public ConcurrentGroupList() {
		super();
	}

	public ConcurrentGroupList(Collection<? extends T> c) {
		super(c);
	}

	/**
	 * Returns true if the list contains the given JID. If the JID
	 * is not found in the list, search the list for groups and look
	 * for the JID in each of the corresponding groups.
	 * 
	 * @param value The target, presumably a JID
	 * @return True if the target is in the list, or in any groups in the list
	 */
	@Override
	public boolean includes(Object value) {
		boolean found = false;
		if (contains(value)) {
			found = true;
		} else if (value instanceof JID) {
			JID target = (JID) value;
			Iterator<Group> iterator = getGroups().iterator();
			while (!found && iterator.hasNext()) {
				found = iterator.next().isUser(target);
			}
		}
		return found;
	}

	/**
	 * Returns the groups that are implied (resolvable) from the items in the list.
	 * 
	 * @return A Set containing the groups in the list
	 */
	@Override
	public Set<Group> getGroups() {
		Set<Group> result = new HashSet<>();
		for (String groupName : getKnownGroupNamesInList()) {
			result.add(Group.resolveFrom(groupName));
		}
		return result;
	}
	
	/**
	 * Accessor uses the  "double-check idiom" (j2se 5.0+) for proper lazy instantiation.
	 * Additionally, the set is not cached until there is at least one group in the list.
	 * 
	 * @return the known group names among the items in the list
	 */
	private Set<String> getKnownGroupNamesInList() {
		Set<String> result = knownGroupNamesInList;
		if (result == null) {
			synchronized(this) {
				result = knownGroupNamesInList;
				if (result == null) {
					result = new HashSet<>();
					// add all the groups into the group set
					Iterator<T> iterator = iterator();
					while (iterator.hasNext()) {
						T listItem = iterator.next();
						Group group = Group.resolveFrom(listItem);
						if (group != null) {
							result.add(group.getName());
						};
					}
					knownGroupNamesInList = result.isEmpty() ? null : result;
				}
			}
		}
		return result;
	}

	/**
	 * This method is called from several of the mutators to keep
	 * the group set in sync with the full list. 
	 * 
	 * @param item The item to be added or removed if it is in the group set
	 * @param addOrRemove True to add, false to remove
	 * @return true if the given item is a group
	 */
	private synchronized boolean syncGroups(Object item, boolean addOrRemove) {
		boolean result = false;
		// only sync if the group list has been instantiated
		if (knownGroupNamesInList != null) {
			Group group = Group.resolveFrom(item);
			if (group != null) {
				result = true;
				if (addOrRemove == ADD) {
					knownGroupNamesInList.add(group.getName());
				} else if (addOrRemove == REMOVE) {
					knownGroupNamesInList.remove(group.getName());
					if (knownGroupNamesInList.isEmpty()) {
						knownGroupNamesInList = null;
					}
				}
			}
		}
		return result;
	}
	
	// below are overrides for the various mutators
	
	@Override
	public T set(int index, T element) {
		T result = super.set(index, element);
		syncGroups(element, ADD);
		return result;
	}

	@Override
	public boolean add(T e) {
		boolean result = super.add(e);
		syncGroups(e, ADD);
		return result;
	}

	@Override
	public void add(int index, T element) {
		super.add(index, element);
		syncGroups(element, ADD);
	}

	@Override
	public T remove(int index) {
		T result = super.remove(index);
		syncGroups(result, REMOVE);
		return result;
	}

	@Override
	public boolean remove(Object o) {
		boolean removed = super.remove(o);
		if (removed) {
			syncGroups(o, REMOVE);
		}
		return removed;
	}

	@Override
	public boolean addIfAbsent(T e) {
		boolean added = super.addIfAbsent(e);
		if (added) {
			syncGroups(e, ADD);
		}
		return added;
	}

	@Override
	public boolean removeAll(Collection<?> c) {
		boolean changed = super.removeAll(c);
		if (changed) {
			// drop the transient set, will be rebuilt when/if needed
			synchronized(this) {
				knownGroupNamesInList = null;
			}
		}
		return changed;
	}

	@Override
	public boolean retainAll(Collection<?> c) {
		boolean changed = super.retainAll(c);
		if (changed) {
			// drop the transient set, will be rebuilt when/if needed
			synchronized(this) {
				knownGroupNamesInList = null;
			}
		}
		return changed;
	}

	@Override
	public int addAllAbsent(Collection<? extends T> c) {
		int added = super.addAllAbsent(c);
		if (added > 0) {
			// drop the transient set, will be rebuilt when/if needed
			synchronized(this) {
				knownGroupNamesInList = null;
			}
		}
		return added;
	}

	@Override
	public void clear() {
		super.clear();
		synchronized(this) {
			knownGroupNamesInList = null;
		}
	}

	@Override
	public boolean addAll(Collection<? extends T> c) {
		boolean changed = super.addAll(c);
		if (changed) {
			// drop the transient set, will be rebuilt when/if needed
			synchronized(this) {
				knownGroupNamesInList = null;
			}
		}
		return changed;
	}

	@Override
	public boolean addAll(int index, Collection<? extends T> c) {
		boolean changed = super.addAll(index, c);
		if (changed) {
			// drop the transient set, will be rebuilt when/if needed
			synchronized(this) {
				knownGroupNamesInList = null;
			}
		}
		return changed;
	}

	private static final boolean ADD = true;
	private static final boolean REMOVE = false;
}