/**
 * 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.aries.blueprint.compendium.cm;

import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.aries.blueprint.BeanProcessor;
import org.apache.aries.blueprint.ServiceProcessor;
import org.apache.aries.blueprint.services.ExtendedBlueprintContainer;
import org.apache.aries.blueprint.utils.JavaUtils;
import org.apache.aries.blueprint.utils.ReflectionUtils;
import org.apache.aries.blueprint.utils.ServiceListener;
import org.osgi.framework.Bundle;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.blueprint.reflect.ServiceMetadata;
import org.osgi.service.cm.ManagedServiceFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * TODO: if we need to make those exported services tied to their references as for other &lt;service/&gt; elements
 * TODO: it becomes a problem as currently we would have to create a specific recipe or something like that
 *
 * @version $Rev$, $Date$
 */
public class CmManagedServiceFactory extends BaseManagedServiceFactory<Object> {

    private static final Logger LOGGER = LoggerFactory.getLogger(CmManagedServiceFactory.class);

    private ExtendedBlueprintContainer blueprintContainer;
    private String id;
    private String factoryPid;
    private List<String> interfaces;
    private int autoExport;
    private int ranking;
    private Map<Object, Object> serviceProperties;
    private String managedComponentName;
    private String componentDestroyMethod;
    private List<ServiceListener> listeners;

    private ServiceRegistration registration;

    public CmManagedServiceFactory(ExtendedBlueprintContainer blueprintContainer) {
        super(blueprintContainer.getBundleContext(), null);
        this.blueprintContainer = blueprintContainer;
    }

    public void init() throws Exception {
        LOGGER.debug("Initializing CmManagedServiceFactory for factoryPid={}", factoryPid);
        Properties props = new Properties();
        props.put(Constants.SERVICE_PID, factoryPid);
        Bundle bundle = blueprintContainer.getBundleContext().getBundle();
        props.put(Constants.BUNDLE_SYMBOLICNAME, bundle.getSymbolicName());
        props.put(Constants.BUNDLE_VERSION, bundle.getHeaders().get(Constants.BUNDLE_VERSION));

        registration = blueprintContainer.getBundleContext().registerService(ManagedServiceFactory.class.getName(), this, (Dictionary) props);
    }

    public void destroy() {
        ServiceUtil.safeUnregister(registration);
        super.destroy();
    }

    public Map<ServiceRegistration, Object> getServiceMap() {
        return Collections.unmodifiableMap(getServices());
    }

    public void setListeners(List<ServiceListener> listeners) {
        this.listeners = listeners;
    }

    public void setId(String id) {
        this.id = id;
    }

    public void setFactoryPid(String factoryPid) {
        this.factoryPid = factoryPid;
    }

    public void setInterfaces(List<String> interfaces) {
        this.interfaces = interfaces;
    }

    public void setAutoExport(int autoExport) {
        this.autoExport = autoExport;
    }

    public void setRanking(int ranking) {
        this.ranking = ranking;
    }

    public void setServiceProperties(Map serviceProperties) {
        this.serviceProperties = serviceProperties;
    }

    public void setManagedComponentName(String managedComponentName) {
        this.managedComponentName = managedComponentName;
    }

    public void setComponentDestroyMethod(String componentDestroyMethod) {
        this.componentDestroyMethod = componentDestroyMethod;
    }

    private void getRegistrationProperties(Dictionary properties, boolean update) {
        String pid = (String) properties.get(Constants.SERVICE_PID);
        CmProperties cm = findServiceProcessor();
        if (cm == null) {
            while (!properties.isEmpty()) {
                properties.remove(properties.keys().nextElement());
            }
        } else {
            if (!cm.getUpdate()) {
                if (update) {
                    while (!properties.isEmpty()) {
                        properties.remove(properties.keys().nextElement());
                    }
                    for (Map.Entry entry : cm.getProperties().entrySet()) {
                        properties.put(entry.getKey(), entry.getValue());
                    }
                } else {
                    cm.updated(properties);
                }
            }
        }
        if (serviceProperties != null) {
            for (Map.Entry entry : serviceProperties.entrySet()) {
                properties.put(entry.getKey(), entry.getValue());
            }
        }
        properties.put(Constants.SERVICE_RANKING, ranking);
        properties.put(Constants.SERVICE_PID, pid);
    }

