/*
 * 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.util.ArrayList;
import java.util.Collection;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.felix.scr.impl.inject.ComponentMethods;
import org.apache.felix.scr.impl.inject.ComponentMethodsImpl;
import org.apache.felix.scr.impl.logger.ComponentLogger;
import org.apache.felix.scr.impl.logger.ScrLogger;
import org.apache.felix.scr.impl.manager.AbstractComponentManager;
import org.apache.felix.scr.impl.manager.ComponentActivator;
import org.apache.felix.scr.impl.manager.ComponentHolder;
import org.apache.felix.scr.impl.manager.ConfigurableComponentHolder;
import org.apache.felix.scr.impl.manager.DependencyManager;
import org.apache.felix.scr.impl.manager.RegionConfigurationSupport;
import org.apache.felix.scr.impl.manager.ScrConfiguration;
import org.apache.felix.scr.impl.metadata.ComponentMetadata;
import org.apache.felix.scr.impl.metadata.TargetedPID;
import org.osgi.framework.Bundle;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.component.ComponentConstants;
import org.osgi.service.component.ComponentException;
import org.osgi.service.component.runtime.ServiceComponentRuntime;
import org.osgi.service.log.LogService;


/**
 * The <code>ComponentRegistry</code> class acts as the global registry for
 * components by name and by component ID.
 */
public class ComponentRegistry
{
    /**
     * Service property for change count. This constant is defined here to avoid
     * a dependency on R7 of the framework.
     * The value of the property is of type {@code Long}.
     */
    private static String PROP_CHANGECOUNT = "service.changecount";


    /**
     * The map of known components indexed by component name. The values are
     * either null (for name reservations) or implementations
     * of the {@link ComponentHolder} interface.
     * <p>
     * The {@link #checkComponentName(String)} will first add an entry to this
     * map with null value to reserve the name. After setting up
     * the component, the {@link #registerComponentHolder(String, ComponentHolder)}
     * method replaces the value of the named entry with the actual
     * {@link ComponentHolder}.
     *
     * @see #checkComponentName(String)
     * @see #registerComponentHolder(String, ComponentHolder)
     * @see #unregisterComponentHolder(String)
     */
    private final Map<ComponentRegistryKey, ComponentHolder<?>> m_componentHoldersByName;

    /**
     * The map of known components indexed by component configuration pid. The values are
     * Sets of the {@link ComponentHolder} interface. Normally, the configuration pid
     * is the component name, but since DS 1.2 (OSGi 4.3), a component may specify a specific
     * pid, and it is possible that different components refer to the same pid. That's why
     * the values of this map are Sets of ComponentHolders, allowing to lookup all components
     * which are using a given configuration pid.
     * This map is used when the ConfigurationSupport detects that a CM pid is updated. When
     * a PID is updated, the ConfigurationSupport listener class invokes the
     * {@link #getComponentHoldersByPid(String)} method which returns an iterator over all
     * components that are using the given pid for configuration.
     * <p>
     *
     * @see #registerComponentHolder(String, ComponentHolder)
     * @see #unregisterComponentHolder(String)
     * @see RegionConfigurationSupport#configurationEvent(org.osgi.service.cm.ConfigurationEvent)
     */
    private final Map<String, Set<ComponentHolder<?>>> m_componentHoldersByPid;

    /**
     * Map of components by component ID. This map indexed by the component
     * ID number (<code>java.lang.Long</code>) contains the actual
     * {@link AbstractComponentManager} instances existing in the system.
     *
     * @see #registerComponentId(AbstractComponentManager)
     * @see #unregisterComponentId(long)
     */
    private final Map<Long, AbstractComponentManager<?>> m_componentsById;

    /**
     * Counter to setup the component IDs as issued by the
     * {@link #registerComponentId(AbstractComponentManager)} method. This
     * counter is only incremented.
     */
    private long m_componentCounter = -1;

    private final Map<ServiceReference<?>, List<Entry<?, ?>>> m_missingDependencies = new HashMap<>( );

    private final ScrLogger m_logger;

    private final ScrConfiguration m_configuration;

    public ComponentRegistry( final ScrConfiguration scrConfiguration, final ScrLogger logger )
    {
        m_configuration = scrConfiguration;
        m_logger = logger;
        m_componentHoldersByName = new HashMap<>();
        m_componentHoldersByPid = new HashMap<>();
        m_componentsById = new HashMap<>();

    }

    //---------- ComponentManager registration by component Id

