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

import com.sun.jini.logging.Levels;
import com.sun.jini.mahalo.TxnManager;
import com.sun.jini.system.FileSystem;

import net.jini.admin.Administrable;

import java.io.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author Sun Microsystems, Inc.
 *
 */
public class MultiLogManager
        implements LogManager, FileModes, Administrable, MultiLogManagerAdmin {
    private static final String LOG_FILE = "Log.";

    /** Logger for persistence related messages */
    private static final Logger persistenceLogger = 
        Logger.getLogger(TxnManager.MAHALO + ".persistence");

    /** Logger for operations related messages */
    private static final Logger operationsLogger = 
        Logger.getLogger(TxnManager.MAHALO + ".operations");

    /** Logger for initialization related messages */
    private static final Logger initLogger = 
        Logger.getLogger(TxnManager.MAHALO + ".init");

    /** Client called during log recovery to process log objects */
    private final LogRecovery client;

    /** Map of log files keyed by their associated cookie */
    private final Map logByID = new HashMap();
    
    /** Lock object used for coordinating access to logByID */
    private final Object logByIDLock = new Object();
    
    /** Flag that is set to true upon destruction */
    private boolean destroyed = false;
    
    /** Persistence directory */
    private String directory = null;
    
    private final static FilenameFilter filter =
        new FilenameFilter() {
            public boolean accept(File dir, String name) {
                if (operationsLogger.isLoggable(Level.FINER)) {
                    operationsLogger.entering(FilenameFilter.class.getName(), 
		        "accept", new Object[] {dir, name});
		}
                final boolean isLog = name.startsWith(LOG_FILE);
                if (operationsLogger.isLoggable(Level.FINER)) {
                    operationsLogger.exiting(FilenameFilter.class.getName(), 
		        "accept", new Boolean(isLog));
		}
                return isLog;
            };
        };

    /**
     * Callback interface for log files to remove themselves from 
     * this manager 
     */ 
    public static interface LogRemovalManager {
        public void release(long cookie);
    }
    
    /**
     * Capability object passed to log files, which is called back upon
     * log removal. 
     */ 
    final LogRemovalManager logMgrRef = new LogRemovalManager() {
        public void release(long cookie) {
	    MultiLogManager.this.release(cookie);
	}
    };
    
    /**
     * Create a non-persistent <code>MultiLogManager</code>.
     */
    public MultiLogManager() {
        directory = null; // just for insurance
        client = null; 
    }
	    
    /**
     * Create a <code>MultiLogManager</code>.
     *
     * @param client who to inform during recovery.
     *
     * @param path where to store logging information.
     */
    public MultiLogManager(LogRecovery client, String path) {
	if (path == null)
	    throw new IllegalArgumentException("MultiLogManager: must use " +
						"non-null path");
	if (client == null)
	    throw new IllegalArgumentException("MultiLogManager: must use " +
						"non-null recovery client");
        this.client = client;
	directory = path; 

	if (!directory.endsWith(File.separator))
	    directory = directory.concat(File.separator);

	if (persistenceLogger.isLoggable(Level.FINEST)) {
            persistenceLogger.log(Level.FINEST,
	    "directory = {0}", directory);
	}

        File tmpfile = new File(directory);
 
        //If you attempt to access the Version file and
        //it does not exist, start with zero.
         try {
            if (!tmpfile.exists())
                if (!tmpfile.mkdirs())
 	            if (persistenceLogger.isLoggable(Level.SEVERE)) {
                        persistenceLogger.log(Level.SEVERE,
		            "Could not create {0}", tmpfile);
		    }
//TODO - ignore???		    
        } catch (SecurityException se) {
            if (persistenceLogger.isLoggable(Level.SEVERE)) {
                persistenceLogger.log(Level.SEVERE,
	        "Error accessing Version File", se);
	    }
//TODO ignore? throw (SecurityException)se.fillInStackTrace();
        }
     }

    // javadoc inherited from supertype
    public ClientLog logFor(long cookie) throws LogException {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(MultiLogManager.class.getName(), 
	        "logFor", new Long(cookie));
	}
	ClientLog cl = null;
	Long key = new Long(cookie);
        Object prev = null;
	
        synchronized(logByIDLock) {
	    if (destroyed)
	        throw new LogException("Manger has been destroyed");
		
	    cl = (ClientLog)logByID.get(key); 
	    if (cl == null) {
	        cl = (directory==null)?
		    (ClientLog)new TransientLogFile(cookie):
		    (ClientLog)new SimpleLogFile(
		        directory + LOG_FILE + cookie, 
		        cookie, logMgrRef);
	        if (persistenceLogger.isLoggable(Level.FINEST)) {
                    persistenceLogger.log(Level.FINEST,
		    "Created ClientLog: {0}", 
		    directory + LOG_FILE + cookie);
		}
 	        prev = logByID.put(key, cl);
	    }

	} 
        if (prev != null)
	    throw new LogException("Previous mapping for cookie(" 
	        + cookie + ") -- internal table corrupt?");
	     
	if (persistenceLogger.isLoggable(Level.FINEST)) {
            persistenceLogger.log(Level.FINEST,
            "Using ClientLog {0} for cookie {1}",
	    new Object[] {cl, new Long(cookie)});
	}
	
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(MultiLogManager.class.getName(), 
	        "logFor", cl);
	}
	return cl;
    }

    // javadoc inherited from supertype
    private void release(long cookie) {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(MultiLogManager.class.getName(), 
	        "release", new Long(cookie));
	}
	Object prev = null;
        synchronized(logByIDLock) {
	    if (destroyed)
	        return;
	    prev = logByID.remove(new Long(cookie));
	}
	if (persistenceLogger.isLoggable(Level.FINEST)) {
            persistenceLogger.log(Level.FINEST,
	    "Released ClientLog for cookie {0}",
	    new Long(cookie));
	    if (prev == null) {
    	        persistenceLogger.log(Level.FINEST,
		"Note: ClientLog already removed");
            }
        }
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(MultiLogManager.class.getName(), 
	        "release");
	}
    }
        
    /**
     *  Consumes the log file and re-constructs a system's
     *  state.
     */
    public void recover() throws LogException {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(MultiLogManager.class.getName(), 
	        "recover");
	}
        // Short-circuit for non-persistent mode
	if (directory == null) return;
	
	Log log = null;
	File tmpfile = null;
	String [] filenames = null;

	/* Called by initialization thread only, 
	 * so don't need to check for destroyed.
	 */

	try {
	    tmpfile = new File(directory);
	    filenames = tmpfile.list(filter);

	    if (filenames.length == 0)
	      return;

            String logName;
	    for (int i = 0; i < filenames.length; i++ ) {
	        logName = directory +  filenames[i];
	        log = new SimpleLogFile(logName, logMgrRef);
		if (persistenceLogger.isLoggable(Level.FINEST)) {
                    persistenceLogger.log(Level.FINEST,
		    "Recovering log: {0}", logName);
		}
		try {
		    log.recover(client);
                
		    /* Called by initialization thread only, 
		     * so doesn't need to be synchronized here.
		     */
		    logByID.put(new Long(log.cookie()),log);
	        } catch (LogException le) {
	            if(persistenceLogger.isLoggable(Level.WARNING)) {
		        persistenceLogger.log(Level.WARNING,
			"Unable to recover log state", le);
		    }
		}
	    }
	} catch (SecurityException se) { // TODO - shouldn't this percolate back up?
	    if(persistenceLogger.isLoggable(Level.WARNING)) {
		persistenceLogger.log(Level.WARNING,
		"Unable to recover log state", se);
	    }
	}
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(MultiLogManager.class.getName(), 
	        "recover");
	}
    }


    /**
     * Retrieves the administration interface for the
     * <code>MultiLogManager</code>
     *
     */
    public Object getAdmin() {
        // TBD - pass capability object instead?
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(MultiLogManager.class.getName(), 
	        "getAdmin");
	}
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(MultiLogManager.class.getName(), 
	        "getAdmin", this);
	}
	return (MultiLogManagerAdmin)this;
    }


    /**
     * Clean up all <code>LogFile</code> objects on behalf of caller.
     *
     * @see com.sun.jini.admin.DestroyAdmin
     * @see com.sun.jini.system.FileSystem
     */
    public void destroy() {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(MultiLogManager.class.getName(), 
	        "destroy");
	}

        // Short-circuit for non-persistent mode
	if (directory == null) {
	    logByID.clear();
	    destroyed = true;
	    return;
	}

	// TBD - Set destroy flag? used by logFor()/release()
	// TBD - Loop over values enum?
	/**
	 * Loop over know logs and invalidate them.
	 */
        synchronized(logByIDLock) {
  	    if (destroyed) // return silently to avoids retries
	        return;
 	    if (logByID.size() > 0) {
	        /* Can't use iterator because slf.invalidate() calls back into
		 * this.release() in order to remove itself from logByID,
		 * which would cause a concurrent modification exception.
		 */
	        Object [] vals = logByID.values().toArray();
		SimpleLogFile slf = null;
	        for (int i=0; i<vals.length; i++) {
		    try {
		        slf = (SimpleLogFile)vals[i];
			if (slf != null) 
			    slf.invalidate();
			else {
  		            if (persistenceLogger.isLoggable(Level.FINEST)) {
                                persistenceLogger.log(Level.FINEST,
				"Observed a null log file entry for: {0}", slf);
			    }
			}
                    } catch (LogException le) {
                        if(persistenceLogger.isLoggable(Levels.HANDLED)) {
		            persistenceLogger.log(Levels.HANDLED,
			        "Unable to recover log state", le);
			}
                    } catch (java.util.NoSuchElementException nsee) {
                        if(persistenceLogger.isLoggable(Levels.HANDLED)) {
		            persistenceLogger.log(Levels.HANDLED,
			        "Problem enumerating internal log state", nsee);
			}
     	            }
	        }
	        logByID.clear();
		destroyed = true;
	    }
	} 
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(MultiLogManager.class.getName(), 
	        "destroy");
	}
    }
}
