/*
 * 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 org.apache.openejb.client;

import org.apache.openejb.client.event.Observes;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * @version $Rev$ $Date$
 */
public class Observers {

    private static final Logger logger = Logger.getLogger("OpenEJB.client");

    private final List<Observer> observers = new ArrayList<Observer>();

    public boolean addObserver(final Object observer) {
        if (observer == null) {
            throw new IllegalArgumentException("observer cannot be null");
        }
        return observers.add(new Observer(observer));
    }

    public boolean removeObserver(final Object listener) {
        if (listener == null) {
            throw new IllegalArgumentException("listener cannot be null");
        }
        return observers.remove(new Observer(listener));
    }

    public void fireEvent(final Object event) {
        if (event == null) {
            throw new IllegalArgumentException("event cannot be null");
        }

        for (final Observer observer : observers) {
            try {
                observer.invoke(event);
            } catch (InvocationTargetException e) {
                final Throwable t = e.getTargetException() == null ? e : e.getTargetException();

                if (e.getTargetException() != null) {
                    logger.log(Level.WARNING, "Observer method invocation failed", t);
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * @version $Rev$ $Date$
     */
    public static class Observer {

        private final Map<Class, Method> methods = new HashMap<Class, Method>();
        private final Object observer;
        private final Method defaultMethod;

        public Observer(final Object observer) {
            if (observer == null) {
                throw new IllegalArgumentException("observer cannot be null");
            }

            this.observer = observer;
            for (final Method method : observer.getClass().getMethods()) {
                if (!isObserver(method)) {
                    continue;
                }

                if (method.getParameterTypes().length > 1) {
                    throw new IllegalArgumentException("@Observes method must have only 1 parameter: " + method.toString());
                }

                if (Modifier.isAbstract(method.getModifiers())) {
                    throw new IllegalArgumentException("@Observes method must not be abstract: " + method.toString());
                }

                if (!Modifier.isPublic(method.getModifiers())) {
                    throw new IllegalArgumentException("@Observes method must be public: " + method.toString());
                }

                final Class<?> type = method.getParameterTypes()[0];

                if (type.isAnnotation()) {
                    throw new IllegalArgumentException("@Observes method parameter must be a concrete class (not an annotation): " + method.toString());
                }

                if (Modifier.isAbstract(type.getModifiers())) {
                    throw new IllegalArgumentException("@Observes method parameter must be a concrete class (not an abstract class): " + method.toString());
                }

                if (type.isInterface()) {
                    throw new IllegalArgumentException("@Observes method parameter must be a concrete class (not an interface): " + method.toString());
                }

                if (type.isArray()) {
                    throw new IllegalArgumentException("@Observes method parameter must be a concrete class (not an array): " + method.toString());
                }

                if (type.isPrimitive()) {
                    throw new IllegalArgumentException("@Observes method parameter must be a concrete class (not a primitive): " + method.toString());
                }

                methods.put(type, method);
            }

            defaultMethod = methods.get(Object.class);

            if (methods.size() == 0) {
                throw new IllegalArgumentException("Object has no @Observes methods. For example: public void observe(@Observes RetryConditionAdded event){...}");
            }
        }

        public void invoke(final Object event) throws InvocationTargetException, IllegalAccessException {
            if (event == null) {
                throw new IllegalArgumentException("event cannot be null");
            }

            Class eventType = event.getClass();
            while (eventType != Object.class) {
                final Method method = methods.get(eventType);

                if (method != null) {
                    method.invoke(observer, event);

                    return;
                }

                eventType = eventType.getSuperclass();
            }
            if (defaultMethod != null) {
                defaultMethod.invoke(observer, event);
            }
        }

        private boolean isObserver(final Method method) {
            for (final Annotation[] annotations : method.getParameterAnnotations()) {
                for (final Annotation annotation : annotations) {
                    if (annotation.annotationType().equals(Observes.class)) {
                        return true;
                    }
                }
            }
            return false;
        }

        @Override
        public boolean equals(final Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            final Observer observer1 = (Observer) o;

            return observer.equals(observer1.observer);
        }

        @Override
        public int hashCode() {
            return observer.hashCode();
        }
    }
}
