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

import static org.apache.felix.dm.impl.ServiceUtil.toR6Dictionary;

import java.util.Arrays;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.Objects;
import java.util.Properties;
import java.util.stream.Stream;

import org.apache.felix.dm.Component;
import org.apache.felix.dm.ConfigurationDependency;
import org.apache.felix.dm.Logger;
import org.apache.felix.dm.PropertyMetaData;
import org.apache.felix.dm.context.AbstractDependency;
import org.apache.felix.dm.context.DependencyContext;
import org.apache.felix.dm.context.Event;
import org.apache.felix.dm.context.EventType;
import org.apache.felix.dm.impl.metatype.MetaTypeProviderImpl;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.osgi.service.metatype.MetaTypeProvider;

/**
 * Implementation for a configuration dependency.
 * 
 * @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
 */
public class ConfigurationDependencyImpl extends AbstractDependency<ConfigurationDependency> implements ConfigurationDependency, ManagedService {
    private volatile Dictionary<String, Object> m_settings;
	private volatile String m_pid;
	private ServiceRegistration<?> m_registration;
	private volatile Class<?>[] m_configTypes;
    private volatile MetaTypeProviderImpl m_metaType;
	private boolean m_mayInvokeUpdateCallback;
	private final Logger m_logger;
	private final BundleContext m_context;
	private volatile boolean m_needsInstance = true;
	private volatile boolean m_optional;
	private volatile boolean m_needsInstanceCalled;

    public ConfigurationDependencyImpl() {
        this(null, null);
    }
	
    public ConfigurationDependencyImpl(BundleContext context, Logger logger) {
        m_context = context;
    	m_logger = logger;
        setRequired(true);
        setCallback("updated");
    }
    
	public ConfigurationDependencyImpl(ConfigurationDependencyImpl prototype) {
	    super(prototype);
	    m_context = prototype.m_context;
	    m_pid = prototype.m_pid;
	    m_logger = prototype.m_logger;
        m_metaType = prototype.m_metaType != null ? new MetaTypeProviderImpl(prototype.m_metaType, this, null) : null;
        m_needsInstance = prototype.needsInstance();
        m_configTypes = prototype.m_configTypes;
	}
		
	@Override
	public ConfigurationDependencyImpl setRequired(boolean required) {
			m_optional = ! required;
			super.setRequired(true); // always required
			return this;
	}
	
    @Override
    public Class<?> getAutoConfigType() {
        return null; // we don't support auto config mode.
    }

	@Override
	public DependencyContext createCopy() {
	    return new ConfigurationDependencyImpl(this);
	}

	/**
	 * Sets a callback method invoked on the instantiated component.
	 */
    public ConfigurationDependencyImpl setCallback(String callback) {
        super.setCallbacks(callback, null);
        return this;
    }
    
    /**
     * Sets a callback method on an external callback instance object.
     * The component is not yet instantiated at the time the callback is invoked.
     * We check if callback instance is null, in this case, the callback will be invoked on the instantiated component.
     */
    public ConfigurationDependencyImpl setCallback(Object instance, String callback) {
        boolean needsInstantiatedComponent = (m_needsInstanceCalled) ? m_needsInstance : (instance == null);
    	return setCallback(instance, callback, needsInstantiatedComponent);
    }

    /**
     * Sets a callback method on an external callback instance object.
     * If needsInstance == true, the component is instantiated at the time the callback is invoked.
     * We check if callback instance is null, in this case, the callback will be invoked on the instantiated component.
     */
    public ConfigurationDependencyImpl setCallback(Object instance, String callback, boolean needsInstance) {
        super.setCallbacks(instance, callback, null);
        needsInstance(needsInstance);
        return this;
    }
        
    /**
     * Sets a type-safe callback method invoked on the instantiated component.
     */
    public ConfigurationDependency setCallback(String callback, Class<?> configType) {
        Objects.nonNull(configType);
        setCallback(callback);
        m_configTypes = configType == null ? null : new Class<?>[] { configType };
        m_pid = (m_pid == null) ? configType.getName() : m_pid;
        return this;
    }

