/*
 * 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 com.sun.jini.outrigger;

import java.io.IOException;
import java.rmi.RemoteException;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.jini.core.event.UnknownEventException;
import net.jini.config.Configuration;
import net.jini.config.ConfigurationException;
import net.jini.security.ProxyPreparer;
import net.jini.space.JavaSpace;

import com.sun.jini.constants.ThrowableConstants;
import com.sun.jini.config.Config;
import com.sun.jini.logging.Levels;
import com.sun.jini.thread.TaskManager;
import com.sun.jini.thread.RetryTask;
import com.sun.jini.thread.WakeupManager;

/**
 * The notifier thread.  This thread is responsible for notifying
 * objects for which interest has been registered.  It operates in
 * transient space as much as possible.  Pending notifications will be
 * lost when the server goes down, but registrations of interest
 * survive across server crashes for persistent servers.
 *
 * 
 *
 * @see JavaSpace#notify
 * @see OutriggerServerImpl#notify
 */
// @see NotifyChit
class Notifier implements com.sun.jini.constants.TimeConstants {
    /** 
     * The object to use for the <code>source</code> when creating
     * events.
     */
    private final JavaSpace source;

    /** Proxy preparer to use on recovered listeners */
    private final ProxyPreparer recoveredListenerPreparer;
    
    /** wakeup manager for <code>NotifyTask</code> */
    private final WakeupManager wakeupMgr = 
	new WakeupManager(new WakeupManager.ThreadDesc(null, true));
    
    /** pending notifications tasks */
    private final TaskManager pending;

    private final static int	MAX_ATTEMPTS = 10;	// max times to retry

    /** Logger for logging event related information */
    private static final Logger logger = 
	Logger.getLogger(OutriggerServerImpl.eventLoggerName);

    /**
     * Create a notifier connected to the given <code>space</code>.
     * @param source the value to use for the <code>source</code> in
     *               remote event objects.
     * @param recoveredListenerPreparer <code>ProxyPreparer</code> to
     *               apply to recovered listeners.
     * @param config a source of configuration data.a
     * @throws ConfigurationException if there is a problem
     *         with the passed configuration.
     * @throws NullPointerException if <code>source</code> or 
     *         <code>config</code> arguments are <code>null</code>.
     */
    Notifier(JavaSpace source, ProxyPreparer recoveredListenerPreparer, 
	     Configuration config) 
	throws ConfigurationException 
    {
	if (source == null)
	    throw new NullPointerException("source must be non-null");
	this.source = source;

	this.recoveredListenerPreparer = recoveredListenerPreparer;

	pending = (TaskManager)Config.getNonNullEntry(config,
	    OutriggerServerImpl.COMPONENT_NAME, "notificationsTaskManager", 
	    TaskManager.class, new TaskManager());
    }

    /**
     * Terminate the notifier, shutting down any threads
     * it has running. This method can assume that
     * the constructor completed.
     */
    void terminate() {
	pending.terminate();
	wakeupMgr.stop();	
	wakeupMgr.cancelAll();	
    }

    /**
     * Queue up an event for delivery.
     * @param sender An object that on request will
     *               attempt to deliver its event
     *               to the associated listener.
     * @throws NullPointerException if <code>sender</code> is
     * <code>null</code>
     */
    void enqueueDelivery(EventSender sender) {
	pending.add(new NotifyTask(sender));
    }

    /*
     * Static stuff for Pending (can't put it in the class, unfortunately).
     */
				// 1 day =hrs  mins secs milliseconds
    private static final long	MAX_TIME = 1 * DAYS;
    private static final long	delays[] = {
				    1 * SECONDS, 5 * SECONDS,
				    10 * SECONDS, 60 * SECONDS, 60 * SECONDS
				};

    static {
	/*
	 * Make the delays the amount of time since the start -- it
	 * is easier to declare the intervals, but the elapsed time is
	 * more <i>useful</i>.
	 */
	for (int i = 1; i < delays.length; i++)
	    delays[i] += delays[i - 1];
    }

    /**
     * A task that represent a notification of matching a particular
     * template under a given transaction.
     */
    private class NotifyTask extends RetryTask {
	/** Who and what to send a event to. */
	private final EventSender sender;	

