/**
 * Licensed under the Educational Community 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
 *
 * <p>http://www.opensource.org/licenses/ecl2.php
 *
 * <p>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 eu.dnetlib.maven.plugin.properties;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
 * Writes project properties for the keys listed in specified properties files. Based on:
 * http://site.kuali.org/maven/plugins/properties-maven-plugin/2.0.1/write-project-properties-mojo.html
 *
 * @author mhorst
 * @goal write-project-properties
 */
public class WritePredefinedProjectProperties extends AbstractMojo {

	private static final String CR = "\r";
	private static final String LF = "\n";
	private static final String TAB = "\t";
	protected static final String PROPERTY_PREFIX_ENV = "env.";
	private static final String ENCODING_UTF8 = "utf8";

	/** @parameter property="properties.includePropertyKeysFromFiles" */
	private String[] includePropertyKeysFromFiles;

	/**
	 * @parameter default-value="${project}"
	 * @required
	 * @readonly
	 */
	protected MavenProject project;

	/**
	 * The file that properties will be written to
	 *
	 * @parameter property="properties.outputFile"
	 *            default-value="${project.build.directory}/properties/project.properties";
	 * @required
	 */
	protected File outputFile;

	/**
	 * If true, the plugin will silently ignore any non-existent properties files, and the build will continue
	 *
	 * @parameter property="properties.quiet" default-value="true"
	 */
	private boolean quiet;

	/**
	 * Comma separated list of characters to escape when writing property values. cr=carriage return, lf=linefeed,
	 * tab=tab. Any other values are taken literally.
	 *
	 * @parameter default-value="cr,lf,tab" property="properties.escapeChars"
	 */
	private String escapeChars;

	/**
	 * If true, the plugin will include system properties when writing the properties file. System properties override
	 * both environment variables and project properties.
	 *
	 * @parameter default-value="false" property="properties.includeSystemProperties"
	 */
	private boolean includeSystemProperties;

	/**
	 * If true, the plugin will include environment variables when writing the properties file. Environment variables
	 * are prefixed with "env". Environment variables override project properties.
	 *
	 * @parameter default-value="false" property="properties.includeEnvironmentVariables"
	 */
	private boolean includeEnvironmentVariables;

	/**
	 * Comma separated set of properties to exclude when writing the properties file
	 *
	 * @parameter property="properties.exclude"
	 */
	private String exclude;

	/**
	 * Comma separated set of properties to write to the properties file. If provided, only the properties matching
	 * those supplied here will be written to the properties file.
	 *
	 * @parameter property="properties.include"
	 */
	private String include;

	/*
	 * (non-Javadoc)
	 * @see org.apache.maven.plugin.AbstractMojo#execute()
	 */
	@Override
	@SuppressFBWarnings({
		"NP_UNWRITTEN_FIELD", "UWF_UNWRITTEN_FIELD"
	})
	public void execute() throws MojoExecutionException, MojoFailureException {
		Properties properties = new Properties();
		// Add project properties
		properties.putAll(project.getProperties());
		if (includeEnvironmentVariables) {
			// Add environment variables, overriding any existing properties with the same key
			properties.putAll(getEnvironmentVariables());
		}
		if (includeSystemProperties) {
			// Add system properties, overriding any existing properties with the same key
			properties.putAll(System.getProperties());
		}

		// Remove properties as appropriate
		trim(properties, exclude, include);

		String comment = "# " + new Date() + "\n";
		List<String> escapeTokens = getEscapeChars(escapeChars);

		getLog().info("Creating " + outputFile);
		writeProperties(outputFile, comment, properties, escapeTokens);
	}

	/**
	 * Provides environment variables.
	 *
	 * @return environment variables
	 */
	protected static Properties getEnvironmentVariables() {
		Properties props = new Properties();
		for (Entry<String, String> entry : System.getenv().entrySet()) {
			props.setProperty(PROPERTY_PREFIX_ENV + entry.getKey(), entry.getValue());
		}
		return props;
	}

	/**
	 * Removes properties which should not be written.
	 *
	 * @param properties
	 * @param omitCSV
	 * @param includeCSV
	 * @throws MojoExecutionException
	 */
	protected void trim(Properties properties, String omitCSV, String includeCSV)
		throws MojoExecutionException {
		List<String> omitKeys = getListFromCSV(omitCSV);
		for (String key : omitKeys) {
			properties.remove(key);
		}

		List<String> includeKeys = getListFromCSV(includeCSV);
		// mh: including keys from predefined properties
		if (includePropertyKeysFromFiles != null && includePropertyKeysFromFiles.length > 0) {
			for (String currentIncludeLoc : includePropertyKeysFromFiles) {
				if (validate(currentIncludeLoc)) {
					Properties p = getProperties(currentIncludeLoc);
					for (String key : p.stringPropertyNames()) {
						includeKeys.add(key);
					}
				}
			}
		}
		if (includeKeys != null && !includeKeys.isEmpty()) {
			// removing only when include keys provided
			Set<String> keys = properties.stringPropertyNames();
			for (String key : keys) {
				if (!includeKeys.contains(key)) {
					properties.remove(key);
				}
			}
		}
	}

	/**
	 * Checks whether file exists.
	 *
	 * @param location
	 * @return true when exists, false otherwise.
	 */
	protected boolean exists(String location) {
		if (StringUtils.isBlank(location)) {
			return false;
		}
		File file = new File(location);
		if (file.exists()) {
			return true;
		}
		ResourceLoader loader = new DefaultResourceLoader();
		Resource resource = loader.getResource(location);
		return resource.exists();
	}

