/*
 * Copyright 2012-2016 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.cli.compiler;

import groovy.lang.Grab;
import groovy.lang.GroovyClassLoader;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.expr.ConstantExpression;

import org.springframework.boot.cli.compiler.dependencies.ArtifactCoordinatesResolver;
import org.springframework.boot.cli.compiler.grape.DependencyResolutionContext;

/**
 * Customizer that allows dependencies to be added during compilation. Adding a dependency
 * results in a {@link Grab @Grab} annotation being added to the primary {@link ClassNode
 * class} is the {@link ModuleNode module} that's being customized.
 * <p>
 * This class provides a fluent API for conditionally adding dependencies. For example:
 * {@code dependencies.ifMissing("com.corp.SomeClass").add(module)}.
 *
 * @author Phillip Webb
 * @author Andy Wilkinson
 */
public class DependencyCustomizer {

	private final GroovyClassLoader loader;

	private final ClassNode classNode;

	private final DependencyResolutionContext dependencyResolutionContext;

	/**
	 * Create a new {@link DependencyCustomizer} instance.
	 * @param loader the current classloader
	 * @param moduleNode the current module
	 * @param dependencyResolutionContext the context for dependency resolution
	 */
	public DependencyCustomizer(GroovyClassLoader loader, ModuleNode moduleNode,
			DependencyResolutionContext dependencyResolutionContext) {
		this.loader = loader;
		this.classNode = moduleNode.getClasses().get(0);
		this.dependencyResolutionContext = dependencyResolutionContext;
	}

	/**
	 * Create a new nested {@link DependencyCustomizer}.
	 * @param parent the parent customizer
	 */
	protected DependencyCustomizer(DependencyCustomizer parent) {
		this.loader = parent.loader;
		this.classNode = parent.classNode;
		this.dependencyResolutionContext = parent.dependencyResolutionContext;
	}

	public String getVersion(String artifactId) {
		return getVersion(artifactId, "");

	}

	public String getVersion(String artifactId, String defaultVersion) {
		String version = this.dependencyResolutionContext.getArtifactCoordinatesResolver()
				.getVersion(artifactId);
		if (version == null) {
			version = defaultVersion;
		}
		return version;
	}

	/**
	 * Create a nested {@link DependencyCustomizer} that only applies if any of the
	 * specified class names are not on the class path.
	 * @param classNames the class names to test
	 * @return a nested {@link DependencyCustomizer}
	 */
	public DependencyCustomizer ifAnyMissingClasses(final String... classNames) {
		return new DependencyCustomizer(this) {
			@Override
			protected boolean canAdd() {
				for (String className : classNames) {
					try {
						DependencyCustomizer.this.loader.loadClass(className);
					}
					catch (Exception ex) {
						return true;
					}
				}
				return false;
			}
		};
	}

	/**
	 * Create a nested {@link DependencyCustomizer} that only applies if all of the
	 * specified class names are not on the class path.
	 * @param classNames the class names to test
	 * @return a nested {@link DependencyCustomizer}
	 */
	public DependencyCustomizer ifAllMissingClasses(final String... classNames) {
		return new DependencyCustomizer(this) {
			@Override
			protected boolean canAdd() {
				for (String className : classNames) {
					try {
						DependencyCustomizer.this.loader.loadClass(className);
						return false;
					}
					catch (Exception ex) {
						// swallow exception and continue
					}
				}
				return DependencyCustomizer.this.canAdd();
			}
		};
	}

	/**
	 * Create a nested {@link DependencyCustomizer} that only applies if the specified
	 * paths are on the class path.
	 * @param paths the paths to test
	 * @return a nested {@link DependencyCustomizer}
	 */
	public DependencyCustomizer ifAllResourcesPresent(final String... paths) {
		return new DependencyCustomizer(this) {
			@Override
			protected boolean canAdd() {
				for (String path : paths) {
					try {
						if (DependencyCustomizer.this.loader.getResource(path) == null) {
							return false;
						}
						return true;
					}
					catch (Exception ex) {
						// swallow exception and continue
					}
				}
				return DependencyCustomizer.this.canAdd();
			}
		};
	}

