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


import java.util.Dictionary;
import java.util.Hashtable;

import org.apache.felix.scr.impl.Activator;
import org.apache.felix.scr.impl.ComponentCommands;
import org.apache.felix.scr.impl.manager.ScrConfiguration;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.log.LogService;
import org.osgi.service.metatype.MetaTypeProvider;


/**
 * The <code>ScrConfiguration</code> class conveys configuration for the
 * Felix DS implementation bundle.
 * <p>
 * <b>Configuration Source</b>
 * <p>
 * <ol>
 * <li>Framework properties: These are read when the Declarative Services
 * implementation is first started.</li>
 * <li>Configuration Admin Service: Properties are provided by means of a
 * <code>ManagedService</code> with Service PID
 * <code>org.apache.felix.scr.ScrService</code>. This class uses an OSGi
 * Service Factory ({@link ScrManagedServiceServiceFactory}) to register the
 * managed service without requiring the Configuration Admin Service API to be
 * required upfront.
 * </li>
 * </ol>
 * <p>
 * See the <i>Configuration</i> section of the
 * <a href="http://felix.apache.org/site/apache-felix-service-component-runtime.html">Apache Felix Service Component Runtime</a>
 * documentation page for detailed information.
 */
public class ScrConfigurationImpl implements ScrConfiguration
{

    private static final String VALUE_TRUE = Boolean.TRUE.toString();

    private static final String LOG_LEVEL_DEBUG = "debug";

    private static final String LOG_LEVEL_INFO = "info";

    private static final String LOG_LEVEL_WARN = "warn";

    private static final String LOG_LEVEL_ERROR = "error";

    private static final String PROP_SHOWTRACE = "ds.showtrace";

    private static final String PROP_SHOWERRORS = "ds.showerrors";

    private final Activator activator;

    private int logLevel;

    private boolean factoryEnabled;

    private boolean keepInstances;

    private boolean infoAsService;

    private boolean cacheMetadata;

    private long lockTimeout = DEFAULT_LOCK_TIMEOUT_MILLISECONDS;

    private long stopTimeout = DEFAULT_STOP_TIMEOUT_MILLISECONDS;

    private long serviceChangecountTimeout = DEFAULT_SERVICE_CHANGECOUNT_TIMEOUT_MILLISECONDS;

    private Boolean globalExtender;

    private volatile BundleContext bundleContext;

    private volatile ServiceRegistration<?> managedServiceRef;

    private volatile ServiceRegistration<?> metatypeProviderRef;

    private ComponentCommands scrCommand;

    public ScrConfigurationImpl(Activator activator )
    {
        this.activator = activator;
    }

    public void start(final BundleContext bundleContext)
    {
        this.bundleContext = bundleContext;

        // listen for Configuration Admin configuration
        final Dictionary<String, Object> msProps = new Hashtable<>();
        msProps.put(Constants.SERVICE_PID, PID);
        msProps.put(Constants.SERVICE_DESCRIPTION, "SCR Configurator");
        msProps.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");


        // Process configure from bundle context properties so they can be predictably
        // overriden by configuration admin later.
        // Note that if the managed service is registered first then it is random which will win since
        // configuration may be delivered asynchronously
        configure( null, false );

        managedServiceRef = bundleContext.registerService("org.osgi.service.cm.ManagedService", new ScrManagedServiceServiceFactory(this),
                msProps);

        final Dictionary<String, Object> mtProps = new Hashtable<>();
        mtProps.put(MetaTypeProvider.METATYPE_PID, PID);
        mtProps.put(Constants.SERVICE_DESCRIPTION, "SCR Configurator MetaTypeProvider");
        mtProps.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");

        metatypeProviderRef = bundleContext.registerService("org.osgi.service.metatype.MetaTypeProvider", new ScrMetaTypeProviderServiceFactory(this),
                mtProps);
    }

