/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2010, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.naming;

import java.util.concurrent.ThreadFactory;
import org.jboss.as.naming.util.FastCopyHashMap;

import javax.naming.Binding;
import javax.naming.Name;
import javax.naming.event.EventContext;
import javax.naming.event.NamespaceChangeListener;
import javax.naming.event.NamingEvent;
import javax.naming.event.NamingListener;
import javax.naming.event.ObjectChangeListener;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.wildfly.security.manager.action.GetAccessControlContextAction;
import org.jboss.threads.JBossThreadFactory;

import static java.security.AccessController.doPrivileged;

/**
 * Coordinator responsible for passing @(code NamingEvent} instances to registered @{code NamingListener} instances.  Two
 * maps are used to managed a mapping between a listener and its configuration as well as a mapping from target name to a list
 * of listener configurations.  These maps are updated atomically on listener add and remove.
 *
 * @author John E. Bailey
 */
public class NamingEventCoordinator {
    private volatile Map<TargetScope, List<ListenerHolder>> holdersByTarget = Collections.emptyMap();
    private volatile Map<NamingListener, ListenerHolder> holdersByListener = Collections.emptyMap();

    private final ThreadFactory threadFactory = new JBossThreadFactory(new ThreadGroup("NamingEventCoordinator-threads"), Boolean.FALSE, null, "%G - %t", null, null, doPrivileged(GetAccessControlContextAction.getInstance()));

    private final Executor executor = Executors.newSingleThreadExecutor(threadFactory);

    static final Integer[] DEFAULT_SCOPES = {EventContext.OBJECT_SCOPE, EventContext.ONELEVEL_SCOPE, EventContext.SUBTREE_SCOPE};

    /**
     * Add a listener to the coordinator with a given target name and event scope.  This information is used when an
     * event is fired to determine whether or not to fire this listener.
     *
     * @param target The target name to lister
     * @param scope The event scope
     * @param namingListener The listener
     */
    synchronized void addListener(final String target, final int scope, final NamingListener namingListener) {
        final TargetScope targetScope = new TargetScope(target, scope);
        // Do we have a holder for this listener
        ListenerHolder holder = holdersByListener.get(namingListener);
        if (holder == null) {
            holder = new ListenerHolder(namingListener, targetScope);
            final Map<NamingListener, ListenerHolder> byListenerCopy = new FastCopyHashMap<NamingListener, ListenerHolder>(holdersByListener);
            byListenerCopy.put(namingListener, holder);
            holdersByListener = byListenerCopy;
        } else {
            holder.addTarget(targetScope);
        }

        List<ListenerHolder> holdersForTarget = holdersByTarget.get(targetScope);
        if (holdersForTarget == null) {
            holdersForTarget = new CopyOnWriteArrayList<ListenerHolder>();
            final Map<TargetScope, List<ListenerHolder>> byTargetCopy = new FastCopyHashMap<TargetScope, List<ListenerHolder>>(holdersByTarget);
            byTargetCopy.put(targetScope, holdersForTarget);
            holdersByTarget = byTargetCopy;
        }
        holdersForTarget.add(holder);
    }

    /**
     * Remove a listener.  Will remove it from all target mappings.  Once this method returns, the listener will no longer
     * receive any events.
     *
     * @param namingListener The listener
     */
    synchronized void removeListener(final NamingListener namingListener) {
        // Do we have a holder for this listener
        final ListenerHolder holder = holdersByListener.get(namingListener);
        if (holder == null) {
            return;
        }

        final Map<NamingListener, ListenerHolder> byListenerCopy = new FastCopyHashMap<NamingListener, ListenerHolder>(holdersByListener);
        byListenerCopy.remove(namingListener);
        holdersByListener = byListenerCopy;

        final Map<TargetScope, List<ListenerHolder>> byTargetCopy = new FastCopyHashMap<TargetScope, List<ListenerHolder>>(holdersByTarget);
        for (TargetScope targetScope : holder.targets) {
            final List<ListenerHolder> holders = holdersByTarget.get(targetScope);
            holders.remove(holder);
            if (holders.isEmpty()) {
                byTargetCopy.remove(targetScope);
            }
        }
        holdersByTarget = byTargetCopy;
    }

