//
// 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
// with 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 java.util.Objects;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

public class SubscriptionMgr {
    protected Logger logger = LogManager.getLogger(getClass());

    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) {
                    logger.warn("Exception on notifying event subscribers: ", e);
                } catch (IllegalAccessException e) {
                    logger.warn("Exception on notifying event subscribers: ", e);
                } catch (InvocationTargetException e) {
                    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);
        }

        @Override
        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;
        }

        @Override
        public int hashCode() {
            return Objects.hash(this.clazz, this.subscriber, this.methodName, this.method);
        }
    }
}
