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

import java.io.IOException;
import java.security.AccessController;
import java.security.DomainCombiner;
import java.security.Permission;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.felix.scr.impl.ComponentRegistry;
import org.apache.felix.scr.impl.logger.ScrLogger;
import org.apache.felix.scr.impl.metadata.TargetedPID;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.cm.ConfigurationEvent;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ConfigurationListener;
import org.osgi.service.cm.ConfigurationPermission;
import org.osgi.service.cm.ManagedService;
import org.osgi.service.log.LogService;

public abstract class RegionConfigurationSupport
{

    private final ScrLogger logger;
    private final ServiceReference<ConfigurationAdmin> caReference;
    private final BundleContext caBundleContext;
    private final Long bundleId;

    private final AtomicInteger referenceCount = new AtomicInteger( 1 );

    // the service registration of the ConfigurationListener service
    private volatile ServiceRegistration<ConfigurationListener> m_registration;

    /**
     *
     * @param bundleContext of the ConfigurationAdmin we are tracking
     * @param registry
     */
    public RegionConfigurationSupport(final ScrLogger logger, final ServiceReference<ConfigurationAdmin> reference)
    {
        this.logger = logger;
        this.caReference = reference;
        final Bundle bundle = reference.getBundle();
        this.bundleId = bundle.getBundleId();
        this.caBundleContext = bundle.getBundleContext();
    }

    public void start()
    {
        // register as listener for configurations
        final Dictionary<String, Object> props = new Hashtable<>();
        props.put( Constants.SERVICE_DESCRIPTION, "Declarative Services Configuration Support Listener" );
        props.put( Constants.SERVICE_VENDOR, "The Apache Software Foundation" );

        // If RegionConfigurationSupport *directly* implements ConfigurationListener then we get NoClassDefFoundError
        // when SCR is started without a wiring to an exporter of Config Admin API. This construction allows the
        // class loading exception to be caught and confined.
        final ConfigurationListener serviceDelegator;
        if ( System.getSecurityManager() != null ) {
            serviceDelegator = new ConfigurationListener()
            {
                @Override
                public void configurationEvent(final ConfigurationEvent event)
                {
                    AccessController.doPrivileged(
                        new PrivilegedAction<Object>()
                        {
                            @Override
                            public Void run()
                            {
                                RegionConfigurationSupport.this.configurationEvent(event);
                                return null;
                            }
                        });
                }
            };
        }
        else
        {
            serviceDelegator = new ConfigurationListener()
            {

                @Override
                public void configurationEvent(final ConfigurationEvent event)
                {
                    RegionConfigurationSupport.this.configurationEvent(event);
                }
            };
        }
        this.m_registration = caBundleContext.registerService(ConfigurationListener.class, serviceDelegator, props );
    }

    public Long getBundleId()
    {
        return bundleId;
    }

    public boolean reference()
    {
        if ( referenceCount.get() == 0 )
        {
            return false;
        }
        referenceCount.incrementAndGet();
        return true;
    }

    public boolean dereference()
    {
        if ( referenceCount.decrementAndGet() == 0 )
        {
            this.m_registration.unregister();
            this.m_registration = null;
            return true;
        }
        return false;
    }

