/**
	Build settings definitions.

	Copyright: © 2013-2014 rejectedsoftware e.K.
	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
	Authors: Sönke Ludwig
*/
module dub.compilers.buildsettings;

import dub.internal.vibecompat.inet.path;

import dub.internal.configy.Attributes;

import std.array : array;
import std.algorithm : filter, any;
import std.path : globMatch;
import std.typecons : BitFlags;
import std.algorithm.iteration : uniq;
import std.range : chain;

/// BuildPlatform specific settings, like needed libraries or additional
/// include paths.
struct BuildSettings {
	import dub.internal.vibecompat.data.serialization : byName;

	TargetType targetType;
	string targetPath;
	string targetName;
	string workingDirectory;
	string mainSourceFile;
	string[] dflags;
	string[] lflags;
	string[] libs;
	string[] linkerFiles;
	string[] sourceFiles;
	string[] injectSourceFiles;
	string[] copyFiles;
	string[] extraDependencyFiles;
	string[] versions;
	string[] debugVersions;
	string[] versionFilters;
	string[] debugVersionFilters;
	string[] importPaths;
	string[] cImportPaths;
	string[] stringImportPaths;
	string[] importFiles;
	string[] stringImportFiles;
	string[] preGenerateCommands;
	string[] postGenerateCommands;
	string[] preBuildCommands;
	string[] postBuildCommands;
	string[] preRunCommands;
	string[] postRunCommands;
	string[string] environments;
	string[string] buildEnvironments;
	string[string] runEnvironments;
	string[string] preGenerateEnvironments;
	string[string] postGenerateEnvironments;
	string[string] preBuildEnvironments;
	string[string] postBuildEnvironments;
	string[string] preRunEnvironments;
	string[string] postRunEnvironments;
	@byName Flags!BuildRequirement requirements;
	@byName Flags!BuildOption options;

	BuildSettings dup()
	const {
		import std.traits: FieldNameTuple;
		import std.algorithm: map;
		import std.typecons: tuple;
		import std.array: assocArray;
		BuildSettings ret;
		foreach (m; FieldNameTuple!BuildSettings) {
			static if (is(typeof(__traits(getMember, ret, m) = __traits(getMember, this, m).dup)))
				__traits(getMember, ret, m) = __traits(getMember, this, m).dup;
			else static if (is(typeof(add(__traits(getMember, ret, m), __traits(getMember, this, m)))))
				add(__traits(getMember, ret, m), __traits(getMember, this, m));
			else static if (is(typeof(__traits(getMember, ret, m) = __traits(getMember, this, m))))
				__traits(getMember, ret, m) = __traits(getMember, this, m);
			else static assert(0, "Cannot duplicate BuildSettings." ~ m);
		}
		assert(ret.targetType == targetType);
		assert(ret.targetName == targetName);
		assert(ret.importPaths == importPaths);
		assert(ret.cImportPaths == cImportPaths);
		return ret;
	}

	/**
	 * Merges $(LREF bs) onto `this` BuildSettings instance. This is called for
	 * sourceLibrary dependencies when they are included in the build to be
	 * merged into the root package build settings as well as configuring
	 * targets for different build types such as `release` or `unittest-cov`.
	 */
	void add(in BuildSettings bs)
	{
		addDFlags(bs.dflags);
		addLFlags(bs.lflags);
		addLibs(bs.libs);
		addLinkerFiles(bs.linkerFiles);
		addSourceFiles(bs.sourceFiles);
		addInjectSourceFiles(bs.injectSourceFiles);
		addCopyFiles(bs.copyFiles);
		addExtraDependencyFiles(bs.extraDependencyFiles);
		addVersions(bs.versions);
		addDebugVersions(bs.debugVersions);
		addVersionFilters(bs.versionFilters);
		addDebugVersionFilters(bs.debugVersionFilters);
		addImportPaths(bs.importPaths);
		addCImportPaths(bs.cImportPaths);
		addStringImportPaths(bs.stringImportPaths);
		addImportFiles(bs.importFiles);
		addStringImportFiles(bs.stringImportFiles);
		addPreGenerateCommands(bs.preGenerateCommands);
		addPostGenerateCommands(bs.postGenerateCommands);
		addPreBuildCommands(bs.preBuildCommands);
		addPostBuildCommands(bs.postBuildCommands);
		addPreRunCommands(bs.preRunCommands);
		addPostRunCommands(bs.postRunCommands);
		addEnvironments(bs.environments);
		addBuildEnvironments(bs.buildEnvironments);
		addRunEnvironments(bs.runEnvironments);
		addPreGenerateEnvironments(bs.preGenerateEnvironments);
		addPostGenerateEnvironments(bs.postGenerateEnvironments);
		addPreBuildEnvironments(bs.preBuildEnvironments);
		addPostBuildEnvironments(bs.postBuildEnvironments);
		addPreRunEnvironments(bs.preRunEnvironments);
		addPostRunEnvironments(bs.postRunEnvironments);
		addRequirements(bs.requirements);
		addOptions(bs.options);
	}

