/*
 *  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.directory.studio.connection.core.event;


import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.directory.studio.connection.core.Connection;
import org.apache.directory.studio.connection.core.ConnectionCoreConstants;
import org.apache.directory.studio.connection.core.ConnectionCorePlugin;
import org.apache.directory.studio.connection.core.ConnectionFolder;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;


/**
 * The ConnectionEventRegistry is a central point to register for connection specific
 * events and to fire events to registered listeners.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class ConnectionEventRegistry
{

    /** The list of threads with suspended event firing. */
    private static List<Long> suspendedEventFiringThreads = new ArrayList<Long>();

    /** The lock used to synchronize event firings */
    protected static Object lock = new Object();

    /** The list with time stamps of recent event firings */
    private static List<Long> fireTimeStamps = new ArrayList<Long>();

    /** A counter for fired events */
    private static long fireCount = 0L;


    /**
     * Checks if event firing is suspended in the current thread.
     *
     * @return true, if event firing is suspended in the current thread
     */
    protected static boolean isEventFiringSuspendedInCurrentThread()
    {
        boolean suspended = suspendedEventFiringThreads.contains( Thread.currentThread().getId() );

        // count the number of fired event in the last second
        // if more then five per second: print a warning
        if ( !suspended )
        {
            fireCount++;

            synchronized ( fireTimeStamps )
            {
                long now = System.currentTimeMillis();

                // remove all time stamps older than one second
                for ( Iterator<Long> it = fireTimeStamps.iterator(); it.hasNext(); )
                {
                    Long ts = it.next();
                    if ( ts + 1000 < now )
                    {
                        it.remove();
                    }
                    else
                    {
                        break;
                    }
                }

                fireTimeStamps.add( now );

                if ( fireTimeStamps.size() > 10 )
                {
                    String message = "Warning: More then " + fireTimeStamps.size() + " events were fired per second!"; //$NON-NLS-1$ //$NON-NLS-2$
                    ConnectionCorePlugin.getDefault().getLog().log(
                        new Status( IStatus.WARNING, ConnectionCoreConstants.PLUGIN_ID, message,
                            new Exception( message ) ) );
                }
            }
        }

        return suspended;
    }


    /**
     * Gets the number of fired events.
     * 
     * @return the number of fired events
     */
    public static long getFireCount()
    {
        return fireCount;
    }


    /**
     * Resumes event firing in the current thread.
     */
    public static void resumeEventFiringInCurrentThread()
    {
        synchronized ( suspendedEventFiringThreads )
        {
            suspendedEventFiringThreads.remove( Thread.currentThread().getId() );
        }
    }


    /**
     * Suspends event firing in the current thread.
     */
    public static void suspendEventFiringInCurrentThread()
    {
        synchronized ( suspendedEventFiringThreads )
        {
            suspendedEventFiringThreads.add( Thread.currentThread().getId() );
        }
    }

    private static final EventManager<ConnectionUpdateListener, EventRunner> connectionUpdateEventManager = new EventManager<ConnectionUpdateListener, EventRunner>();


    /**
     * Adds the connection update listener.
     *
     * @param listener the listener
     * @param runner the runner
     */
    public static void addConnectionUpdateListener( ConnectionUpdateListener listener, EventRunner runner )
    {
        connectionUpdateEventManager.addListener( listener, runner );
    }


    /**
     * Removes the connection update listener.
     *
     * @param listener the listener
     */
    public static void removeConnectionUpdateListener( ConnectionUpdateListener listener )
    {
        connectionUpdateEventManager.removeListener( listener );
    }


    /**
     * Notifies each {@link ConnectionUpdateListener} about the opened connection.
     * Uses the {@link EventRunner}s.
     *
     * @param connection the opened connection
     * @param source the source
     */
    public static void fireConnectionOpened( final Connection connection, final Object source )
    {
        EventRunnableFactory<ConnectionUpdateListener> factory = new EventRunnableFactory<ConnectionUpdateListener>()
        {
            public EventRunnable createEventRunnable( final ConnectionUpdateListener listener )
            {
                return new EventRunnable()
                {
                    public void run()
                    {
                        listener.connectionOpened( connection );
                    }
                };
            }
        };
        connectionUpdateEventManager.fire( factory );
    }


    /**
     * Notifies each {@link ConnectionUpdateListener} about the closed connection.
     * Uses the {@link EventRunner}s.
     *
     * @param connection the closed connection
     * @param source the source
     */
    public static void fireConnectionClosed( final Connection connection, final Object source )
    {
        EventRunnableFactory<ConnectionUpdateListener> factory = new EventRunnableFactory<ConnectionUpdateListener>()
        {
            public EventRunnable createEventRunnable( final ConnectionUpdateListener listener )
            {
                return new EventRunnable()
                {
                    public void run()
                    {
                        listener.connectionClosed( connection );
                    }
                };
            }
        };
        connectionUpdateEventManager.fire( factory );
    }


    /**
     * Notifies each {@link ConnectionUpdateListener} about the updated connection.
     * Uses the {@link EventRunner}s.
     *
     * @param connection the updated connection
     * @param source the source
     */
    public static void fireConnectionUpdated( final Connection connection, final Object source )
    {
        EventRunnableFactory<ConnectionUpdateListener> factory = new EventRunnableFactory<ConnectionUpdateListener>()
        {
            public EventRunnable createEventRunnable( final ConnectionUpdateListener listener )
            {
                return new EventRunnable()
                {
                    public void run()
                    {
                        listener.connectionUpdated( connection );
                    }
                };
            }
        };
        connectionUpdateEventManager.fire( factory );
    }


    /**
     * Notifies each {@link ConnectionUpdateListener} about the added connection.
     * Uses the {@link EventRunner}s.
     *
     * @param connection the added connection
     * @param source the source
     */
    public static void fireConnectionAdded( final Connection connection, final Object source )
    {
        EventRunnableFactory<ConnectionUpdateListener> factory = new EventRunnableFactory<ConnectionUpdateListener>()
        {
            public EventRunnable createEventRunnable( final ConnectionUpdateListener listener )
            {
                return new EventRunnable()
                {
                    public void run()
                    {
                        listener.connectionAdded( connection );
                    }
                };
            }
        };
        connectionUpdateEventManager.fire( factory );
    }


    /**
     * Notifies each {@link ConnectionUpdateListener} about the removed connection.
     * Uses the {@link EventRunner}s.
     *
     * @param connection the removed connection
     * @param source the source
     */
    public static void fireConnectionRemoved( final Connection connection, final Object source )
    {
        EventRunnableFactory<ConnectionUpdateListener> factory = new EventRunnableFactory<ConnectionUpdateListener>()
        {
            public EventRunnable createEventRunnable( final ConnectionUpdateListener listener )
            {
                return new EventRunnable()
                {
                    public void run()
                    {
                        listener.connectionRemoved( connection );
                    }
                };
            }
        };
        connectionUpdateEventManager.fire( factory );
    }


    /**
     * Notifies each {@link ConnectionUpdateListener} about the modified connection folder.
     * Uses the {@link EventRunner}s.
     *
     * @param connectionFolder the modified connection folder
     * @param source the source
     */
    public static void fireConnectonFolderModified( final ConnectionFolder connectionFolder, final Object source )
    {
        EventRunnableFactory<ConnectionUpdateListener> factory = new EventRunnableFactory<ConnectionUpdateListener>()
        {
            public EventRunnable createEventRunnable( final ConnectionUpdateListener listener )
            {
                return new EventRunnable()
                {
                    public void run()
                    {
                        listener.connectionFolderModified( connectionFolder );
                    }
                };
            }
        };
        connectionUpdateEventManager.fire( factory );
    }


    /**
     * Notifies each {@link ConnectionUpdateListener} about the added connection folder.
     * Uses the {@link EventRunner}s.
     *
     * @param connectionFolder the added connection folder
     * @param source the source
     */
    public static void fireConnectonFolderAdded( final ConnectionFolder connectionFolder, final Object source )
    {
        EventRunnableFactory<ConnectionUpdateListener> factory = new EventRunnableFactory<ConnectionUpdateListener>()
        {
            public EventRunnable createEventRunnable( final ConnectionUpdateListener listener )
            {
                return new EventRunnable()
                {
                    public void run()
                    {
                        listener.connectionFolderAdded( connectionFolder );
                    }
                };
            }
        };
        connectionUpdateEventManager.fire( factory );
    }


    /**
     * Notifies each {@link ConnectionUpdateListener} about the removed connection folder.
     * Uses the {@link EventRunner}s.
     *
     * @param connectionFolder the removed connection folder
     * @param source the source
     */
    public static void fireConnectonFolderRemoved( final ConnectionFolder connectionFolder, final Object source )
    {
        EventRunnableFactory<ConnectionUpdateListener> factory = new EventRunnableFactory<ConnectionUpdateListener>()
        {
            public EventRunnable createEventRunnable( final ConnectionUpdateListener listener )
            {
                return new EventRunnable()
                {
                    public void run()
                    {
                        listener.connectionFolderRemoved( connectionFolder );
                    }
                };
            }
        };
        connectionUpdateEventManager.fire( factory );
    }

    public static class EventManager<L, R extends EventRunner>
    {
        private Map<L, EventRunner> listeners = new HashMap<L, EventRunner>();


        /**
         * Adds the listener.
         *
         * @param listener the listener
         * @param runner the runner
         */
        public void addListener( L listener, R runner )
        {
            assert listener != null;
            assert runner != null;

            synchronized ( listeners )
            {
                if ( !listeners.containsKey( listener ) )
                {
                    listeners.put( listener, runner );
                }
            }
        }


        /**
         * Removes the listener.
         *
         * @param listener the listener
         */
        public void removeListener( L listener )
        {
            synchronized ( listeners )
            {
                if ( listeners.containsKey( listener ) )
                {
                    listeners.remove( listener );
                }
            }
        }


        /**
         * Notifies each {@link ConnectionUpdateListener} about the removed connection.
         * Uses the {@link EventRunner}s.
         *
         * @param connection the removed connection
         * @param source the source
         */
        public void fire( EventRunnableFactory<L> factory )
        {
            if ( isEventFiringSuspendedInCurrentThread() )
            {
                return;
            }

            Map<L, EventRunner> clone = new HashMap<L, EventRunner>( listeners );
            for ( final L listener : clone.keySet() )
            {
                EventRunner runner = clone.get( listener );
                synchronized ( lock )
                {
                    EventRunnable runnable = factory.createEventRunnable( listener );
                    runner.execute( runnable );
                }
            }
        }
    }
}
