/*
 * 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.openjpa.event;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.MetaDataDefaults;
import org.apache.openjpa.util.InvalidStateException;

/**
 * Manager that can be used to track and notify listeners on lifecycle events.
 *  This class is optimized for event firing rather than for adding and
 * removing listeners, which are O(n) operations. This class also does not
 * maintain perfect set semantics for listeners; it is possible to wind up
 * having the same listener invoked multiple times for a single event if it
 * is added to this manager multiple times with different classes, or with
 * a base class and its subclass.
 *
 * @author Steve Kim
 * @author Abe White
 * @since 0.3.3
 */
public class LifecycleEventManager
    implements CallbackModes, Serializable {

    private static final long serialVersionUID = 1L;

    private static final Exception[] EMPTY_EXCEPTIONS = new Exception[0];

    private static final Localizer _loc = Localizer.forPackage(
        LifecycleEventManager.class);

    private Map<Class<?>, ListenerList> _classListeners = null;
    private ListenerList _listeners = null;
    // odd-element: Listener even-element: Class[]
    private List<Object> _addListeners = new LinkedList<>();
    private List<Object> _remListeners = new LinkedList<>();
    private List<Exception> _exceps = new LinkedList<>();
    private boolean _firing = false;
    private boolean _fail = false;
    private boolean _failFast = false;
    private boolean _activated = false;  // set to true once modified

    /**
     * Whether this LifeCycleEventManager has had at least one listener or callback
     * registered.  Used for a quick test when firing events.
     * @return boolean
     */
    public boolean isActive(ClassMetaData meta) {
        return _activated ||
            meta.getLifecycleMetaData().is_activated() ||
            meta.getRepository().is_systemListenersActivated();
    }

    /**
     * Whether to fail after first exception when firing events to listeners.
     */
    public boolean isFailFast() {
        return _failFast;
    }

    /**
     * Whether to fail after first exception when firing events to listeners.
     */
    public void setFailFast(boolean failFast) {
        _failFast = failFast;
    }

    /**
     * Register a lifecycle listener for the given classes. If the classes
     * array is null, register for all classes.
     */
    public synchronized void addListener(Object listener, Class<?>[] classes) {
        if (listener == null)
            return;
        if (classes != null && classes.length == 0)
            return;
        _activated = true;
        if (_firing) {
            _addListeners.add(listener);
            _addListeners.add(classes);
            return;
        }

        if (classes == null) {
            if (_listeners == null)
                _listeners = new ListenerList(5);
            _listeners.add(listener);
            return;
        }

        if (_classListeners == null)
            _classListeners = new HashMap<>();
        ListenerList listeners;
        for (Class<?> aClass : classes) {
            listeners = _classListeners.get(aClass);
            if (listeners == null) {
                listeners = new ListenerList(3);
                _classListeners.put(aClass, listeners);
            }
            listeners.add(listener);
        }
    }

    /**
     * Remove the given listener.
     */
    public synchronized void removeListener(Object listener) {
        if (_firing) {
            _remListeners.add(listener);
            return;
        }

        if (_listeners != null && _listeners.remove(listener))
            return;
        if (_classListeners != null) {
            ListenerList listeners;
            for (ListenerList objects : _classListeners.values()) {
                listeners = objects;
                listeners.remove(listener);
            }
        }
    }

    /**
     * Return whether there are listeners or callbacks for the given source.
     */
    public boolean hasPersistListeners(Object source, ClassMetaData meta) {
        return hasHandlers(source, meta, LifecycleEvent.BEFORE_PERSIST)
            || hasHandlers(source, meta, LifecycleEvent.AFTER_PERSIST)
            || hasHandlers(source, meta,
                LifecycleEvent.AFTER_PERSIST_PERFORMED);
    }

    /**
     * Return whether there are listeners or callbacks for the given source.
     */
    public boolean hasDeleteListeners(Object source, ClassMetaData meta) {
        return hasHandlers(source, meta, LifecycleEvent.BEFORE_DELETE)
            || hasHandlers(source, meta, LifecycleEvent.AFTER_DELETE)
            || hasHandlers(source, meta, LifecycleEvent.AFTER_DELETE_PERFORMED);
    }

    /**
     * Return whether there are listeners or callbacks for the given source.
     */
    public boolean hasClearListeners(Object source, ClassMetaData meta) {
        return hasHandlers(source, meta, LifecycleEvent.BEFORE_CLEAR)
            || hasHandlers(source, meta, LifecycleEvent.AFTER_CLEAR);
    }

    /**
     * Return whether there are listeners or callbacks for the given source.
     */
    public boolean hasLoadListeners(Object source, ClassMetaData meta) {
        return hasHandlers(source, meta, LifecycleEvent.AFTER_LOAD);
    }

    /**
     * Return whether there are listeners or callbacks for the given source.
     */
    public boolean hasStoreListeners(Object source, ClassMetaData meta) {
        return hasHandlers(source, meta, LifecycleEvent.BEFORE_STORE)
            || hasHandlers(source, meta, LifecycleEvent.AFTER_STORE);
    }

    /**
     * Return whether there are listeners or callbacks for the given source.
     */
    public boolean hasUpdateListeners(Object source, ClassMetaData meta) {
        return hasHandlers(source, meta, LifecycleEvent.BEFORE_UPDATE)
            || hasHandlers(source, meta, LifecycleEvent.AFTER_UPDATE_PERFORMED);
    }

    /**
     * Return whether there are listeners or callbacks for the given source.
     */
    public boolean hasDirtyListeners(Object source, ClassMetaData meta) {
        return hasHandlers(source, meta, LifecycleEvent.BEFORE_DIRTY)
            || hasHandlers(source, meta, LifecycleEvent.AFTER_DIRTY);
    }

    /**
     * Return whether there are listeners or callbacks for the given source.
     */
    public boolean hasDetachListeners(Object source, ClassMetaData meta) {
        return hasHandlers(source, meta, LifecycleEvent.BEFORE_DETACH)
            || hasHandlers(source, meta, LifecycleEvent.AFTER_DETACH);
    }

    /**
     * Return whether there are listeners or callbacks for the given source.
     */
    public boolean hasAttachListeners(Object source, ClassMetaData meta) {
        return hasHandlers(source, meta, LifecycleEvent.BEFORE_ATTACH)
            || hasHandlers(source, meta, LifecycleEvent.AFTER_ATTACH);
    }

    private boolean hasHandlers(Object source, ClassMetaData meta, int type) {
        return hasCallbacks(source, meta, type)
            || hasListeners(source, meta, type);
    }

    /**
     * Return true if any callbacks are registered for the given source and
     * event type.
     */
    private boolean hasCallbacks(Object source, ClassMetaData meta, int type) {
        LifecycleCallbacks[] callbacks = meta.getLifecycleMetaData().
            getCallbacks(type);
        if (callbacks.length == 0)
            return false;
        for (LifecycleCallbacks callback : callbacks)
            if (callback.hasCallback(source, type))
                return true;
        return false;
    }

    /**
     * Return true if any listeners are registered for the given source and
     * event type.
     */
    private boolean hasListeners(Object source,
        ClassMetaData meta, int type) {
        if (meta.getLifecycleMetaData().getIgnoreSystemListeners())
            return false;
        if (fireEvent(null, source, null, type, _listeners, true, null) == Boolean.TRUE)
            return true;
        ListenerList system = meta.getRepository().getSystemListeners();
        if (!system.isEmpty() && fireEvent(null, source, null, type, system,
            true, null) == Boolean.TRUE)
            return true;
        if (_classListeners != null) {
            Class<?> c = source == null ? meta.getDescribedType() : source.getClass();
            do {
                if (fireEvent(null, source, null, type, _classListeners.get(c), true, null) == Boolean.TRUE)
                    return true;
                c = c.getSuperclass();
            } while (c != null && c != Object.class);
        }
        return false;
    }

    /**
     * Fire lifecycle event to all registered listeners without an argument.
     */
    public Exception[] fireEvent(Object source,
        ClassMetaData meta, int type) {
        return fireEvent(source, null, meta, type);
    }

    /**
     * Fire lifecycle event to all registered listeners.
     */
    public synchronized Exception[] fireEvent(Object source, Object related,
        ClassMetaData meta, int type) {
        boolean reentrant = _firing;
        _firing = true;
        List<Exception> exceptions = (reentrant) ? new LinkedList<>() : _exceps;
        MetaDataDefaults def = meta.getRepository().getMetaDataFactory().
            getDefaults();

        boolean callbacks = def.getCallbacksBeforeListeners(type);
        if (callbacks)
            makeCallbacks(source, related, meta, type, exceptions);

        LifecycleEvent ev = (LifecycleEvent) fireEvent(null, source, related,
            type, _listeners, false, exceptions);

        if (_classListeners != null) {
            Class<?> c = source == null ? meta.getDescribedType() : source.getClass();
            do {
                ev = (LifecycleEvent) fireEvent(ev, source, related, type,
                    _classListeners.get(c), false, exceptions);
                c = c.getSuperclass();
            } while (c != null && c != Object.class);
        }

        // make system listeners
        if (!meta.getLifecycleMetaData().getIgnoreSystemListeners()) {
            ListenerList system = meta.getRepository().getSystemListeners();
            fireEvent(ev, source, related, type, system, false, exceptions);
        }

        if (!callbacks)
            makeCallbacks(source, related, meta, type, exceptions);

        // create return array before clearing exceptions
        Exception[] ret;
        if (exceptions.isEmpty())
            ret = EMPTY_EXCEPTIONS;
        else
            ret = exceptions.toArray
                (new Exception[exceptions.size()]);

        // if this wasn't a reentrant call, catch up with calls to add
        // and remove listeners made while firing
        if (!reentrant) {
            _firing = false;
            _fail = false;
            if (!_addListeners.isEmpty())
                for (Iterator<Object> itr = _addListeners.iterator(); itr.hasNext();) {
                    addListener(itr.next(), (Class[]) itr.next());
                }
            if (!_remListeners.isEmpty())
                for (Object remListener : _remListeners) {
                    removeListener(remListener);
                }
            _addListeners.clear();
            _remListeners.clear();
            _exceps.clear();
        }
        return ret;
    }

    /**
     * Make callbacks, recording any exceptions in the given collection.
     */
    private void makeCallbacks(Object source, Object related,
        ClassMetaData meta, int type, Collection<Exception> exceptions) {
        // make lifecycle callbacks
        LifecycleCallbacks[] callbacks = meta.getLifecycleMetaData().
            getCallbacks(type);
        for (int i = 0; !_fail && i < callbacks.length; i++) {
            try {
                callbacks[i].makeCallback(source, related, type);
            } catch (Exception e) {
                exceptions.add(e);
                if (_failFast)
                    _fail = true;
            }
        }
    }

    /**
     * Fire an event with the given source and type to the given list of
     * listeners. The event may have already been constructed.
     */
    private Object fireEvent(LifecycleEvent ev, Object source, Object rel,
        int type, ListenerList listeners, boolean mock, List<Exception> exceptions) {
        if (listeners == null || !listeners.hasListeners(type))
            return null;

        Object listener;
        boolean responds;
        for (int i = 0, size = listeners.size(); !_fail && i < size; i++) {
            listener = listeners.get(i);
            if (size == 1)
                responds = true;
            else if (listener instanceof ListenerAdapter) {
                responds = ((ListenerAdapter) listener).respondsTo(type);
                if (!responds)
                    continue;
            } else {
                responds = false;
            }
            try {
                switch (type) {
                    case LifecycleEvent.BEFORE_CLEAR:
                    case LifecycleEvent.AFTER_CLEAR:
                        if (responds || listener instanceof ClearListener) {
                            if (mock)
                                return Boolean.TRUE;
                            if (ev == null)
                                ev = new LifecycleEvent(source, type);
                            if (type == LifecycleEvent.BEFORE_CLEAR)
                                ((ClearListener) listener).beforeClear(ev);
                            else
                                ((ClearListener) listener).afterClear(ev);
                        }
                        break;
                    case LifecycleEvent.BEFORE_PERSIST:
                    case LifecycleEvent.AFTER_PERSIST:
                        if (responds || listener instanceof PersistListener) {
                            if (mock)
                                return Boolean.TRUE;
                            if (ev == null)
                                ev = new LifecycleEvent(source, type);
                            if (type == LifecycleEvent.BEFORE_PERSIST)
                                ((PersistListener) listener).beforePersist(ev);
                            else
                                ((PersistListener) listener).afterPersist(ev);
                        }
                        break;
                    case LifecycleEvent.BEFORE_DELETE:
                    case LifecycleEvent.AFTER_DELETE:
                        if (responds || listener instanceof DeleteListener) {
                            if (mock)
                                return Boolean.TRUE;
                            if (ev == null)
                                ev = new LifecycleEvent(source, type);
                            if (type == LifecycleEvent.BEFORE_DELETE)
                                ((DeleteListener) listener).beforeDelete(ev);
                            else
                                ((DeleteListener) listener).afterDelete(ev);
                        }
                        break;
                    case LifecycleEvent.BEFORE_DIRTY:
                    case LifecycleEvent.AFTER_DIRTY:
                    case LifecycleEvent.BEFORE_DIRTY_FLUSHED:
                    case LifecycleEvent.AFTER_DIRTY_FLUSHED:
                        if (responds || listener instanceof DirtyListener) {
                            if (mock)
                                return Boolean.TRUE;
                            if (ev == null)
                                ev = new LifecycleEvent(source, type);
                            switch (type) {
                                case LifecycleEvent.BEFORE_DIRTY:
                                    ((DirtyListener) listener).beforeDirty(ev);
                                    break;
                                case LifecycleEvent.AFTER_DIRTY:
                                    ((DirtyListener) listener).afterDirty(ev);
                                    break;
                                case LifecycleEvent.BEFORE_DIRTY_FLUSHED:
                                    ((DirtyListener) listener)
                                        .beforeDirtyFlushed(ev);
                                    break;
                                case LifecycleEvent.AFTER_DIRTY_FLUSHED:
                                    ((DirtyListener) listener)
                                        .afterDirtyFlushed(ev);
                                    break;
                            }
                        }
                        break;
                    case LifecycleEvent.AFTER_LOAD:
                    case LifecycleEvent.AFTER_REFRESH:
                        if (responds || listener instanceof LoadListener) {
                            if (mock)
                                return Boolean.TRUE;
                            if (ev == null)
                                ev = new LifecycleEvent(source, type);
                            if (type == LifecycleEvent.AFTER_LOAD)
                                ((LoadListener) listener).afterLoad(ev);
                            else
                                ((LoadListener) listener).afterRefresh(ev);
                        }
                        break;
                    case LifecycleEvent.BEFORE_STORE:
                    case LifecycleEvent.AFTER_STORE:
                        if (responds || listener instanceof StoreListener) {
                            if (mock)
                                return Boolean.TRUE;
                            if (ev == null)
                                ev = new LifecycleEvent(source, type);
                            if (type == LifecycleEvent.BEFORE_STORE)
                                ((StoreListener) listener).beforeStore(ev);
                            else
                                ((StoreListener) listener).afterStore(ev);
                        }
                        break;
                    case LifecycleEvent.BEFORE_DETACH:
                    case LifecycleEvent.AFTER_DETACH:
                        if (responds || listener instanceof DetachListener) {
                            if (mock)
                                return Boolean.TRUE;
                            if (ev == null)
                                ev = new LifecycleEvent(source, rel, type);
                            if (type == LifecycleEvent.BEFORE_DETACH)
                                ((DetachListener) listener).beforeDetach(ev);
                            else
                                ((DetachListener) listener).afterDetach(ev);
                        }
                        break;
                    case LifecycleEvent.BEFORE_ATTACH:
                    case LifecycleEvent.AFTER_ATTACH:
                        if (responds || listener instanceof AttachListener) {
                            if (mock)
                                return Boolean.TRUE;
                            if (ev == null)
                                ev = new LifecycleEvent(source, rel, type);
                            if (type == LifecycleEvent.BEFORE_ATTACH)
                                ((AttachListener) listener).beforeAttach(ev);
                            else
                                ((AttachListener) listener).afterAttach(ev);
                        }
                        break;

                    case LifecycleEvent.AFTER_PERSIST_PERFORMED:
                        if (responds || listener instanceof PostPersistListener) {
                            if (mock)
                                return Boolean.TRUE;
                            if (ev == null)
                                ev = new LifecycleEvent(source, rel, type);
                            ((PostPersistListener) listener).afterPersistPerformed(ev);
                        }
                        break;
                    case LifecycleEvent.BEFORE_UPDATE:
                    case LifecycleEvent.AFTER_UPDATE_PERFORMED:
                        if (responds || listener instanceof UpdateListener) {
                            if (mock)
                                return Boolean.TRUE;
                            if (ev == null)
                                ev = new LifecycleEvent(source, rel, type);
                            if (type == LifecycleEvent.BEFORE_UPDATE)
                                ((UpdateListener) listener).beforeUpdate(ev);
                            else
                                ((UpdateListener) listener).afterUpdatePerformed(ev);
                        }
                        break;
                    case LifecycleEvent.AFTER_DELETE_PERFORMED:
                        if (responds || listener instanceof PostDeleteListener){
                            if (mock)
                                return Boolean.TRUE;
                            if (ev == null)
                                ev = new LifecycleEvent(source, rel, type);
                            ((PostDeleteListener) listener).afterDeletePerformed(ev);
                        }
                        break;
                    default:
                        throw new InvalidStateException(_loc.get("unknown-lifecycle-event", Integer.toString(type)));
                }
            }
            catch (Exception e) {
                exceptions.add(e);
                if (_failFast)
                    _fail = true;
            }
        }
        return ev;
    }

    /**
     * Interface that facades to other lifecycle listener interfaces can
     * implement to choose which events to respond to based on their delegate.
     * This is more efficient than registering as a listener for all events
     * but only responding to some.
     */
    public interface ListenerAdapter {

        /**
         * Return whether this instance responds to the given event type from
         * {@link LifecycleEvent}.
         */
        boolean respondsTo(int eventType);
    }

    /**
     * Extended list that tracks what event types its elements care about.
     * Maintains set semantics as well.
     */
    public static class ListenerList extends ArrayList<Object> {

        
        private static final long serialVersionUID = 1L;
        private int _types = 0;

        public ListenerList(int size) {
            super(size);
        }

        public ListenerList(ListenerList copy) {
            super(copy);
            _types = copy._types;
        }

        public boolean hasListeners(int type) {
            return (_types & (2 << type)) > 0;
        }

        @Override
        public boolean add(Object listener) {
            if (contains(listener))
                return false;
            super.add(listener);
            _types |= getEventTypes(listener);
            return true;
        }

        @Override
        public boolean remove(Object listener) {
            if (!super.remove(listener))
                return false;

            // recompute types mask
            _types = 0;
            for (int i = 0; i < size(); i++)
                _types |= getEventTypes(get(i));
            return true;
        }

        /**
         * Return a mask of the event types the given listener processes.
         */
        private static int getEventTypes(Object listener) {
            int types = 0;
            if (listener instanceof ListenerAdapter) {
                ListenerAdapter adapter = (ListenerAdapter) listener;
                for (int i = 0; i < LifecycleEvent.ALL_EVENTS.length; i++)
                    if (adapter.respondsTo(LifecycleEvent.ALL_EVENTS[i]))
                        types |= 2 << LifecycleEvent.ALL_EVENTS[i];
                return types;
            }

            if (listener instanceof PersistListener) {
                types |= 2 << LifecycleEvent.BEFORE_PERSIST;
                types |= 2 << LifecycleEvent.AFTER_PERSIST;
            }
            if (listener instanceof ClearListener) {
                types |= 2 << LifecycleEvent.BEFORE_CLEAR;
                types |= 2 << LifecycleEvent.AFTER_CLEAR;
            }
            if (listener instanceof DeleteListener) {
                types |= 2 << LifecycleEvent.BEFORE_DELETE;
                types |= 2 << LifecycleEvent.AFTER_DELETE;
            }
            if (listener instanceof DirtyListener) {
                types |= 2 << LifecycleEvent.BEFORE_DIRTY;
                types |= 2 << LifecycleEvent.AFTER_DIRTY;
                types |= 2 << LifecycleEvent.BEFORE_DIRTY_FLUSHED;
                types |= 2 << LifecycleEvent.AFTER_DIRTY_FLUSHED;
            }
            if (listener instanceof LoadListener) {
                types |= 2 << LifecycleEvent.AFTER_LOAD;
                types |= 2 << LifecycleEvent.AFTER_REFRESH;
            }
            if (listener instanceof StoreListener) {
                types |= 2 << LifecycleEvent.BEFORE_STORE;
                types |= 2 << LifecycleEvent.AFTER_STORE;
            }
            if (listener instanceof DetachListener) {
                types |= 2 << LifecycleEvent.BEFORE_DETACH;
                types |= 2 << LifecycleEvent.AFTER_DETACH;
            }
            if (listener instanceof AttachListener) {
                types |= 2 << LifecycleEvent.BEFORE_ATTACH;
                types |= 2 << LifecycleEvent.AFTER_ATTACH;
			}
			return types;
		}
	}
}
