/*******************************************************************************
 * Copyright (c) 2010, 2015 Sonatype, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Stuart McCulloch (Sonatype, Inc.) - initial API and implementation
 *******************************************************************************/
package org.eclipse.sisu.inject;

import java.util.Map;
import java.util.logging.Level;

import com.google.inject.Binding;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.ProvisionException;
import com.google.inject.spi.Element;
import com.google.inject.spi.Elements;

/**
 * Utility methods for dealing with container logging and recovery.
 * <p>
 * Set <b>-Dsisu.debug</b> to send detailed tracing to the console.
 */
public final class Logs
{
    // ----------------------------------------------------------------------
    // Static initialization
    // ----------------------------------------------------------------------

    static
    {
        String newLine;
        boolean toConsole;
        try
        {
            newLine = System.getProperty( "line.separator", "\n" );
            final String debug = System.getProperty( "sisu.debug", "false" );
            toConsole = "".equals( debug ) || "true".equalsIgnoreCase( debug );
        }
        catch ( final RuntimeException e )
        {
            newLine = "\n";
            toConsole = false;
        }
        NEW_LINE = newLine;
        Sink sink;
        try
        {
            sink = toConsole ? new ConsoleSink() : new SLF4JSink();
        }
        catch ( final RuntimeException e )
        {
            sink = new JULSink();
        }
        catch ( final LinkageError e )
        {
            sink = new JULSink();
        }
        SINK = sink;
    }

    // ----------------------------------------------------------------------
    // Constants
    // ----------------------------------------------------------------------

    public static final String NEW_LINE;

    private static final String SISU = "Sisu";

    private static final Sink SINK;

    public static final boolean TRACE_ENABLED = SINK.isTraceEnabled();

    // ----------------------------------------------------------------------
    // Constructors
    // ----------------------------------------------------------------------

    private Logs()
    {
        // static utility class, not allowed to create instances
    }

    // ----------------------------------------------------------------------
    // Utility methods
    // ----------------------------------------------------------------------

    /**
     * Logs a trace message; uses "{}" format anchors. Pass {@link Throwable}s in last parameter for special handling.
     * 
     * @param format The trace message format
     * @param arg1 First object to format
     * @param arg2 Second object to format
     */
    public static void trace( final String format, final Object arg1, final Object arg2 )
    {
        if ( TRACE_ENABLED )
        {
            SINK.trace( format( format( format, arg1 ), arg2 ), arg2 instanceof Throwable ? (Throwable) arg2 : null );
        }
    }

    /**
     * Logs a warning message; uses "{}" format anchors. Pass {@link Throwable}s in last parameter for special handling.
     * 
     * @param format The warning message format
     * @param arg1 First object to format
     * @param arg2 Second object to format
     */
    public static void warn( final String format, final Object arg1, final Object arg2 )
    {
        SINK.warn( format( format( format, arg1 ), arg2 ), arg2 instanceof Throwable ? (Throwable) arg2 : null );
    }

    /**
     * Helper method for catching {@link Throwable}s; severe errors such as {@link ThreadDeath} are always re-thrown.
     * 
     * @param problem The problem
     */
    public static void catchThrowable( final Throwable problem )
    {
        for ( Throwable cause = problem; cause != null; cause = cause.getCause() )
        {
            if ( cause instanceof ThreadDeath || cause instanceof VirtualMachineError )
            {
                throw (Error) cause; // must immediately re-throw severe errors
            }
        }
    }

    /**
     * Helper method for throwing {@link Throwable}s; checked exceptions are wrapped as {@link ProvisionException}s.
     * 
     * @param problem The problem
     */
    public static void throwUnchecked( final Throwable problem )
    {
        if ( problem instanceof RuntimeException )
        {
            throw (RuntimeException) problem;
        }
        if ( problem instanceof Error )
        {
            throw (Error) problem;
        }
        // this cast lets us load the 'Logs' class and log messages even if Guice is not available
        throw RuntimeException.class.cast( new ProvisionException( problem.toString(), problem ) );
    }

    /**
     * Returns an identity string for the given object.
     * 
     * @see System#identityHashCode(Object)
     * @param object The object
     * @return Identity string of the object.
     */
    public static String identityToString( final Object object )
    {
        return null == object ? null : object.getClass().getName() + '@'
            + Integer.toHexString( System.identityHashCode( object ) );
    }

    /**
     * Returns a string representation of the given {@link Module}.
     * 
     * @param module The module
     * @return String representation of the module.
     */
    public static String toString( final Module module )
    {
        final StringBuilder buf = new StringBuilder( identityToString( module ) );
        buf.append( NEW_LINE ).append( NEW_LINE );
        buf.append( "-----[elements]----------------------------------------------------------------" ).append( NEW_LINE );
        int i = 0;
        for ( final Element e : Elements.getElements( module ) )
        {
            buf.append( i++ ).append( ". " ).append( e ).append( NEW_LINE );
        }
        return buf.append( "-------------------------------------------------------------------------------" ).append( NEW_LINE ).toString();
    }

