/*
 * Copyright 2016 DiffPlug
 *
 * 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 com.diffplug.spotless;

import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;

import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.artifacts.ResolveException;
import org.gradle.api.artifacts.dsl.RepositoryHandler;
import org.gradle.testfixtures.ProjectBuilder;

import com.diffplug.common.base.Errors;
import com.diffplug.common.base.StandardSystemProperty;
import com.diffplug.common.base.Suppliers;
import com.diffplug.common.collect.ImmutableSet;
import com.diffplug.common.io.Files;

public class TestProvisioner {
	public static Project gradleProject(File dir) {
		File userHome = new File(StandardSystemProperty.USER_HOME.value());
		return ProjectBuilder.builder()
				.withGradleUserHomeDir(new File(userHome, ".gradle"))
				.withProjectDir(dir)
				.build();
	}

	/**
	 * Creates a Provisioner for the given repositories.
	 *
	 * The first time a project is created, there are ~7 seconds of configuration
	 * which will go away for all subsequent runs.
	 *
	 * Every call to resolve will take about 1 second, even when all artifacts are resolved.
	 */
	private static Provisioner createWithRepositories(Consumer<RepositoryHandler> repoConfig) {
		// Running this takes ~3 seconds the first time it is called. Probably because of classloading.
		File tempDir = Files.createTempDir();
		Project project = TestProvisioner.gradleProject(tempDir);
		repoConfig.accept(project.getRepositories());
		return (withTransitives, mavenCoords) -> {
			Dependency[] deps = mavenCoords.stream()
					.map(project.getDependencies()::create)
					.toArray(Dependency[]::new);
			Configuration config = project.getConfigurations().detachedConfiguration(deps);
			config.setTransitive(withTransitives);
			config.setDescription(mavenCoords.toString());
			try {
				return config.resolve();
			} catch (ResolveException e) {
				/* Provide Maven coordinates in exception message instead of static string 'detachedConfiguration' */
				throw new ResolveException(config.getDescription(), e);
			} finally {
				// delete the temp dir
				try {
					java.nio.file.Files.walk(tempDir.toPath())
							.sorted(Comparator.reverseOrder())
							.map(Path::toFile)
							.forEach(File::delete);
				} catch (IOException e) {
					throw Errors.asRuntime(e);
				}
			}
		};
	}

	/** Creates a Provisioner which will cache the result of previous calls. */
	@SuppressWarnings("unchecked")
	private static Provisioner caching(String name, Supplier<Provisioner> input) {
		File spotlessDir = new File(StandardSystemProperty.USER_DIR.value()).getParentFile();
		File testlib = new File(spotlessDir, "testlib");
		File cacheFile = new File(testlib, "build/tmp/testprovisioner." + name + ".cache");

		Map<ImmutableSet<String>, ImmutableSet<File>> cached;
		if (cacheFile.exists()) {
			try (ObjectInputStream inputStream = new ObjectInputStream(Files.asByteSource(cacheFile).openBufferedStream())) {
				cached = (Map<ImmutableSet<String>, ImmutableSet<File>>) inputStream.readObject();
			} catch (IOException | ClassNotFoundException e) {
				throw Errors.asRuntime(e);
			}
		} else {
			cached = new HashMap<>();
		}
		return (withTransitives, mavenCoordsRaw) -> {
			ImmutableSet<String> mavenCoords = ImmutableSet.copyOf(mavenCoordsRaw);
			synchronized (TestProvisioner.class) {
				ImmutableSet<File> result = cached.get(mavenCoords);
				// double-check that depcache pruning hasn't removed them since our cache cached them
				boolean needsToBeSet = result == null || !result.stream().allMatch(file -> file.exists() && file.isFile() && file.length() > 0);
				if (needsToBeSet) {
					result = ImmutableSet.copyOf(input.get().provisionWithTransitives(withTransitives, mavenCoords));
					cached.put(mavenCoords, result);
					try (ObjectOutputStream outputStream = new ObjectOutputStream(Files.asByteSink(cacheFile).openBufferedStream())) {
						outputStream.writeObject(cached);
					} catch (IOException e) {
						throw Errors.asRuntime(e);
					}
				}
				return result;
			}
		};
	}

	/** Creates a Provisioner for the jcenter repo. */
	public static Provisioner jcenter() {
		return jcenter.get();
	}

	private static final Supplier<Provisioner> jcenter = Suppliers.memoize(() -> {
		return caching("jcenter", () -> createWithRepositories(repo -> repo.jcenter()));
	});

	/** Creates a Provisioner for the mavenCentral repo. */
	public static Provisioner mavenCentral() {
		return mavenCentral.get();
	}

	private static final Supplier<Provisioner> mavenCentral = Suppliers.memoize(() -> {
		return caching("mavenCentral", () -> createWithRepositories(repo -> repo.mavenCentral()));
	});

	/** Creates a Provisioner for the local maven repo for development purpose. */
	public static Provisioner mavenLocal() {
		return mavenLocal.get();
	}

	private static final Supplier<Provisioner> mavenLocal = () -> createWithRepositories(repo -> repo.mavenLocal());

	/** Creates a Provisioner for the Sonatype snapshots maven repo for development purpose. */
	public static Provisioner snapshots() {
		return snapshots.get();
	}

	private static final Supplier<Provisioner> snapshots = () -> createWithRepositories(repo -> {
		repo.maven(setup -> {
			setup.setUrl("https://oss.sonatype.org/content/repositories/snapshots");
		});
	});
}

@@ -4021,16 +4021,129 @@
 ap%3C%3E();%0A
+%09%09%09try %7B%0A%09%09%09%09Files.createParentDirs(cacheFile);%0A%09%09%09%7D catch (IOException e) %7B%0A%09%09%09%09throw Errors.asRuntime(e);%0A%09%09%09%7D%0A
 %09%09%7D%0A%09%09re

Fix a rare no-parent-directory bug in TestProvisioner (only triggered if you run tests in IDE without having run the gradle build at all).