package org.apache.commons.jcs.utils.discovery;

/*
 * 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.
 */

import java.io.IOException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.apache.commons.jcs.engine.behavior.IRequireScheduler;
import org.apache.commons.jcs.engine.behavior.IShutdownObserver;
import org.apache.commons.jcs.utils.discovery.behavior.IDiscoveryListener;
import org.apache.commons.jcs.utils.net.HostNameUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * This service creates a listener that can create lateral caches and add them to the no wait list.
 * <p>
 * It also creates a sender that periodically broadcasts its availability.
 * <p>
 * The sender also broadcasts a request for other caches to broadcast their addresses.
 * <p>
 * @author Aaron Smuts
 */
public class UDPDiscoveryService
    implements IShutdownObserver, IRequireScheduler
{
    /** The logger */
    private static final Log log = LogFactory.getLog( UDPDiscoveryService.class );

    /** thread that listens for messages */
    private Thread udpReceiverThread;

    /** the runnable that the receiver thread runs */
    private UDPDiscoveryReceiver receiver;

    /** the runnable that sends messages via the clock daemon */
    private UDPDiscoverySenderThread sender = null;

    /** attributes */
    private UDPDiscoveryAttributes udpDiscoveryAttributes = null;

    /** is this shut down? */
    private boolean shutdown = false;

    /** This is a set of services that have been discovered. */
    private Set<DiscoveredService> discoveredServices = new CopyOnWriteArraySet<DiscoveredService>();

    /** This a list of regions that are configured to use discovery. */
    private final Set<String> cacheNames = new CopyOnWriteArraySet<String>();

    /** Set of listeners. */
    private final Set<IDiscoveryListener> discoveryListeners = new CopyOnWriteArraySet<IDiscoveryListener>();

    /**
     * @param attributes
     */
    public UDPDiscoveryService( UDPDiscoveryAttributes attributes)
    {
        udpDiscoveryAttributes = (UDPDiscoveryAttributes) attributes.clone();

        try
        {
            // todo, you should be able to set this
            udpDiscoveryAttributes.setServiceAddress( HostNameUtil.getLocalHostAddress() );
        }
        catch ( UnknownHostException e )
        {
            log.error( "Couldn't get localhost address", e );
        }

        try
        {
            // todo need some kind of recovery here.
            receiver = new UDPDiscoveryReceiver( this, getUdpDiscoveryAttributes().getUdpDiscoveryAddr(),
                                                 getUdpDiscoveryAttributes().getUdpDiscoveryPort() );
        }
        catch ( IOException e )
        {
            log.error( "Problem creating UDPDiscoveryReceiver, address ["
                + getUdpDiscoveryAttributes().getUdpDiscoveryAddr() + "] port ["
                + getUdpDiscoveryAttributes().getUdpDiscoveryPort() + "] we won't be able to find any other caches", e );
        }

        // create a sender thread
        sender = new UDPDiscoverySenderThread( getUdpDiscoveryAttributes(), getCacheNames() );
    }

    /**
     * @see org.apache.commons.jcs.engine.behavior.IRequireScheduler#setScheduledExecutorService(java.util.concurrent.ScheduledExecutorService)
     */
    @Override
    public void setScheduledExecutorService(ScheduledExecutorService scheduledExecutor)
    {
        if (sender != null)
        {
            scheduledExecutor.scheduleAtFixedRate(sender, 0, 15, TimeUnit.SECONDS);
        }

        /** removes things that have been idle for too long */
        UDPCleanupRunner cleanup = new UDPCleanupRunner( this );
        // I'm going to use this as both, but it could happen
        // that something could hang around twice the time using this as the
        // delay and the idle time.
        scheduledExecutor.scheduleAtFixedRate(cleanup, 0, getUdpDiscoveryAttributes().getMaxIdleTimeSec(), TimeUnit.SECONDS);
    }

    /**
     * Send a passive broadcast in response to a request broadcast. Never send a request for a
     * request. We can respond to our own requests, since a request broadcast is not intended as a
     * connection request. We might want to only send messages, so we would send a request, but
     * never a passive broadcast.
     */
    protected void serviceRequestBroadcast()
    {
        UDPDiscoverySender sender1 = null;
        try
        {
            // create this connection each time.
            // more robust
            sender1 = new UDPDiscoverySender( getUdpDiscoveryAttributes().getUdpDiscoveryAddr(),
                                             getUdpDiscoveryAttributes().getUdpDiscoveryPort() );

            sender1.passiveBroadcast( getUdpDiscoveryAttributes().getServiceAddress(), getUdpDiscoveryAttributes()
                .getServicePort(), this.getCacheNames() );

            // todo we should consider sending a request broadcast every so
            // often.

            if ( log.isDebugEnabled() )
            {
                log.debug( "Called sender to issue a passive broadcast" );
            }
        }
        catch ( Exception e )
        {
            log.error( "Problem calling the UDP Discovery Sender. address ["
                + getUdpDiscoveryAttributes().getUdpDiscoveryAddr() + "] port ["
                + getUdpDiscoveryAttributes().getUdpDiscoveryPort() + "]", e );
        }
        finally
        {
            try
            {
                if ( sender1 != null )
                {
                    sender1.destroy();
                }
            }
            catch ( Exception e )
            {
                log.error( "Problem closing Passive Broadcast sender, while servicing a request broadcast.", e );
            }
        }
    }

    /**
     * Adds a region to the list that is participating in discovery.
     * <p>
     * @param cacheName
     */
    public void addParticipatingCacheName( String cacheName )
    {
        cacheNames.add( cacheName );
        sender.setCacheNames( getCacheNames() );
    }

    /**
     * Removes the discovered service from the list and calls the discovery listener.
     * <p>
     * @param service
     */
    public void removeDiscoveredService( DiscoveredService service )
    {
        boolean contained = getDiscoveredServices().remove( service );

        if ( contained )
        {
            if ( log.isInfoEnabled() )
            {
                log.info( "Removing " + service );
            }
        }

        for (IDiscoveryListener listener : getDiscoveryListeners())
        {
            listener.removeDiscoveredService( service );
        }
    }

    /**
     * Add a service to the list. Update the held copy if we already know about it.
     * <p>
     * @param discoveredService discovered service
     */
    protected void addOrUpdateService( DiscoveredService discoveredService )
    {
        synchronized ( getDiscoveredServices() )
        {
            // Since this is a set we can add it over an over.
            // We want to replace the old one, since we may add info that is not part of the equals.
            // The equals method on the object being added is intentionally restricted.
            if ( !getDiscoveredServices().contains( discoveredService ) )
            {
                if ( log.isInfoEnabled() )
                {
                    log.info( "Set does not contain service. I discovered " + discoveredService );
                }
                if ( log.isDebugEnabled() )
                {
                    log.debug( "Adding service in the set " + discoveredService );
                }
                getDiscoveredServices().add( discoveredService );
            }
            else
            {
                if ( log.isDebugEnabled() )
                {
                    log.debug( "Set contains service." );
                }
                if ( log.isDebugEnabled() )
                {
                    log.debug( "Updating service in the set " + discoveredService );
                }

                // Update the list of cache names if it has changed.
                DiscoveredService theOldServiceInformation = null;
                // need to update the time this sucks. add has no effect convert to a map
                for (DiscoveredService service1 : getDiscoveredServices())
                {
                    if ( discoveredService.equals( service1 ) )
                    {
                        theOldServiceInformation = service1;
                        break;
                    }
                }
                if ( theOldServiceInformation != null )
                {
                    if ( !theOldServiceInformation.getCacheNames().equals( discoveredService.getCacheNames() ) )
                    {
                        if ( log.isInfoEnabled() )
                        {
                            log.info( "List of cache names changed for service: " + discoveredService );
                        }
                    }
                }

                // replace it, we want to reset the payload and the last heard from time.
                getDiscoveredServices().remove( discoveredService );
                getDiscoveredServices().add( discoveredService );
            }
        }
        // Always Notify the listeners
        // If we don't do this, then if a region using the default config is initialized after notification,
        // it will never get the service in it's no wait list.
        // Leave it to the listeners to decide what to do.
        for (IDiscoveryListener listener : getDiscoveryListeners())
        {
            listener.addDiscoveredService( discoveredService );
        }

    }

    /**
     * Get all the cache names we have facades for.
     * <p>
     * @return ArrayList
     */
    protected ArrayList<String> getCacheNames()
    {
        ArrayList<String> names = new ArrayList<String>();
        names.addAll( cacheNames );
        return names;
    }

    /**
     * @param attr The UDPDiscoveryAttributes to set.
     */
    public void setUdpDiscoveryAttributes( UDPDiscoveryAttributes attr )
    {
        this.udpDiscoveryAttributes = attr;
    }

    /**
     * @return Returns the lca.
     */
    public UDPDiscoveryAttributes getUdpDiscoveryAttributes()
    {
        return this.udpDiscoveryAttributes;
    }

    /**
     * Start necessary receiver thread
     */
    public void startup()
    {
        udpReceiverThread = new Thread( receiver );
        udpReceiverThread.setDaemon( true );
        // udpReceiverThread.setName( t.getName() + "--UDPReceiver" );
        udpReceiverThread.start();
    }

    /**
     * Shuts down the receiver.
     */
    @Override
    public void shutdown()
    {
        if ( !shutdown )
        {
            shutdown = true;

            if ( log.isInfoEnabled() )
            {
                log.info( "Shutting down UDP discovery service receiver." );
            }

            try
            {
                // no good way to do this right now.
                receiver.shutdown();
                udpReceiverThread.interrupt();
            }
            catch ( Exception e )
            {
                log.error( "Problem interrupting UDP receiver thread." );
            }

            if ( log.isInfoEnabled() )
            {
                log.info( "Shutting down UDP discovery service sender." );
            }

            // also call the shutdown on the sender thread itself, which
            // will result in a remove command.
            try
            {
                sender.shutdown();
            }
            catch ( Exception e )
            {
                log.error( "Problem issuing remove broadcast via UDP sender." );
            }
        }
        else
        {
            if ( log.isDebugEnabled() )
            {
                log.debug( "Shutdown already called." );
            }
        }
    }

    /**
     * @return Returns the discoveredServices.
     */
    public synchronized Set<DiscoveredService> getDiscoveredServices()
    {
        return discoveredServices;
    }

    /**
     * @return the discoveryListeners
     */
    private Set<IDiscoveryListener> getDiscoveryListeners()
    {
        return discoveryListeners;
    }

    /**
     * @return the discoveryListeners
     */
    public Set<IDiscoveryListener> getCopyOfDiscoveryListeners()
    {
        Set<IDiscoveryListener> copy = new HashSet<IDiscoveryListener>();
        copy.addAll( getDiscoveryListeners() );
        return copy;
    }

    /**
     * Adds a listener.
     * <p>
     * @param listener
     * @return true if it wasn't already in the set
     */
    public boolean addDiscoveryListener( IDiscoveryListener listener )
    {
        return getDiscoveryListeners().add( listener );
    }

    /**
     * Removes a listener.
     * <p>
     * @param listener
     * @return true if it was in the set
     */
    public boolean removeDiscoveryListener( IDiscoveryListener listener )
    {
        return getDiscoveryListeners().remove( listener );
    }
}