	/**
	 * Create a nested {@link DependencyCustomizer} that only applies at least one of the
	 * specified paths is on the class path.
	 * @param paths the paths to test
	 * @return a nested {@link DependencyCustomizer}
	 */
	public DependencyCustomizer ifAnyResourcesPresent(final String... paths) {
		return new DependencyCustomizer(this) {
			@Override
			protected boolean canAdd() {
				for (String path : paths) {
					try {
						if (DependencyCustomizer.this.loader.getResource(path) != null) {
							return true;
						}
						return false;
					}
					catch (Exception ex) {
						// swallow exception and continue
					}
				}
				return DependencyCustomizer.this.canAdd();
			}
		};
	}

	/**
	 * Add dependencies and all of their dependencies. The group ID and version of the
	 * dependencies are resolved from the modules using the customizer's
	 * {@link ArtifactCoordinatesResolver}.
	 * @param modules The module IDs
	 * @return this {@link DependencyCustomizer} for continued use
	 */
	public DependencyCustomizer add(String... modules) {
		for (String module : modules) {
			add(module, null, null, true);
		}
		return this;
	}

	/**
	 * Add a single dependency and, optionally, all of its dependencies. The group ID and
	 * version of the dependency are resolved from the module using the customizer's
	 * {@link ArtifactCoordinatesResolver}.
	 * @param module The module ID
	 * @param transitive {@code true} if the transitive dependencies should also be added,
	 * otherwise {@code false}.
	 * @return this {@link DependencyCustomizer} for continued use
	 */
	public DependencyCustomizer add(String module, boolean transitive) {
		return add(module, null, null, transitive);
	}

	/**
	 * Add a single dependency with the specified classifier and type and, optionally, all
	 * of its dependencies. The group ID and version of the dependency are resolved from
	 * the module by using the customizer's {@link ArtifactCoordinatesResolver}.
	 * @param module The module ID
	 * @param classifier The classifier, may be {@code null}
	 * @param type The type, may be {@code null}
	 * @param transitive {@code true} if the transitive dependencies should also be added,
	 * otherwise {@code false}.
	 * @return this {@link DependencyCustomizer} for continued use
	 */
	public DependencyCustomizer add(String module, String classifier, String type,
			boolean transitive) {
		if (canAdd()) {
			ArtifactCoordinatesResolver artifactCoordinatesResolver = this.dependencyResolutionContext
					.getArtifactCoordinatesResolver();
			this.classNode.addAnnotation(
					createGrabAnnotation(artifactCoordinatesResolver.getGroupId(module),
							artifactCoordinatesResolver.getArtifactId(module),
							artifactCoordinatesResolver.getVersion(module), classifier,
							type, transitive));
		}
		return this;
	}

	private AnnotationNode createGrabAnnotation(String group, String module,
			String version, String classifier, String type, boolean transitive) {
		AnnotationNode annotationNode = new AnnotationNode(new ClassNode(Grab.class));
		annotationNode.addMember("group", new ConstantExpression(group));
		annotationNode.addMember("module", new ConstantExpression(module));
		annotationNode.addMember("version", new ConstantExpression(version));
		if (classifier != null) {
			annotationNode.addMember("classifier", new ConstantExpression(classifier));
		}
		if (type != null) {
			annotationNode.addMember("type", new ConstantExpression(type));
		}
		annotationNode.addMember("transitive", new ConstantExpression(transitive));
		annotationNode.addMember("initClass", new ConstantExpression(false));
		return annotationNode;
	}

	/**
	 * Strategy called to test if dependencies can be added. Subclasses override as
	 * required. Returns {@code true} by default.
	 * @return {@code true} if dependencies can be added, otherwise {@code false}
	 */
	protected boolean canAdd() {
		return true;
	}

	/**
	 * Returns the {@link DependencyResolutionContext}.
	 * @return the dependency resolution context
	 */
	public DependencyResolutionContext getDependencyResolutionContext() {
		return this.dependencyResolutionContext;
	}

}