	void addDFlags(in string[] value...) { dflags = chain(dflags, value.dup).uniq.array; }
	void prependDFlags(in string[] value...) { prepend(dflags, value); }
	void removeDFlags(in string[] value...) { remove(dflags, value); }
	void addLFlags(in string[] value...) { lflags ~= value; }
	void prependLFlags(in string[] value...) { prepend(lflags, value, false); }
	void addLibs(in string[] value...) { add(libs, value); }
	void addLinkerFiles(in string[] value...) { add(linkerFiles, value); }
	void addSourceFiles(in string[] value...) { add(sourceFiles, value); }
	void prependSourceFiles(in string[] value...) { prepend(sourceFiles, value); }
	void removeSourceFiles(in string[] value...) { removePaths(sourceFiles, value); }
	void addInjectSourceFiles(in string[] value...) { add(injectSourceFiles, value); }
	void addCopyFiles(in string[] value...) { add(copyFiles, value); }
	void addExtraDependencyFiles(in string[] value...) { add(extraDependencyFiles, value); }
	void addVersions(in string[] value...) { add(versions, value); }
	void addDebugVersions(in string[] value...) { add(debugVersions, value); }
	void addVersionFilters(in string[] value...) { add(versionFilters, value); }
	void addDebugVersionFilters(in string[] value...) { add(debugVersionFilters, value); }
	void addImportPaths(in string[] value...) { add(importPaths, value); }
	void addCImportPaths(in string[] value...) { add(cImportPaths, value); }
	void addStringImportPaths(in string[] value...) { add(stringImportPaths, value); }
	void prependStringImportPaths(in string[] value...) { prepend(stringImportPaths, value); }
	void addImportFiles(in string[] value...) { add(importFiles, value); }
	void addStringImportFiles(in string[] value...) { addSI(stringImportFiles, value); }
	void addPreGenerateCommands(in string[] value...) { add(preGenerateCommands, value, false); }
	void addPostGenerateCommands(in string[] value...) { add(postGenerateCommands, value, false); }
	void addPreBuildCommands(in string[] value...) { add(preBuildCommands, value, false); }
	void addPostBuildCommands(in string[] value...) { add(postBuildCommands, value, false); }
	void addPreRunCommands(in string[] value...) { add(preRunCommands, value, false); }
	void addPostRunCommands(in string[] value...) { add(postRunCommands, value, false); }
	void addEnvironments(in string[string] value) { add(environments, value); }
	void updateEnvironments(in string[string] value) { update(environments, value); }
	void addBuildEnvironments(in string[string] value) { add(buildEnvironments, value); }
	void updateBuildEnvironments(in string[string] value) { update(buildEnvironments, value); }
	void addRunEnvironments(in string[string] value) { add(runEnvironments, value); }
	void updateRunEnvironments(in string[string] value) { update(runEnvironments, value); }
	void addPreGenerateEnvironments(in string[string] value) { add(preGenerateEnvironments, value); }
	void updatePreGenerateEnvironments(in string[string] value) { update(preGenerateEnvironments, value); }
	void addPostGenerateEnvironments(in string[string] value) { add(postGenerateEnvironments, value); }
	void updatePostGenerateEnvironments(in string[string] value) { update(postGenerateEnvironments, value); }
	void addPreBuildEnvironments(in string[string] value) { add(preBuildEnvironments, value); }
	void updatePreBuildEnvironments(in string[string] value) { update(preBuildEnvironments, value); }
	void addPostBuildEnvironments(in string[string] value) { add(postBuildEnvironments, value); }
	void updatePostBuildEnvironments(in string[string] value) { update(postBuildEnvironments, value); }
	void addPreRunEnvironments(in string[string] value) { add(preRunEnvironments, value); }
	void updatePreRunEnvironments(in string[string] value) { update(preRunEnvironments, value); }
	void addPostRunEnvironments(in string[string] value) { add(postRunEnvironments, value); }
	void updatePostRunEnvironments(in string[string] value) { update(postRunEnvironments, value); }
	void addRequirements(in BuildRequirement[] value...) { foreach (v; value) this.requirements |= v; }
	void addRequirements(in Flags!BuildRequirement value) { this.requirements |= value; }
	void addOptions(in BuildOption[] value...) { foreach (v; value) this.options |= v; }
	void addOptions(in Flags!BuildOption value) { this.options |= value; }
	void removeOptions(in BuildOption[] value...) { foreach (v; value) this.options &= ~v; }
	void removeOptions(in Flags!BuildOption value) { this.options &= ~value; }

private:
	static auto filterDuplicates(T)(ref string[] arr, in T vals, bool noDuplicates = true)
	{
		return noDuplicates
			? vals.filter!(filtered => !arr.any!(item => item == filtered)).array
			: vals;
	}