    /**
     * Sets a type-safe callback method on an external callback instance object.
     * The component is not yet instantiated at the time the callback is invoked.
     */
    public ConfigurationDependency setCallback(Object instance, String callback, Class<?> configType) {
        Objects.nonNull(configType);
        setCallback(instance, callback);
        m_configTypes = configType == null ? null : new Class<?>[] { configType };
        m_pid = (m_pid == null) ? configType.getName() : m_pid;
        return this;
    }
    
    /**
     * Sets a type-safe callback method on an external callback instance object.
     * If needsInstance == true, the component is instantiated at the time the callback is invoked.
     */
    public ConfigurationDependencyImpl setCallback(Object instance, String callback, Class<?> configType, boolean needsInstance) {
        setCallback(instance, callback, needsInstance);
        m_configTypes = configType == null ? null : new Class<?>[] { configType };
        return this;
    }
    
    /**
     * Specifies if the component instance must be started when this dependency is started. True by default.
     */
    @Override
    public ConfigurationDependencyImpl needsInstance(boolean needsInstance) {
    	m_needsInstance = needsInstance;
    	m_needsInstanceCalled = true;
        return this;
    }
    
    @Override
    public ConfigurationDependencyImpl setConfigType(Class<?> ... configTypes) {
    	m_configTypes = configTypes;
    	return this;
    }

    /**
     * This method indicates to ComponentImpl if the component must be instantiated when this Dependency is started.
     * If the callback has to be invoked on the component instance, then the component
     * instance must be instantiated at the time the Dependency is started because when "CM" calls ConfigurationDependencyImpl.updated()
     * callback, then at this point we have to synchronously delegate the callback to the component instance, and re-throw to CM
     * any exceptions (if any) thrown by the component instance updated callback.
     */
    @Override
    public boolean needsInstance() {
        return m_needsInstance;
    }
    
    @Override
    public void start() {
        BundleContext context = m_component.getBundleContext();
        if (context != null) { // If null, we are in a test environment
	        Properties props = new Properties();
	        props.put(Constants.SERVICE_PID, m_pid);
	        ManagedService ms = this;
	        if (m_metaType != null) {
	            ms = m_metaType;
	            props.put(MetaTypeProvider.METATYPE_PID, m_pid);
	            String[] ifaces = new String[] { ManagedService.class.getName(), MetaTypeProvider.class.getName() };
	            m_registration = context.registerService(ifaces, ms, toR6Dictionary(props));
	        } else {
	            m_registration = context.registerService(ManagedService.class.getName(), ms, toR6Dictionary(props));
	        }
        }
        super.start();
    }

    @Override
    public void stop() {
        if (m_registration != null) {
            try {
                m_registration.unregister();
            } catch (IllegalStateException e) {}
        	m_registration = null;
        }
        super.stop();
    }
    
	public ConfigurationDependency setPid(String pid) {
		ensureNotActive();
		m_pid = pid;
		return this;
	}
		
    @Override
    public String getSimpleName() {
        return m_pid;
    }
    
    @Override
    public String getFilter() {
        return null;
    }

    public String getType() {
        return "configuration";
    }
            
    public ConfigurationDependency add(PropertyMetaData properties)
    {
        createMetaTypeImpl();
        m_metaType.add(properties);
        return this;
    }

    public ConfigurationDependency setDescription(String description)
    {
        createMetaTypeImpl();
        m_metaType.setDescription(description);
        return this;
    }

    public ConfigurationDependency setHeading(String heading)
    {
        createMetaTypeImpl();
        m_metaType.setName(heading);
        return this;
    }
    
    public ConfigurationDependency setLocalization(String path)
    {
        createMetaTypeImpl();
        m_metaType.setLocalization(path);
        return this;
    }
    
	@SuppressWarnings("unchecked")
	@Override
	public Dictionary<String, Object> getProperties() {
		if (m_settings == null) {
            throw new IllegalStateException("cannot find configuration");
		}
		return m_settings;
	}
	    
