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

import com.sun.jini.logging.Levels;

import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.io.SyncFailedException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;

/** 
 * This class provides a pool of <tt>LogStream</tt> objects.  Each
 * <tt>LogStream</tt> has an associated <tt>FileDescriptor</tt>, which
 * is the system resource we are trying to manage. This pool limits the
 * (user configurable) number of concurrent, open <tt>FileDescriptor</tt>s.
 *
 * @author Sun Microsystems, Inc.
 *
 * @since 1.1
 */
class StreamPool {
    // Class fields
    /** Logger for lease related messages */
    private static final Logger persistenceLogger = 
        MailboxImpl.persistenceLogger;
    
    /** 
     * Maximum limit for the number of concurrent <tt>LogStream</tt>s
     * in the stream pool.
     */
    private final int maxPoolSize;

    /** Holds stream references by associated key */
    private final HashMap pool;

    /**
     * Holds stream references in least recently used (released) order.
     * It's used in order determine which stream to discard upon
     * reaching the maximum pool limit.
     */
    private final LinkedList freeList;

    /**
     * Simple constructor that creates a pool of given <code>size</code>.
     *
     * @exception IllegalArgumentException Thrown if the value of 
     *                <tt>maxPoolSize</tt> is less than 1.
     */
    StreamPool(int size) {
        if (size < 1) 
            throw new IllegalArgumentException(
		"Pool size must be greater than 0."); 
        maxPoolSize = size;
        pool = new HashMap(maxPoolSize);
        freeList = new LinkedList();

        if (persistenceLogger.isLoggable(Level.FINEST)) {
            persistenceLogger.log(Level.FINEST,
	        "Created StreamPool of size {0}", 
		new Integer(maxPoolSize));
	}
    }

    /**
     * Returns a <tt>ControlLog</tt> object for the specified <tt>file</tt>
     * from the pool if it already exists.
     * Otherwise, it creates a new instance and adds it to the pool.
     *
     * @exception IOException if an I/O error occurs
     */
    synchronized ControlLog getControlLog(File file) 
	throws IOException 
    {
	StreamKey key = new StreamKey(file, StreamType.CONTROL);
	ControlLog log = (ControlLog)pool.get(key);
	if (log != null) { // found it!
	    if (freeList.remove(key) == false)
	        throw new InternalMailboxException("Did not find re-used control log "
	            + "stream in freeList.");
	} else { // Log was not found, so attempt to add it

	    ensurePoolSpace();

            //
	    // Create new ControlLog and add it the pool.
            //
            log = new ControlLog(file, key);
            pool.put(key, log);

            if(freeList.remove(key))
                throw new InternalMailboxException("Found newly created ControlLog "
                    + "in freeList");
	}

        return log;
    }

    /**
     * Returns a <tt>LogInputStream</tt> object from the pool if it 
     * already exists. Otherwise, it creates a new instance and adds 
     * it to the pool.
     *
     * @exception IOException if an I/O error occurs
     */
    synchronized LogInputStream getLogInputStream(File file, long offset) 
	throws IOException
    {
	StreamKey key = new StreamKey(file, StreamType.INPUT);
	LogInputStream in = (LogInputStream)pool.get(key);
	if (in != null) { //found it!
	    if (freeList.remove(key) == false)
	        throw new InternalMailboxException("Did not find re-used input log "
	            + "stream in freelist.");
	} 

	if (in == null ||            // if log not found OR 
	    in.getOffset() > offset) // current read offset is past desired 
	{                            // then create a new log and add it
	    ensurePoolSpace();

	    in = new LogInputStream(file, key);
	    pool.put(key, in);

            if(freeList.remove(key))
                throw new InternalMailboxException("Found newly created ControlLog "
                    + "on freeList");
	}

	// Sanity check for offset value
	if (offset > file.length()) 
	    throw new EOFException("Attempting to read past end of file.");

	// Check if log offset needs adjusting. 
	// By this point in.offset <= offset.
	while (in.getOffset() < offset) {
	    in.skip(offset - in.getOffset());
	}

	return in;
    }