	// Append `vals` to `arr` without adding duplicates.
	static void add(ref string[] arr, in string[] vals, bool noDuplicates = true)
	{
		// vals might contain duplicates, add each val individually
		foreach (val; vals)
			arr ~= filterDuplicates(arr, [val], noDuplicates);
	}
	// Append `vals` to `aa`
	static void add(ref string[string] aa, in string[string] vals)
	{
		// vals might contain duplicated keys, add each val individually
		foreach (key, val; vals)
			if (key !in aa)
				aa[key] = val;
	}
	// Update `vals` to `aa`
	static void update(ref string[string] aa, in string[string] vals)
	{
		// If there are duplicate keys, they will be ignored and overwritten.
		foreach (key, val; vals)
			aa[key] = val;
	}

	unittest
	{
		auto ary = ["-dip1000", "-vgc"];
		BuildSettings.add(ary, ["-dip1000", "-vgc"]);
		assert(ary == ["-dip1000", "-vgc"]);
		BuildSettings.add(ary, ["-dip1001", "-vgc"], false);
		assert(ary == ["-dip1000", "-vgc", "-dip1001", "-vgc"]);
		BuildSettings.add(ary, ["-dupflag", "-notdupflag", "-dupflag"]);
		assert(ary == ["-dip1000", "-vgc", "-dip1001", "-vgc", "-dupflag", "-notdupflag"]);
	}

	// Prepend `arr` by `vals` without adding duplicates.
	static void prepend(ref string[] arr, in string[] vals, bool noDuplicates = true)
	{
		import std.range : retro;
		// vals might contain duplicates, add each val individually
		foreach (val; vals.retro)
			arr = filterDuplicates(arr, [val], noDuplicates) ~ arr;
	}

	unittest
	{
		auto ary = ["-dip1000", "-vgc"];
		BuildSettings.prepend(ary, ["-dip1000", "-vgc"]);
		assert(ary == ["-dip1000", "-vgc"]);
		BuildSettings.prepend(ary, ["-dip1001", "-vgc"], false);
		assert(ary == ["-dip1001", "-vgc", "-dip1000", "-vgc"]);
		BuildSettings.prepend(ary, ["-dupflag", "-notdupflag", "-dupflag"]);
		assert(ary == ["-notdupflag", "-dupflag", "-dip1001", "-vgc", "-dip1000", "-vgc"]);
	}

	// add string import files (avoids file name duplicates in addition to path duplicates)
	static void addSI(ref string[] arr, in string[] vals)
	{
		bool[string] existing;
		foreach (v; arr) existing[NativePath(v).head.name] = true;
		foreach (v; vals) {
			auto s = NativePath(v).head.name;
			if (s !in existing) {
				existing[s] = true;
				arr ~= v;
			}
		}
	}

	unittest
	{
		auto ary = ["path/foo.txt"];
		BuildSettings.addSI(ary, ["path2/foo2.txt"]);
		assert(ary == ["path/foo.txt", "path2/foo2.txt"]);
		BuildSettings.addSI(ary, ["path2/foo.txt"]); // no duplicate basenames
		assert(ary == ["path/foo.txt", "path2/foo2.txt"]);
	}

	static bool pathMatch(string path, string pattern)
	{
		import std.functional : memoize;

		alias nativePath = memoize!((string stringPath) => NativePath(stringPath));

		return nativePath(path) == nativePath(pattern) || globMatch(path, pattern);
	}

	static void removeValuesFromArray(alias Match)(ref string[] arr, in string[] vals)
	{
		bool matches(string s)
		{
			return vals.any!(item => Match(s, item));
		}
		arr = arr.filter!(s => !matches(s)).array;
	}

	static void removePaths(ref string[] arr, in string[] vals)
	{
		removeValuesFromArray!(pathMatch)(arr, vals);
	}

	unittest
	{
		auto ary = ["path1", "root/path1", "root/path2", "root2/path1"];
		BuildSettings.removePaths(ary, ["path1"]);
		assert(ary == ["root/path1", "root/path2", "root2/path1"]);
		BuildSettings.removePaths(ary, ["*/path1"]);
		assert(ary == ["root/path2"]);
		BuildSettings.removePaths(ary, ["foo", "bar", "root/path2"]);
		assert(ary == []);
	}