    private void updateComponentProperties(Object bean, Dictionary props) {
        CmManagedProperties cm = findBeanProcessor();
        if (cm != null) {
            cm.updated(bean, props);
        }
    }

    private CmManagedProperties findBeanProcessor() {
        for (BeanProcessor beanProcessor : blueprintContainer.getProcessors(BeanProcessor.class)) {
            if (beanProcessor instanceof CmManagedProperties) {
                CmManagedProperties cm = (CmManagedProperties) beanProcessor;
                if (managedComponentName.equals(cm.getBeanName()) && "".equals(cm.getPersistentId())) {
                    return cm;
                }
            }
        }
        return null;
    }

    private CmProperties findServiceProcessor() {
        for (ServiceProcessor processor : blueprintContainer.getProcessors(ServiceProcessor.class)) {
            if (processor instanceof CmProperties) {
                CmProperties cm = (CmProperties) processor;
                if (id.equals(cm.getServiceId())) {
                    return cm;
                }
            }
        }
        return null;
    }

    private Method findDestroyMethod(Class clazz, Class... args) {
        Method method = null;
        if (componentDestroyMethod != null && componentDestroyMethod.length() > 0) {
            List<Method> methods = ReflectionUtils.findCompatibleMethods(clazz, componentDestroyMethod, args);
            if (methods != null && !methods.isEmpty()) {
                method = methods.get(0);
            }
        }
        return method;
    }

    protected Object doCreate(Dictionary properties) throws Exception {
        updateComponentProperties(null, copy(properties));
        Object component = blueprintContainer.getComponentInstance(managedComponentName);
        getRegistrationProperties(properties, false);
        return component;
    }

    protected Object doUpdate(Object service, Dictionary properties) throws Exception {
        updateComponentProperties(service, copy(properties));
        getRegistrationProperties(properties, true);
        return service;
    }

    protected void doDestroy(Object service, Dictionary properties, int code) throws Exception {
        Method method = findDestroyMethod(service.getClass(), int.class);
        if (method != null) {
            try {
                method.invoke(service, code);
            } catch (Exception e) {
                LOGGER.info("Error destroying component", e);
            }
        } else {
            method = findDestroyMethod(service.getClass());
            if (method != null) {
                try {
                    method.invoke(service);
                } catch (Exception e) {
                    LOGGER.info("Error destroying component", e);
                }
            }
        }
    }

    protected void postRegister(Object service, Dictionary properties, ServiceRegistration registration) {
        if (listeners != null && !listeners.isEmpty()) {
            Hashtable props = new Hashtable();
            JavaUtils.copy(properties, props);
            for (ServiceListener listener : listeners) {
                listener.register(service, props);
            }
        }
    }

    protected void preUnregister(Object service, Dictionary properties, ServiceRegistration registration) {
        if (listeners != null && !listeners.isEmpty()) {
            Hashtable props = new Hashtable();
            JavaUtils.copy(properties, props);
            for (ServiceListener listener : listeners) {
                listener.unregister(service, props);
            }
        }
    }

    protected String[] getExposedClasses(Object service) {
        Class serviceClass = service.getClass();
        Set<String> classes;
        switch (autoExport) {
            case ServiceMetadata.AUTO_EXPORT_INTERFACES:
                classes = ReflectionUtils.getImplementedInterfaces(new HashSet<String>(), serviceClass);
                break;
            case ServiceMetadata.AUTO_EXPORT_CLASS_HIERARCHY:
                classes = ReflectionUtils.getSuperClasses(new HashSet<String>(), serviceClass);
                break;
            case ServiceMetadata.AUTO_EXPORT_ALL_CLASSES:
                classes = ReflectionUtils.getSuperClasses(new HashSet<String>(), serviceClass);
                classes = ReflectionUtils.getImplementedInterfaces(classes, serviceClass);
                break;
            default:
                classes = new HashSet<String>(interfaces);
                break;
        }
        return classes.toArray(new String[classes.size()]);
    }

    private Hashtable copy(Dictionary source) {
        Hashtable ht = new Hashtable();
        JavaUtils.copy(ht, source);
        return ht;
    }

}