    /**
     * The return value is only relevant for the call from {@link #configurationEvent(ConfigurationEvent)}
     * in the case of a deleted configuration which is not a factory configuration!
     */
    public boolean configureComponentHolder(final ComponentHolder<?> holder)
    {

        // 112.7 configure unless configuration not required
        if ( !holder.getComponentMetadata().isConfigurationIgnored() )
        {
            final BundleContext bundleContext = holder.getActivator().getBundleContext();
            if ( bundleContext == null )
            {
                return false;// bundle was stopped concurrently with configuration deletion
            }
            final List<String> confPids = holder.getComponentMetadata().getConfigurationPid();

            final ConfigurationAdmin ca = getConfigAdmin( bundleContext );
            try
            {
                for ( final String confPid : confPids )
                {
                    final Collection<Configuration> factory = findFactoryConfigurations( ca, confPid,
                        bundleContext.getBundle() );
                    if ( !factory.isEmpty() )
                    {
                        boolean created = false;
                        for ( Configuration config : factory )
                        {
                            logger.log( LogService.LOG_DEBUG,
                                "Configuring holder {0} with factory configuration {1}, change count {2}", null,
                                holder, config, config.getChangeCount() );
                            if ( checkBundleLocation( config, bundleContext.getBundle() ) )
                            {
                                long changeCount = config.getChangeCount();
                                ServiceReference<ManagedService> ref = getManagedServiceReference(bundleContext);
                                created |= holder.configurationUpdated( new TargetedPID( config.getPid() ),
                                    new TargetedPID( config.getFactoryPid() ), config.getProcessedProperties(ref), changeCount );
                            }
                        }
                        if ( !created )
                        {
                            return false;
                        }
                    }
                    else
                    {
                        // check for configuration and configure the holder
                        Configuration singleton = findSingletonConfiguration( ca, confPid, bundleContext.getBundle() );
                        if ( singleton != null )
                        {
                            logger.log( LogService.LOG_DEBUG,
                                "Configuring holder {0} with configuration {1}, change count {2}", null,
                                holder, singleton, singleton.getChangeCount() );
                            if ( singleton != null && checkBundleLocation( singleton, bundleContext.getBundle() ) )
                            {
                                long changeCount = singleton.getChangeCount();
                                ServiceReference<ManagedService> ref = getManagedServiceReference(bundleContext);
                                holder.configurationUpdated( new TargetedPID( singleton.getPid() ), null,
                                    singleton.getProcessedProperties(ref), changeCount );
                            }
                            else
                            {
                                return false;
                            }
                        }
                        else
                        {
                            return false;
                        }
                    }
                }
                return !confPids.isEmpty();
            }
            finally
            {
                try
                {
                    bundleContext.ungetService( caReference );
                }
                catch ( IllegalStateException e )
                {
                    // ignore, bundle context was shut down during the above.
                }
            }
        }
        return false;
    }

    // ---------- ConfigurationListener