	/**
	 * Validates resource location.
	 *
	 * @param location
	 * @return true when valid, false otherwise
	 * @throws MojoExecutionException
	 */
	protected boolean validate(String location) throws MojoExecutionException {
		boolean exists = exists(location);
		if (exists) {
			return true;
		}
		if (quiet) {
			getLog().info("Ignoring non-existent properties file '" + location + "'");
			return false;
		} else {
			throw new MojoExecutionException("Non-existent properties file '" + location + "'");
		}
	}

	/**
	 * Provides input stream.
	 *
	 * @param location
	 * @return input stream
	 * @throws IOException
	 */
	protected InputStream getInputStream(String location) throws IOException {
		File file = new File(location);
		if (file.exists()) {
			return new FileInputStream(location);
		}
		ResourceLoader loader = new DefaultResourceLoader();
		Resource resource = loader.getResource(location);
		return resource.getInputStream();
	}

	/**
	 * Creates properties for given location.
	 *
	 * @param location
	 * @return properties for given location
	 * @throws MojoExecutionException
	 */
	protected Properties getProperties(String location) throws MojoExecutionException {
		InputStream in = null;
		try {
			Properties properties = new Properties();
			in = getInputStream(location);
			if (location.toLowerCase().endsWith(".xml")) {
				properties.loadFromXML(in);
			} else {
				properties.load(in);
			}
			return properties;
		} catch (IOException e) {
			throw new MojoExecutionException("Error reading properties file " + location, e);
		} finally {
			IOUtils.closeQuietly(in);
		}
	}

	/**
	 * Provides escape characters.
	 *
	 * @param escapeChars
	 * @return escape characters
	 */
	protected List<String> getEscapeChars(String escapeChars) {
		List<String> tokens = getListFromCSV(escapeChars);
		List<String> realTokens = new ArrayList<>();
		for (String token : tokens) {
			String realToken = getRealToken(token);
			realTokens.add(realToken);
		}
		return realTokens;
	}

	/**
	 * Provides real token.
	 *
	 * @param token
	 * @return real token
	 */
	protected String getRealToken(String token) {
		if (token.equalsIgnoreCase("CR")) {
			return CR;
		} else if (token.equalsIgnoreCase("LF")) {
			return LF;
		} else if (token.equalsIgnoreCase("TAB")) {
			return TAB;
		} else {
			return token;
		}
	}

	/**
	 * Returns content.
	 *
	 * @param comment
	 * @param properties
	 * @param escapeTokens
	 * @return content
	 */
	protected String getContent(String comment, Properties properties, List<String> escapeTokens) {
		List<String> names = new ArrayList<>(properties.stringPropertyNames());
		Collections.sort(names);
		StringBuilder sb = new StringBuilder();
		if (!StringUtils.isBlank(comment)) {
			sb.append(comment);
		}
		for (String name : names) {
			String value = properties.getProperty(name);
			String escapedValue = escape(value, escapeTokens);
			sb.append(name + "=" + escapedValue + "\n");
		}
		return sb.toString();
	}

	/**
	 * Writes properties to given file.
	 *
	 * @param file
	 * @param comment
	 * @param properties
	 * @param escapeTokens
	 * @throws MojoExecutionException
	 */
	protected void writeProperties(
		File file, String comment, Properties properties, List<String> escapeTokens)
		throws MojoExecutionException {
		try {
			String content = getContent(comment, properties, escapeTokens);
			FileUtils.writeStringToFile(file, content, StandardCharsets.UTF_8);
		} catch (IOException e) {
			throw new MojoExecutionException("Error creating properties file", e);
		}
	}

	/**
	 * Escapes characters.
	 *
	 * @param s
	 * @param escapeChars
	 * @return
	 */
	protected String escape(String s, List<String> escapeChars) {
		String result = s;
		for (String escapeChar : escapeChars) {
			result = result.replace(escapeChar, getReplacementToken(escapeChar));
		}
		return result;
	}

	/**
	 * Provides replacement token.
	 *
	 * @param escapeChar
	 * @return replacement token
	 */
	protected String getReplacementToken(String escapeChar) {
		if (escapeChar.equals(CR)) {
			return "\\r";
		} else if (escapeChar.equals(LF)) {
			return "\\n";
		} else if (escapeChar.equals(TAB)) {
			return "\\t";
		} else {
			return "\\" + escapeChar;
		}
	}

	/**
	 * Returns list from csv.
	 *
	 * @param csv
	 * @return list of values generated from CSV
	 */
	protected static final List<String> getListFromCSV(String csv) {
		if (StringUtils.isBlank(csv)) {
			return new ArrayList<>();
		}
		List<String> list = new ArrayList<>();
		String[] tokens = StringUtils.split(csv, ",");
		for (String token : tokens) {
			list.add(token.trim());
		}
		return list;
	}

	public void setIncludeSystemProperties(boolean includeSystemProperties) {
		this.includeSystemProperties = includeSystemProperties;
	}

	public void setEscapeChars(String escapeChars) {
		this.escapeChars = escapeChars;
	}

	public void setIncludeEnvironmentVariables(boolean includeEnvironmentVariables) {
		this.includeEnvironmentVariables = includeEnvironmentVariables;
	}

	public void setExclude(String exclude) {
		this.exclude = exclude;
	}

	public void setInclude(String include) {
		this.include = include;
	}

	public void setQuiet(boolean quiet) {
		this.quiet = quiet;
	}

	/**
	 * Sets property files for which keys properties should be included.
	 *
	 * @param includePropertyKeysFromFiles
	 */
	public void setIncludePropertyKeysFromFiles(String[] includePropertyKeysFromFiles) {
		if (includePropertyKeysFromFiles != null) {
			this.includePropertyKeysFromFiles = Arrays
				.copyOf(includePropertyKeysFromFiles, includePropertyKeysFromFiles.length);
		}
	}
}
