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


import java.io.PrintStream;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.apache.felix.scr.impl.config.ScrConfiguration;
import org.apache.felix.utils.extender.AbstractExtender;
import org.apache.felix.utils.extender.Extension;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.service.component.ComponentConstants;
import org.osgi.service.log.LogService;
import org.osgi.util.tracker.ServiceTracker;


/**
 * This activator is used to cover requirement described in section 112.8.1 @@ -27,14
 * 37,202 @@ in active bundles.
 *
 */
public class Activator extends AbstractExtender
{
    //  name of the LogService class (this is a string to not create a reference to the class)
    static final String LOGSERVICE_CLASS = "org.osgi.service.log.LogService";

    // name of the PackageAdmin class (this is a string to not create a reference to the class)
    static final String PACKAGEADMIN_CLASS = "org.osgi.service.packageadmin.PackageAdmin";

    // Our configuration from bundle context properties and Config Admin
    private static ScrConfiguration m_configuration = new ScrConfiguration();

    // this bundle's context
    private static BundleContext m_context;

    // this bundle
    private static Bundle m_bundle;

    // the log service to log messages to
    private static volatile ServiceTracker m_logService;

    // the package admin service (see BindMethod.getParameterClass)
    private static volatile ServiceTracker m_packageAdmin;

    // map of BundleComponentActivator instances per Bundle indexed by Bundle id
    private Map<Long, BundleComponentActivator> m_componentBundles;

    // registry of managed component
    private ComponentRegistry m_componentRegistry;

    //  thread acting upon configurations
    private ComponentActorThread m_componentActor;

    public Activator() {
        setSynchronous(true);
    }

    /**
     * Registers this instance as a (synchronous) bundle listener and loads the
     * components of already registered bundles.
     *
     * @param context The <code>BundleContext</code> of the SCR implementation
     *      bundle.
     */
    public void start( BundleContext context ) throws Exception
    {
        m_context = context;
        m_bundle = context.getBundle();
        super.start(context);
    }

    protected void doStart() throws Exception {
        // require the log service
        m_logService = new ServiceTracker( m_context, LOGSERVICE_CLASS, null );
        m_logService.open();

        // prepare component registry
        m_componentBundles = new HashMap<Long, BundleComponentActivator>();
        m_componentRegistry = new ComponentRegistry( m_context );

        // get the configuration
        m_configuration.start( m_context );

        // log SCR startup
        log( LogService.LOG_INFO, m_bundle, " Version = {0}",
            new Object[] {m_bundle.getHeaders().get( Constants.BUNDLE_VERSION )}, null );

        // create and start the component actor
        m_componentActor = new ComponentActorThread();
        Thread t = new Thread(m_componentActor, "SCR Component Actor");
        t.setDaemon( true );
        t.start();

        super.doStart();

        // register the Gogo and old Shell commands
        ScrCommand scrCommand = ScrCommand.register(m_context, m_componentRegistry, m_configuration);
        m_configuration.setScrCommand( scrCommand );
    }


    /**
     * Unregisters this instance as a bundle listener and unloads all components
     * which have been registered during the active life time of the SCR
     * implementation bundle.
     */
    public void doStop() throws Exception
    {
        // stop tracking
        super.doStop();

        // dispose component registry
        m_componentRegistry.dispose();

        // terminate the actor thread
        if ( m_componentActor != null )
        {
            m_componentActor.terminate();
            m_componentActor = null;
        }

        // close the LogService tracker now
        if ( m_logService != null )
        {
            m_logService.close();
            m_logService = null;
        }

        // close the PackageAdmin tracker now
        if ( m_packageAdmin != null )
        {
            m_packageAdmin.close();
            m_packageAdmin = null;
        }

        // remove the reference to the component context
        m_context = null;
    }


    //---------- Component Management -----------------------------------------


    @Override
    protected Extension doCreateExtension(final Bundle bundle) throws Exception
    {
        return new ScrExtension(bundle);
    }