	static void remove(ref string[] arr, in string[] vals)
	{
		removeValuesFromArray!((a, b) => a == b)(arr, vals);
	}

	unittest
	{
		import std.string : join;

		auto ary = ["path1", "root/path1", "root/path2", "root2/path1"];
		BuildSettings.remove(ary, ["path1"]);
		assert(ary == ["root/path1", "root/path2", "root2/path1"]);
		BuildSettings.remove(ary, ["root/path*"]);
		assert(ary == ["root/path1", "root/path2", "root2/path1"]);
		BuildSettings.removePaths(ary, ["foo", "root/path2", "bar", "root2/path1"]);
		assert(ary == ["root/path1"]);
		BuildSettings.remove(ary, ["root/path1", "foo"]);
		assert(ary == []);
	}
}

enum BuildSetting {
	dflags            = 1<<0,
	lflags            = 1<<1,
	libs              = 1<<2,
	sourceFiles       = 1<<3,
	copyFiles         = 1<<4,
	versions          = 1<<5,
	debugVersions     = 1<<6,
	importPaths       = 1<<7,
	cImportPaths      = 1<<8,
	stringImportPaths = 1<<9,
	options           = 1<<10,
	none = 0,
	commandLine = dflags|copyFiles,
	commandLineSeparate = commandLine|lflags,
	all = dflags|lflags|libs|sourceFiles|copyFiles|versions|debugVersions|importPaths|cImportPaths|stringImportPaths|options,
	noOptions = all & ~options
}

enum TargetType {
	autodetect,
	none,
	executable,
	library,
	sourceLibrary,
	dynamicLibrary,
	staticLibrary,
	object
}

enum BuildRequirement {
	none = 0,                     /// No special requirements
	allowWarnings        = 1<<0,  /// Warnings do not abort compilation
	silenceWarnings      = 1<<1,  /// Don't show warnings
	disallowDeprecations = 1<<2,  /// Using deprecated features aborts compilation
	silenceDeprecations  = 1<<3,  /// Don't show deprecation warnings
	disallowInlining     = 1<<4,  /// Avoid function inlining, even in release builds
	disallowOptimization = 1<<5,  /// Avoid optimizations, even in release builds
	requireBoundsCheck   = 1<<6,  /// Always perform bounds checks
	requireContracts     = 1<<7,  /// Leave assertions and contracts enabled in release builds
	relaxProperties      = 1<<8,  /// DEPRECATED: Do not enforce strict property handling (-property)
	noDefaultFlags       = 1<<9,  /// Do not issue any of the default build flags (e.g. -debug, -w, -property etc.) - use only for development purposes
}

enum BuildOption {
	none = 0,                     /// Use compiler defaults
	debugMode = 1<<0,             /// Compile in debug mode (enables contracts, -debug)
	releaseMode = 1<<1,           /// Compile in release mode (disables assertions and bounds checks, -release)
	coverage = 1<<2,              /// Enable code coverage analysis (-cov)
	debugInfo = 1<<3,             /// Enable symbolic debug information (-g)
	debugInfoC = 1<<4,            /// Enable symbolic debug information in C compatible form (-gc)
	alwaysStackFrame = 1<<5,      /// Always generate a stack frame (-gs)
	stackStomping = 1<<6,         /// Perform stack stomping (-gx)
	inline = 1<<7,                /// Perform function inlining (-inline)
	noBoundsCheck = 1<<8,         /// Disable all bounds checking (-noboundscheck)
	optimize = 1<<9,              /// Enable optimizations (-O)
	profile = 1<<10,              /// Emit profiling code (-profile)
	unittests = 1<<11,            /// Compile unit tests (-unittest)
	verbose = 1<<12,              /// Verbose compiler output (-v)
	ignoreUnknownPragmas = 1<<13, /// Ignores unknown pragmas during compilation (-ignore)
	syntaxOnly = 1<<14,           /// Don't generate object files (-o-)
	warnings = 1<<15,             /// Enable warnings (-wi)
	warningsAsErrors = 1<<16,     /// Treat warnings as errors (-w)
	ignoreDeprecations = 1<<17,   /// Do not warn about using deprecated features (-d)
	deprecationWarnings = 1<<18,  /// Warn about using deprecated features (-dw)
	deprecationErrors = 1<<19,    /// Stop compilation upon usage of deprecated features (-de)
	property = 1<<20,             /// DEPRECATED: Enforce property syntax (-property)
	profileGC = 1<<21,            /// Profile runtime allocations
	pic = 1<<22,                  /// Generate position independent code
	betterC = 1<<23,              /// Compile in betterC mode (-betterC)
	lowmem = 1<<24,               /// Compile in low-memory mode (-lowmem)
	coverageCTFE = 1<<25,         /// Enable code coverage analysis including at compile-time (-cov=ctfe)
	color = 1<<26,                /// Colorize output (-color)

