/**
 * 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.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
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.services.ExtendedBlueprintContainer;
import org.apache.aries.blueprint.utils.ReflectionUtils;
import org.osgi.framework.Bundle;
import org.osgi.framework.Constants;
import org.osgi.service.blueprint.container.ReifiedType;
import org.osgi.service.blueprint.reflect.BeanMetadata;
import org.osgi.service.cm.ConfigurationAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * TODO
 *
 * @version $Rev$, $Date$
 */
public class CmManagedProperties implements ManagedObject, BeanProcessor {

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

    private ExtendedBlueprintContainer blueprintContainer;
    private ConfigurationAdmin configAdmin;
    private ManagedObjectManager managedObjectManager;
    private String persistentId;
    private String updateStrategy;
    private String updateMethod;
    private String beanName;

    private final Object lock = new Object();
    private final Set<Object> beans = new HashSet<Object>();
    private Dictionary<String, Object> properties;
    private boolean initialized;

    public ExtendedBlueprintContainer getBlueprintContainer() {
        return blueprintContainer;
    }

    public void setBlueprintContainer(ExtendedBlueprintContainer blueprintContainer) {
        this.blueprintContainer = blueprintContainer;
    }

    public ConfigurationAdmin getConfigAdmin() {
        return configAdmin;
    }

    public void setConfigAdmin(ConfigurationAdmin configAdmin) {
        this.configAdmin = configAdmin;
    }

    public void setManagedObjectManager(ManagedObjectManager managedObjectManager) {
        this.managedObjectManager = managedObjectManager;
    }
    
    public ManagedObjectManager getManagedObjectManager() {
        return managedObjectManager;
    }
    
    public Bundle getBundle() {
        return blueprintContainer.getBundleContext().getBundle();
    }
    
    public String getPersistentId() {
        return persistentId;
    }

    public void setPersistentId(String persistentId) {
        this.persistentId = persistentId;
    }

    public String getUpdateStrategy() {
        return updateStrategy;
    }

    public void setUpdateStrategy(String updateStrategy) {
        this.updateStrategy = updateStrategy;
    }

    public String getUpdateMethod() {
        return updateMethod;
    }

    public void setUpdateMethod(String updateMethod) {
        this.updateMethod = updateMethod;
    }

    public String getBeanName() {
        return beanName;
    }

    public void setBeanName(String beanName) {
        this.beanName = beanName;
    }
    
    public void init() throws Exception {
        LOGGER.debug("Initializing CmManagedProperties for bean={} / pid={}", beanName, persistentId);
        
        Properties props = new Properties();
        props.put(Constants.SERVICE_PID, persistentId);
        Bundle bundle = blueprintContainer.getBundleContext().getBundle();
        props.put(Constants.BUNDLE_SYMBOLICNAME, bundle.getSymbolicName());
        props.put(Constants.BUNDLE_VERSION, bundle.getHeaders().get(Constants.BUNDLE_VERSION));
                
        synchronized (lock) {
            managedObjectManager.register(this, props);
        }
    }

    public void destroy() {
        managedObjectManager.unregister(this);
    }

    public void updated(final Dictionary props) {
        if (!initialized) {
            properties = props;
            initialized = true;
            return;
        }
        LOGGER.debug("Configuration updated for bean={} / pid={}", beanName, persistentId);
        synchronized (lock) {
            properties = props;
            for (Object bean : beans) {
                updated(bean, properties);
            }
        }
    }

    public void updated(Object bean, final Dictionary props) {
        LOGGER.debug("Configuration updated for bean={} / pid={}", beanName, persistentId);
        synchronized (lock) {
            properties = props;
            if (bean != null) {
                inject(bean, false);
            }
        }
    }

    public Object beforeInit(Object bean, String beanName, BeanCreator beanCreator, BeanMetadata beanData) {
        if (beanName != null && beanName.equals(this.beanName)) {
            LOGGER.debug("Adding bean for bean={} / pid={}", beanName, persistentId);
            synchronized (lock) {
                beans.add(bean);
                inject(bean, true);
            }
        }
        return bean;
    }

