package com.alibaba.otter.canal.client.adapter.config.bind;

import java.beans.PropertyDescriptor;
import java.util.*;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.support.ResourceEditorRegistrar;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.core.convert.ConversionService;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.validation.*;

import com.alibaba.otter.canal.client.adapter.config.common.PropertySources;

/**
 * Validate some {@link Properties} (or optionally
 * {@link org.springframework.core.env.PropertySources}) by binding them to an
 * object of a specified type and then optionally running a {@link Validator}
 * over it.
 *
 * @param <T> the target type
 * @author Dave Syer
 */
public class PropertiesConfigurationFactory<T> implements FactoryBean<T>, ApplicationContextAware, MessageSourceAware, InitializingBean {

    private static final char[] EXACT_DELIMITERS       = { '_', '.', '[' };

    private static final char[] TARGET_NAME_DELIMITERS = { '_', '.' };

    private static final Log    logger                 = LogFactory.getLog(PropertiesConfigurationFactory.class);

    private boolean             ignoreUnknownFields    = true;

    private boolean             ignoreInvalidFields;

    private boolean             exceptionIfInvalid     = true;

    private PropertySources     propertySources;

    private final T             target;

    private Validator           validator;

    private ApplicationContext  applicationContext;

    private MessageSource       messageSource;

    private boolean             hasBeenBound           = false;

    private boolean             ignoreNestedProperties = false;

    private String              targetName;

    private ConversionService   conversionService;

    private boolean             resolvePlaceholders    = true;

    /**
     * Create a new {@link PropertiesConfigurationFactory} instance.
     *
     * @param target the target object to bind too
     * @see #PropertiesConfigurationFactory(Class)
     */
    public PropertiesConfigurationFactory(T target){
        Assert.notNull(target, "target must not be null");
        this.target = target;
    }

    /**
     * Create a new {@link PropertiesConfigurationFactory} instance.
     *
     * @param type the target type
     * @see #PropertiesConfigurationFactory(Class)
     */
    @SuppressWarnings("unchecked")
    public PropertiesConfigurationFactory(Class<?> type){
        Assert.notNull(type, "type must not be null");
        this.target = (T) BeanUtils.instantiate(type);
    }

    /**
     * Flag to disable binding of nested properties (i.e. those with period
     * separators in their paths). Can be useful to disable this if the name prefix
     * is empty and you don't want to ignore unknown fields.
     *
     * @param ignoreNestedProperties the flag to set (default false)
     */
    public void setIgnoreNestedProperties(boolean ignoreNestedProperties) {
        this.ignoreNestedProperties = ignoreNestedProperties;
    }

    /**
     * Set whether to ignore unknown fields, that is, whether to ignore bind
     * parameters that do not have corresponding fields in the target object.
     * <p>
     * Default is "true". Turn this off to enforce that all bind parameters must
     * have a matching field in the target object.
     *
     * @param ignoreUnknownFields if unknown fields should be ignored
     */
    public void setIgnoreUnknownFields(boolean ignoreUnknownFields) {
        this.ignoreUnknownFields = ignoreUnknownFields;
    }

    /**
     * Set whether to ignore invalid fields, that is, whether to ignore bind
     * parameters that have corresponding fields in the target object which are not
     * accessible (for example because of null values in the nested path).
     * <p>
     * Default is "false". Turn this on to ignore bind parameters for nested objects
     * in non-existing parts of the target object graph.
     *
     * @param ignoreInvalidFields if invalid fields should be ignored
     */
    public void setIgnoreInvalidFields(boolean ignoreInvalidFields) {
        this.ignoreInvalidFields = ignoreInvalidFields;
    }

    /**
     * Set the target name.
     *
     * @param targetName the target name
     */
    public void setTargetName(String targetName) {
        this.targetName = targetName;
    }

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

