/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.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://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.felix.cm.impl;

import java.text.MessageFormat;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceReference;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.log.LogService;
import org.osgi.util.tracker.ServiceTracker;

/**
 * Log implementation either logging to a {@code LogService} or to {@code System.err}.
 * The logger can be get using the static {@link #logger} field.
 *
 * The logger is initialized through {@link #start(BundleContext)} and {@link #set(ServiceReference)}.
 * It gets cleaned up through {@link #stop()}.
 */
public class Log
{
    /** The shared logger instance. */
    public static final Log logger = new Log();

    /**
     * The name of the bundle context property defining the maximum log level
     * (value is "felix.cm.loglevel"). The log level setting is only used if
     * there is no OSGi LogService available. Otherwise this setting is ignored.
     * <p>
     * This value of this property is expected to be an integer number
     * corresponding to the log level values of the OSGi LogService. That is 1
     * for errors, 2 for warnings, 3 for informational messages and 4 for debug
     * messages. The default value is 2, such that only warnings and errors are
     * logged in the absence of a LogService.
     */
    private static final String CM_LOG_LEVEL = "felix.cm.loglevel";

    // The name of the LogService (not using the class, which might be missing)
    private static final String LOG_SERVICE_NAME = "org.osgi.service.log.LogService";

    private static final int CM_LOG_LEVEL_DEFAULT = 2;

    // the ServiceTracker to emit log services (see log(int, String, Throwable))
    @SuppressWarnings("rawtypes")
    private volatile ServiceTracker logTracker;

    // the maximum log level when no LogService is available
    private volatile int logLevel = CM_LOG_LEVEL_DEFAULT;

    private volatile ServiceReference<ConfigurationAdmin> serviceReference;

    /**
     * Start the tracker for the logger and set the log level according to the configuration.
     * @param bundleContext The bundle context
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public void start( final BundleContext bundleContext)
    {
        // track the log service using a ServiceTracker
        logTracker = new ServiceTracker( bundleContext, LOG_SERVICE_NAME , null );
        logTracker.open();

        // assign the log level
        String logLevelProp = bundleContext.getProperty( CM_LOG_LEVEL );
        if ( logLevelProp == null )
        {
            logLevel = CM_LOG_LEVEL_DEFAULT;
        }
        else
        {
            try
            {
                logLevel = Integer.parseInt( logLevelProp );
            }
            catch ( NumberFormatException nfe )
            {
                logLevel = CM_LOG_LEVEL_DEFAULT;
            }
        }
    }

    /**
     * Set the service reference to the configuration admin in order to include this
     * in every log message.
     * @param ref The service reference
     */
    public void set(final ServiceReference<ConfigurationAdmin> ref)
    {
        this.serviceReference = ref;
    }

    /**
     * Stop the log service tracker and clear the service reference
     */
    public void stop()
    {
        if ( logTracker != null )
        {
            logTracker.close();
            logTracker = null;
        }
        serviceReference = null;
    }

    /**
     * Is the log level enabled?
     * @param level The level
     * @return {@code true} if enabled
     */
    public boolean isLogEnabled( final int level )
    {
        return level <= logLevel;
    }

    /**
     * Log a message in the given level.
     * If arguments are provided and contain a {@code ServiceReference} then
     * the argument is replaced with the result of {@link #toString(ServiceReference)}.
     *
     * @param level The log level
     * @param format The message text
     * @param args The optional arguments
     */
    public void log( final int level, final String format, final Object[] args )
    {
        @SuppressWarnings("rawtypes")
        final ServiceTracker tracker = this.logTracker;
        final Object log = tracker == null ? null : tracker.getService();
        if ( log != null || isLogEnabled( level ) )
        {
            Throwable throwable = null;
            String message = format;

            if ( args != null && args.length > 0 )
            {
                for(int i=0; i<args.length; i++)
                {
                    if ( args[i] instanceof ServiceReference )
                    {
                        args[i] = toString((ServiceReference<?>)args[i]);
                    }
                }
                if ( args[args.length - 1] instanceof Throwable )
                {
                    throwable = ( Throwable ) args[args.length - 1];
                }
                message = MessageFormat.format( format, args );
            }

            log( level, message, throwable );
        }
    }

    /**
     * Log the message with the given level and throwable.
     * @param level The log level
     * @param message The message
     * @param t The exception
     */
    public void log( final int level, final String message, final Throwable t )
    {
        // log using the LogService if available
        @SuppressWarnings("rawtypes")
        final ServiceTracker tracker = this.logTracker;
        final Object log = tracker == null ? null : tracker.getService();
        if ( log != null )
        {
            ( ( LogService ) log ).log( serviceReference, level, message, t );
            return;
        }

        // Otherwise only log if more serious than the configured level
        if ( isLogEnabled( level ) )
        {
            String code;
            switch ( level )
            {
                case LogService.LOG_INFO:
                    code = "*INFO *";
                    break;

                case LogService.LOG_WARNING:
                    code = "*WARN *";
                    break;

                case LogService.LOG_ERROR:
                    code = "*ERROR*";
                    break;

                case LogService.LOG_DEBUG:
                default:
                    code = "*DEBUG*";
            }

            System.err.println( code + " " + message );
            if ( t != null )
            {
                t.printStackTrace( System.err );
            }
        }
    }

    /**
     * Create a string representation of the service reference
     * @param ref The service reference
     * @return The string representation
     */
    private static String toString( final ServiceReference<?> ref )
    {
        String[] ocs = ( String[] ) ref.getProperty( "objectClass" );
        StringBuilder buf = new StringBuilder( "[" );
        for ( int i = 0; i < ocs.length; i++ )
        {
            buf.append( ocs[i] );
            if ( i < ocs.length - 1 )
                buf.append( ", " );
        }

        buf.append( ", id=" ).append( ref.getProperty( Constants.SERVICE_ID ) );

        Bundle provider = ref.getBundle();
        if ( provider != null )
        {
            buf.append( ", bundle=" ).append( provider.getBundleId() );
            buf.append( '/' ).append( Activator.getLocation(provider) );
        }
        else
        {
            buf.append( ", unregistered" );
        }

        buf.append( "]" );
        return buf.toString();
    }
}

