package cz.cuni.amis.utils.listener;

import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.EventListener;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * This object is implementing listeners list, where you may store both type of references to 
 * the listeners (strong reference / weak reference).
 * <BR><BR>
 * It takes quite of effort to maintain the list with both strong / weak references,
 * therefore the class was created.
 * <BR><BR>
 * Because we don't know what method we will be calling on the listeners the public
 * interface ListenerNotifier exists. If you want to fire all the listeners, just
 * implement this interface and stuff it to the notify() method of the "Listeners".
 * (This somehow resembles the Stored Procedure pattern...)
 * <BR><BR>
 * Another interface is used for removing listeners. You may want to go through the listeners
 * list and mark some of them to be removed (according to some rule like 'instanceof'). See
 * static interface ListenerRemover.
 * <BR><BR>
 * The class is thread-safe.
 * 
 * @author Jimmy
 */
@SuppressWarnings("hiding")
public class Listeners<Listener extends EventListener> {	
	
	/**
	 * Used to raise the event in the listeners.
	 * 
	 * @author Jimmy
	 *
	 * @param <Listener>
	 */
	public static interface ListenerNotifier<Listener extends EventListener> {
		
		public void notify(Listener listener);
		
	}
	
	/**
	 * Used as a visitor to the listeners that should tell which listeners to remove...
	 * 
	 * @author Jimmy
	 *
	 * @param <Listener>
	 */
	public static interface ListenerRemover {
		 
		/**
		 * If returns true, the 'listener' will be removed from the list (detached).
		 * @param listener
		 * @return
		 */
		public boolean remove(EventListener listener);
		
	}
	
	/**
	 * Abstract class that stores the Listeners and can fire the event when needed.
	 * @author Jimmy
	 */	
    private static abstract class ListenerStore<Listener extends EventListener> {
    	
        /**
         * May return null if listener was gc()ed.
         * <BR><BR>
         * If null is returned, we will remove the entry from the listeners list.
         * @return
         */      
        public abstract Listener getListener();
        
    }

    /**
     * Listener store class referencing the FlagListener with "strong" reference.
     * @author Jimmy
     */
    private static class StrongListenerStore<Listener extends EventListener> extends ListenerStore<Listener> {

        private Listener listener;

        public StrongListenerStore(Listener listener) {
            this.listener = listener;
        }

        public Listener getListener() {
            return listener;
        }
		
    }
    
    /**
     * Listener store class referencing the FlagListener with weak reference.
     * @author Jimmy
     *
     * @param <T>
     */
    private static class WeakListenerStore<Listener extends EventListener> extends ListenerStore<Listener> {
        
        private WeakReference<Listener> listenerReference = null;
        
        public WeakListenerStore(Listener listener) {
            this.listenerReference = new WeakReference<Listener>(listener);
        }
        
        public Listener getListener() {
            return listenerReference.get();
        }
        
    }
    
    /**
     * Access to this field is synchronized through this field.
     */
    private ConcurrentLinkedQueue<ListenerStore<Listener>> listeners = new ConcurrentLinkedQueue<ListenerStore<Listener>>(); 
    
    /**
     * Adds listener with strong reference to it.
     * @param listener
     */
    public void addStrongListener(Listener listener) {
    	if (listener == null) throw new IllegalArgumentException("Listeners.addStrongListener(): can't add 'null' as a listener");
    	synchronized(listeners) {
    		listeners.add(new StrongListenerStore<Listener>(listener));
    	}
    }
    
    /**
     * Adds listener with weak reference to it.
     * @param listener
     */
    public void addWeakListener(Listener listener) {
    	if (listener == null) throw new IllegalArgumentException("Listeners.addWeakListener(): can't add 'null' as a listener");
    	synchronized(listeners) {
    		listeners.add(new WeakListenerStore<Listener>(listener));
    	}
    }
    
    /**
     * Removes all listeners that are equal() to this one.
     * @param listener
     */
    public void removeEqualListener(EventListener listener) {
    	if (listener == null) return;
    	synchronized(listeners) {
    		Iterator<ListenerStore<Listener>> iterator = listeners.iterator();
    		while(iterator.hasNext()) {
    			Listener storedListener = (iterator.next()).getListener();
    			if (storedListener == null) {
    				iterator.remove();
    				continue;
    			}
    			if (listener.equals(storedListener)) iterator.remove();
    		}    		
    	}
    }
    