    public Object afterInit(Object bean, String beanName, BeanCreator beanCreator, BeanMetadata beanData) {
        return bean;
    }

    public void beforeDestroy(Object bean, String beanName) {
        if (beanName.equals(this.beanName)) {
            LOGGER.debug("Removing bean for bean={} / pid={}", beanName, persistentId);
            synchronized (lock) {
                beans.remove(bean);
            }
        }
    }

    public void afterDestroy(Object bean, String beanName) {
    }

    private void inject(Object bean, boolean initial) {
        LOGGER.debug("Injecting bean for bean={} / pid={}", beanName, persistentId);
        LOGGER.debug("Configuration: {}", properties);
        if (initial || "container-managed".equals(updateStrategy)) {
            if (properties != null) {
                for (Enumeration<String> e = properties.keys(); e.hasMoreElements();) {
                    String key = e.nextElement();
                    Object val = properties.get(key);
                    String setterName = "set" + Character.toUpperCase(key.charAt(0));
                    if (key.length() > 0) {
                        setterName += key.substring(1);
                    }
                    Set<Method> validSetters = new LinkedHashSet<Method>();
                    List<Method> methods = new ArrayList<Method>(Arrays.asList(bean.getClass().getMethods()));
                    methods.addAll(Arrays.asList(bean.getClass().getDeclaredMethods()));
                    for (Method method : methods) {
                        if (method.getName().equals(setterName)) {
                            if (shouldSkip(method)) {
                                continue;
                            }
                            Class methodParameterType = method.getParameterTypes()[0];
                            Object propertyValue;
                            try {
                                propertyValue = blueprintContainer.getConverter().convert(val, new ReifiedType(methodParameterType));
                            } catch (Throwable t) {
                                LOGGER.debug("Unable to convert value for setter: " + method, t);
                                continue;
                            }
                            if (methodParameterType.isPrimitive() && propertyValue == null) {
                                LOGGER.debug("Null can not be assigned to {}: {}", methodParameterType.getName(), method);
                                continue;
                            }
                            if (validSetters.add(method)) {
                                try {
                                    method.invoke(bean, propertyValue);
                                } catch (Exception t) {
                                    LOGGER.debug("Setter can not be invoked: " + method, getRealCause(t));
                                }
                            }
                        }
                    }
                    if (validSetters.isEmpty()) {
                        LOGGER.debug("Unable to find a valid setter method for property {} and value {}", key, val);
                    }
                }
            }
        } else if ("component-managed".equals(updateStrategy) && updateMethod != null) {
            List<Method> methods = ReflectionUtils.findCompatibleMethods(bean.getClass(), updateMethod, new Class[] { Map.class });
            Map map = null;
            if (properties != null) {
                map = new HashMap();
                for (Enumeration<String> e = properties.keys(); e.hasMoreElements();) {
                    String key = e.nextElement();
                    Object val = properties.get(key);
                    map.put(key, val);
                }
            }
            for (Method method : methods) {
                try {
                    method.invoke(bean, map);
                } catch (Throwable t) {
                    LOGGER.warn("Unable to call method " + method + " on bean " + beanName, getRealCause(t));
                }
            }
        }
    }

    private boolean shouldSkip(Method method) {
        String msg = null;
        if (method.getParameterTypes().length == 0) {
            msg = "takes no parameters";
        } else if (method.getParameterTypes().length > 1) {
            msg = "takes more than one parameter";
        } else if (method.getReturnType() != Void.TYPE) {
            msg = "returns a value";
        } else if (Modifier.isAbstract(method.getModifiers())) {
            msg = "is abstract";
        } else if (!Modifier.isPublic(method.getModifiers())) {
            msg = "is not public";
        } else if (Modifier.isStatic(method.getModifiers())) {
            msg = "is static";
        }
        if (msg != null) {
            LOGGER.debug("Skipping setter {} because it " + msg, method);
            return true;
        } else {
            return false;
        }
    }

    private static Throwable getRealCause(Throwable t) {
        if (t instanceof InvocationTargetException && t.getCause() != null) {
            return t.getCause();
        }
        return t;
    }

}
