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

import java.lang.ref.WeakReference;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.sun.jini.collection.WeakTable;
import com.sun.jini.landlord.LeasedResource;
import com.sun.jini.thread.InterruptedStatusThread;
import com.sun.jini.thread.WakeupManager;

/**
 * Lease manager that aggressively expires leases as their expiration times
 * occur.  Also schedules and manages expiration warning events.
 * <p>
 * Note, unlike Mahalo's <code>LeaseExpirationManager</code> (which this
 * was seeded from), we make no attempt to make it generic because of
 * the need to schedule expiration warning events.
 *
 * 
 */
class LeaseExpirationMgr implements WeakTable.KeyGCHandler {
    /** Logger for logging messages for this class */
    static final Logger logger = Logger.getLogger("com.sun.jini.norm");

    /** 
     * Map of sets to task tickets.
     * <p>
     * A Note on Synchronization
     * <p>
     * Whenever we operate on the <code>ticketMap</code> we hold
     * the lock on the key being used.  This is necessary because
     * expiration and warning sender tasks need to remove tickets from
     * the map but at the same time a renewal may be updating the map
     * to associate the set with a new ticket.  If we don't synchronize
     * there is a small window where a task could remove the ticket
     * for its replacement.
     */
    private WeakTable		ticketMap = new WeakTable(this); 

    /** Ref to the main server object has all the top level methods */
    private NormServerBaseImpl	server;

    /** Queue of tasks, ordered by time */
    private WakeupManager	runQueue = new WakeupManager();

    /** Queue of tasks to expire sets */
    final List expireQueue = new LinkedList();

    /** Thread to expire sets */
    private final Thread expireThread = new ExpirationThread();

    /**
     * Create a <code>LeaseExpirationMgr</code> to aggressively expire
     * the leases of the passed <code>NormServerBaseImpl</code>
     */
    LeaseExpirationMgr(NormServerBaseImpl server) {
	this.server = server;
	expireThread.start();
    }

    /**
     * Terminate the <code>LeaseExpirationMgr</code>, killing any 
     * threads it has started
     */
    void terminate() {
	runQueue.stop();
	runQueue.cancelAll();
	expireThread.interrupt();
    }
    
    /**
     * Notifies the manager of a new lease being created.
     *
     * @param resource the resource associated with the new lease
     */
    void register(LeasedResource resource) {
	// Need to synchronize because schedule manipulates 
	// ticketMap.
	synchronized (resource) {
	    schedule(resource);
	}
    }

    /**
     * Notifies the manager of a lease being renewed. <p>
     *
     * This method assumes the lock on <code>set</code> is owned by the
     * current thread.
     *
     * @param resource the set for which tasks have to be rescheduled
     */
    void reschedule(LeasedResource resource) {
	/*
	 * Remove the old event.  This method is only called
	 * (indirectly) from NormServerBaseImpl.renew() so we know that
	 * we own the lock on resource.
	 */
	WakeupManager.Ticket ticket =
	    (WakeupManager.Ticket) ticketMap.remove(resource);
	if (ticket != null) {
	    runQueue.cancel(ticket);
	}
	// Schedule the new event
	schedule(resource);	
    }

    /**
     * Schedule a leased resource to be reaped in the future. Called
     * when a resource gets a lease, a lease is renewed, and during log
     * recovery.
     * <p>
     * This method assumes the lock on <code>resource</code> is owned by
     * the current thread.
     */
    void schedule(LeasedResource resource) {
	WakeupManager.Ticket ticket;
	final LeaseSet set = (LeaseSet) resource;
	MgrTask task;

	if (set.haveWarningRegistration()) {
	    task = new SendWarning(set);	    
	    ticket = runQueue.schedule(set.getWarningTime(), task);	    
	} else {
	    task = new QueueExpiration(set);
	    ticket = runQueue.schedule(set.getExpiration(), task);
	}
	
	/*
	 * No window here because the tasks only use the ticket after
	 * they acquire the lock on their set, but we still own the lock
	 * on the set.
	 */
	task.setTicket(ticket);
	ticketMap.getOrAdd(set, ticket);
    }

    // purposefully inherit doc comment from supertype
    // Called when LeaseResource we are tracking is garbage collected
    public void keyGC(Object value) {
	final WakeupManager.Ticket ticket = (WakeupManager.Ticket) value;
	runQueue.cancel(ticket);
    }

    /**
     * Expires sets queued for expiration.  Perform the expiration in a
     * separate thread because the operation will block if a snapshot is going
     * on.  It's OK for an expiration to block other expirations, which need
     * not be timely, but using the separate thread avoids blocking renewal
     * warnings, which should be timely.
     */
    private class ExpirationThread extends InterruptedStatusThread {

