/*
 * 
 * Copyright 2004 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * The contents of this file are subject to the Sun Community Source
 * License v3.0/Jini Technology Specific Attachment v 1.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://wwws.sun.com/software/jini/licensing/SCSL3_JiniTSA1.html.
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Reference Code is Jini Technology Core Platform code, v2.0. The
 * Developer of the Reference Code is Sun Microsystems, Inc.
 * 
 * Contributor(s): Sun Microsystems, Inc.
 * 
 * The contents of this file comply with the Jini Technology Core
 * Platform Compatibility Kit, v2.0A.
 * 
 * Tester(s): Sun Microsystems, Inc.
 * 
 * Test Platform(s):
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Solaris(TM)SPARC/x86
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Linux (Intel x86)
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Windows (Intel Platform)
 * 
 */
package com.sun.jini.norm.event;

import java.io.IOException;
import java.io.Serializable;
import java.rmi.MarshalledObject;
import java.rmi.RemoteException;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import net.jini.core.event.RemoteEvent;
import net.jini.core.event.RemoteEventListener;
import net.jini.core.event.UnknownEventException;
import net.jini.security.ProxyPreparer;

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

/**
 * Representation of an event type the supports a single registrant.
 * The registrant, event ID, and event sequence number information
 * portions of this object are preserved when serialized.  The send
 * monitor, and currently scheduled event sends are not.
 *
 * @author Sun Microsystems, Inc.
 */
// $$$ I am not sure if this class does too much locking, or too little.
// It does too little in that it does not do any locking during serialization
// It does too much in that Norm's current locking discipline means that for
// the most part this class does not have to worry about multiple threads
// of control entering at one time.
// Before we make this a general utility we have to re-think the locking
// strategy.
public class EventType implements Serializable {
    private static final long serialVersionUID = 2;

    /** Logger for logging messages for this class */
    private static final Logger logger = Logger.getLogger("com.sun.jini.norm");

    /**
     * Listener registered for events of this type.  Stored in 
     * marshalled form so object the <code>EventType</code> object can
     * be recovered even if the listener's codebase is not available.
     * If this field is <code>null</code>, <code>listener</code> and 
     * <code>handback</code> will be also.  
     * @serial
     */
    private MarshalledObject marshalledListener;

    /** Transient cache of listener in unmarshalled form */
    private transient RemoteEventListener listener;

    /**
     * The proxy preparer to use to prepare a newly unmarshalled listener, or
     * null if this instance was created using an already prepared listener,
     * which is how instances are created initially.
     */
    private transient ProxyPreparer recoveredListenerPreparer;

    /**
     * Handback object associated with current listener.
     * @serial
     */
    private MarshalledObject handback;

    /** 
     * Sequence number of the current listener/handback pair, incremented
     * every time a new listener is set (even if the objects 
     * are equivalent)
     * @serial
     */
    private long registrationNumber;

    /**
     * Last event sequence number we used
     * @serial
     */
    private long lastSeqNum;

    /**
     * Our event ID
     * @serial
     */
    private long evID;

    /** 
     * Object we check with to ensure leases have not expired and notify
     * when we get a definite exception during an event send attempt.
     * If this field is <code>null</code> generator will be also.
     */
    private transient SendMonitor monitor;

    /** 
     * Event type generator that created us. If this field is
     * <code>null</code> monitor will be also.
     */
    private transient EventTypeGenerator generator;

    /**
     * Simple constructor.  Initially the last sequence number is set to 0.
     * @param generator EventTypeGenerator that is creating this event type
     * @param monitor Object which is to monitor the sending of events
     *	      of this type
     * @param evID event ID of this event type
     * @param listener the listener events of this type should go to
     * @param handback the object that should be passed to listener
     *        as part of the event
     * @throws IOException if the listener cannot be serialized 
     */
    EventType(EventTypeGenerator generator, SendMonitor monitor, long evID,
	      RemoteEventListener listener, MarshalledObject handback)
        throws IOException
    {
	if (generator == null) {
	    throw new NullPointerException("EventType(): Must create event " +
                "type objects with a non-null generator");
	}

	if (monitor == null) {
	    throw new NullPointerException("EventType(): Must create event " +
                "type objects with a non-null monitor");
	}

	this.generator = generator;
	this.monitor = monitor;
	this.evID = evID;
	setLastSequenceNumber(0);
	setListener(listener, handback);
    }    

    /** Utility method to null out listener */
    private void clearListener() {
	listener = null;
	handback = null;
	marshalledListener = null;
    }

    /**
     * (Re)set the listener for this type of event.  Any pending
     * events that have not yet been sent will be sent to the new
     * listener, passing the new handback.  Setting the listener to
     * <code>null</code> will cancel the sending of all pending
     * events.  
     *
     * @param listener the listener events of this type should go to
     * @param handback the object that should be passed to listener
     *        as part of the event
     * @throws IOException if listener cannot be serialized
     */
    public synchronized void setListener(RemoteEventListener listener, 
					 MarshalledObject    handback)
        throws IOException
    {
	registrationNumber++;
	
	if (listener == null) {
	    clearListener();
	} else {	    
	    marshalledListener = new MarshalledObject(listener);
	    this.listener = listener;
	    this.handback = handback;
	}
    }