    public void stop()
    {
        if (this.managedServiceRef != null)
        {
            this.managedServiceRef.unregister();
            this.managedServiceRef = null;
        }

        if (this.metatypeProviderRef != null)
        {
            this.metatypeProviderRef.unregister();
            this.metatypeProviderRef = null;
        }

        this.bundleContext = null;
    }

    public void setScrCommand(ComponentCommands scrCommand)
    {
        this.scrCommand = scrCommand;
        scrCommand.updateProvideScrInfoService(infoAsService());
    }

    // Called from the ScrManagedService.updated method to reconfigure
    void configure( Dictionary<String, ?> config, boolean fromConfig )
    {
        Boolean newGlobalExtender;
        Boolean oldGlobalExtender;
        synchronized (this)
        {
            if ( config == null )
            {
                if (!fromConfig)
                {
                    if (this.bundleContext == null)
                    {
                        logLevel = LogService.LOG_ERROR;
                        factoryEnabled = false;
                        keepInstances = false;
                        infoAsService = false;
                        lockTimeout = DEFAULT_LOCK_TIMEOUT_MILLISECONDS;
                        stopTimeout = DEFAULT_STOP_TIMEOUT_MILLISECONDS;
                        serviceChangecountTimeout = DEFAULT_SERVICE_CHANGECOUNT_TIMEOUT_MILLISECONDS;
                        newGlobalExtender = false;
                        cacheMetadata = false;
                    }
                    else
                    {
                        logLevel = getDefaultLogLevel();
                        factoryEnabled = getDefaultFactoryEnabled();
                        keepInstances = getDefaultKeepInstances();
                        infoAsService = getDefaultInfoAsService();
                        lockTimeout = getDefaultLockTimeout();
                        stopTimeout = getDefaultStopTimeout();
                        serviceChangecountTimeout = getServiceChangecountTimeout();
                        newGlobalExtender = getDefaultGlobalExtender();
                        cacheMetadata = getDefaultCacheMetadata();
                    }
                }
                else
                {
                    newGlobalExtender = this.globalExtender;
                }
            }
            else
            {
                logLevel = getLogLevel( config.get( PROP_LOGLEVEL ) );
                factoryEnabled = VALUE_TRUE.equalsIgnoreCase( String.valueOf( config.get( PROP_FACTORY_ENABLED ) ) );
                keepInstances = VALUE_TRUE.equalsIgnoreCase( String.valueOf( config.get( PROP_DELAYED_KEEP_INSTANCES ) ) );
                infoAsService = VALUE_TRUE.equalsIgnoreCase( String.valueOf( config.get( PROP_INFO_SERVICE) ) );
                Long timeout = ( Long ) config.get( PROP_LOCK_TIMEOUT );
                lockTimeout = timeout == null? DEFAULT_LOCK_TIMEOUT_MILLISECONDS: timeout;
                timeout = ( Long ) config.get( PROP_STOP_TIMEOUT );
                stopTimeout = timeout == null? DEFAULT_STOP_TIMEOUT_MILLISECONDS: timeout;
                newGlobalExtender = VALUE_TRUE.equalsIgnoreCase( String.valueOf( config.get( PROP_GLOBAL_EXTENDER) ) );
                cacheMetadata = VALUE_TRUE.equalsIgnoreCase(
                    String.valueOf(config.get(PROP_CACHE_METADATA)));
            }
            if ( scrCommand != null )
            {
                scrCommand.updateProvideScrInfoService( infoAsService() );
            }
            oldGlobalExtender = this.globalExtender;
            this.globalExtender = newGlobalExtender;
        }
        if ( newGlobalExtender != oldGlobalExtender )
        {
            activator.restart( newGlobalExtender );
        }
    }

    /**
     * Returns the current log level.
     * Note that this log level is not used with an R7 LogService implementation.
     * @return
     */
    @Override
    public int getLogLevel()
    {
        return logLevel;
    }


    @Override
    public boolean isFactoryEnabled()
    {
        return factoryEnabled;
    }


    @Override
    public boolean keepInstances()
    {
        return keepInstances;
    }

    @Override
    public boolean infoAsService()
    {
        return infoAsService;
    }