    /**
     * Called by the Configuration Admin service if a configuration is updated
     * or removed.
     * <p>
     * This method is really only called upon configuration changes; it is not
     * called for existing configurations upon startup of the Configuration
     * Admin service. To bridge this gap, the
     * {@link ComponentRegistry#serviceChanged(org.osgi.framework.ServiceEvent)} method called when the
     * Configuration Admin service is registered calls #configureComponentHolders which calls this method for all
     * existing configurations to be able to forward existing configurations to
     * components.
     *
     * @param event The configuration change event
     */
    public void configurationEvent(ConfigurationEvent event)
    {
        final TargetedPID pid = new TargetedPID( event.getPid() );
        String rawFactoryPid = event.getFactoryPid();
        final TargetedPID factoryPid = rawFactoryPid == null? null: new TargetedPID( rawFactoryPid );

        // iterate over all components which must be configured with this pid
        // (since DS 1.2, components may specify a specific configuration PID (112.4.4 configuration-pid)
        Collection<ComponentHolder<?>> holders = getComponentHolders( factoryPid != null? factoryPid: pid );

        logger.log( LogService.LOG_DEBUG,
            "configurationEvent: Handling {0} of Configuration PID={1} for component holders {2}", null,
            getEventType( event ), pid, holders );

        for ( ComponentHolder<?> componentHolder : holders )
        {
            if ( !componentHolder.getComponentMetadata().isConfigurationIgnored() )
            {
                switch (event.getType())
                {
                    case ConfigurationEvent.CM_DELETED:
                        if ( factoryPid != null || !configureComponentHolder( componentHolder ) )
                        {
                            componentHolder.configurationDeleted( pid, factoryPid );
                        }
                        break;

                    case ConfigurationEvent.CM_UPDATED:
                    {
                        final ComponentActivator activator = componentHolder.getActivator();
                        if ( activator == null )
                        {
                            break;
                        }

                        final BundleContext bundleContext = activator.getBundleContext();
                        if ( bundleContext == null )
                        {
                            break;
                        }

                        TargetedPID targetedPid = factoryPid == null? pid: factoryPid;
                        TargetedPID oldTargetedPID = componentHolder.getConfigurationTargetedPID( pid, factoryPid );
                        if ( factoryPid != null || targetedPid.equals( oldTargetedPID )
                            || targetedPid.bindsStronger( oldTargetedPID ) )
                        {
                            final ConfigurationInfo configInfo = getConfigurationInfo( pid, targetedPid,
                                componentHolder, bundleContext );
                            if ( configInfo != null )
                            {
                                if ( checkBundleLocation( configInfo.getBundleLocation(), bundleContext.getBundle() ) )
                                {
                                    // The below seems to be unnecessary - and if put in, the behaviour is not spec compliant anymore:
                                    // if a component has a required configuration and a modified method, the component must not be
                                    // reactivated
                                    // If this is replacing a weaker targetedPID delete the old one.
                                    // if ( factoryPid == null && !targetedPid.equals(oldTargetedPID) && oldTargetedPID != null)
                                    //{
                                    //componentHolder.configurationDeleted( pid, factoryPid );
                                    //}
                                    componentHolder.configurationUpdated( pid, factoryPid, configInfo.getProps(),
                                        configInfo.getChangeCount() );
                                }
                            }
                        }

                        break;
                    }
                    case ConfigurationEvent.CM_LOCATION_CHANGED:
                    {
                        //TODO is this logic correct for factory pids????
                        final ComponentActivator activator = componentHolder.getActivator();
                        if ( activator == null )
                        {
                            break;
                        }

                        final BundleContext bundleContext = activator.getBundleContext();
                        if ( bundleContext == null )
                        {
                            break;
                        }

                        TargetedPID targetedPid = factoryPid == null? pid: factoryPid;
                        TargetedPID oldTargetedPID = componentHolder.getConfigurationTargetedPID( pid, factoryPid );
                        if ( targetedPid.equals( oldTargetedPID ) )
                        {
                            //this sets the location to this component's bundle if not already set.  OK here
                            //since it used to be set to this bundle, ok to reset it
                            final ConfigurationInfo configInfo = getConfigurationInfo( pid, targetedPid,
                                componentHolder, bundleContext );
                            if ( configInfo != null )
                            {
                                logger.log( LogService.LOG_DEBUG,
                                    "LocationChanged event, same targetedPID {0}, location now {1}, change count {2}", null,
                                    targetedPid, configInfo.getBundleLocation(),
                                            configInfo.getChangeCount() );
                                if ( configInfo.getProps() == null )
                                {
                                    throw new IllegalStateException( "Existing Configuration with pid " + pid
                                        + " has had its properties set to null and location changed.  We expected a delete event first." );
                                }
                                //this config was used on this component.  Does it still match?
                                if ( !checkBundleLocation( configInfo.getBundleLocation(), bundleContext.getBundle() ) )
                                {
                                    //no, delete it
                                    componentHolder.configurationDeleted( pid, factoryPid );
                                    //maybe there's another match
                                    configureComponentHolder( componentHolder );

                                }
                                //else still matches
                            }
                            break;
                        }
                        boolean better = targetedPid.bindsStronger( oldTargetedPID );
                        if ( better )
                        {
                            //this sets the location to this component's bundle if not already set.  OK here
                            //because if it is set to this bundle we will use it.
                            final ConfigurationInfo configInfo = getConfigurationInfo( pid, targetedPid,
                                componentHolder, bundleContext );
                            if ( configInfo != null )
                            {
                                logger.log( LogService.LOG_DEBUG,
                                    "LocationChanged event, better targetedPID {0} compared to {1}, location now {2}, change count {3}", null,
                                     targetedPid, oldTargetedPID, configInfo.getBundleLocation(),
                                            configInfo.getChangeCount());
                                if ( configInfo.getProps() == null )
                                {
                                    //location has been changed before any properties are set.  We don't care.  Wait for an updated event with the properties
                                    break;
                                }
                                //this component was not configured with this config.  Should it be now?
                                if ( checkBundleLocation( configInfo.getBundleLocation(), bundleContext.getBundle() ) )
                                {
                                    if ( oldTargetedPID != null )
                                    {
                                        //this is a better match, delete old before setting new
                                        componentHolder.configurationDeleted( pid, factoryPid );
                                    }
                                    componentHolder.configurationUpdated( pid, factoryPid, configInfo.getProps(),
                                        configInfo.getChangeCount() );
                                }
                            }
                        }
                        //else worse match, do nothing
                        else
                        {
                            logger.log( LogService.LOG_DEBUG,
                                "LocationChanged event, worse targetedPID {0} compared to {1}, do nothing", null,
                                targetedPid, oldTargetedPID  );
                        }
                        break;
                    }
                    default:
                        logger.log( LogService.LOG_WARNING, "Unknown ConfigurationEvent type {0}", null,
                            event.getType() );
                }
            }
        }
    }

