/*
 * 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.pivot.tutorials.explorer.tools;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.Iterator;

import org.apache.pivot.collections.Group;
import org.apache.pivot.collections.HashMap;
import org.apache.pivot.collections.HashSet;
import org.apache.pivot.util.ImmutableIterator;
import org.apache.pivot.util.ListenerList;
import org.apache.pivot.util.Vote;
import org.apache.pivot.wtk.Component;
import org.apache.pivot.wtk.Container;

/**
 * A component that monitors a source component for events.
 */
public class EventLogger extends Container {
    /**
     * Event logger skin interface. Event logger skins must implement this.
     */
    public interface Skin {
        /**
         * Clears the event log.
         */
        public void clearLog();

        /**
         * Select/Deselect all Events to log.
         * @param select
         * if true, all events will be selected for the log,
         * otherwise all events will be deselected
         */
        public void selectAllEvents(boolean select);
    }

    /**
     * A read-only group of events that an event logger is capable of firing.
     * To make an event logger actually fire declared events, callers add them
     * to the event logger's include event group.
     */
    public final class DeclaredEventGroup implements Group<Method>, Iterable<Method> {
        private DeclaredEventGroup() {
        }

        @Override
        public boolean add(Method event) {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean remove(Method event) {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean contains(Method event) {
            return declaredEvents.contains(event);
        }

        @Override
        public Iterator<Method> iterator() {
            return new ImmutableIterator<Method>(declaredEvents.iterator());
        }
    }

    /**
     * A read/write group of events that an event logger will actually fire.
     * This group is guaranteed to be a subset of the declared event group.
     */
    public final class IncludeEventGroup implements Group<Method>, Iterable<Method> {
        private IncludeEventGroup() {
        }

        @Override
        public boolean add(Method event) {
            boolean added = false;

            if (!declaredEvents.contains(event)) {
                throw new IllegalArgumentException("Event has not been declared.");
            }

            if (!includeEvents.contains(event)) {
                includeEvents.add(event);
                eventLoggerListeners.eventIncluded(EventLogger.this, event);
                added = true;
            }

            return added;
        }

        @Override
        public boolean remove(Method event) {
            boolean removed = false;

            if (includeEvents.contains(event)) {
                includeEvents.remove(event);
                eventLoggerListeners.eventExcluded(EventLogger.this, event);
                removed = true;
            }

            return removed;
        }

        @Override
        public boolean contains(Method event) {
            return includeEvents.contains(event);
        }

        @Override
        public Iterator<Method> iterator() {
            return new ImmutableIterator<Method>(includeEvents.iterator());
        }
    }

    /**
     * Event logger invocation handler.
     */
    private class LoggerInvocationHandler implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method event, Object[] arguments) throws Throwable {
            if (includeEvents.contains(event)) {
                eventLoggerListeners.eventFired(EventLogger.this, event, arguments);
            }

            Object result = null;
            Class<?> returnType = event.getReturnType();
            if (returnType == Vote.class) {
                result = Vote.APPROVE;
            } else if (returnType == Boolean.TYPE) {
                result = false;
            }

            return result;
        }
    }

    /**
     * Event logger listener list.
     */
    private static class EventLoggerListenerList extends ListenerList<EventLoggerListener>
        implements EventLoggerListener {
        @Override
        public void sourceChanged(EventLogger eventLogger, Component previousSource) {
            for (EventLoggerListener listener : this) {
                listener.sourceChanged(eventLogger, previousSource);
            }
        }

        @Override
        public void eventIncluded(EventLogger eventLogger, Method event) {
            for (EventLoggerListener listener : this) {
                listener.eventIncluded(eventLogger, event);
            }
        }

        @Override
        public void eventExcluded(EventLogger eventLogger, Method event) {
            for (EventLoggerListener listener : this) {
                listener.eventExcluded(eventLogger, event);
            }
        }

        @Override
        public void eventFired(EventLogger eventLogger, Method event, Object[] arguments) {
            for (EventLoggerListener listener : this) {
                listener.eventFired(eventLogger, event, arguments);
            }
        }
    }

    private Component source = null;

    private HashMap<Class<?>, Object> eventListenerProxies = new HashMap<Class<?>, Object>();
    private LoggerInvocationHandler loggerInvocationHandler = new LoggerInvocationHandler();

    private HashSet<Method> declaredEvents = new HashSet<Method>();
    private DeclaredEventGroup declaredEventGroup = new DeclaredEventGroup();

    private HashSet<Method> includeEvents = new HashSet<Method>();
    private IncludeEventGroup includeEventGroup = new IncludeEventGroup();

    private EventLoggerListenerList eventLoggerListeners = new EventLoggerListenerList();

    /**
     * Creates a new event logger that is not tied to any source component.
     */
    public EventLogger() {
        this(null);
    }

    /**
     * Creates a new event logger that will log events on the specified source.
     */
    public EventLogger(Component source) {
        setSource(source);
        setSkin(new EventLoggerSkin());
    }

    /**
     * Gets this event logger's source component.
     *
     * @return
     * The source component, or <tt>null</tt> if no source has been set.
     */
    public Component getSource() {
        return source;
    }

    /**
     * Sets this event logger's source component.
     *
     * @param source
     * The source component, or <tt>null</tt> to clear the source.
     */
    public void setSource(Component source) {
        Component previousSource = this.source;

        if (source != previousSource) {
            if (previousSource != null) {
                unregisterEventListeners();
            }

            this.source = source;

            declaredEvents.clear();
            includeEvents.clear();

            if (source != null) {
                registerEventListeners();
            }

            eventLoggerListeners.sourceChanged(this, previousSource);
        }
    }