    /**
     * Assigns a unique ID to the component, internally registers the
     * component under that ID and returns the assigned component ID.
     *
     * @param componentManager The {@link AbstractComponentManager} for which
     *      to assign a component ID and which is to be internally registered
     *
     * @return the assigned component ID
     */
    final long registerComponentId( final AbstractComponentManager<?> componentManager )
    {
        long componentId;
        synchronized ( m_componentsById )
        {
            componentId = ++m_componentCounter;

            m_componentsById.put( componentId, componentManager );
        }

        return componentId;
    }


    /**
     * Unregisters the component with the given component ID from the internal
     * registry. After unregistration, the component ID should be considered
     * invalid.
     *
     * @param componentId The ID of the component to be removed from the
     *      internal component registry.
     */
    final void unregisterComponentId( final long componentId )
    {
        synchronized ( m_componentsById )
        {
            m_componentsById.remove( componentId );
        }
    }


    //---------- ComponentHolder registration by component name

    /**
     * Checks whether the component name is "globally" unique or not. If it is
     * unique, it is reserved until the actual component is registered with
     * {@link #registerComponentHolder(String, ComponentHolder)} or until
     * it is unreserved by calling {@link #unregisterComponentHolder(String)}.
     * If a component with the same name has already been reserved or registered
     * a ComponentException is thrown with a descriptive message.
     *
     * @param bundle the bundle registering the component
     * @param name the component name to check and reserve
     * @throws ComponentException if the name is already in use by another
     *      component.
     */
    final ComponentRegistryKey checkComponentName( final Bundle bundle, final String name )
    {
        // register the name if no registration for that name exists already
        final ComponentRegistryKey key = new ComponentRegistryKey( bundle, name );
        ComponentHolder<?> existingRegistration = null;
        boolean present;
        synchronized ( m_componentHoldersByName )
        {
            present = m_componentHoldersByName.containsKey( key );
            if ( !present )
            {
                m_componentHoldersByName.put( key, null );
            }
            else
            {
                existingRegistration = m_componentHoldersByName.get( key );
            }
        }

        // there was a registration already, throw an exception and use the
        // existing registration to provide more information if possible
        if ( present )
        {
            String message = "The component name '" + name + "' has already been registered";

            if ( existingRegistration != null )
            {
                Bundle cBundle = existingRegistration.getActivator().getBundleContext().getBundle();
                ComponentMetadata cMeta = existingRegistration.getComponentMetadata();

                StringBuilder buf = new StringBuilder( message );
                buf.append( " by Bundle " ).append( cBundle.getBundleId() );
                if ( cBundle.getSymbolicName() != null )
                {
                    buf.append( " (" ).append( cBundle.getSymbolicName() ).append( ")" );
                }
                buf.append( " as Component of Class " ).append( cMeta.getImplementationClassName() );
                message = buf.toString();
            }

            throw new ComponentException( message );
        }

        return key;
    }


    /**
     * Registers the given component under the given name. If the name has not
     * already been reserved calling {@link #checkComponentName(String)} this
     * method throws a {@link ComponentException}.
     *
     * @param name The name to register the component under
     * @param componentHolder The component to register
     *
     * @throws ComponentException if the name has not been reserved through
     *      {@link #checkComponentName(String)} yet.
     */
    final void registerComponentHolder( final ComponentRegistryKey key, ComponentHolder<?> componentHolder )
    {
        m_logger.log(LogService.LOG_DEBUG,
                "Registering component with pid {0} for bundle {1}", null,
                componentHolder.getComponentMetadata().getConfigurationPid(), key.getBundleId());
        synchronized ( m_componentHoldersByName )
        {
            // only register the component if there is a m_registration for it !
            if ( m_componentHoldersByName.get( key ) != null )
            {
                // this is not expected if all works ok
                throw new ComponentException( "The component name '{0}" + componentHolder.getComponentMetadata().getName()
                    + "' has already been registered." );
            }

            m_componentHoldersByName.put( key, componentHolder );
        }

        synchronized (m_componentHoldersByPid)
        {
            // See if the component declares a specific configuration pid (112.4.4 configuration-pid)
            List<String> configurationPids = componentHolder.getComponentMetadata().getConfigurationPid();

            for ( String configurationPid: configurationPids )
            {
                // Since several components may refer to the same configuration pid, we have to
                // store the component holder in a Set, in order to be able to lookup every
                // components from a given pid.
                Set<ComponentHolder<?>> set = m_componentHoldersByPid.get( configurationPid );
                if ( set == null )
                {
                    set = new HashSet<>();
                    m_componentHoldersByPid.put( configurationPid, set );
                }
                set.add( componentHolder );
            }
        }
        this.updateChangeCount();
  }

