/*
 * Copyright 2012-2017 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
 *
 *      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.springframework.boot.autoconfigureprocessor;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.tools.FileObject;
import javax.tools.StandardLocation;

/**
 * Annotation processor to store certain annotations from auto-configuration classes in a
 * property file.
 *
 * @author Madhura Bhave
 * @author Phillip Webb
 */
@SupportedAnnotationTypes({ "org.springframework.context.annotation.Configuration",
		"org.springframework.boot.autoconfigure.condition.ConditionalOnClass",
		"org.springframework.boot.autoconfigure.AutoConfigureBefore",
		"org.springframework.boot.autoconfigure.AutoConfigureAfter",
		"org.springframework.boot.autoconfigure.AutoConfigureOrder" })
public class AutoConfigureAnnotationProcessor extends AbstractProcessor {

	protected static final String PROPERTIES_PATH = "META-INF/"
			+ "spring-autoconfigure-metadata.properties";

	private Map<String, String> annotations;

	private final Properties properties = new Properties();

	public AutoConfigureAnnotationProcessor() {
		Map<String, String> annotations = new LinkedHashMap<String, String>();
		addAnnotations(annotations);
		this.annotations = Collections.unmodifiableMap(annotations);
	}

	protected void addAnnotations(Map<String, String> annotations) {
		annotations.put("Configuration",
				"org.springframework.context.annotation.Configuration");
		annotations.put("ConditionalOnClass",
				"org.springframework.boot.autoconfigure.condition.ConditionalOnClass");
		annotations.put("AutoConfigureBefore",
				"org.springframework.boot.autoconfigure.AutoConfigureBefore");
		annotations.put("AutoConfigureAfter",
				"org.springframework.boot.autoconfigure.AutoConfigureAfter");
		annotations.put("AutoConfigureOrder",
				"org.springframework.boot.autoconfigure.AutoConfigureOrder");
	}

	@Override
	public boolean process(Set<? extends TypeElement> annotations,
			RoundEnvironment roundEnv) {
		for (Map.Entry<String, String> entry : this.annotations.entrySet()) {
			process(roundEnv, entry.getKey(), entry.getValue());
		}
		if (roundEnv.processingOver()) {
			try {
				writeProperties();
			}
			catch (Exception ex) {
				throw new IllegalStateException("Failed to write metadata", ex);
			}
		}
		return false;
	}

	private void process(RoundEnvironment roundEnv, String propertyKey,
			String annotationName) {
		TypeElement annotationType = this.processingEnv.getElementUtils()
				.getTypeElement(annotationName);
		if (annotationType != null) {
			for (Element element : roundEnv.getElementsAnnotatedWith(annotationType)) {
				Element enclosingElement = element.getEnclosingElement();
				if (enclosingElement != null
						&& enclosingElement.getKind() == ElementKind.PACKAGE) {
					processElement(element, propertyKey, annotationName);
				}
			}
		}
	}

	private void processElement(Element element, String propertyKey,
			String annotationName) {
		try {
			String qualifiedName = getQualifiedName(element);
			AnnotationMirror annotation = getAnnotation(element, annotationName);
			if (qualifiedName != null && annotation != null) {
				List<Object> values = getValues(annotation);
				this.properties.put(qualifiedName + "." + propertyKey,
						toCommaDelimitedString(values));
				this.properties.put(qualifiedName, "");
			}
		}
		catch (Exception ex) {
			throw new IllegalStateException(
					"Error processing configuration meta-data on " + element, ex);
		}
	}

	private AnnotationMirror getAnnotation(Element element, String type) {
		if (element != null) {
			for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
				if (type.equals(annotation.getAnnotationType().toString())) {
					return annotation;
				}
			}
		}
		return null;
	}

	private String toCommaDelimitedString(List<Object> list) {
		StringBuilder result = new StringBuilder();
		for (Object item : list) {
			result.append(result.length() != 0 ? "," : "");
			result.append(item);
		}
		return result.toString();
	}

	@SuppressWarnings("unchecked")
	private List<Object> getValues(AnnotationMirror annotation) {
		List<Object> result = new ArrayList<Object>();
		for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : annotation
				.getElementValues().entrySet()) {
			String attributeName = entry.getKey().getSimpleName().toString();
			if ("name".equals(attributeName) || "value".equals(attributeName)) {
				Object value = entry.getValue().getValue();
				if (value instanceof List) {
					for (AnnotationValue annotationValue : (List<AnnotationValue>) value) {
						result.add(annotationValue.getValue());
					}
				}
				else {
					result.add(value);
				}
			}
		}
		return result;
	}

	private String getQualifiedName(Element element) {
		if (element != null) {
			TypeElement enclosingElement = getEnclosingTypeElement(element.asType());
			if (enclosingElement != null) {
				return getQualifiedName(enclosingElement) + "$"
						+ ((DeclaredType) element.asType()).asElement().getSimpleName()
								.toString();
			}
			if (element instanceof TypeElement) {
				return ((TypeElement) element).getQualifiedName().toString();
			}
		}
		return null;
	}

	private TypeElement getEnclosingTypeElement(TypeMirror type) {
		if (type instanceof DeclaredType) {
			DeclaredType declaredType = (DeclaredType) type;
			Element enclosingElement = declaredType.asElement().getEnclosingElement();
			if (enclosingElement != null && enclosingElement instanceof TypeElement) {
				return (TypeElement) enclosingElement;
			}
		}
		return null;
	}

	private void writeProperties() throws IOException {
		if (!this.properties.isEmpty()) {
			FileObject file = this.processingEnv.getFiler()
					.createResource(StandardLocation.CLASS_OUTPUT, "", PROPERTIES_PATH);
			OutputStream outputStream = file.openOutputStream();
			try {
				this.properties.store(outputStream, null);
			}
			finally {
				outputStream.close();
			}
		}
	}

}
