package com.baidu.disconf.client.addons.properties;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionVisitor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringValueResolver;

/**
 * 具有 reloadable 的 property bean
 * 扩展了 DefaultPropertyPlaceholderConfigurer
 * 特性如下：
 * 1. 启动时 监控 动态config，并维护它们与相应bean的关系
 * 2. 当动态config变动时，此configurer会进行reload
 * 3. reload 时会 compare config value, and set value for beans
 */
public class ReloadingPropertyPlaceholderConfigurer extends DefaultPropertyPlaceholderConfigurer implements
        InitializingBean, DisposableBean, IReloadablePropertiesListener, ApplicationContextAware {

    protected static final Logger logger = LoggerFactory.getLogger(ReloadingPropertyPlaceholderConfigurer.class);

    // 默认的 property 标识符
    private String placeholderPrefix = DEFAULT_PLACEHOLDER_PREFIX;

    private String placeholderSuffix = DEFAULT_PLACEHOLDER_SUFFIX;

    private String beanName;

    private BeanFactory beanFactory;
    private Properties[] propertiesArray;

    /**
     * 对于被标记为动态的，进行 构造 property dependency
     * 非动态的，则由原来的spring进行处理
     *
     * @param strVal
     * @param props
     * @param visitedPlaceholders
     *
     * @return
     *
     * @throws BeanDefinitionStoreException
     */
    protected String parseStringValue(String strVal, Properties props, Set visitedPlaceholders)
            throws BeanDefinitionStoreException {

        DynamicProperty dynamic = null;

        // replace reloading prefix and suffix by "normal" prefix and suffix.
        // remember all the "dynamic" placeholders encountered.
        StringBuffer buf = new StringBuffer(strVal);
        int startIndex = strVal.indexOf(this.placeholderPrefix);
        while (startIndex != -1) {
            int endIndex = buf.toString().indexOf(this.placeholderSuffix, startIndex + this.placeholderPrefix.length());
            if (endIndex != -1) {
                if (currentBeanName != null && currentPropertyName != null) {
                    String placeholder = buf.substring(startIndex + this.placeholderPrefix.length(), endIndex);
                    placeholder = getPlaceholder(placeholder);
                    if (dynamic == null) {
                        dynamic = getDynamic(currentBeanName, currentPropertyName, strVal);
                    }
                    addDependency(dynamic, placeholder);
                } else {
                    logger.warn("dynamic property outside bean property value - ignored: " + strVal);
                }
                startIndex = endIndex - this.placeholderPrefix.length() + this.placeholderPrefix.length() +
                        this.placeholderSuffix.length();
                startIndex = strVal.indexOf(this.placeholderPrefix, startIndex);
            } else {
                startIndex = -1;
            }
        }
        // then, business as usual. no recursive reloading placeholders please.
        return super.parseStringValue(buf.toString(), props, visitedPlaceholders);
    }

    /**
     * @param currentBeanName     当前的bean name
     * @param currentPropertyName 当前它的属性
     * @param orgStrVal           原来的值
     *
     * @return
     */
    private DynamicProperty getDynamic(String currentBeanName, String currentPropertyName, String orgStrVal) {
        DynamicProperty dynamic = new DynamicProperty(currentBeanName, currentPropertyName, orgStrVal);
        DynamicProperty found = dynamicProperties.get(dynamic);
        if (found != null) {
            return found;
        }
        dynamicProperties.put(dynamic, dynamic);
        return dynamic;
    }

    private Properties lastMergedProperties;

    /**
     * merge property and record last merge
     *
     * @return
     *
     * @throws IOException
     */
    protected Properties mergeProperties() throws IOException {
        Properties properties = super.mergeProperties();
        this.lastMergedProperties = properties;
        return properties;
    }

    /**
     * 当配置更新时，被调用
     *
     * @param event
     */
    public void propertiesReloaded(PropertiesReloadedEvent event) {

        Properties oldProperties = lastMergedProperties;

        try {
            //
            Properties newProperties = mergeProperties();

            //
            // 获取哪些 dynamic property 被影响
            //
            Set<String> placeholders = placeholderToDynamics.keySet();
            Set<DynamicProperty> allDynamics = new HashSet<DynamicProperty>();
            for (String placeholder : placeholders) {
                String newValue = newProperties.getProperty(placeholder);
                String oldValue = oldProperties.getProperty(placeholder);
                if (newValue != null && !newValue.equals(oldValue) || newValue == null && oldValue != null) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Property changed detected: " + placeholder +
                                (newValue != null ? "=" + newValue : " removed"));
                    }
                    List<DynamicProperty> affectedDynamics = placeholderToDynamics.get(placeholder);
                    allDynamics.addAll(affectedDynamics);
                }
            }

            //
            // 获取受影响的beans
            //
            Map<String, List<DynamicProperty>> dynamicsByBeanName = new HashMap<String, List<DynamicProperty>>();
            Map<String, Object> beanByBeanName = new HashMap<String, Object>();
            for (DynamicProperty dynamic : allDynamics) {
                String beanName = dynamic.getBeanName();
                List<DynamicProperty> l = dynamicsByBeanName.get(beanName);

                if (l == null) {
                    dynamicsByBeanName.put(beanName, (l = new ArrayList<DynamicProperty>()));
                    Object bean = null;
                    try {
                        bean = applicationContext.getBean(beanName);
                        beanByBeanName.put(beanName, bean);
                    } catch (BeansException e) {
                        // keep dynamicsByBeanName list, warn only once.
                        logger.error("Error obtaining bean " + beanName, e);
                    }

                    //
                    // say hello
                    //
                    try {
                        if (bean instanceof IReconfigurationAware) {
                            ((IReconfigurationAware) bean).beforeReconfiguration();  // hello!
                        }
                    } catch (Exception e) {
                        logger.error("Error calling beforeReconfiguration on " + beanName, e);
                    }
                }
                l.add(dynamic);
            }

            //
            // 处理受影响的bean
            //
            Collection<String> beanNames = dynamicsByBeanName.keySet();
            for (String beanName : beanNames) {
                Object bean = beanByBeanName.get(beanName);
                if (bean == null) // problems obtaining bean, earlier
                {
                    continue;
                }
                BeanWrapper beanWrapper = new BeanWrapperImpl(bean);

                // for all affected ...
                List<DynamicProperty> dynamics = dynamicsByBeanName.get(beanName);
                for (DynamicProperty dynamic : dynamics) {
                    String propertyName = dynamic.getPropertyName();
                    String unparsedValue = dynamic.getUnparsedValue();

                    // obtain an updated value, including dependencies
                    String newValue;
                    removeDynamic(dynamic);
                    currentBeanName = beanName;
                    currentPropertyName = propertyName;
                    try {
                        newValue = parseStringValue(unparsedValue, newProperties, new HashSet());
                    } finally {
                        currentBeanName = null;
                        currentPropertyName = null;
                    }
                    if (logger.isInfoEnabled()) {
                        logger.info("Updating property " + beanName + "." + propertyName + " to " + newValue);
                    }

                    // assign it to the bean
                    try {
                        beanWrapper.setPropertyValue(propertyName, newValue);
                    } catch (BeansException e) {
                        logger.error("Error setting property " + beanName + "." + propertyName + " to " + newValue, e);
                    }
                }
            }

            //
            // say goodbye.
            //
            for (String beanName : beanNames) {
                Object bean = beanByBeanName.get(beanName);
                try {

                    if (bean instanceof IReconfigurationAware) {
                        ((IReconfigurationAware) bean).afterReconfiguration();
                    }
                } catch (Exception e) {
                    logger.error("Error calling afterReconfiguration on " + beanName, e);
                }
            }

        } catch (IOException e) {
            logger.error("Error trying to reload net.unicon.iamlabs.spring.properties.example.net.unicon.iamlabs" +
                    ".spring" + ".properties: " + e.getMessage(), e);
        }
    }

    /**
     *
     */
    static class DynamicProperty {
        final String beanName;
        final String propertyName;
        final String unparsedValue;
        List<String> placeholders = new ArrayList<String>();

        /**
         * @param beanName
         * @param propertyName
         * @param unparsedValue
         */
        public DynamicProperty(String beanName, String propertyName, String unparsedValue) {
            this.beanName = beanName;
            this.propertyName = propertyName;
            this.unparsedValue = unparsedValue;
        }

        public void addPlaceholder(String placeholder) {
            placeholders.add(placeholder);
        }

        public String getUnparsedValue() {
            return unparsedValue;
        }

        public String getBeanName() {
            return beanName;
        }

        public String getPropertyName() {
            return propertyName;
        }

        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            final DynamicProperty that = (DynamicProperty) o;

            if (beanName != null ? !beanName.equals(that.beanName) : that.beanName != null) {
                return false;
            }
            if (propertyName != null ? !propertyName.equals(that.propertyName) : that.propertyName != null) {
                return false;
            }

            return true;
        }

        public int hashCode() {
            int result;
            result = (beanName != null ? beanName.hashCode() : 0);
            result = 29 * result + (propertyName != null ? propertyName.hashCode() : 0);
            return result;
        }
    }

    private Map<DynamicProperty, DynamicProperty> dynamicProperties = new HashMap<DynamicProperty, DynamicProperty>();
    private Map<String, List<DynamicProperty>> placeholderToDynamics = new HashMap<String, List<DynamicProperty>>();

    /**
     * 建立 placeholder 与 dynamic 的对应关系
     *
     * @param dynamic
     * @param placeholder
     */
    private void addDependency(DynamicProperty dynamic, String placeholder) {
        List<DynamicProperty> l = placeholderToDynamics.get(placeholder);
        if (l == null) {
            l = new ArrayList<DynamicProperty>();
            placeholderToDynamics.put(placeholder, l);
        }
        if (!l.contains(dynamic)) {
            l.add(dynamic);
        }
        dynamic.addPlaceholder(placeholder);
    }

    /**
     * 删除 placeholder 与 dynamic 的对应关系
     *
     * @param dynamic
     */
    private void removeDynamic(DynamicProperty dynamic) {
        List<String> placeholders = dynamic.placeholders;
        for (String placeholder : placeholders) {
            List<DynamicProperty> l = placeholderToDynamics.get(placeholder);
            l.remove(dynamic);
        }
        dynamic.placeholders.clear();
        dynamicProperties.remove(dynamic);
    }

    private String currentBeanName;
    private String currentPropertyName;

    /**
     * copy & paste, just so we can insert our own visitor.
     * 启动时 进行配置的解析
     */
    protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props)
            throws BeansException {

        BeanDefinitionVisitor visitor =
                new ReloadingPropertyPlaceholderConfigurer.PlaceholderResolvingBeanDefinitionVisitor(props);
        String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames();
        for (int i = 0; i < beanNames.length; i++) {
            // Check that we're not parsing our own bean definition,
            // to avoid failing on unresolvable placeholders in net.unicon.iamlabs.spring.properties.example.net
            // .unicon.iamlabs.spring.properties file locations.
            if (!(beanNames[i].equals(this.beanName) && beanFactoryToProcess.equals(this.beanFactory))) {
                this.currentBeanName = beanNames[i];
                try {
                    BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(beanNames[i]);
                    try {
                        visitor.visitBeanDefinition(bd);
                    } catch (BeanDefinitionStoreException ex) {
                        throw new BeanDefinitionStoreException(bd.getResourceDescription(), beanNames[i],
                                ex.getMessage());
                    }
                } finally {
                    currentBeanName = null;
                }
            }
        }

        StringValueResolver stringValueResolver = new PlaceholderResolvingStringValueResolver(props);

        // New in Spring 2.5: resolve placeholders in alias target names and aliases as well.
        beanFactoryToProcess.resolveAliases(stringValueResolver);

        // New in Spring 3.0: resolve placeholders in embedded values such as annotation attributes.
        beanFactoryToProcess.addEmbeddedValueResolver(stringValueResolver);
    }

    /**
     * afterPropertiesSet
     * 将自己 添加 property listener
     */
    public void afterPropertiesSet() {
        for (Properties properties : propertiesArray) {
            if (properties instanceof ReloadableProperties) {
                logger.debug("add property listener: " + properties.toString());
                ((ReloadableProperties) properties).addReloadablePropertiesListener(this);
            }
        }
    }

    /**
     * destroy
     * 删除 property listener
     *
     * @throws Exception
     */
    public void destroy() throws Exception {
        for (Properties properties : propertiesArray) {
            if (properties instanceof ReloadableProperties) {
                logger.debug("remove property listener: " + properties.toString());
                ((ReloadableProperties) properties).removeReloadablePropertiesListener(this);
            }
        }
    }

    /**
     * 替换掉spring的 config resolver，这样我们才可以解析掉自己的config
     */
    private class PlaceholderResolvingBeanDefinitionVisitor extends BeanDefinitionVisitor {

        private final Properties props;

        public PlaceholderResolvingBeanDefinitionVisitor(Properties props) {
            this.props = props;
        }

        protected void visitPropertyValues(MutablePropertyValues pvs) {
            PropertyValue[] pvArray = pvs.getPropertyValues();
            for (PropertyValue pv : pvArray) {
                currentPropertyName = pv.getName();
                try {
                    Object newVal = resolveValue(pv.getValue());
                    if (!ObjectUtils.nullSafeEquals(newVal, pv.getValue())) {
                        pvs.addPropertyValue(pv.getName(), newVal);
                    }
                } finally {
                    currentPropertyName = null;
                }
            }
        }

        protected String resolveStringValue(String strVal) throws BeansException {
            return parseStringValue(strVal, this.props, new HashSet());
        }
    }

    /**
     *
     */
    protected class PlaceholderResolvingStringValueResolver implements StringValueResolver {

        private final Properties props;

        public PlaceholderResolvingStringValueResolver(Properties props) {
            this.props = props;
        }

        @Override
        public String resolveStringValue(String strVal) throws BeansException {
            return parseStringValue(strVal, this.props, new HashSet());
        }
    }

    /**
     * the application context is needed to find the beans again during reconfiguration
     */
    private ApplicationContext applicationContext;

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public void setProperties(Properties properties) {
        setPropertiesArray(new Properties[] {properties});
    }

    public void setPropertiesArray(Properties[] propertiesArray) {
        this.propertiesArray = propertiesArray;
        super.setPropertiesArray(propertiesArray);
    }

    public void setPlaceholderPrefix(String placeholderPrefix) {
        this.placeholderPrefix = placeholderPrefix;
        super.setPlaceholderPrefix(placeholderPrefix);
    }

    public void setPlaceholderSuffix(String placeholderSuffix) {
        this.placeholderSuffix = placeholderSuffix;
        super.setPlaceholderSuffix(placeholderPrefix);
    }

    public void setBeanName(String beanName) {
        this.beanName = beanName;
        super.setBeanName(beanName);
    }

    public void setBeanFactory(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
        super.setBeanFactory(beanFactory);
    }
}