    /**
     * Returns the component registered under the given name or <code>null</code>
     * if no component is registered yet.
     */
    public final ComponentHolder<?> getComponentHolder( final Bundle bundle, final String name )
    {
        synchronized ( m_componentHoldersByName )
        {
            return m_componentHoldersByName.get( new ComponentRegistryKey( bundle, name ) );
        }
    }

    /**
     * Returns the set of ComponentHolder instances whose configuration pids are matching
     * the given pid.
     * @param pid the pid candidate
     * @return the set of ComponentHolders matching the singleton pid supplied
     */
    public final Collection<ComponentHolder<?>> getComponentHoldersByPid(TargetedPID targetedPid)
    {
        String pid = targetedPid.getServicePid();
        Set<ComponentHolder<?>> componentHoldersUsingPid = new HashSet<>();
        synchronized (m_componentHoldersByPid)
        {
            Set<ComponentHolder<?>> set = m_componentHoldersByPid.get(pid);
            // only return the entry if non-null and not a reservation
            if (set != null)
            {
                for (ComponentHolder<?> holder: set)
                {
                    Bundle bundle = holder.getActivator().getBundleContext().getBundle();
                    if (targetedPid.matchesTarget(bundle))
                    {
                        componentHoldersUsingPid.add( holder );
                    }
                }
            }
        }
        return componentHoldersUsingPid;
    }

    /**
     * Returns an array of all values currently stored in the component holders
     * map. The entries in the array are either String types for component
     * name reservations or {@link ComponentHolder} instances for actual
     * holders of components.
     */
    public final List<ComponentHolder<?>> getComponentHolders()
    {
    	List<ComponentHolder<?>> all = new ArrayList<>();
        synchronized ( m_componentHoldersByName )
        {
        	all.addAll(m_componentHoldersByName.values());
        }
        return all;
    }

    public final List<ComponentHolder<?>> getComponentHolders(Bundle...bundles)
    {
    	List<ComponentHolder<?>> all =getComponentHolders();
        List<ComponentHolder<?>> holders = new ArrayList<>();
        for ( ComponentHolder<?> holder: all)
        {
        	ComponentActivator activator = holder.getActivator();
        	if (activator != null)
        	{
        	    try
        	    {
            		Bundle holderBundle = activator.getBundleContext().getBundle();
            		for (Bundle b: bundles)
            		{
            			if (b == holderBundle)
            			{
            				holders.add(holder);
            			}
            		}
        	    }
        	    catch ( IllegalStateException ise)
        	    {
        	        // ignore inactive bundles
        	    }
        	}
        }
        return holders;
    }


    /**
     * Removes the component registered under that name. If no component is
     * yet registered but the name is reserved, it is unreserved.
     * <p>
     * After calling this method, the name can be reused by other components.
     */
    final void unregisterComponentHolder( final Bundle bundle, final String name )
    {
        unregisterComponentHolder( new ComponentRegistryKey( bundle, name ) );
    }


    /**
     * Removes the component registered under that name. If no component is
     * yet registered but the name is reserved, it is unreserved.
     * <p>
     * After calling this method, the name can be reused by other components.
     */
    final void unregisterComponentHolder( final ComponentRegistryKey key )
    {
        ComponentHolder<?> component;
        synchronized ( m_componentHoldersByName )
        {
            component = m_componentHoldersByName.remove( key );
        }

        if (component != null) {
            m_logger.log(LogService.LOG_DEBUG,
                    "Unregistering component with pid {0} for bundle {1}", null,
                    component.getComponentMetadata().getConfigurationPid(), key.getBundleId());
            synchronized (m_componentHoldersByPid)
            {
                List<String> configurationPids = component.getComponentMetadata().getConfigurationPid();
                for ( String configurationPid: configurationPids )
                {
                    Set<ComponentHolder<?>> componentsForPid = m_componentHoldersByPid.get( configurationPid );
                    if ( componentsForPid != null )
                    {
                        componentsForPid.remove( component );
                        if ( componentsForPid.size() == 0 )
                        {
                            m_componentHoldersByPid.remove( configurationPid );
                        }
                    }
                }
            }
            this.updateChangeCount();
        }
    }

    //---------- base configuration support