    protected abstract Collection<ComponentHolder<?>> getComponentHolders(TargetedPID pid);

    private String getEventType(ConfigurationEvent event)
    {
        switch (event.getType())
        {
            case ConfigurationEvent.CM_UPDATED:
                return "UPDATED";
            case ConfigurationEvent.CM_DELETED:
                return "DELETED";
            case ConfigurationEvent.CM_LOCATION_CHANGED:
                return "LOCATION CHANGED";
            default:
                return "Unkown event type: " + event.getType();
        }

    }

    private static class ConfigurationInfo
    {
        private final Dictionary<String, Object> props;
        private final String bundleLocation;
        private final long changeCount;

        public ConfigurationInfo(Dictionary<String, Object> props, String bundleLocation, long changeCount)
        {
            this.props = props;
            this.bundleLocation = bundleLocation;
            this.changeCount = changeCount;
        }

        public long getChangeCount()
        {
            return changeCount;
        }

        public Dictionary<String, Object> getProps()
        {
            return props;
        }

        public String getBundleLocation()
        {
            return bundleLocation;
        }

    }

    /**
     * This gets config admin, gets the requested configuration, extracts the info we need from it, and ungets config admin.
     * Some versions of felix config admin do not allow access to configurations after the config admin instance they were obtained from
     * are ungot.  Extracting the info we need into "configInfo" solves this problem.
     *
     * @param pid TargetedPID for the desired configuration
     * @param targetedPid the targeted factory pid for a factory configuration or the pid for a singleton configuration
     * @param componentHolder ComponentHolder that holds the old change count (for r4 fake change counting)
     * @param bundleContext BundleContext to get the CA from
     * @return ConfigurationInfo object containing the info we need from the configuration.
     */
    private ConfigurationInfo getConfigurationInfo(final TargetedPID pid, TargetedPID targetedPid,
        ComponentHolder<?> componentHolder, final BundleContext bundleContext)
    {
        try
        {
            final ConfigurationAdmin ca = getConfigAdmin( bundleContext );
            try
            {
                Configuration[] configs = ca.listConfigurations( filter( pid.getRawPid() ) );
                if ( configs != null && configs.length > 0 )
                {
                    Configuration config = configs[0];
                    ServiceReference<ManagedService> ref = getManagedServiceReference(bundleContext);
                    return new ConfigurationInfo( config.getProcessedProperties(ref), config.getBundleLocation(),
                        config.getChangeCount() );
                }
            }
            catch ( IOException e )
            {
                logger.log( LogService.LOG_WARNING, "Failed reading configuration for pid={0}", e, pid);
            }
            catch ( InvalidSyntaxException e )
            {
                logger.log( LogService.LOG_WARNING, "Failed reading configuration for pid={0}", e, pid);
            }
            finally
            {
                bundleContext.ungetService( caReference );
            }
        }
        catch ( IllegalStateException ise )
        {
            // If the bundle has been stopped concurrently
            logger.log( LogService.LOG_WARNING, "Bundle in unexpected state", ise );
        }
        return null;
    }