    /**
     * Gets the declared event group, a read-only group that includes the
     * complete list of events that this event logger's source declares.
     *
     * @return
     * the declared events group.
     */
    public DeclaredEventGroup getDeclaredEvents() {
        return declaredEventGroup;
    }

    /**
     * Gets the include events group, which callers can use to include or
     * exclude declared events from those that get fired by this logger.
     * This group is guaranteed to be a subset of the declared event group
     * (attempts to add events to this group that are not included in the
     * declared event group will fail).
     *
     * @return
     * The include events group.
     */
    public IncludeEventGroup getIncludeEvents() {
        return includeEventGroup;
    }

    /**
     * Clears the event log.
     */
    public void clearLog() {
        EventLogger.Skin eventLoggerSkin = (EventLogger.Skin)getSkin();
        eventLoggerSkin.clearLog();
    }

    /**
     * Select/Deselect all Events to log.
     * @param select
     * if true, all events will be selected for the log,
     * otherwise all events will be deselected
     */
    public void selectAllEvents(boolean select) {
        // Include or exclude each possible method from the group of monitored events
        IncludeEventGroup includeEventsLocal = getIncludeEvents();
        for (Method event : declaredEvents) {
            if (select) {
                includeEventsLocal.add(event);
            } else {
                includeEventsLocal.remove(event);
            }
        }
        // Update the skin (Checkboxes)
        EventLogger.Skin eventLoggerSkin = (EventLogger.Skin)getSkin();
        eventLoggerSkin.selectAllEvents(select);
    }

    /**
     * Registers event listeners on this event logger's source.
     */
    private void registerEventListeners() {
        Method[] methods = source.getClass().getMethods();

        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];

            if (ListenerList.class.isAssignableFrom(method.getReturnType())
                && (method.getModifiers() & Modifier.STATIC) == 0) {
                ParameterizedType genericType = (ParameterizedType)method.getGenericReturnType();
                Type[] typeArguments = genericType.getActualTypeArguments();

                if (typeArguments.length == 1) {
                    Class<?> listenerInterface = (Class<?>)typeArguments[0];

                    if (!listenerInterface.isInterface()) {
                        throw new RuntimeException(listenerInterface.getName()
                            + " is not an interface.");
                    }

                    Method[] interfaceMethods = listenerInterface.getMethods();
                    for (int j = 0; j < interfaceMethods.length; j++) {
                        Method interfaceMethod = interfaceMethods[j];
                        declaredEvents.add(interfaceMethod);
                    }

                    // Get the listener list
                    Object listenerList;
                    try {
                        listenerList = method.invoke(source);
                    } catch (InvocationTargetException exception) {
                        throw new RuntimeException(exception);
                    } catch (IllegalAccessException exception) {
                        throw new RuntimeException(exception);
                    }

                    // Get the listener for this interface
                    Object listener = eventListenerProxies.get(listenerInterface);
                    if (listener == null) {
                        listener = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                            new Class<?>[]{listenerInterface}, loggerInvocationHandler);
                        eventListenerProxies.put(listenerInterface, listener);
                    }

                    // Add the listener
                    Class<?> listenerListClass = listenerList.getClass();
                    Method addMethod;
                    try {
                        addMethod = listenerListClass.getMethod("add", Object.class);
                    } catch (NoSuchMethodException exception) {
                        throw new RuntimeException(exception);
                    }

                    try {
                        addMethod.invoke(listenerList, listener);
                    } catch (IllegalAccessException exception) {
                        throw new RuntimeException(exception);
                    } catch (InvocationTargetException exception) {
                        throw new RuntimeException(exception);
                    }
                }
            }
        }
    }

    /**
     * Unregisters event listeners on this event logger's source.
     */
    private void unregisterEventListeners() {
        Method[] methods = source.getClass().getMethods();

        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];

            if (ListenerList.class.isAssignableFrom(method.getReturnType())
                && (method.getModifiers() & Modifier.STATIC) == 0) {
                ParameterizedType genericType = (ParameterizedType)method.getGenericReturnType();
                Type[] typeArguments = genericType.getActualTypeArguments();

                if (typeArguments.length == 1) {
                    Class<?> listenerInterface = (Class<?>)typeArguments[0];

                    // Get the listener list
                    Object listenerList;
                    try {
                        listenerList = method.invoke(source);
                    } catch (InvocationTargetException exception) {
                        throw new RuntimeException(exception);
                    } catch (IllegalAccessException exception) {
                        throw new RuntimeException(exception);
                    }

                    // Get the listener for this interface
                    Object listener = eventListenerProxies.get(listenerInterface);

                    // Remove the listener
                    Class<?> listenerListClass = listenerList.getClass();
                    Method removeMethod;
                    try {
                        removeMethod = listenerListClass.getMethod("remove", Object.class);
                    } catch (NoSuchMethodException exception) {
                        throw new RuntimeException(exception);
                    }

                    try {
                        removeMethod.invoke(listenerList, listener);
                    } catch (IllegalAccessException exception) {
                        throw new RuntimeException(exception);
                    } catch (InvocationTargetException exception) {
                        throw new RuntimeException(exception);
                    }
                }
            }
        }
    }

    /**
     * Gets the event logger listener list.
     */
    public ListenerList<EventLoggerListener> getEventLoggerListeners() {
        return eventLoggerListeners;
    }
}