    protected class ScrExtension implements Extension {

        private final Bundle bundle;
        private final CountDownLatch started;

        public ScrExtension(Bundle bundle) {
            this.bundle = bundle;
            this.started = new CountDownLatch(1);
        }

        public void start() {
            try {
                loadComponents( ScrExtension.this.bundle );
            } finally {
                started.countDown();
            }
        }

        public void destroy() {
            try {
                this.started.await(m_configuration.stopTimeout(), TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
                log( LogService.LOG_WARNING, m_bundle, "The wait for bundle {0}/{1} being started before destruction has been interrupted.",
                        new Object[] {bundle.getSymbolicName(), bundle.getBundleId()}, e );
            }
            disposeComponents( this.bundle );
        }
    }

    /**
     * Loads the components of the given bundle. If the bundle has no
     * <i>Service-Component</i> header, this method has no effect. The
     * fragments of a bundle are not checked for the header (112.4.1).
     * <p>
     * This method calls the {@link Bundle#getBundleContext()} method to find
     * the <code>BundleContext</code> of the bundle. If the context cannot be
     * found, this method does not load components for the bundle.
     */
    private void loadComponents( Bundle bundle )
    {
        if ( bundle.getHeaders().get( "Service-Component" ) == null )
        {
            // no components in the bundle, abandon
            return;
        }

        // there should be components, load them with a bundle context
        BundleContext context = bundle.getBundleContext();
        if ( context == null )
        {
            log( LogService.LOG_ERROR, m_bundle, "Cannot get BundleContext of bundle {0}/{1}",
                new Object[] {bundle.getSymbolicName(), bundle.getBundleId()}, null );
            return;
        }

        // FELIX-1666 method is called for the LAZY_ACTIVATION event and
        // the started event. Both events cause this method to be called;
        // so we have to make sure to not load components twice
        // FELIX-2231 Mark bundle loaded early to prevent concurrent loading
        // if LAZY_ACTIVATION and STARTED event are fired at the same time
        final boolean loaded;
        final Long bundleId = bundle.getBundleId();
        synchronized ( m_componentBundles )
        {
            if ( m_componentBundles.containsKey( bundleId ) )
            {
                loaded = true;
            }
            else
            {
                m_componentBundles.put( bundleId, null );
                loaded = false;
            }
        }

        // terminate if already loaded (or currently being loaded)
        if ( loaded )
        {
            log( LogService.LOG_DEBUG, m_bundle, "Components for bundle {0}/{1} already loaded. Nothing to do.",
                new Object[] {bundle.getSymbolicName(), bundle.getBundleId()}, null );
            return;
        }

        try
        {
            BundleComponentActivator ga = new BundleComponentActivator( m_componentRegistry, m_componentActor, context,
                m_configuration );

            // replace bundle activator in the map
            synchronized ( m_componentBundles )
            {
                m_componentBundles.put( bundleId, ga );
            }
        }
        catch ( Exception e )
        {
            // remove the bundle id from the bundles map to ensure it is
            // not marked as being loaded
            synchronized ( m_componentBundles )
            {
                m_componentBundles.remove( bundleId );
            }

            if ( e instanceof IllegalStateException && bundle.getState() != Bundle.ACTIVE )
            {
                log(
                    LogService.LOG_DEBUG,
                    m_bundle,
                    "Bundle {0}/{1} has been stopped while trying to activate its components. Trying again when the bundles gets started again.",
                new Object[] {bundle.getSymbolicName(), bundle.getBundleId()},
                    e );
            }
            else
            {
                log( LogService.LOG_ERROR, m_bundle, "Error while loading components of bundle {0}/{1}",
                new Object[] {bundle.getSymbolicName(), bundle.getBundleId()}, e );
            }
        }
    }