    private ServiceReference<ManagedService> getManagedServiceReference(BundleContext bundleContext)
    {
        try {
            Collection<ServiceReference<ManagedService>> refs = bundleContext.getServiceReferences(ManagedService.class,
                    "(&(service.bundleid=" + String.valueOf(bundleContext.getBundle().getBundleId()) + ")(!(service.pid=*)))");
            if ( !refs.isEmpty() ) {
                return refs.iterator().next();
            }
        } catch (InvalidSyntaxException e) {
            // this should never happen,
        }
        return bundleContext.registerService(ManagedService.class, new ManagedService() {

            @Override
            public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
                // nothing to do

            }
        }, null).getReference();
    }

    private String filter(String rawPid)
    {
        return "(service.pid=" + rawPid + ")";
    }

    /**
     * Returns the configuration whose PID equals the given pid. If no such
     * configuration exists, <code>null</code> is returned.
     *
     * @param ca Configuration Admin service
     * @param pid Pid for desired configuration
     * @param bundle bundle of the component we are configuring (used in targeted pids)
     * @return configuration with the specified Pid
     */
    public Configuration findSingletonConfiguration(final ConfigurationAdmin ca, final String pid, Bundle bundle)
    {
        final String filter = getTargetedPidFilter( pid, bundle, Constants.SERVICE_PID );
        final Configuration[] cfg = findConfigurations( ca, filter );
        if ( cfg == null )
        {
            return null;
        }
        String longest = null;
        Configuration best = null;
        for ( Configuration config : cfg )
        {
            if ( checkBundleLocation( config, bundle ) )
            {
                String testPid = config.getPid();
                if ( longest == null || testPid.length() > longest.length() )
                {
                    longest = testPid;
                    best = config;
                }
            }

        }
        return best;
    }

    /**
     * Returns all configurations whose factory PID equals the given factory PID
     * or <code>null</code> if no such configurations exist
     *
     * @param ca ConfigurationAdmin service
     * @param factoryPid factory Pid we want the configurations for
     * @param bundle bundle we're working for (for location and location permission)
     * @return the configurations specifying the supplied factory Pid.
     */
    private Collection<Configuration> findFactoryConfigurations(final ConfigurationAdmin ca, final String factoryPid,
        Bundle bundle)
    {
        final String filter = getTargetedPidFilter( factoryPid, bundle, ConfigurationAdmin.SERVICE_FACTORYPID );
        Configuration[] configs = findConfigurations( ca, filter );
        if ( configs == null )
        {
            return Collections.emptyList();
        }
        Map<String, Configuration> configsByPid = new HashMap<>();
        for ( Configuration config : configs )
        {
            if ( checkBundleLocation( config, bundle ) )
            {
                Configuration oldConfig = configsByPid.get( config.getPid() );
                if ( oldConfig == null )
                {
                    configsByPid.put( config.getPid(), config );
                }
                else
                {
                    String newPid = config.getFactoryPid();
                    String oldPid = oldConfig.getFactoryPid();
                    if ( newPid.length() > oldPid.length() )
                    {
                        configsByPid.put( config.getPid(), config );
                    }
                }
            }
        }
        return configsByPid.values();
    }

    private boolean checkBundleLocation(Configuration config, Bundle bundle)
    {
        if ( config == null )
        {
            return false;
        }
        String configBundleLocation = config.getBundleLocation();
        return checkBundleLocation( configBundleLocation, bundle );
    }

    private boolean checkBundleLocation(String configBundleLocation, Bundle bundle)
    {
        boolean result;
        if ( configBundleLocation == null )
        {
            result = true;
        }
        else if ( configBundleLocation.startsWith( "?" ) )
        {
            //multilocation
            result = bundle.hasPermission(
                new ConfigurationPermission( configBundleLocation, ConfigurationPermission.TARGET ) );
        }
        else
        {
            result = configBundleLocation.equals( bundle.getLocation() );
        }
        logger.log( LogService.LOG_DEBUG, "checkBundleLocation: location {0}, returning {1}", null,
            configBundleLocation, result );
        return result;
    }

    private Configuration[] findConfigurations(final ConfigurationAdmin ca, final String filter)
    {
        try
        {
            return ca.listConfigurations( filter );
        }
        catch ( IOException ioe )
        {
            logger.log( LogService.LOG_WARNING, "Problem listing configurations for filter={0}", ioe,
                filter  );
        }
        catch ( InvalidSyntaxException ise )
        {
            logger.log( LogService.LOG_ERROR, "Invalid Configuration selection filter {0}", ise, filter);
        }

        // no factories in case of problems
        return null;
    }

    private String getTargetedPidFilter(String pid, Bundle bundle, String key)
    {
        String bsn = bundle.getSymbolicName();
        String version = bundle.getVersion().toString();
        String location = escape( bundle.getLocation() );

        StringBuilder sb = new StringBuilder();

        sb.append("(|(");

        sb.append(key);
        sb.append('=');
        sb.append(pid);

        sb.append(")(");

        sb.append(key);
        sb.append('=');
        sb.append(pid);
        sb.append('|');
        sb.append(bsn);

        sb.append(")(");

        sb.append(key);
        sb.append('=');
        sb.append(pid);
        sb.append('|');
        sb.append(bsn);
        sb.append('|');
        sb.append(version);

        sb.append(")(");

        sb.append(key);
        sb.append('=');
        sb.append(pid);
        sb.append('|');
        sb.append(bsn);
        sb.append('|');
        sb.append(version);
        sb.append('|');
        sb.append(location);

        sb.append("))");

        return sb.toString();
    }

    /**
     * see core spec 3.2.7.  Escape \*() with preceding \
     * @param value
     * @return escaped string
     */
    static final String escape(String value)
    {
        StringBuilder sb = null;

        int index = 0;

        for (int i = 0; i < value.length(); i++) {
            char c = value.charAt(i);

            switch (c) {
                case '\\':
                case '*':
                case '(':
                case ')':
                    if (sb == null) {
                        sb = new StringBuilder();
                    }

                    sb.append(value, index, i);
                    sb.append('\\');
                    sb.append(c);

                    index = i + 1;

                    break;
            }
        }

        if (sb == null) {
            return value;
        }

        if (index < value.length()) {
            sb.append(value, index, value.length());
        }

        return sb.toString();
    }

    private ConfigurationAdmin getConfigAdmin(BundleContext bundleContext)
    {
        return bundleContext.getService( caReference );
    }

    private static class CMDomainCombiner implements DomainCombiner
    {

        private final CMProtectionDomain domain;

        CMDomainCombiner(final Bundle bundle)
        {
            this.domain = new CMProtectionDomain(bundle);
        }

        @Override
        public ProtectionDomain[] combine(final ProtectionDomain[] arg0,
                final ProtectionDomain[] arg1) {
            return new ProtectionDomain[] { domain };
        }

    }

    private static class CMProtectionDomain extends ProtectionDomain {

        private final Bundle bundle;

        CMProtectionDomain(final Bundle bundle) {
            super(null, null);
            this.bundle = bundle;
        }

        @Override
        public boolean implies(final Permission permission) {
            try {
                return bundle.hasPermission(permission);
            } catch (IllegalStateException e) {
                return false;
            }
        }
    }
}