    @Override
    public long lockTimeout()
    {
        return lockTimeout;
    }

    @Override
    public long stopTimeout()
    {
        return stopTimeout;
    }

    @Override
    public boolean globalExtender()
    {
        return globalExtender;
    }

    @Override
    public boolean cacheMetadata()
    {
        return cacheMetadata;
    }

    @Override
    public long serviceChangecountTimeout()
    {
        return serviceChangecountTimeout;
    }

    private boolean getDefaultFactoryEnabled()
    {
        return VALUE_TRUE.equals( bundleContext.getProperty( PROP_FACTORY_ENABLED ) );
    }


    private boolean getDefaultKeepInstances()
    {
        return VALUE_TRUE.equals( bundleContext.getProperty( PROP_DELAYED_KEEP_INSTANCES ) );
    }


    private int getDefaultLogLevel()
    {
        return getLogLevel( bundleContext.getProperty( PROP_LOGLEVEL ) );
    }

    private boolean getDefaultInfoAsService()
    {
        return VALUE_TRUE.equalsIgnoreCase( bundleContext.getProperty( PROP_INFO_SERVICE) );
    }

    private long getDefaultLockTimeout()
    {
        String val = bundleContext.getProperty( PROP_LOCK_TIMEOUT);
        if ( val == null)
        {
            return DEFAULT_LOCK_TIMEOUT_MILLISECONDS;
        }
        return Long.parseLong( val );
    }

    private long getDefaultStopTimeout()
    {
        String val = bundleContext.getProperty( PROP_STOP_TIMEOUT);
        if ( val == null)
        {
            return DEFAULT_STOP_TIMEOUT_MILLISECONDS;
        }
        return Long.parseLong( val );
    }

    private long getServiceChangecountTimeout()
    {
        String val = bundleContext.getProperty( PROP_SERVICE_CHANGECOUNT_TIMEOUT );
        if ( val == null)
        {
            return DEFAULT_SERVICE_CHANGECOUNT_TIMEOUT_MILLISECONDS;
        }
        return Long.parseLong( val );
    }

    private boolean getDefaultGlobalExtender()
    {
        return VALUE_TRUE.equalsIgnoreCase( bundleContext.getProperty( PROP_GLOBAL_EXTENDER) );
    }

    private boolean getDefaultCacheMetadata()
    {
        return VALUE_TRUE.equalsIgnoreCase(
            bundleContext.getProperty(PROP_CACHE_METADATA));
    }

    private int getLogLevel( final Object levelObject )
    {
        if ( levelObject != null )
        {
            if ( levelObject instanceof Number )
            {
                return ( ( Number ) levelObject ).intValue();
            }

            String levelString = levelObject.toString();
            try
            {
                return Integer.parseInt( levelString );
            }
            catch ( NumberFormatException nfe )
            {
                // might be a descriptive name
            }

            if ( LOG_LEVEL_DEBUG.equalsIgnoreCase( levelString ) )
            {
                return LogService.LOG_DEBUG;
            }
            else if ( LOG_LEVEL_INFO.equalsIgnoreCase( levelString ) )
            {
                return LogService.LOG_INFO;
            }
            else if ( LOG_LEVEL_WARN.equalsIgnoreCase( levelString ) )
            {
                return LogService.LOG_WARNING;
            }
            else if ( LOG_LEVEL_ERROR.equalsIgnoreCase( levelString ) )
            {
                return LogService.LOG_ERROR;
            }
        }

        // check ds.showtrace property
        if ( VALUE_TRUE.equalsIgnoreCase( bundleContext.getProperty( PROP_SHOWTRACE ) ) )
        {
            return LogService.LOG_DEBUG;
        }

        // next check ds.showerrors property
        if ( "false".equalsIgnoreCase( bundleContext.getProperty( PROP_SHOWERRORS ) ) )
        {
            return -1; // no logging at all !!
        }

        // default log level (errors only)
        return LogService.LOG_ERROR;
    }
}
