/*
 * 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.dubbo.config.spring;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.dubbo.common.utils.Assert;
import org.apache.dubbo.config.ArgumentConfig;
import org.apache.dubbo.config.MethodConfig;
import org.apache.dubbo.config.ReferenceConfig;
import org.apache.dubbo.config.bootstrap.DubboBootstrap;
import org.apache.dubbo.config.spring.beans.factory.annotation.AnnotationPropertyValuesAdapter;
import org.apache.dubbo.config.spring.beans.factory.annotation.ReferenceBeanBuilder;
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertyResolver;
import org.springframework.validation.DataBinder;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class ReferenceBeanManager implements ApplicationContextAware {
    public static final String BEAN_NAME = "dubboReferenceBeanManager";
    private final Log logger = LogFactory.getLog(getClass());
    private Map<String, ReferenceBean> configMap = new ConcurrentHashMap<>();
    private ApplicationContext applicationContext;
    private volatile boolean initialized = false;


    public void addReference(ReferenceBean referenceBean) throws Exception {
        Assert.notNull(referenceBean.getId(), "The id of ReferenceBean cannot be empty");
        //TODO generate reference bean id and unique cache key
        String key = referenceBean.getId();
        ReferenceBean oldReferenceBean = configMap.get(key);
        if (oldReferenceBean != null) {
            if (referenceBean != oldReferenceBean) {
                logger.warn("Found duplicated ReferenceBean id: " + key);
            }
            return;
        }
        configMap.put(key, referenceBean);

        // if add reference after prepareReferenceBeans(), should init it immediately.
        if (initialized) {
            initReferenceBean(referenceBean);
        }
    }

    public ReferenceBean get(String id) {
        return configMap.get(id);
    }

    public Collection<ReferenceBean> getReferences() {
        return configMap.values();
    }

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

    /**
     * Initialize all reference beans, call at Dubbo starting
     * @throws Exception
     */
    public void prepareReferenceBeans() throws Exception {
        // prepare all reference beans
        Map<String, ReferenceBean> referenceBeanMap = applicationContext.getBeansOfType(ReferenceBean.class, true, false);
        for (ReferenceBean referenceBean : referenceBeanMap.values()) {
            addReference(referenceBean);
        }

        for (ReferenceBean referenceBean : getReferences()) {
            initReferenceBean(referenceBean);
        }
        initialized = true;
    }

    /**
     * NOTE: This method should only call after all dubbo config beans and all property resolvers is loaded.
     *
     * @param referenceBean
     * @throws Exception
     */
    private void initReferenceBean(ReferenceBean referenceBean) throws Exception {

        if (referenceBean.getReferenceConfig() != null) {
            return;
        }

        Environment environment = applicationContext.getEnvironment();
        Map<String, Object> referenceProps = referenceBean.getReferenceProps();
        if (referenceProps == null) {
            MutablePropertyValues propertyValues = referenceBean.getPropertyValues();
            if (propertyValues == null) {
                throw new RuntimeException("ReferenceBean is invalid, missing 'propertyValues'");
            }
            referenceProps = toReferenceProps(propertyValues, environment);
        }

        //resolve placeholders
        resolvePlaceholders(referenceProps, environment);

        //create real ReferenceConfig
        ReferenceConfig referenceConfig = ReferenceBeanBuilder.create(new AnnotationAttributes(new LinkedHashMap<>(referenceProps)), applicationContext)
                .defaultInterfaceClass(referenceBean.getObjectType())
                .build();

        referenceBean.setReferenceConfig(referenceConfig);

        // register ReferenceConfig
        DubboBootstrap.getInstance().reference(referenceConfig);

    }

    private void resolvePlaceholders(Map<String, Object> referenceProps, PropertyResolver propertyResolver) {
        for (Map.Entry<String, Object> entry : referenceProps.entrySet()) {
            Object value = entry.getValue();
            if (value instanceof String) {
                String valueToResovle = (String) value;
                entry.setValue(propertyResolver.resolveRequiredPlaceholders(valueToResovle));
            } else if (value instanceof String[]) {
                String[] strings = (String[]) value;
                for (int i = 0; i < strings.length; i++) {
                    strings[i] = propertyResolver.resolveRequiredPlaceholders(strings[i]);
                }
                entry.setValue(strings);
            }
        }
    }

    private Map<String, Object> toReferenceProps(MutablePropertyValues propertyValues, PropertyResolver propertyResolver) {
        Map<String, Object> referenceProps;
        referenceProps = new LinkedHashMap<>();
        for (PropertyValue propertyValue : propertyValues.getPropertyValueList()) {
            String propertyName = propertyValue.getName();
            Object value = propertyValue.getValue();
            if ("methods".equals(propertyName)) {
                ManagedList managedList = (ManagedList) value;
                List<MethodConfig> methodConfigs = new ArrayList<>();
                for (Object el : managedList) {
                    MethodConfig methodConfig = createMethodConfig(((BeanDefinitionHolder) el).getBeanDefinition(), propertyResolver);
                    methodConfigs.add(methodConfig);
                }
                value = methodConfigs.toArray(new MethodConfig[0]);
            } else if ("parameters".equals(propertyName)) {
                value = createParameterMap((ManagedMap) value, propertyResolver);
            }
            if (value instanceof RuntimeBeanReference) {
                RuntimeBeanReference beanReference = (RuntimeBeanReference) value;
                value = applicationContext.getBean(beanReference.getBeanName());
            }
            referenceProps.put(propertyName, value);
        }
        return referenceProps;
    }

    private MethodConfig createMethodConfig(BeanDefinition beanDefinition, PropertyResolver propertyResolver) {
        Map<String, Object> attributes = new LinkedHashMap<>();
        MutablePropertyValues pvs = beanDefinition.getPropertyValues();
        for (PropertyValue propertyValue : pvs.getPropertyValueList()) {
            String propertyName = propertyValue.getName();
            Object value = propertyValue.getValue();
            if ("arguments".equals(propertyName)) {
                ManagedList managedList = (ManagedList) value;
                List<ArgumentConfig> argumentConfigs = new ArrayList<>();
                for (Object el : managedList) {
                    ArgumentConfig argumentConfig = createArgumentConfig(((BeanDefinitionHolder) el).getBeanDefinition(), propertyResolver);
                    argumentConfigs.add(argumentConfig);
                }
                value = argumentConfigs.toArray(new ArgumentConfig[0]);
            } else if ("parameters".equals(propertyName)) {
                value = createParameterMap((ManagedMap) value, propertyResolver);
            }

            if (value instanceof RuntimeBeanReference) {
                RuntimeBeanReference beanReference = (RuntimeBeanReference) value;
                value = applicationContext.getBean(beanReference.getBeanName());
            }
            attributes.put(propertyName, value);
        }
        MethodConfig methodConfig = new MethodConfig();
        DataBinder dataBinder = new DataBinder(methodConfig);
        dataBinder.bind(new AnnotationPropertyValuesAdapter(attributes, propertyResolver));
        return methodConfig;
    }

    private ArgumentConfig createArgumentConfig(BeanDefinition beanDefinition, PropertyResolver propertyResolver) {
        ArgumentConfig argumentConfig = new ArgumentConfig();
        DataBinder dataBinder = new DataBinder(argumentConfig);
        dataBinder.bind(beanDefinition.getPropertyValues());
        return argumentConfig;
    }

    private Map<String, String> createParameterMap(ManagedMap managedMap, PropertyResolver propertyResolver) {
        Map<String, String> map = new LinkedHashMap<>();
        Set<Map.Entry<String, TypedStringValue>> entrySet = managedMap.entrySet();
        for (Map.Entry<String, TypedStringValue> entry : entrySet) {
            map.put(entry.getKey(), entry.getValue().getValue());
        }
        return map;
    }


}
