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

import java.util.Set;

/**
 * Subclass of <code>QueryWatcher</code> for non-transactional if
 * exists reads. Resolves with the first matching
 * transition where the transaction is <code>null</code> and the entry
 * is visible (the entry's current state is ignored) or if 
 * the locked entry set goes empty.
 */
class ReadIfExistsWatcher extends QueryWatcher implements IfExistsWatcher {
    /**
     * The set of entries that would match but are currently
     * unavailable (e.g. they are locked). We only keep
     * the ids, not the entries themselves.
     */
    private final Set lockedEntries;

    /**
     * Set <code>true</code> once the query thread is 
     * done processing the backlog. Once this is 
     * <code>true</code> it is ok to resolve if
     * <code>lockedEntries</code> is empty.
     */
    private boolean backlogFinished = false;

    /**
     * Create a new <code>ReadIfExistsWatcher</code>.
     * @param expiration the initial expiration time
     *        for this <code>TransitionWatcher</code> in 
     *        milliseconds since the beginning of the epoch.
     * @param timestamp the value that is used
     *        to sort <code>TransitionWatcher</code>s.
     * @param startOrdinal the highest ordinal associated
     *        with operations that are considered to have occurred 
     *        before the operation associated with this watcher.
     * @param lockedEntries Set of entries (by their IDs)
     *        that match but are unavailable. Must be non-empty.
     *        Keeps a reference to this object.
     * @throws NullPointerException if <code>lockedEntries</code> is
     *         <code>null</code>.
     */
    ReadIfExistsWatcher(long expiration, long timestamp, long startOrdinal, 
			Set lockedEntries)
    {
	super(expiration, timestamp, startOrdinal);

	if (lockedEntries == null) 
	    throw new NullPointerException("lockedEntries must be non-null");
	
	this.lockedEntries = lockedEntries;
    }

    boolean isInterested(EntryTransition transition, long ordinal) {
	/* If we are unresolved pretty much all transitions
	 * are interesting because we may need to update
	 * lockedEntries (the only exception is read locks
	 * being resolved, but there is no way to tell that
	 * from the transition, and if there was a read locked 
	 * entry that was visible to us most of the time
	 * we would not be here at all (e.g. we never have been
	 * created or the write that created it would have caused
	 * us to go away).
	 * 
	 * Note, !isResolved() without the lock will result only in
	 * false positives, not false negatives - it will only
	 * cause isInterested() to return false if we are resolved,
	 * we may still return true if we are resolved though.
	 */

	return (ordinal>startOrdinal) && !isResolved();
    }

    synchronized void process(EntryTransition transition, long now) {
	if (isResolved())
	    return; // Already done.

	final EntryRep rep = transition.getHandle().rep();
	final boolean isAvailable = transition.isAvailable();

	/* If the entry was available at one time to the null 
	 * transaction we can just resolve.
	 */
	if (isAvailable && (transition.getTxn() == null)) {
	    resolve(rep, null);
	} else if (isAvailable) { // && getTxn() != null
	    /* If we are here transition.getTxn() must be != null 
	     * and the entry was available, implying that it was an entry
	     * written under a transaction and is interesting to 
	     * us, but not yet visible. We need to add it lockedEntries
	     * even if has been removed since it could have gotten
	     * replaced before it was removed. If we did not
	     * add it we would be acting on the future.
	     */
	    lockedEntries.add(rep.id());
	} else {
	    /* Must not be available, transition must mark
	     * the resolution of the transaction in such away
	     * that the entry has been removed, remove it 
	     * from the set and see if that makes the set empty.
	     */
	    lockedEntries.remove(rep.id());
	    if (backlogFinished && lockedEntries.isEmpty())
		resolve(null, null);
	}
    }

    synchronized boolean catchUp(EntryTransition transition, long now) {
	if (isResolved())
	    return true; // Already done.
	
	final EntryHandle handle = transition.getHandle();
	final EntryRep rep = handle.rep();
	final boolean isAvailable = transition.isAvailable();

	/* If the entry was available at one time to the null 
	 * transaction we can just resolve.
	 */
	if (isAvailable && (transition.getTxn() == null)) {
	    resolve(rep, null);
	    return true;
	} 

	if (isAvailable) { // && getTxn() != null
	    /* If we are here transition.getTxn() must be != null 
 	     * and the entry was available, implying that it was an entry
	     * written under a transaction and is interesting to 
	     * us, but not yet visible. We only add if it has not
	     * already been removed.  It might have gotten replaced
	     * before removal, but since we won't let this query
	     * get resolved with a definitive null before we get past
	     * the point in journal where it was removed it is ok
	     * to never put it in (and if we did put it in it might
	     * never get removed since process() may have already
	     * processed the removal).
	     */
	    synchronized (handle) {
		if (!handle.removed()) {
		    lockedEntries.add(rep.id());
		}		
	    }

	    // Either way, still not resolved.
	    return false;
	}
	 
	/* Must not be available, transition must mark
	 * the resolution of the transaction in such away
	 * that the entry has been removed, remove it 
	 * from the set (don't need to check for empty
	 * because we haven't gotten to the point
	 * where we can resolve with a definitive null.)
	 */
	lockedEntries.remove(rep.id());
	return false;
    }

    /**
     * Once the backlog is complete we can resolve if 
     * lockedEntries is/becomes empty.
     */
    public synchronized void caughtUp() {
	backlogFinished = true;
	
	if (isResolved())
	    return; // Don't much mater.

	if (lockedEntries.isEmpty())
	    resolve(null, null);	
    }

    public synchronized boolean isLockedEntrySetEmpty() {
	if (!isResolved())
	    throw new IllegalStateException("Query not yet resolved");	
	return lockedEntries.isEmpty();
    }
}