    /**
     * Returns <code>true</code> if there is a listener registered for this 
     * event type.
     */
    public boolean haveListener() {
	return marshalledListener != null;
    }

    /**
     * Convince method to get the listener.
     * @return the listener, or <code>null</code> if the listener can't be
     *	       unpacked or prepared, or there is no listener
     */
    private RemoteEventListener getListener() {
	if (!haveListener()) 
	    return null;

	if (listener != null) 
	    return listener;

	// There is a listener, but it is not unpacked yet, try to unpack
	RemoteEventListener unpreparedListener = null;
	try {
	    unpreparedListener =
		(RemoteEventListener) marshalledListener.get();
	} catch (IOException e) {
	    logger.log(Levels.HANDLED,
		       "Problem unmarshalling listener -- will retry later",
		       e);
	    // $$$ is this really the right thing to do?
	    // we probably really have a corrupted marshalledListener here
	} catch (ClassNotFoundException e) {
	    logger.log(Levels.HANDLED,
		       "Problem unmarshalling listener -- will retry later",
		       e);
	}

	if (unpreparedListener != null) {
	    // Prepare the listener
	    try {
		listener = (RemoteEventListener)
		    recoveredListenerPreparer.prepareProxy(unpreparedListener);
	    } catch (RemoteException e) {
		logger.log(Levels.HANDLED,
			   "Problem preparing listener -- will retry later",
			   e);
	    } catch (SecurityException e) {
		logger.log(Levels.HANDLED,
			   "Problem preparing listener -- will retry later",
			   e);
	    }
	}

	return listener;
    }

    /**
     * Atomically clear the current registration if its sequence
     * number matches the passed in sequence number.  If the
     * replacement occurs it will have the same effect as calling
     * <code>setListener(null, null)</code>.
     * <p>
     * Can be used by code that needs to remove event registrations in
     * response to exceptions thrown during event delivery without
     * risking clobbering new registrations.
     * @param oldSequenceNumber sequence number of the 
     *        registration that had a problem 
     * @return <code>true</code> if the state of the object was
     * changed and <code>false</code> otherwise
     * @see EventType#setListener 
     */
    public synchronized boolean clearListenerIfSequenceMatch(
        long oldSequenceNumber) 
    {
	if (oldSequenceNumber == registrationNumber) {
	    clearListener();
	    return true;
	}

	return false;
    }

    /**
     * Set the object's notion of the last sequence number.  The next event
     * scheduled to be sent will have a sequence number one greater than
     * the value past to this call.
     * <p>
     * Note: this method is not synchronized.
     * @param seqNum value for the last sequence number
     */
    public void setLastSequenceNumber(long seqNum) {
	lastSeqNum = seqNum;
    }

    /**
     * Return the sequence number of the last event that was scheduled
     * to be sent.  Intended primarily for creating
     * <code>EventRegistration</code> objects and the like.
     */
    public synchronized long getLastSequenceNumber () {
	return lastSeqNum;
    }
    
    /**
     * Return the <code>long</code> that was uniquely associated with this
     * object when it was created.
     */
    public long getEventID() {
	return evID;
    }

    /**
     * Schedule the sending of an event.  This event will be sent to
     * the currently registered listener.  If the listener changes
     * before the the event is successfully sent the event will be sent
     * to the new listener.  If the current listener is
     * <code>null</code> this call will have no affect aside from
     * incrementing the sequence number.
     * @param factory an object that will be used to create the
     * <code>Event</code> object when necessary
     * @return the sequence number assigned to the event
     * @throws IllegalStateException if this method is called
     *         after the object has be deserialized and before
     *         <code>restoreTransientState</code> has been called
     * @see EventType#restoreTransientState
     */
    public synchronized long sendEvent(EventFactory factory) {
	if (generator == null) {
	    // Have not had our state restored, complain
	    throw new IllegalStateException("EventType.sendEvent:" +
	        "called before state was fully restored");
	}

	// Even if there is no listener, an event has occurred, so
	// increment the sequence number (note this a stronger
	// guarantee that the Jini(TM) Distributed Event Specification,
	// but one that is required by the LRS spec).
	lastSeqNum++;

	// If we don't have a listener we don't need do anything else
	if (!haveListener())
	    return lastSeqNum;
	
	final TaskManager mgr = generator.getTaskManager();
	final WakeupManager wMgr = generator.getWakeupManager();
	mgr.add(new SendTask(mgr, wMgr, factory, lastSeqNum));
	
	return lastSeqNum;
    }

    /**
     * Increment the sequence number and return the result. This
     * method is useful if an event occurs that needs to be noted
     * but from some reason it is impossible to deliver the event.
     *
     * @return the new value for the sequence number
     */
    public synchronized long bumpSequenceNumber() {
	return ++lastSeqNum;
    }