    @SuppressWarnings("rawtypes")
	@Override
    public void updated(Dictionary settings) throws ConfigurationException {
    	// Handle the update in the component executor thread. Any exception thrown during the component updated callback will be 
    	// synchronously awaited and re-thrown to the CM thread.
    	// We schedule the update in the component executor in order to avoid race conditions,
    	// like when the component is stopping while we receive a configuration update, or if the component restarts
    	// while the configuration is being updated, or if the getProperties method is invoked while we are losing the configuration ...
    	// there are many racy situations, and the safe way to handle them is to schedule the updated callback in the component executor.    	
    	InvocationUtil.invokeUpdated(m_component.getExecutor(), () -> doUpdated(settings));
    }
    
    @SuppressWarnings({"unchecked", "rawtypes"})
    private void doUpdated(Dictionary settings) throws Exception {
    	// Reset the flag that tells if the callback can be invoked.
    	m_mayInvokeUpdateCallback = true;
        Dictionary<String, Object> oldSettings = m_settings;
        
        // FELIX-5192: we have to handle the following race condition: one thread stops a component (removes it from a DM object);
        // another thread removes the configuration (from ConfigurationAdmin). in this case we may be called in our
        // ManagedService.updated(null), but our component instance has been destroyed and does not exist anymore.
        // In this case: do nothing.   
        if (! super.isStarted()) {
            return;
        }        

    	if (settings == null && m_optional) {
    		// Provide a default empty configuration
    		settings = new Hashtable<>();
    		settings.put(Constants.SERVICE_PID, m_pid);
    	}

        if (oldSettings == null && settings == null) {
            // CM has started but our configuration is not still present in the CM database.
        	return;        		
        }

        // If this is initial settings, or a configuration update, we handle it synchronously.
        // We'll conclude that the dependency is available only if invoking updated did not cause
        // any ConfigurationException.  
        invokeUpdated(settings);
        
        // At this point, we have accepted the configuration.        
        m_settings = settings;

        if ((oldSettings == null) && (settings != null)) {
            // Notify the component that our dependency is available.
            m_component.handleEvent(this, EventType.ADDED, new ConfigurationEventImpl(m_pid, settings));
        }
        else if ((oldSettings != null) && (settings != null)) {
            // Notify the component that our dependency has changed.
            m_component.handleEvent(this, EventType.CHANGED, new ConfigurationEventImpl(m_pid, settings));
        }
        else if ((oldSettings != null) && (settings == null)) {
            // Notify the component that our dependency has been removed.
            // Notice that the component will be stopped, and then all required dependencies will be unbound
            // (including our configuration dependency).
            m_component.handleEvent(this, EventType.REMOVED, new ConfigurationEventImpl(m_pid, oldSettings));
        }
    }

    @Override
    public void invokeCallback(EventType type, Event ... event) {
        switch (type) {
        case ADDED:
            try {
            	// Won't invoke if we already invoked from the doUpdate method.
            	// The case when we really invoke may happen when the component is stopped , then restarted.
            	// At this point, we have to re-invoke the component updated callback.
                invokeUpdated(((ConfigurationEventImpl) event[0]).getProperties());
            } catch (Throwable err) {
                logConfigurationException(err);
            }
            break;
        case CHANGED:
            // We already did that synchronously, from our doUpdated method
            break;
        case REMOVED:
            // The state machine is stopping us. Reset for the next time the state machine calls invokeCallback(ADDED)
            m_mayInvokeUpdateCallback = true;
            break;
        default:
            break;
        }
    }
     
    private static <T> T[] concat(T first, T[] second) {
    	  T[] result = Arrays.copyOf(second, second.length + 1);
    	  result[0] = first;
    	  System.arraycopy(second, 0, result, 1, second.length);
    	  return result;
    }
    
    private static <T> T[] concat(T first, T second, T[] third) {
  	  T[] result = Arrays.copyOf(third, third.length + 2);
  	  result[0] = first;
  	  result[1] = second;
  	  System.arraycopy(third, 0, result, 2, third.length);
  	  return result;
    }
    