    /**
     * Returns a string representation of the given {@link Injector}.
     * 
     * @param injector The injector
     * @return String representation of the injector.
     */
    public static String toString( final Injector injector )
    {
        final StringBuilder buf = new StringBuilder( identityToString( injector ) );
        if ( null != injector.getParent() )
        {
            buf.append( " parent: " ).append( identityToString( injector.getParent() ) );
        }
        buf.append( NEW_LINE ).append( NEW_LINE );
        buf.append( "-----[explicit bindings]-------------------------------------------------------" ).append( NEW_LINE );
        int i = 0;
        final Map<Key<?>, Binding<?>> explicitBindings = injector.getBindings();
        for ( final Binding<?> b : explicitBindings.values() )
        {
            buf.append( i++ ).append( ". " ).append( b ).append( NEW_LINE );
        }
        buf.append( "-----[implicit bindings]-------------------------------------------------------" ).append( NEW_LINE );
        for ( final Binding<?> b : injector.getAllBindings().values() )
        {
            if ( !explicitBindings.containsKey( b.getKey() ) )
            {
                buf.append( i++ ).append( ". " ).append( b ).append( NEW_LINE );
            }
        }
        return buf.append( "-------------------------------------------------------------------------------" ).append( NEW_LINE ).toString();
    }

    // ----------------------------------------------------------------------
    // Implementation methods
    // ----------------------------------------------------------------------

    /**
     * Replaces the first available formatting anchor with the given object.
     * 
     * @param format The format string
     * @param arg The object to format
     */
    private static String format( final String format, final Object arg )
    {
        final int len = format.length();
        boolean detailed = true;
        int cursor = 0;
        for ( char prevChar = ' ', currChar; cursor < len; prevChar = currChar, cursor++ )
        {
            currChar = format.charAt( cursor );
            if ( prevChar == '{' && currChar == '}' )
            {
                break; // replace anchor with String.valueOf
            }
            if ( prevChar == '<' && currChar == '>' )
            {
                detailed = false;
                break; // use Logs.identityToString instead
            }
        }
        if ( cursor >= len )
        {
            return format;
        }
        final StringBuilder buf = new StringBuilder();
        if ( --cursor > 0 )
        {
            buf.append( format.substring( 0, cursor ) );
        }
        try
        {
            buf.append( detailed ? arg : identityToString( arg ) );
        }
        catch ( final RuntimeException e )
        {
            buf.append( e );
        }
        cursor += 2;
        if ( cursor < len )
        {
            buf.append( format.substring( cursor, len ) );
        }
        return buf.toString();
    }

    // ----------------------------------------------------------------------
    // Implementation types
    // ----------------------------------------------------------------------

    /**
     * Something that accepts formatted messages.
     */
    private interface Sink
    {
        /**
         * @return {@code true} if trace is enabled; otherwise {@code false}
         */
        boolean isTraceEnabled();

        /**
         * Accepts a trace message and optional exception cause.
         * 
         * @param message The trace message
         * @param cause The exception cause
         */
        void trace( String message, Throwable cause );

        /**
         * Accepts a warning message and optional exception cause.
         * 
         * @param message The warning message
         * @param cause The exception cause
         */
        void warn( String message, Throwable cause );
    }

    /**
     * {@link Sink}s messages to the system console.
     */
    static final class ConsoleSink
        implements Sink
    {
        // ----------------------------------------------------------------------
        // Constants
        // ----------------------------------------------------------------------

        private static final String TRACE = "TRACE: " + SISU + " - ";

        private static final String WARN = "WARN: " + SISU + " - ";

        // ----------------------------------------------------------------------
        // Public methods
        // ----------------------------------------------------------------------

        public boolean isTraceEnabled()
        {
            return true;
        }

        public void trace( final String message, final Throwable cause )
        {
            System.out.println( TRACE + message );
            if ( null != cause )
            {
                cause.printStackTrace( System.out );
            }
        }

        public void warn( final String message, final Throwable cause )
        {
            System.err.println( WARN + message );
            if ( null != cause )
            {
                cause.printStackTrace( System.err );
            }
        }
    }

    /**
     * {@link Sink}s messages to the JDK.
     */
    static final class JULSink
        implements Sink
    {
        // ----------------------------------------------------------------------
        // Implementation fields
        // ----------------------------------------------------------------------

        private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger( SISU );

        // ----------------------------------------------------------------------
        // Public methods
        // ----------------------------------------------------------------------

        public boolean isTraceEnabled()
        {
            return logger.isLoggable( Level.FINER );
        }

        public void trace( final String message, final Throwable cause )
        {
            logger.log( Level.FINER, message, cause );
        }

        public void warn( final String message, final Throwable cause )
        {
            logger.log( Level.WARNING, message, cause );
        }
    }

    /**
     * {@link Sink}s messages via SLF4J.
     */
    static final class SLF4JSink
        implements Sink
    {
        // ----------------------------------------------------------------------
        // Implementation fields
        // ----------------------------------------------------------------------

        private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger( SISU );

        // ----------------------------------------------------------------------
        // Public methods
        // ----------------------------------------------------------------------

        public boolean isTraceEnabled()
        {
            return logger.isTraceEnabled();
        }

        public void trace( final String message, final Throwable cause )
        {
            logger.trace( message, cause );
        }

        public void warn( final String message, final Throwable cause )
        {
            logger.warn( message, cause );
        }
    }
}