	// for internal usage
	_docs = 1<<27,                // Write ddoc to docs
	_ddox = 1<<28,                // Compile docs.json
}

struct Flags (T) {
	import dub.internal.vibecompat.data.serialization : ignore;
	import dub.internal.vibecompat.data.json : Json;

	@ignore BitFlags!T values;

	public this(T opt) @safe pure nothrow @nogc
	{
		this.values = opt;
	}

	public this(BitFlags!T v) @safe pure nothrow @nogc
	{
		this.values = v;
	}

	alias values this;

	public Json toJson() const
	{
		import std.conv : to;
		import std.traits : EnumMembers;

		auto json = Json.emptyArray;

		static foreach (em; EnumMembers!T) {
			static if (em != 0) {
				if (values & em) {
					json ~= em.to!string;
				}
			}
		}

		return json;
	}

	public static Flags!T fromJson(Json json)
	{
		import std.conv : to;
		import std.exception : enforce;

		BitFlags!T flags;

		enforce(json.type == Json.Type.array, "Should be an array");
		foreach (jval; json) {
			flags |= jval.get!string.to!T;
		}

		return Flags!T(flags);
	}

	/**
	 * Reads a list of flags from a JSON/YAML document and converts them
	 * to our internal representation.
	 *
	 * Flags inside of dub code are stored as a `BitFlags`,
	 * but they are specified in the recipe using an array of their name.
	 * This routine handles the conversion from `string[]` to `BitFlags!T`.
	 */
	public static Flags!T fromYAML (scope ConfigParser!(Flags!T) p)
	{
		import dub.internal.dyaml.node;
		import std.exception;
		import std.conv;

		enforce(p.node.nodeID == NodeID.sequence, "Should be a sequence");
		typeof(return) res;
		foreach (str; p.node.sequence)
			res |= str.as!string.to!T;
		return res;
	}
}

unittest
{
	import dub.internal.vibecompat.data.json;

	auto opts = Flags!BuildOption(BuildOption.debugMode | BuildOption.debugInfo | BuildOption.warningsAsErrors);
	const str = serializeToJsonString(opts);
	assert(str == `["debugMode","debugInfo","warningsAsErrors"]`);
	assert(deserializeJson!(typeof(opts))(str) == opts);
}

unittest
{
	import dub.internal.configy.Read;

	static struct Config
	{
		Flags!BuildRequirement flags;
	}

	auto c = parseConfigString!Config(`
{
	"flags": [ "allowWarnings", "noDefaultFlags", "disallowInlining" ]
}
`, __FILE__);
	assert(c.flags.allowWarnings);
	c.flags.allowWarnings = false;
	assert(c.flags.noDefaultFlags);
	c.flags.noDefaultFlags = false;
	assert(c.flags.disallowInlining);
	c.flags.disallowInlining = false;
	assert(c.flags == c.flags.init);
}

/**
	All build options that will be inherited upwards in the dependency graph

	Build options in this category fulfill one of the following properties:
	$(UL
		$(LI The option affects the semantics of the generated code)
		$(LI The option affects if a certain piece of code is valid or not)
		$(LI The option enabled meta information in dependent projects that are useful for the dependee (e.g. debug information))
	)
*/
enum Flags!BuildOption inheritedBuildOptions =
	BuildOption.debugMode | BuildOption.releaseMode
	| BuildOption.coverage | BuildOption.coverageCTFE | BuildOption.debugInfo | BuildOption.debugInfoC
	| BuildOption.alwaysStackFrame | BuildOption.stackStomping | BuildOption.inline
	| BuildOption.noBoundsCheck | BuildOption.profile | BuildOption.ignoreUnknownPragmas
	| BuildOption.syntaxOnly | BuildOption.warnings	| BuildOption.warningsAsErrors
	| BuildOption.ignoreDeprecations | BuildOption.deprecationWarnings
	| BuildOption.deprecationErrors | BuildOption.property | BuildOption.profileGC
	| BuildOption.pic;

deprecated("Use `Flags!BuildOption` instead")
public alias BuildOptions = Flags!BuildOption;

deprecated("Use `Flags!BuildRequirement` instead")
public alias BuildRequirements = Flags!BuildRequirement;