	/**
	 * Create an object to represent this list of chits needing
	 * notification.
	 * @param sender An object that on request will
	 *               attempt to deliver its event
	 *               to the associated listener.
	 * @throws NullPointerException if <code>sender</code> is
	 * <code>null</code>
	 */
	NotifyTask(EventSender sender) {
	    super(Notifier.this.pending, Notifier.this.wakeupMgr);
	    if (sender == null)
		throw new NullPointerException("sender must be non-null");
	    this.sender = sender;
	}

	/**
	 * Try to notify the target.  Return <code>true</code> if the
	 * notification was successful.
	 * <p>
	 * We know that we are the only one dealing with the given chit
	 * because <code>runAfter</code> makes sure of it.
	 */
	public boolean tryOnce() {
	    long curTime = System.currentTimeMillis();
	    if (curTime - startTime() > MAX_TIME) {
		if (logger.isLoggable(Levels.FAILED)) {
		    logger.log(Levels.FAILED, 
			"giving up on delivering event, keeping registration");
		}

		return true;	// just stop here, we are declaring "success"
	    }

	    boolean successful = true;	// notification successful?
	    try {
		sender.sendEvent(source, curTime, recoveredListenerPreparer);
	    } catch (UnknownEventException e) {
		// they didn't want to know about this, so stop them getting
		// future notifications, too.
		logFailure("UnknownEventException", Level.FINER, true, e);
		sender.cancelRegistration();
		// this is still "successful" -- we know to stop sending this
	    } catch (RemoteException e) {
		final int cat = ThrowableConstants.retryable(e);

		if (cat == ThrowableConstants.BAD_INVOCATION ||
		    cat == ThrowableConstants.BAD_OBJECT)
		{
		    // Listener probably bad, retry likely to fail.
		    logFailure("definite exception", Level.INFO, true, e);
		    sender.cancelRegistration();
		} else if (cat == ThrowableConstants.INDEFINITE) {
		    // try, try, again
		    logFailure("indefinite exception", Levels.FAILED,
			       false, e);
		    successful = false;	
		} else if (cat == ThrowableConstants.UNCATEGORIZED) {
		    // Same as above but log differently.
		    logFailure("uncategorized exception", Level.INFO, false, 
			       e);
		    successful = false;	
		} else {
		    logger.log(Level.WARNING, "ThrowableConstants.retryable " +
			"returned out of range value, " + cat,
			new AssertionError(e));
		    successful = false;			    
		}
	    } catch (IOException e) {
		// corrupted listener? unlikely to get better, cancel
		logFailure("IOException", Level.INFO, true, e);
		sender.cancelRegistration();
	    } catch (ClassNotFoundException e) {
		// probably a codebase problem, retry
		logFailure("ClassNotFoundException", Levels.FAILED, false, e);
		successful = false;			
	    } catch (RuntimeException e) {
		/* bad listener, or preparer, either way unlikely to
		 * get better
		 */
		logFailure("RuntimeException", Level.INFO, true, e);
		sender.cancelRegistration();
	    }

	    if (!successful && attempt() > MAX_ATTEMPTS) {
		if (logger.isLoggable(Levels.FAILED)) {
		    logger.log(Levels.FAILED, 
			"giving up on delivering event, keeping registration");
		}
		return true;		// as successful as we're going to be
	    }

	    return successful;
	}

	public boolean runAfter(java.util.List list, int max) {
	    for (int i = 0; i < max; i++) {
		Object task = list.get(i);
		if (task instanceof NotifyTask) {
		    NotifyTask nt = (NotifyTask)task;
		    if (sender.runAfter(nt.sender))
			return true;
		}
	    }
	    return false;
	}

	/** Log a failed delivery attempt */
	private void logFailure(String exceptionDescription, Level level,
				boolean terminal, Throwable t) 
	{
	    if (logger.isLoggable(level)) {
		logger.log(level, "Encountered " + exceptionDescription +
		     "while preparing to send/sending event, " +
		     (terminal?"dropping":"keeping") +  " registration", t);
	    }
	}
    }
}