    /**
     * Removes all listeners that are == to this one (not equal()! must be the same object).
     * @param listener
     */
    public void removeListener(EventListener listener) {
    	if (listener == null) return;
    	synchronized(listeners) {
    		Iterator<ListenerStore<Listener>> iterator = listeners.iterator();
    		while(iterator.hasNext()) {
    			Listener storedListener = (iterator.next()).getListener();
    			if (storedListener == null) {
    				iterator.remove();
    				continue;
    			}
    			if (listener == storedListener) iterator.remove();
    		}    		
    	}
    }
    
    /**
     * Calls notifier.notify() on each of the stored listeners, allowing you to execute stored
     * command.
     * 
     * @param notifier
     */
    public void notify(ListenerNotifier<Listener> notifier) {
    	
    	// THE PROBLEM HERE IS ... WHEN THIS TRIGGERS DELETION OF THE LISTENER
    	// FROM THE LISTENERS -> we're in the same thread and whooops! iterator exception...
    	// Solution: use ConcurrentLinkedQueue
    	
    	synchronized(listeners) {
    		Iterator<ListenerStore<Listener>> iterator = listeners.iterator();
    		while(iterator.hasNext()) {
    			Listener storedListener = (iterator.next()).getListener();
    			if (storedListener == null) {
    				iterator.remove();
    				continue;
    			}
    			notifier.notify(storedListener);
    		}
    	}
    }
    
    /**
     * Returns true if at least one equals listener to the param 'listener' is found.	 
     * @param listener
     * @return
     */
    public boolean isEqualListening(EventListener listener) {
    	if (listener == null) return false;
    	synchronized(listeners) {
    		Iterator<ListenerStore<Listener>> iterator = listeners.iterator();
    		while(iterator.hasNext()) {
    			Listener storedListener = (iterator.next()).getListener();
    			if (storedListener == null) {
    				iterator.remove();
    				continue;
    			}
    			if (listener.equals(storedListener)) return true;
    		}
    	}
    	return false;
    }
    
    /**
     * Returns true if at least one == listener to the param 'listener' is found.
     * <BR><BR>
     * Not using equal() but pointer ==.
     * 	 
     * @param listener
     * @return
     */
    public boolean isListening(EventListener listener) {
    	if (listener == null) return false;
    	synchronized(listeners) {
    		Iterator<ListenerStore<Listener>> iterator = listeners.iterator();
    		while(iterator.hasNext()) {
    			Listener storedListener = (iterator.next()).getListener();
    			if (storedListener == null) {
    				iterator.remove();
    				continue;
    			}
    			if (listener == storedListener) return true;
    		}
    	}
    	return false;
    }
    
    public void clearListeners() {
    	synchronized(listeners) {
    		listeners.clear();
    	}
    }
    
    /**
     * Returns count of listners in the list, note that this may not be exact as we store also
     * listeners with weak listeners, but the list will be purged in next opportunity (like raising
     * event, removing listener).
     * <p><p>
     * <Beware that, unlike in most collections, this method is
     * <em>NOT</em> a constant-time operation. Because of the
     * asynchronous nature of used queue, determining the current
     * number of elements requires an O(n) traversal.
     * 
     * @return
     */
    public int count() {
    	synchronized(listeners) {
    		return listeners.size();
    	}
    }
    
    /**
     * This will iterate over all of the listeners and do the query "whether the listner should be
     * removed from the Listeners object". 
     * <BR><BR>
     * If 'remover' returns true to the listener, the listener
     * is removed.
     * 
     * @param remover
     */
    public void remove(ListenerRemover remover) {
    	synchronized(listeners) {
    		Iterator<ListenerStore<Listener>> iterator = listeners.iterator();
    		while(iterator.hasNext()) {
    			Listener storedListener = (iterator.next()).getListener();
    			if (storedListener == null) {
    				iterator.remove();
    				continue;
    			}
    			if (remover.remove(storedListener)) {
    				iterator.remove();
    			}
    		}
    	}
    }

}