    static CallbackTypeDef createCallbackType(Logger logger, Component service, Class<?>[] configTypes, Dictionary<?, ?> settings) {
        Class<?>[][] sigs = new Class[][] { { Dictionary.class }, { Component.class, Dictionary.class }, {} };
        Object[][] args = new Object[][] { { settings }, { service, settings }, {} };

        if (configTypes != null && configTypes.length > 0 && configTypes[0] != null) {
            try {
                // if the configuration is null, it means we are losing it, and since we pass a null dictionary for other callback
                // (that accepts a Dictionary), then we should have the same behavior and also pass a null conf proxy object when
                // the configuration is lost.
            	Dictionary<String, Object> declaredServiceProperties = ServiceUtil.toR6Dictionary(EMPTY_PROPERTIES);
            	if (service instanceof ComponentImpl) {
            		declaredServiceProperties = ((ComponentImpl) service).getDeclaredServiceProperties();
            	}            	
            	Object[] configurables = new Object[configTypes.length];
            	for (int i = 0 ; i < configTypes.length; i ++) {
            		configurables[i] = settings != null ? Configurable.create(configTypes[i], settings, declaredServiceProperties) : null;
                    logger.debug("Using configuration-type injecting using %s as possible configType.", configTypes[i].getSimpleName());
            	}            	
            	
                sigs = new Class[][] { 
                    { Dictionary.class }, 
                    { Component.class, Dictionary.class }, 
                    concat(Component.class, configTypes), 
                    configTypes , 
                    concat(Dictionary.class, configTypes),
                    concat(Component.class, Dictionary.class, configTypes),
                    {}
                };
                args = new Object[][] { 
                    { settings }, 
                    { service, settings }, 
                    concat(service, configurables), 
                    configurables,
                    concat(settings, configurables),
                    concat(service, settings, configurables),
                    {}
                };
            }
            catch (Exception e) {
                // This is not something we can recover from, use the defaults above...
                logger.warn("Failed to create configurable for configuration type %s!", e, configTypes != null ? Arrays.toString(configTypes) : null);
            }
        }

        return new CallbackTypeDef(sigs, args);
    }

    // Called from the configuration component internal queue. 
    private void invokeUpdated(Dictionary<?, ?> settings) throws Exception {
        if (m_mayInvokeUpdateCallback) {
        	m_mayInvokeUpdateCallback = false;
            
            // FELIX-5155: if component impl is an internal DM adapter, we must not invoke the callback on it
            // because in case there is an external callback instance specified for the configuration callback,
            // then we don't want to invoke it now. The external callback instance will be invoked
            // on the other actual configuration dependency copied into the actual component instance created by the
            // adapter.
            
            Object mainComponentInstance = m_component.getInstance();
            if (mainComponentInstance instanceof AbstractDecorator || m_component.injectionDisabled()) {
                return;
            }
            
            Object[] instances = super.getInstances(); // never null, either the callback instance or the component instances            
            
            CallbackTypeDef callbackInfo = createCallbackType(m_logger, m_component, m_configTypes, settings);
            boolean callbackFound = false;
            for (int i = 0; i < instances.length; i++) {
                try {
                	// Only inject if the component instance is not a prototype instance
                	InvocationUtil.invokeCallbackMethod(instances[i], m_add, callbackInfo.m_sigs, callbackInfo.m_args);
                	callbackFound |= true;
                }
                catch (NoSuchMethodException e) {
                    // if the method does not exist, ignore it
                }
            }
            
            if (! callbackFound) {
                String[] instanceClasses = Stream.of(instances).map(c -> c.getClass().getName()).toArray(String[]::new);
                m_logger.log(Logger.LOG_ERROR, "\"" + m_add + "\" configuration callback not found in any of the component classes: " + Arrays.toString(instanceClasses));                    
            }
        }
    }
    
    private synchronized void createMetaTypeImpl() {
        if (m_metaType == null) {
            m_metaType = new MetaTypeProviderImpl(m_pid, m_context, m_logger, this, null);
        }
    }
        
    private void logConfigurationException(Throwable err) {
        m_logger.log(Logger.LOG_ERROR, "Got exception while handling configuration update for pid " + m_pid, err);
    }
}