    /**
     * Set the message source.
     *
     * @param messageSource the message source
     */
    @Override
    public void setMessageSource(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    /**
     * Set the property sources.
     *
     * @param propertySources the property sources
     */
    public void setPropertySources(PropertySources propertySources) {
        this.propertySources = propertySources;
    }

    /**
     * Set the conversion service.
     *
     * @param conversionService the conversion service
     */
    public void setConversionService(ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    /**
     * Set the validator.
     *
     * @param validator the validator
     */
    public void setValidator(Validator validator) {
        this.validator = validator;
    }

    /**
     * Set a flag to indicate that an exception should be raised if a Validator is
     * available and validation fails.
     *
     * @param exceptionIfInvalid the flag to set
     * @deprecated as of 1.5, do not specify a {@link Validator} if validation
     * should not occur
     */
    @Deprecated
    public void setExceptionIfInvalid(boolean exceptionIfInvalid) {
        this.exceptionIfInvalid = exceptionIfInvalid;
    }

    /**
     * Flag to indicate that placeholders should be replaced during binding. Default
     * is true.
     *
     * @param resolvePlaceholders flag value
     */
    public void setResolvePlaceholders(boolean resolvePlaceholders) {
        this.resolvePlaceholders = resolvePlaceholders;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        bindPropertiesToTarget();
    }

    @Override
    public Class<?> getObjectType() {
        if (this.target == null) {
            return Object.class;
        }
        return this.target.getClass();
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

    @Override
    public T getObject() throws Exception {
        if (!this.hasBeenBound) {
            bindPropertiesToTarget();
        }
        return this.target;
    }

    public void bindPropertiesToTarget() throws BindException {
        Assert.state(this.propertySources != null, "PropertySources should not be null");
        try {
            if (logger.isTraceEnabled()) {
                logger.trace("Property Sources: " + this.propertySources);

            }
            this.hasBeenBound = true;
            doBindPropertiesToTarget();
        } catch (BindException ex) {
            if (this.exceptionIfInvalid) {
                throw ex;
            }
            logger.error("Failed to load Properties validation bean. " + "Your Properties may be invalid.", ex);
        }
    }

    private void doBindPropertiesToTarget() throws BindException {
        RelaxedDataBinder dataBinder = (this.targetName != null ? new RelaxedDataBinder(this.target,
            this.targetName) : new RelaxedDataBinder(this.target));
        if (this.validator != null && this.validator.supports(dataBinder.getTarget().getClass())) {
            dataBinder.setValidator(this.validator);
        }
        if (this.conversionService != null) {
            dataBinder.setConversionService(this.conversionService);
        }
        dataBinder.setAutoGrowCollectionLimit(Integer.MAX_VALUE);
        dataBinder.setIgnoreNestedProperties(this.ignoreNestedProperties);
        dataBinder.setIgnoreInvalidFields(this.ignoreInvalidFields);
        dataBinder.setIgnoreUnknownFields(this.ignoreUnknownFields);
        customizeBinder(dataBinder);
        if (this.applicationContext != null) {
            ResourceEditorRegistrar resourceEditorRegistrar = new ResourceEditorRegistrar(this.applicationContext,
                this.applicationContext.getEnvironment());
            resourceEditorRegistrar.registerCustomEditors(dataBinder);
        }
        Iterable<String> relaxedTargetNames = getRelaxedTargetNames();
        Set<String> names = getNames(relaxedTargetNames);
        PropertyValues propertyValues = getPropertySourcesPropertyValues(names, relaxedTargetNames);
        dataBinder.bind(propertyValues);
        if (this.validator != null) {
            dataBinder.validate();
        }
        checkForBindingErrors(dataBinder);
    }

    private Iterable<String> getRelaxedTargetNames() {
        return (this.target != null
                && StringUtils.hasLength(this.targetName) ? new RelaxedNames(this.targetName) : null);
    }

    private Set<String> getNames(Iterable<String> prefixes) {
        Set<String> names = new LinkedHashSet<>();
        if (this.target != null) {
            PropertyDescriptor[] descriptors = BeanUtils.getPropertyDescriptors(this.target.getClass());
            for (PropertyDescriptor descriptor : descriptors) {
                String name = descriptor.getName();
                if (!name.equals("class")) {
                    RelaxedNames relaxedNames = RelaxedNames.forCamelCase(name);
                    if (prefixes == null) {
                        for (String relaxedName : relaxedNames) {
                            names.add(relaxedName);
                        }
                    } else {
                        for (String prefix : prefixes) {
                            for (String relaxedName : relaxedNames) {
                                names.add(prefix + "." + relaxedName);
                                names.add(prefix + "_" + relaxedName);
                            }
                        }
                    }
                }
            }
        }
        return names;
    }

    private PropertyValues getPropertySourcesPropertyValues(Set<String> names, Iterable<String> relaxedTargetNames) {
        PropertyNamePatternsMatcher includes = getPropertyNamePatternsMatcher(names, relaxedTargetNames);
        return new PropertySourcesPropertyValues(this.propertySources, names, includes, this.resolvePlaceholders);
    }

    private PropertyNamePatternsMatcher getPropertyNamePatternsMatcher(Set<String> names,
                                                                       Iterable<String> relaxedTargetNames) {
        if (this.ignoreUnknownFields && !isMapTarget()) {
            // Since unknown fields are ignored we can filter them out early to save
            // unnecessary calls to the PropertySource.
            return new DefaultPropertyNamePatternsMatcher(EXACT_DELIMITERS, true, names);
        }
        if (relaxedTargetNames != null) {
            // We can filter properties to those starting with the target name, but
            // we can't do a complete filter since we need to trigger the
            // unknown fields check
            Set<String> relaxedNames = new HashSet<>();
            for (String relaxedTargetName : relaxedTargetNames) {
                relaxedNames.add(relaxedTargetName);
            }
            return new DefaultPropertyNamePatternsMatcher(TARGET_NAME_DELIMITERS, true, relaxedNames);
        }
        // Not ideal, we basically can't filter anything
        return PropertyNamePatternsMatcher.ALL;
    }

    private boolean isMapTarget() {
        return this.target != null && Map.class.isAssignableFrom(this.target.getClass());
    }

    private void checkForBindingErrors(RelaxedDataBinder dataBinder) throws BindException {
        BindingResult errors = dataBinder.getBindingResult();
        if (errors.hasErrors()) {
            logger.error("Properties configuration failed validation");
            for (ObjectError error : errors.getAllErrors()) {
                logger.error(this.messageSource != null ? this.messageSource.getMessage(error, Locale.getDefault())
                                                          + " (" + error + ")" : error);
            }
            if (this.exceptionIfInvalid) {
                throw new BindException(errors);
            }
        }
    }

    /**
     * Customize the data binder.
     *
     * @param dataBinder the data binder that will be used to bind and validate
     */
    protected void customizeBinder(DataBinder dataBinder) {
    }
}