    /**
     * Returns a <tt>LogOutputStream</tt> object for the specified <tt>file</tt>
     * from the pool if it already exists. 
     * Otherwise, it creates a new instance and adds it to the pool.
     *
     * @exception IOException if an I/O error occurs
     */
    synchronized LogOutputStream getLogOutputStream(File file, long offset) 
	throws IOException 
    {
	StreamKey key = new StreamKey(file, StreamType.OUTPUT);
	LogOutputStream out = (LogOutputStream)pool.get(key);

	if (out != null) { // found it!
	    if (freeList.remove(key) == false)
	        throw new InternalMailboxException("Did not find re-used output log "
	            + "stream in freelist");

	     // Sanity check to see if we are still in sync. If not,
	     // then we need to close this stream and create another
	     // one (done in the next code block).
	    if (out.getOffset() != offset) {
	        removeLogStream(out);
	        out = null;
	    }
	}

        // Check to see if we need to create another log.
	if (out == null) { 

	    ensurePoolSpace();

	    if (offset == 0L) { // Create new log, without appending
	        out = new LogOutputStream(file, key, false);
	    } else { // Create new log, appending to existing file
	        long len = file.length();

	        if (offset > len) 
	            throw new EOFException("Attempting to write past end "
	                + "of file");

		if (offset < len) {
		    RandomAccessFile raf = new RandomAccessFile(file, "rw");
		    raf.setLength(offset);
		    raf.close();
		}

		out = new LogOutputStream(file, key, true);
	    }

            pool.put(key, out);

            if (freeList.remove(key))
                throw new InternalMailboxException("Found newly created output log "
                    + "in freeList");

	}

        return out;
    }

    /**
     * Ensures that room is available in the pool. If the pool is currently
     * full, then the least recently used <tt>LogStream</tt> will be removed 
     * and closed to make room. This method will block if the pool is full and
     * no <tt>LogStream</tt> objects can be closed. 
     *
     * @exception IOException if an I/O error occurs
     */
    private synchronized void ensurePoolSpace() throws IOException {
        if (pool.size() >= maxPoolSize) {
            while(freeList.size() < 1) {
                try {
		    wait();
		} catch (InterruptedException ie) { ; }
	    }
	    StreamKey key = (StreamKey)freeList.removeFirst();
	    LogStream els = (LogStream)pool.remove(key);
	    els.close();
	}
    }

    /**
     * Marks a stream as available for closing.
     * A log will only be closed if a new log is requested and the
     * pool has reached its maximum size.
     */
    synchronized void releaseLogStream(LogStream stream) {
        StreamKey key = (StreamKey)stream.getKey();
        if (pool.get(key) == null)
            throw new InternalMailboxException("Not managing stream: " 
                + stream + ":" + key + " -- release failed");
	freeList.add(key);
	notifyAll();
    }

    /**
     * Removes the given <tt>LogStream</tt> from the pool and closes it,
     * if possible. The intent is for this method to be called for 
     * unusable logs so that they will no longer be returned by a 
     * subsequent call to one of the "get" methods.
     */
    synchronized void removeLogStream(LogStream stream) {
        StreamKey key = (StreamKey)stream.getKey();
        if (pool.remove(key) == null)
            throw new InternalMailboxException("Not managing stream: " 
                + stream + ":" + key + " -- remove failed");

        // Remove it from freeList, if present
        freeList.remove(key);
        try {
            stream.close();
	} catch (IOException ioe) {
	    // Note the exception, but otherwise ignore
	    if (persistenceLogger.isLoggable(Levels.HANDLED)) {
                persistenceLogger.log(Levels.HANDLED,
		    "Exception closing Log", ioe);
	    }
	}
    }

    //
    // Debug use only!
    //
    synchronized int getPoolSize() { return pool.size(); }
    synchronized int getFreeSize() { return freeList.size(); }
    synchronized void dump() { 
	System.out.println("Pool:\n" + pool); 
	System.out.println("Free:\n" + freeList); 
    }
}