    /**
     * Fire a naming event.  An event will be created with the provided information and sent to each listener that matches
     * the target and scope information.
     *
     * @param context The event context generating the event.
     * @param name The target name the event represents
     * @param existingBinding The existing binding at the provided name
     * @param newBinding The new binding at the provided name
     * @param type The event type
     * @param changeInfo The change info for the event
     * @param scopes The scopes this event should be fired against
     */
    void fireEvent(final EventContext context, final Name name, final Binding existingBinding, final Binding newBinding, int type, final String changeInfo, final Integer... scopes) {
        final String target = name.toString();
        final Set<Integer> scopeSet = new HashSet<Integer>(Arrays.asList(scopes));
        final NamingEvent event = new NamingEvent(context, type, newBinding, existingBinding, changeInfo);

        final Set<ListenerHolder> holdersToFire = new HashSet<ListenerHolder>();

        // Check for OBJECT_SCOPE based listeners
        if (scopeSet.contains(EventContext.OBJECT_SCOPE)) {
            final TargetScope targetScope = new TargetScope(target, EventContext.OBJECT_SCOPE);
            final List<ListenerHolder> holders = holdersByTarget.get(targetScope);
            if (holders != null) {
                for (ListenerHolder holder : holders) {
                    holdersToFire.add(holder);
                }
            }
        }

        // Check for ONELEVEL_SCOPE based listeners
        if (scopeSet.contains(EventContext.ONELEVEL_SCOPE) && !name.isEmpty()) {
            final TargetScope targetScope = new TargetScope(name.getPrefix(name.size() - 1).toString(), EventContext.ONELEVEL_SCOPE);
            final List<ListenerHolder> holders = holdersByTarget.get(targetScope);
            if (holders != null) {
                for (ListenerHolder holder : holders) {
                    holdersToFire.add(holder);
                }
            }
        }

        // Check for SUBTREE_SCOPE based listeners
        if (scopeSet.contains(EventContext.SUBTREE_SCOPE) && !name.isEmpty()) {
            for (int i = 1; i < name.size(); i++) {
                final Name parentName = name.getPrefix(i);
                final TargetScope targetScope = new TargetScope(parentName.toString(), EventContext.SUBTREE_SCOPE);
                final List<ListenerHolder> holders = holdersByTarget.get(targetScope);
                if (holders != null) {
                    for (ListenerHolder holder : holders) {
                        holdersToFire.add(holder);
                    }
                }
            }
        }

        executor.execute(new FireEventTask(holdersToFire, event));
    }

    private class FireEventTask implements Runnable {
        private final Set<ListenerHolder> listenerHolders;
        private final NamingEvent event;

        private FireEventTask(Set<ListenerHolder> listenerHolders, NamingEvent event) {
            this.listenerHolders = listenerHolders;
            this.event = event;
        }

        @Override
        public void run() {
            for (ListenerHolder holder : listenerHolders) {
                final NamingListener listener = holder.listener;
                switch (event.getType()) {
                    case NamingEvent.OBJECT_ADDED:
                        if (listener instanceof NamespaceChangeListener)
                            ((NamespaceChangeListener) listener).objectAdded(event);
                        break;
                    case NamingEvent.OBJECT_REMOVED:
                        if (listener instanceof NamespaceChangeListener)
                            ((NamespaceChangeListener) listener).objectRemoved(event);
                        break;
                    case NamingEvent.OBJECT_RENAMED:
                        if (listener instanceof NamespaceChangeListener)
                            ((NamespaceChangeListener) listener).objectRenamed(event);
                        break;
                    case NamingEvent.OBJECT_CHANGED:
                        if (listener instanceof ObjectChangeListener)
                            ((ObjectChangeListener) listener).objectChanged(event);
                        break;
                }
            }
        }
    }

    private class ListenerHolder {
        private volatile Set<TargetScope> targets = new HashSet<TargetScope>();
        private final NamingListener listener;

        private ListenerHolder(final NamingListener listener, final TargetScope initialTarget) {
            this.listener = listener;
            addTarget(initialTarget);
        }

        private synchronized void addTarget(final TargetScope targetScope) {
            targets.add(targetScope);
        }
    }

    private class TargetScope {
        private final String target;
        private final int scope;

        private TargetScope(String target, int scope) {
            this.target = target;
            this.scope = scope;
        }

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

            TargetScope that = (TargetScope) o;

            if (scope != that.scope) return false;
            if (target != null ? !target.equals(that.target) : that.target != null) return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = target != null ? target.hashCode() : 0;
            result = 31 * result + scope;
            return result;
        }
    }
}
