// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// the License.  You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.utils.events;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;

public class SubscriptionMgr {
    protected final static Logger s_logger = Logger.getLogger(SubscriptionMgr.class);
	
	private static SubscriptionMgr s_instance = new SubscriptionMgr();
	
	private Map<String, List<SubscriberInfo>> registry;
	
	private SubscriptionMgr() {
		registry = new HashMap<String, List<SubscriberInfo>>();
	}
	
	public static SubscriptionMgr getInstance() {
		return s_instance;
	}
	
	public <T> void subscribe(String subject, T subscriber, String listenerMethod) 
		throws SecurityException, NoSuchMethodException {
		
		synchronized(this) {
			List<SubscriberInfo> l = getAndSetSubscriberList(subject);
			
			Class<?> clazz = subscriber.getClass();
			SubscriberInfo subscribeInfo = new SubscriberInfo(clazz, subscriber, listenerMethod);
			
			if(!l.contains(subscribeInfo))
				l.add(subscribeInfo);
		}
	}
	
	public <T> void unsubscribe(String subject, T subscriber, String listenerMethod) {
		synchronized(this) {
			List<SubscriberInfo> l = getSubscriberList(subject);
			if(l != null) {
				for(SubscriberInfo info : l) {
					if(info.isMe(subscriber.getClass(), subscriber, listenerMethod)) {
						l.remove(info);
						return;
					}
				}
			}
		}
	}
	
	public void notifySubscribers(String subject, Object sender, EventArgs args) {
		
		List<SubscriberInfo> l = getExecutableSubscriberList(subject);
		if(l != null) {
			for(SubscriberInfo info : l) {
				try {
					info.execute(sender, args);
				} catch (IllegalArgumentException e) {
					s_logger.warn("Exception on notifying event subscribers: ", e);
				} catch (IllegalAccessException e) {
					s_logger.warn("Exception on notifying event subscribers: ", e);
				} catch (InvocationTargetException e) {
					s_logger.warn("Exception on notifying event subscribers: ", e);
				}
			}
		}
	}
	
	private List<SubscriberInfo> getAndSetSubscriberList(String subject) {
		List<SubscriberInfo> l = registry.get(subject);
		if(l == null) {
			l = new ArrayList<SubscriberInfo>();
			registry.put(subject, l);
		}
		
		return l;
	}
	
	private List<SubscriberInfo> getSubscriberList(String subject) {
		return registry.get(subject);
	}
	
	private synchronized List<SubscriberInfo> getExecutableSubscriberList(String subject) {
		List<SubscriberInfo> l = registry.get(subject);
		if(l != null) {
			// do a shadow clone
			ArrayList<SubscriberInfo> clonedList = new ArrayList<SubscriberInfo>(l.size());
			for(SubscriberInfo info : l) 
				clonedList.add(info);
			
			return clonedList;
		}
		return null;
	}
	
	private static class SubscriberInfo {
		private Class<?> clazz;
		private Object subscriber;
		private String methodName;
		private Method method;
		
		public SubscriberInfo(Class<?> clazz, Object subscriber, String methodName) 
			throws SecurityException, NoSuchMethodException {
			
			this.clazz = clazz;
			this.subscriber = subscriber;
			this.methodName = methodName;
			for(Method method : clazz.getMethods()) {
				if(method.getName().equals(methodName)) {
					Class<?>[] paramTypes = method.getParameterTypes();
					if(paramTypes != null && paramTypes.length == 2 && 
						paramTypes[0] == Object.class &&
						EventArgs.class.isAssignableFrom(paramTypes[1])) {
						this.method = method;
						
						break;
					}
				}
			}
			if(this.method == null)
				throw new NoSuchMethodException();
		}
		
		public void execute(Object sender, EventArgs args) 
			throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
			
			method.invoke(subscriber, sender, args);
		}
		
		public boolean isMe(Class<?> clazz, Object subscriber, String methodName) {
			return this.clazz == clazz && 
				this.subscriber == subscriber &&
				this.methodName.equals(methodName);
		}
		
		public boolean equals(Object o) {
			if(o == null)
				return false;
			
			if(o instanceof SubscriberInfo) {
				return this.clazz == ((SubscriberInfo)o).clazz && 
					this.subscriber == ((SubscriberInfo)o).subscriber &&
					this.methodName.equals(((SubscriberInfo)o).methodName);
			}
			return false;
		}
	}
}
