/*
 * Copyright 2002-2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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.springframework.amqp.rabbit.config;

import java.util.Map;

import org.w3c.dom.Element;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;

/**
 * @author Dave Syer
 * @author Gary Russell
 * @author Felipe Gutierrez
 * @author Artem Bilan
 *
 */
public abstract class AbstractExchangeParser extends AbstractSingleBeanDefinitionParser {

	private static final ThreadLocal<Element> CURRENT_ELEMENT = new ThreadLocal<>();

	private static final String ARGUMENTS_ELEMENT = "exchange-arguments";

	private static final String DURABLE_ATTRIBUTE = "durable";

	private static final String AUTO_DELETE_ATTRIBUTE = "auto-delete";

	private static final String DELAYED_ATTRIBUTE = "delayed";

	private static final String BINDINGS_ELE = "bindings";

	private static final String BINDING_ELE = "binding";

	protected static final String BINDING_QUEUE_ATTR = "queue";

	protected static final String BINDING_EXCHANGE_ATTR = "exchange";

	protected static final String BINDING_ARGUMENTS = "binding-arguments";

	private static final String REF_ATTRIBUTE = "ref";

	@Override
	protected boolean shouldGenerateIdAsFallback() {
		return true;
	}

	@Override
	protected boolean shouldParseNameAsAliases() {
		Element element = CURRENT_ELEMENT.get();
		try {
			return element == null || !element.hasAttribute(ID_ATTRIBUTE);
		}
		finally {
			CURRENT_ELEMENT.remove();
		}
	}

	@Override
	protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
		String exchangeName = element.getAttribute(NAME_ATTRIBUTE);
		builder.addConstructorArgValue(new TypedStringValue(exchangeName));
		parseBindings(element, parserContext, builder, exchangeName);

		NamespaceUtils.addConstructorArgBooleanValueIfAttributeDefined(builder, element, DURABLE_ATTRIBUTE, true);
		NamespaceUtils.addConstructorArgBooleanValueIfAttributeDefined(builder, element, AUTO_DELETE_ATTRIBUTE,
				false);
		NamespaceUtils.setValueIfAttributeDefined(builder, element, DELAYED_ATTRIBUTE);
		NamespaceUtils.setValueIfAttributeDefined(builder, element, "internal");

		this.parseArguments(element, ARGUMENTS_ELEMENT, parserContext, builder, null);

		NamespaceUtils.parseDeclarationControls(element, builder);
		CURRENT_ELEMENT.set(element);
	}

	protected void parseBindings(Element element, ParserContext parserContext, BeanDefinitionBuilder builder,
			String exchangeName) {
		Element bindingsElement = DomUtils.getChildElementByTagName(element, BINDINGS_ELE);
		doParseBindings(element, parserContext, exchangeName, bindingsElement, this);
	}

	protected void doParseBindings(Element element, ParserContext parserContext,
			String exchangeName, Element bindings, AbstractExchangeParser parser) {
		if (bindings != null) {
			for (Element binding : DomUtils.getChildElementsByTagName(bindings, BINDING_ELE)) {
				BeanDefinitionBuilder bindingBuilder = parser.parseBinding(exchangeName, binding,
						parserContext);
				NamespaceUtils.parseDeclarationControls(element, bindingBuilder);
				BeanDefinition beanDefinition = bindingBuilder.getBeanDefinition();
				registerBeanDefinition(new BeanDefinitionHolder(beanDefinition, parserContext.getReaderContext()
						.generateBeanName(beanDefinition)), parserContext.getRegistry());
			}
		}
	}

	protected abstract BeanDefinitionBuilder parseBinding(String exchangeName, Element binding,
			ParserContext parserContext);

	protected void parseDestination(Element binding, ParserContext parserContext, BeanDefinitionBuilder builder) {
		String queueAttribute = binding.getAttribute(BINDING_QUEUE_ATTR);
		String exchangeAttribute = binding.getAttribute(BINDING_EXCHANGE_ATTR);
		boolean hasQueueAttribute = StringUtils.hasText(queueAttribute);
		boolean hasExchangeAttribute = StringUtils.hasText(exchangeAttribute);
		if (hasQueueAttribute == hasExchangeAttribute) {
			parserContext.getReaderContext().error("Binding must have exactly one of 'queue' or 'exchange'", binding);
		}
		if (hasQueueAttribute) {
			builder.addPropertyReference("destinationQueue", queueAttribute);
		}
		if (hasExchangeAttribute) {
			builder.addPropertyReference("destinationExchange", exchangeAttribute);
		}

		this.parseArguments(binding, BINDING_ARGUMENTS, parserContext, builder, "arguments");
	}

	private void parseArguments(Element element, String argumentsElementName, ParserContext parserContext,
								BeanDefinitionBuilder builder, String propertyName) {
		Element argumentsElement = DomUtils.getChildElementByTagName(element, argumentsElementName);
		if (argumentsElement != null) {

			String ref = argumentsElement.getAttribute(REF_ATTRIBUTE);
			Map<?, ?> map = parserContext.getDelegate().parseMapElement(argumentsElement,
					builder.getRawBeanDefinition());
			if (StringUtils.hasText(ref)) {
				if (map != null && map.size() > 0) {
					parserContext.getReaderContext().error("You cannot have both a 'ref' and a nested map", element);
				}
				if (propertyName == null) {
					builder.addConstructorArgReference(ref);
				}
				else {
					builder.addPropertyReference(propertyName, ref);
				}
			}
			else {
				if (propertyName == null) {
					builder.addConstructorArgValue(map);
				}
				else {
					builder.addPropertyValue(propertyName, map);
				}
			}
		}
	}

}
