/*
 * 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.configurationprocessor;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;

import org.junit.Assert;
import org.junit.rules.TemporaryFolder;

import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationsample.ConfigurationProperties;
import org.springframework.boot.configurationsample.NestedConfigurationProperty;
import org.springframework.boot.junit.compiler.TestCompiler;
import org.springframework.boot.junit.compiler.TestCompiler.TestCompilationTask;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.FileSystemUtils;

/**
 * A TestProject contains a copy of a subset of test sample code.
 * <p>
 * Why a copy? Because when doing incremental build testing, we need to make modifications
 * to the contents of the 'test project'. But we don't want to actually modify the
 * original content itself.
 *
 * @author Kris De Volder
 */
public class TestProject {

	private static final Class<?>[] ALWAYS_INCLUDE = { ConfigurationProperties.class,
			NestedConfigurationProperty.class };

	/**
	 * Contains copies of the original source so we can modify it safely to test
	 * incremental builds.
	 */
	private File sourceFolder;

	private TestCompiler compiler;

	private Set<File> sourceFiles = new LinkedHashSet<>();

	public TestProject(TemporaryFolder tempFolder, Class<?>... classes)
			throws IOException {
		this.sourceFolder = tempFolder.newFolder();
		this.compiler = new TestCompiler(tempFolder) {
			@Override
			protected File getSourceFolder() {
				return TestProject.this.sourceFolder;
			}
		};
		Set<Class<?>> contents = new HashSet<>(Arrays.asList(classes));
		contents.addAll(Arrays.asList(ALWAYS_INCLUDE));
		copySources(contents);
	}

	private void copySources(Set<Class<?>> contents) throws IOException {
		for (Class<?> type : contents) {
			copySources(type);
		}
	}

	private void copySources(Class<?> type) throws IOException {
		File original = getOriginalSourceFile(type);
		File target = getSourceFile(type);
		target.getParentFile().mkdirs();
		FileCopyUtils.copy(original, target);
		this.sourceFiles.add(target);
	}

	public File getSourceFile(Class<?> type) {
		return new File(this.sourceFolder, TestCompiler.sourcePathFor(type));
	}

	public ConfigurationMetadata fullBuild() {
		TestConfigurationMetadataAnnotationProcessor processor = new TestConfigurationMetadataAnnotationProcessor(
				this.compiler.getOutputLocation());
		TestCompilationTask task = this.compiler.getTask(this.sourceFiles);
		deleteFolderContents(this.compiler.getOutputLocation());
		task.call(processor);
		return processor.getMetadata();
	}

	public ConfigurationMetadata incrementalBuild(Class<?>... toRecompile) {
		TestConfigurationMetadataAnnotationProcessor processor = new TestConfigurationMetadataAnnotationProcessor(
				this.compiler.getOutputLocation());
		TestCompilationTask task = this.compiler.getTask(toRecompile);
		task.call(processor);
		return processor.getMetadata();
	}

	private void deleteFolderContents(File outputFolder) {
		FileSystemUtils.deleteRecursively(outputFolder);
		outputFolder.mkdirs();
	}

	/**
	 * Retrieve File relative to project's output folder.
	 * @param relativePath the relative path
	 * @return the output file
	 */
	public File getOutputFile(String relativePath) {
		Assert.assertFalse(new File(relativePath).isAbsolute());
		return new File(this.compiler.getOutputLocation(), relativePath);
	}

	/**
	 * Add source code at the end of file, just before last '}'
	 * @param target the target
	 * @param snippetStream the snippet stream
	 * @throws Exception if the source cannot be added
	 */
	public void addSourceCode(Class<?> target, InputStream snippetStream)
			throws Exception {
		File targetFile = getSourceFile(target);
		String contents = getContents(targetFile);
		int insertAt = contents.lastIndexOf('}');
		String additionalSource = FileCopyUtils
				.copyToString(new InputStreamReader(snippetStream));
		contents = contents.substring(0, insertAt) + additionalSource
				+ contents.substring(insertAt);
		putContents(targetFile, contents);
	}

	/**
	 * Delete source file for given class from project.
	 * @param type the class to delete
	 */
	public void delete(Class<?> type) {
		File target = getSourceFile(type);
		target.delete();
		this.sourceFiles.remove(target);
	}

	/**
	 * Restore source code of given class to its original contents.
	 * @param type the class to revert
	 * @throws IOException on IO error
	 */
	public void revert(Class<?> type) throws IOException {
		Assert.assertTrue(getSourceFile(type).exists());
		copySources(type);
	}

	/**
	 * Add source code of given class to this project.
	 * @param type the class to add
	 * @throws IOException on IO error
	 */
	public void add(Class<?> type) throws IOException {
		Assert.assertFalse(getSourceFile(type).exists());
		copySources(type);
	}

	public void replaceText(Class<?> type, String find, String replace) throws Exception {
		File target = getSourceFile(type);
		String contents = getContents(target);
		contents = contents.replace(find, replace);
		putContents(target, contents);
	}

	/**
	 * Find the 'original' source code for given test class. Clients or subclasses should
	 * have no need to know about these. They should work only with the copied source
	 * code.
	 */
	private File getOriginalSourceFile(Class<?> type) {
		return new File(TestCompiler.SOURCE_FOLDER, TestCompiler.sourcePathFor(type));
	}

	private static void putContents(File targetFile, String contents)
			throws FileNotFoundException, IOException, UnsupportedEncodingException {
		FileCopyUtils.copy(new StringReader(contents), new FileWriter(targetFile));
	}

	private static String getContents(File file) throws Exception {
		return FileCopyUtils.copyToString(new FileReader(file));
	}

}