    /**
     * Factory method to issue {@link ComponentHolder} instances to manage
     * components described by the given component <code>metadata</code>.
     */
    public <S> ComponentHolder<S> createComponentHolder( ComponentActivator activator, ComponentMetadata metadata, ComponentLogger logger )
    {
        return new DefaultConfigurableComponentHolder<>(activator, metadata, logger);
    }

    static class DefaultConfigurableComponentHolder<S> extends ConfigurableComponentHolder<S>
    {
        public DefaultConfigurableComponentHolder(ComponentActivator activator, ComponentMetadata metadata, ComponentLogger logger)
        {
            super(activator, metadata, logger);
        }

        @Override
        protected ComponentMethods<S> createComponentMethods()
        {
            return new ComponentMethodsImpl<>();
        }
    }


    //---------- ServiceListener

    //---------- Helper method

    private final ThreadLocal<List<ServiceReference<?>>> circularInfos = new ThreadLocal<List<ServiceReference<?>>> ()
    {

        @Override
        protected List<ServiceReference<?>> initialValue()
        {
            return new ArrayList<>();
        }
    };


    /**
     * Track getService calls by service reference.
     * @param serviceReference
     * @return true is we have encountered a circular dependency, false otherwise.
     */
    public <T> boolean enterCreate(final ServiceReference<T> serviceReference)
    {
        List<ServiceReference<?>> info = circularInfos.get();
        if (info.contains(serviceReference))
        {
            m_logger.log(LogService.LOG_ERROR,
                "Circular reference detected trying to get service {0}\n stack of references: {1}", new Exception("stack trace"),
                serviceReference, new Info(info));
            return true;
        }
        m_logger.log(LogService.LOG_DEBUG,
            "getService  {0}: stack of references: {1}", null,
            serviceReference, info);
        info.add(serviceReference);
        return false;
    }


    private class Info
    {

        private final List<ServiceReference<?>> info;


        public Info(List<ServiceReference<?>> info)
        {
            this.info = info;
        }


        @Override
        public String toString()
        {
        	StringBuilder sb = new StringBuilder();
            for (ServiceReference<?> sr: info)
            {
                sb.append("ServiceReference: ").append(sr).append("\n");
                List<Entry<?, ?>> entries = m_missingDependencies.get(sr);
                if (entries != null)
                {
                    for (Entry<?, ?> entry: entries)
                    {
                        sb.append("    Dependency: ").append(entry.getDm()).append("\n");
                    }
                }
            }
            return sb.toString();
        }

    }

    public <T> void leaveCreate(final ServiceReference<T> serviceReference)
    {
        List<ServiceReference<?>> info = circularInfos.get();
        if (info != null)
        {
            if (!info.isEmpty() && info.iterator().next().equals(serviceReference))
            {
                circularInfos.remove();
            }
            else
            {
                info.remove(serviceReference);
            }
        }

    }

    /**
     * Schedule late binding of now-available reference on a different thread.  The late binding cannot occur on this thread
     * due to service registry circular reference detection. We cannot wait for the late binding before returning from the initial
     * getService call because of synchronization in the service registry.
     * @param serviceReference
     * @param actor
     */
    public synchronized <T> void missingServicePresent( final ServiceReference<T> serviceReference, ComponentActorThread actor )
    {
        final List<Entry<?, ?>> dependencyManagers = m_missingDependencies.remove( serviceReference );
        if ( dependencyManagers != null )
        {

            Runnable runnable = new Runnable()
            {

                @Override
                @SuppressWarnings("unchecked")
                public void run()
                {
                    for ( Entry<?, ?> entry : dependencyManagers )
                    {
                        ((DependencyManager<?, T>)entry.getDm()).invokeBindMethodLate( serviceReference, entry.getTrackingCount() );
                    }
                    m_logger.log(LogService.LOG_DEBUG,
                        "Ran {0} asynchronously", null, this);
                }

                @Override
                public String toString()
                {
                    return "Late binding task of reference " + serviceReference + " for dependencyManagers " + dependencyManagers;
                }

            } ;
            m_logger.log(LogService.LOG_DEBUG,
                "Scheduling runnable {0} asynchronously", null, runnable);
            actor.schedule( runnable );
        }
    }