	ExpirationThread() {
	    super("expire lease sets thread");
	    setDaemon(true);
	}

	public void run() {
	    while (!hasBeenInterrupted()) {
		try {
		    Runnable task;
		    synchronized (expireQueue) {
			if (expireQueue.isEmpty()) {
			    expireQueue.wait();
			    continue;
			}
			task = (Runnable) expireQueue.remove(0);
		    }
		    task.run();
		} catch (InterruptedException e) {
		    return;
		} catch (Throwable t) {
		    logger.log(Level.INFO,
			       "Exception in lease set expiration thread -- " +
			       "attempting to continue",
			       t);
		}
	    }
	}
    }

    /**
     * Utility base class for our tasks, mainly provides the the proper
     * locking for manipulating the ticketMap.
     */
    private abstract class MgrTask implements Runnable {
	/** Resource this task is to operate on */
	protected final WeakReference resourceRef;
	
	/** Ticket for this task */
	private WakeupManager.Ticket ticket;

	/** 
	 * Simple constructor.
	 *
	 * @param set the set this task is to operate on
	 */
	protected MgrTask(LeaseSet set) {
	    resourceRef = new WeakReference(set);
	}

	/** Set the ticket associated with this task. */
	private void setTicket(WakeupManager.Ticket ticket) {
	    this.ticket = ticket;
	}

	/**
	 * Removes this task's ticket from the ticket map iff this
	 * task's ticket is in the map.  Returns the
	 * <code>LeaseSet</code> this task is to operate on or
	 * <code>null</code> if this task should stop.
	 */
	protected LeaseSet removeOurTicket() {
	    final LeaseSet set = (LeaseSet) resourceRef.get();
	    if (set != null) {
		synchronized (set) {
		    final WakeupManager.Ticket currentTicket = 
			(WakeupManager.Ticket) ticketMap.get(set);
		    if (ticket.equals(currentTicket)) {
			ticketMap.remove(set);
		    } else {
			/*
			 * Someone removed us after we were committed to
			 * run -- we should stop.
			 */
			return null;
		    }
		}
	    }

	    return set;
	}

	// purposefully inherit doc comment from supertype
	public abstract void run();
    }

    /** Task that queues a task to expire a lease set. */
    private class QueueExpiration extends MgrTask {
	QueueExpiration(LeaseSet set) {
	    super(set);
	}

	public void run() {
	    LeaseSet set = removeOurTicket();
	    if (set != null) {
		synchronized (expireQueue) {
		    expireQueue.add(new Expiration(set));
		    expireQueue.notifyAll();
		}
	    }
	}
    }

    /**
     * Objects that do the actual expiration of the set in question,
     * stuck in <code>expireQueue</code>.
     */
    private class Expiration implements Runnable {

	private LeaseSet set;
	/**
	 * Create a <code>Expiration</code> task for the passed resource.
	 *
	 * @param set the set this task is to operate on
	 */
	private Expiration(LeaseSet set) {
	    this.set = set;
	}

	// purposefully inherit doc comment from supertype
	public void run() {
	    server.expireIfTime(set);
	    /*
	     * Note we don't care if it's actually time or not, if it
	     * is not the task will be rescheduled by the renewal.
	     */
	}
    }

    /**
     * Objects that do the schedule the warning events, also schedules
     * an expiration task.
     */
    private class SendWarning extends MgrTask {
	/**
	 * Create a <code>SendWarning</code> task for the passed resource.
	 *
	 * @param set the set this task is to operate on
	 */
	private SendWarning(LeaseSet set) {
	    super(set);
	}

	// purposefully inherit doc comment from supertype
	public void run() {
	    final LeaseSet s = (LeaseSet) resourceRef.get();
	    if (s == null) {
		// set is gone, no work to do
		return;
	    }

	    /*
	     * By holding this lock we prevent other threads from
	     * scheduling new tasks for this set...if we have been
	     * replaced we will return before scheduling a new task, if
	     * we have not been we will schedule the new task and it can
	     * be cleanly removed by any renew that is happening at the
	     * same time.
	     */
	    synchronized (s) {
		final LeaseSet set = removeOurTicket();
		if (set == null) {
		    // set is gone, or our task was replaced, no work to do
		    return;
		}

		// Send event
		server.sendWarningEvent(set);

		// Schedule expiration task
		final MgrTask task = new QueueExpiration(set);
		final WakeupManager.Ticket newTicket =
		    runQueue.schedule(set.getExpiration(), task);
		task.setTicket(newTicket);
		ticketMap.getOrAdd(set, newTicket);
	    }
	}
    }
}