    /**
     * Unloads components of the given bundle. If no components have been loaded
     * for the bundle, this method has no effect.
     */
    private void disposeComponents( Bundle bundle )
    {
        final Object ga;
        synchronized ( m_componentBundles )
        {
            ga = m_componentBundles.remove( bundle.getBundleId() );
        }

        if ( ga != null )
        {
            try
            {
                int reason = isStopping()
                        ? ComponentConstants.DEACTIVATION_REASON_DISPOSED
                        : ComponentConstants.DEACTIVATION_REASON_BUNDLE_STOPPED;
                ( ( BundleComponentActivator ) ga ).dispose( reason );
            }
            catch ( Exception e )
            {
                log( LogService.LOG_ERROR, m_bundle, "Error while disposing components of bundle {0}/{1}",
                    new Object[] {bundle.getSymbolicName(), bundle.getBundleId()}, e );
            }
        }
    }

    @Override
    protected void debug(Bundle bundle, String msg) {
        log( LogService.LOG_DEBUG, bundle, msg, null );
    }

    @Override
    protected void warn(Bundle bundle, String msg, Throwable t) {
        log( LogService.LOG_WARNING, bundle, msg, t );
    }

    @Override
    protected void error(String msg, Throwable t) {
        log( LogService.LOG_DEBUG, m_bundle, msg, t );
    }

    public static void log( int level, Bundle bundle, String pattern, Object[] arguments, Throwable ex )
    {
        if ( isLogEnabled( level ) )
        {
            final String message = MessageFormat.format( pattern, arguments );
            log( level, bundle, message, ex );
        }
    }

    /**
     * Returns <code>true</code> if logging for the given level is enabled.
     */
    public static boolean isLogEnabled( int level )
    {
        return m_configuration.getLogLevel() >= level;
    }

    /**
     * Method to actually emit the log message. If the LogService is available,
     * the message will be logged through the LogService. Otherwise the message
     * is logged to stdout (or stderr in case of LOG_ERROR level messages),
     *
     * @param level The log level to log the message at
     * @param message The message to log
     * @param ex An optional <code>Throwable</code> whose stack trace is written,
     *      or <code>null</code> to not log a stack trace.
     */
    public static void log( int level, Bundle bundle, String message, Throwable ex )
    {
        if ( isLogEnabled( level ) )
        {
            ServiceTracker t = m_logService;
            Object logger = ( t != null ) ? t.getService() : null;
            if ( logger == null )
            {
                // output depending on level
                PrintStream out = ( level == LogService.LOG_ERROR ) ? System.err : System.out;

                // level as a string
                StringBuffer buf = new StringBuffer();
                switch ( level )
                {
                    case ( LogService.LOG_DEBUG     ):
                        buf.append( "DEBUG: " );
                        break;
                    case ( LogService.LOG_INFO     ):
                        buf.append( "INFO : " );
                        break;
                    case ( LogService.LOG_WARNING     ):
                        buf.append( "WARN : " );
                        break;
                    case ( LogService.LOG_ERROR     ):
                        buf.append( "ERROR: " );
                        break;
                    default:
                        buf.append( "UNK  : " );
                        break;
                }

                // bundle information
                if ( bundle != null )
                {
                    buf.append( bundle.getSymbolicName() );
                    buf.append( " (" );
                    buf.append( bundle.getBundleId() );
                    buf.append( "): " );
                }

                // the message
                buf.append( message );

                // keep the message and the stacktrace together
                synchronized ( out)
                {
                    out.println( buf );
                    if ( ex != null )
                    {
                        ex.printStackTrace( out );
                    }
                }
            }
            else
            {
                ( ( LogService ) logger ).log( level, message, ex );
            }
        }
    }


    public static Object getPackageAdmin()
    {
        if ( m_packageAdmin == null )
        {
            synchronized ( Activator.class )
            {
                if ( m_packageAdmin == null )
                {
                    m_packageAdmin = new ServiceTracker( m_context, PACKAGEADMIN_CLASS, null );
                    m_packageAdmin.open();
                }
            }
        }

        return m_packageAdmin.getService();
    }
}