    public synchronized <S, T> void registerMissingDependency( DependencyManager<S, T> dependencyManager, ServiceReference<T> serviceReference, int trackingCount )
    {
        //check that the service reference is from scr
        if ( serviceReference.getProperty( ComponentConstants.COMPONENT_NAME ) == null || serviceReference.getProperty( ComponentConstants.COMPONENT_ID ) == null )
        {
            m_logger.log(LogService.LOG_DEBUG,
                "Missing service {0} for dependency manager {1} is not a DS service, cannot resolve circular dependency", null,
                serviceReference, dependencyManager);
            return;
        }
        List<Entry<?, ?>> dependencyManagers = m_missingDependencies.get( serviceReference );
        if ( dependencyManagers == null )
        {
            dependencyManagers = new ArrayList<>();
            m_missingDependencies.put( serviceReference, dependencyManagers );
        }
        dependencyManagers.add( new Entry<>( dependencyManager, trackingCount ) );
        m_logger.log(LogService.LOG_DEBUG,
            "Dependency managers {0} waiting for missing service {1}", null,
            dependencyManagers, serviceReference);
        }

    private static class Entry<S,T>
    {
        private final DependencyManager<S, T> dm;
        private final int trackingCount;

        private Entry( DependencyManager<S, T> dm, int trackingCount )
        {
            this.dm = dm;
            this.trackingCount = trackingCount;
        }

        public DependencyManager<S, T> getDm()
        {
            return dm;
        }

        public int getTrackingCount()
        {
            return trackingCount;
        }

        @Override
        public String toString()
        {
            return dm.toString() + "@" + trackingCount;
        }
    }

    private final ConcurrentMap<Long, RegionConfigurationSupport> bundleToRcsMap = new ConcurrentHashMap<>();

    public RegionConfigurationSupport registerRegionConfigurationSupport(
            ServiceReference<ConfigurationAdmin> reference) {
        Bundle bundle = reference.getBundle();
        if (bundle == null) {
            return null;
        }
        RegionConfigurationSupport trialRcs = new RegionConfigurationSupport(m_logger, reference, bundle) {
            @Override
            protected Collection<ComponentHolder<?>> getComponentHolders(TargetedPID pid)
            {
                return ComponentRegistry.this.getComponentHoldersByPid(pid);
            }
        };
        return registerRegionConfigurationSupport(trialRcs);
    }

    public RegionConfigurationSupport registerRegionConfigurationSupport(
			RegionConfigurationSupport trialRcs) {
		Long bundleId = trialRcs.getBundleId();
		RegionConfigurationSupport existing = null;
		RegionConfigurationSupport previous = null;
		while (true)
		{
			existing = bundleToRcsMap.putIfAbsent(bundleId, trialRcs);
			if (existing == null)
			{
				trialRcs.start();
				return trialRcs;
			}
			if (existing == previous)
			{
				//the rcs we referenced is still current
				return existing;
			}
			if (existing.reference())
			{
				//existing can still be used
				previous = existing;
			}
			else
			{
				//existing was discarded in another thread, start over
				previous = null;
			}
		}
	}

	public void unregisterRegionConfigurationSupport(
			RegionConfigurationSupport rcs) {
		if (rcs.dereference())
		{
			bundleToRcsMap.remove(rcs.getBundleId());
		}

	}

	private final Object changeCountLock = new Object();

    private volatile long changeCount;

    private volatile Timer timer;

    private volatile ServiceRegistration<ServiceComponentRuntime> registration;

    public Dictionary<String, Object> getServiceRegistrationProperties()
    {
        final Dictionary<String, Object> props = new Hashtable<>();
        props.put(PROP_CHANGECOUNT, this.changeCount);

        return props;
    }

    public void setRegistration(final ServiceRegistration<ServiceComponentRuntime> reg)
    {
        this.registration = reg;
    }

    public void updateChangeCount()
    {
        if ( registration != null )
        {
            synchronized ( changeCountLock )
            {
                this.changeCount++;
                final long count = this.changeCount;
                if ( this.timer == null )
                {
                    this.timer = new Timer();
                }
                try
                {
                    timer.schedule(new TimerTask()
                        {

                            @Override
                            public void run() {
                                synchronized ( changeCountLock )
                                {
                                    if ( changeCount == count )
                                    {
                                        try
                                        {
                                            registration.setProperties(getServiceRegistrationProperties());
                                        }
                                        catch ( final IllegalStateException ise)
                                        {
                                            // we ignore this as this might happen on shutdown
                                        }
                                        timer.cancel();
                                        timer = null;
                                    }
                                }
                            }
                        }, m_configuration.serviceChangecountTimeout());
                }
                catch (Exception e) {
                    m_logger.log(LogService.LOG_WARNING,
                        "Service changecount Timer for {0} had a problem", e,
                        registration.getReference());
                }
            }
        }
    }
}
