/*
 * @(#)AbstractSingleSourceLoggerFactory.java
 *
 * Copyright (C) 2004 Matt Albrecht
 * groboclown@users.sourceforge.net
 * http://groboutils.sourceforge.net
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a
 *  copy of this software and associated documentation files (the "Software"),
 *  to deal in the Software without restriction, including without limitation
 *  the rights to use, copy, modify, merge, publish, distribute, sublicense,
 *  and/or sell copies of the Software, and to permit persons to whom the
 *  Software is furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in
 *  all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 *  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 *  DEALINGS IN THE SOFTWARE.
 */

package net.sourceforge.groboutils.codecoverage.v2.logger;

import java.io.Writer;
import java.io.IOException;

import java.util.Properties;
import java.util.HashSet;
import java.util.Set;

import net.sourceforge.groboutils.codecoverage.v2.IChannelLogger;
import net.sourceforge.groboutils.codecoverage.v2.IChannelLoggerFactory;


/**
 * A shared mark writer.
 *
 * @author    Matt Albrecht <a href="mailto:groboclown@users.sourceforge.net">groboclown@users.sourceforge.net</a>
 * @version   $Date: 2004/07/07 09:39:10 $
 * @since     April 16, 2004
 */
public abstract class AbstractSingleSourceLoggerFactory
        implements ISingleSource, IChannelLoggerFactory, Runnable
{
    public static final String WRITES_PER_FLUSH_PROP = "writes-per-flush";
    private static final int DEFAULT_WRITES_PER_FLUSH = 2;
    private static final int INIT_BUFFER_SIZE = 4096;
    
    public static final String MILLIS_PER_FLUSH_PROP = "millis-per-flush";
    private static final long DEFAULT_MILLIS_PER_FLUSH = 0;
    
    public static final String USE_CACHE_PROP = "use-cache";
    
    // remember that StringBuffers are thread safe
    private StringBuffer buffer = new StringBuffer( INIT_BUFFER_SIZE );
    private Writer source;
    private int writesPerFlush = DEFAULT_WRITES_PER_FLUSH;
    private long millisPerFlush = DEFAULT_MILLIS_PER_FLUSH;
    private boolean reloadSourceAfterError = false;
    
    // due to how we access the writeCount outside synch blocks, it needs to be
    // volatile.
    private volatile int writeCount = 0;
    
    private boolean isSetup = false;
    private boolean useCache = true;
    private Thread flushThread = null;
    
    private Set covered = new HashSet( 20000, 0.75f );
    
    // synchronize on a private member, so nobody else can interfere
    // with our threaded behavior.  Make this the string buffer, to
    // speed up with the string buffer operations (it is thread safe,
    // after all, and string buffers synchronize on "this", not an inner
    // object).
    private Object sync = buffer;
    
    
    /**
     * Need a thread to flush the buffer.
     */
    private class FlushRunner implements Runnable
    {
        public void run()
        {
            Thread t = Thread.currentThread();
            try
            {
                while (true)
                {
                    Thread.sleep( millisPerFlush );
                    flushBuffer();
                    if (t.interrupted())
                    {
                        break;
                    }
                }
            }
            catch (InterruptedException e)
            {
                // stop the run thread
//e.printStackTrace();
            }
        }
    }
    
    
    
    public IChannelLogger createChannelLogger(
            String propertyPrefix, Properties props, short channelIndex )
    {
        // we only set ourself up once
        synchronized( this.sync )
        {
            if (!this.isSetup)
            {
                setupProps( propertyPrefix, props );
                
                this.source = setupSource();
                
                addShutdownHook();
                this.isSetup = true;
            }
        }
        
        return new SingleSourceLogger( channelIndex, this );
    }
    
    
    /**
     * Outputs all the data associated with a probe to the shared single source.
     * This class manages its own buffering for safety.
     */
    public void cover( short channelIndex, String classSig,
            short methodIndex, short markIndex )
    {
        synchronized (this.sync)
        {
            if (this.source == null)
            {
                if (this.reloadSourceAfterError)
                {
                    // don't write this time, but write next time.
                    this.source = setupSource();
                    if (this.source == null)
                    {
                        // no need to continue - we don't want the buffer to get
                        // huge while we're not flushing it.
//System.out.println("++ source is null");
                        return;
                    }
                }
                else
                {
                    // same as the comment for the above return.
                    // they could be merged into a single block, but
                    // let's reduce the number of if statements where
                    // necessary.
//System.out.println("++ source is null and don't reload source");
                    return;
                }
                
                // We had a failed source, but now we're going to resume
                // where we know we left off.
                // We're not sure where we failed, so add a line
                // boundary to the beginning of our buffer.
                // Yes, this is slow, but so is setting up the source.
                this.buffer.insert( 0, '\n' );
                this.writeCount = this.writesPerFlush;
            }
        }
        
        // For the non-cached version, this may be a bit slower, but
        // it avoids locking on the buffer until absolutely necessary.
        StringBuffer sb = new StringBuffer();
        appendToLog( sb, channelIndex, classSig, methodIndex, markIndex );
        String s = sb.toString();
        if (this.useCache)
        {
            synchronized (this.sync)
            {
                if (this.covered.contains( s ))
                {
                    // early out
//System.out.println("++ cache already contains "+s);
                    return;
                }
                // else add the temp string, and do flush checking
//System.out.println("++ adding ["+s+"] to cache");
                this.covered.add( s );
            }
        }
        // else, just blindly add the string to our buffer.
        // Note that buffers are thread safe, so we don't need an extra
        // sync around this
        this.buffer.append( s );
        
        // if writesPerFlush is < 0, then we always flush, but we need
        // to keep track of the writeCount due to the way flushBuffer is
        // implemented.  If writesPerFlush is 0, then we never flush.
        // Since the write count is an int, singlular operations (like inc
        // and assign, the only two operations we use on it) are thread safe
        int wpf = this.writesPerFlush;
        if (wpf != 0 && ++this.writeCount >= wpf)
        {
            flushBuffer();
        }
    }
    
    
    /**
     * This is the shutdown hook.  We flush the buffer to the source,
     * then we call <tt>close()</tt> on the writer.
     */
    public void run()
    {
//System.out.println("++ Start shutdown hook");
        synchronized( this.buffer )
        {
            if (this.flushThread != null)
            {
                this.flushThread.interrupt();
                this.flushThread = null;
            }
            
            // this is a modified cut-n-paste of the cover command above.
            // however, we also close off the stream with a close();
            
            if (this.source == null)
            {
                if (this.reloadSourceAfterError)
                {
                    this.source = setupSource();
                }
                if (this.source == null)
                {
//System.out.println("++ Early leave from shutdown hook");
            
                    // Don't keep generating source files if another shutdown
                    // hook is registered for code coverage.  Force us not to!
                    this.reloadSourceAfterError = false;
                    return;
                }
                this.buffer.insert( 0, '\n' );
            }
            
            flushBuffer();
            
            try
            {
                this.source.close();
            }
            catch (Exception e)
            {
                // ignore
//e.printStackTrace();
            }
            
            // Don't keep generating source files if another shutdown
            // hook is registered for code coverage.  Force us not to!
            this.source = null;
            this.reloadSourceAfterError = false;
//System.out.println("++ End of shutdown hook");
        }
    }
    
    
    /**
     * Setup the source writer.  This can return <tt>null</tt>.  Its
     * actions will be synchronized for you.
     */
    protected abstract Writer setupSource();
    
    
    /**
     * Extend this method to setup your own properties.  Be sure to
     * call the super's setupProps, though.  Be sure to keep the
     * properties around for how to connect to the source.
     */
    protected void setupProps( String prefix, Properties props )
    {
        String wpf = props.getProperty( prefix + WRITES_PER_FLUSH_PROP );
        if (wpf != null)
        {
            try
            {
                this.writesPerFlush = Integer.parseInt( wpf );
            }
            catch (NumberFormatException e)
            {
                // default
                this.writesPerFlush = DEFAULT_WRITES_PER_FLUSH;
//e.printStackTrace();
            }
        }
//System.out.println("++ writes per flush = "+this.writesPerFlush);
        
        String uc = props.getProperty( prefix + USE_CACHE_PROP );
        if (uc != null)
        {
            this.useCache = Boolean.valueOf( uc ).booleanValue();
        }
        
        
        String mpf = props.getProperty( prefix + MILLIS_PER_FLUSH_PROP );
        if (mpf != null)
        {
            try
            {
                this.millisPerFlush = Long.parseLong( mpf );
            }
            catch (NumberFormatException e)
            {
                this.millisPerFlush = DEFAULT_MILLIS_PER_FLUSH;
//e.printStackTrace();
            }
        }
//System.out.println("++ millis per flush = "+this.millisPerFlush);
        if (this.millisPerFlush > 0)
        {
            if (this.flushThread == null)
            {
                // create our flush thread
                Thread t = new Thread( new FlushRunner() );
                t.setDaemon( true );
                t.setPriority( Thread.MIN_PRIORITY );
                t.setName( "Code Coverage Flusher" );
                t.start();
                this.flushThread = t;
            }
        }
    }
    
    
    /**
     * Set to <tt>true</tt> if you want to try to reconnect to the
     * source everytime the I/O write fails.  This can be changed
     * at any time.
     */
    protected void setReloadSourceAfterError( boolean yes )
    {
        this.reloadSourceAfterError = yes;
    }
    
    
    /**
     * A consistent way to flush the write buffer.  Set as "private final" to
     * improve performance.  Note that this method blocks.
     */
    private final void flushBuffer()
    {
        synchronized (this.sync)
        {
            if (this.writeCount <= 0 || this.source == null)
            {
                // there were no writes since the last flush - exit.
//System.out.println("++ Nothing to flush.");
                return;
            }
            try
            {
//System.out.println("++ Writing to source.");
                this.source.write( this.buffer.toString() );
                
                // ensure we flush out the source.
                this.source.flush();
                
                // only delete and reset count if the write doesn't fail.
                // This ensures that the data is actually written.  Since
                // we don't care about duplicates (on line boundaries,
                // that is), this should be safe.  Using a setLength is
                // faster (avoids a System.arraycopy call) than delete.
                this.buffer.setLength( 0 );
                this.writeCount = 0;
            }
            catch (IOException e)
            {
                e.printStackTrace();
                // failed I/O, so signal this.
                this.source = null;
            }
        }
    }
    
    
    
    protected void addShutdownHook()
    {
        Class c = Runtime.class;
        try
        {
            java.lang.reflect.Method m = c.getMethod(
                "addShutdownHook", new Class[] { Thread.class } );
            Thread t = new Thread( this,
                this.getClass().getName()+" shutdown hook" );
            m.invoke( Runtime.getRuntime(), new Object[] { t } );
        }
        catch (Exception ex)
        {
            // prolly JDK 1.3 not supported.
            // but it's not necessary...
//ex.printStackTrace();
        }
    }
    
    
    private static final void appendToLog( StringBuffer log,
            short channelIndex, String classSig, short methodIndex,
            short markIndex )
    {
        log.append( channelIndex ).
            append( ',' ).append( classSig ).append( ',' ).
            append( DirectoryChannelLogger.createCoverString(
                methodIndex, markIndex ) );
    }
}