    /**
     * Restore the transient state of this object after recovering it
     * from a serialization stream.  None of the arguments can be
     * <code>null</code>.
     * <p>
     * Note: this method is not synchronized.  
     * @param generator the <code>EventTypeGenerator</code> that was used
     *        to create this EventType object originally
     * @param monitor the object that monitors the progress of events
     *        set by this object
     * @param recoveredListenerPreparer the proxy preparer to use to prepare
     *	      listeners recovered from persistent storage
     */
    public void restoreTransientState(EventTypeGenerator generator,  
                                      SendMonitor monitor,
				      ProxyPreparer recoveredListenerPreparer)
    {
	if (generator == null) {
	    throw new NullPointerException("EventType.restoreTransientState:" +
	        "Must call with a non-null generator");
	}
	if (monitor == null) {
	    throw new NullPointerException("EventType.restoreTransientState:" +
	        "Must call with a non-null monitor");
	}
	if (recoveredListenerPreparer == null) {
	    throw new NullPointerException("EventType.restoreTransientState:" +
	        "Must call with a non-null recoveredListenerPreparer");
	}
	this.generator = generator;
	this.monitor = monitor;
	this.recoveredListenerPreparer = recoveredListenerPreparer;
	generator.recoverEventID(evID);
    }

    /**
     * Subclass of <code>RetryTask</code> used by <code>EventType</code>
     * to send events.
     */
    private class SendTask extends RetryTask {
	/** Max time we are willing to let a send attempt to go on for */
	final static private long MAX_TIME = 1000 * 60 * 60 * 24; //~ 1 Day
	
	/** Factory used to create the <code>RemoteEvent</code> to be sent */
	final private EventFactory eventFactory;

	/** Sequence number the event should have */
	final private long seqNum;

	/** Cached event */
	private RemoteEvent event;
	
	/** 
	 * Registration sequence number of the listener/handback pair
	 * event was built for
	 */
	private long eventForRegistrationNumber = -1;

	/**
	 * Simple constructor.
	 * @param taskManager <code>TaskManager</code> this task is to be
	 *                    put into
	 * @param eventFactory <code>EventFactory</code> that will be used
	 *                     to create the event to be sent
	 * @param seqNum      the sequence number of the event
	 */
	private SendTask(TaskManager taskManager, WakeupManager wakeupManager,
			 EventFactory eventFactory, long seqNum)
	{
	    super(taskManager, wakeupManager);
	    this.eventFactory = eventFactory;
	    this.seqNum = seqNum;
	}

	// Inherit java doc from super type
	public boolean tryOnce() {
	    final long now = System.currentTimeMillis();
	    if (now - startTime() > MAX_TIME)
		return true;	// we have been trying too long, stop here

	    if (!EventType.this.monitor.isCurrent())
		return true;	// lease gone, stop here

	    // Local copies of listener and handback so they won't
	    // be clobbered by setListener calls
	    RemoteEventListener listener;
	    MarshalledObject handback;
	    long registrationNumber;
	    
	    synchronized (EventType.this) {
		if (!EventType.this.haveListener())
		    return true; // No currently registered listener, stop here

		listener = EventType.this.getListener();
		if (listener == null) {
		    return false; // There is a listener, but we can't unpack
				  // it -- schedule a retry later
		}
		handback = EventType.this.handback;
		registrationNumber = EventType.this.registrationNumber;
	    }

	    // If the handback has changed we need to create a new
	    // event object (we approximate this test by checking the
	    // registrationNumber

	    if (event == null || 
		eventForRegistrationNumber != registrationNumber) 
	    {
		event = eventFactory.createEvent(EventType.this.evID, seqNum,
						 handback);
		eventForRegistrationNumber = registrationNumber;
	    }

	    // Try sending 
	    try {
		listener.notify(event);
		return true;
	    } catch (Throwable t) {
		// Classify the exception using ThrowableConstants, if
		// it is a bad object or uncategorized (which must be
		// a UnknownEventException) drop the event
		// registration, if it is a bad invocation mark the
		// try is done (since re-sending this event won't work), 
		// but don't drop the registration.  If indefinite return 
		// false so a retry will be scheduled.
		final int cat = ThrowableConstants.retryable(t);
		if (cat == ThrowableConstants.INDEFINITE) {
		    logger.log(Levels.HANDLED,
			       "Problem sending event -- will retry later",
			       t);
		    return false;
		} else if (cat == ThrowableConstants.BAD_INVOCATION) {
		    logger.log(Level.INFO, "Problem sending event", t);
		    return true;
		} else {
		    EventType.this.monitor.definiteException(EventType.this,
		        event, registrationNumber, t);
		    logger.log(Level.INFO, "Problem sending event", t);
		    return true;
		}
	    }
	    
	}

	// Inherit java doc from super type
	public boolean runAfter(List tasks, int size) {
	    // We don't need to run these tasks in any particular order
	    return false;
	}
    }
}
