/*
 * Copyright (C)2005-2019 Haxe Foundation
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

package haxe.macro;

import haxe.macro.Expr;

/**
	All these methods can be called for compiler configuration macros.
**/
#if hl
@:hlNative("macro")
#end
class Compiler {
	/**
		A conditional compilation flag can be set on the command line using
		`-D key=value`.

		Returns the value of a compiler flag.

		If the compiler flag is defined but no value is set,
		`Compiler.getDefine` returns `"1"` (e.g. `-D key`).

		If the compiler flag is not defined, `Compiler.getDefine` returns
		`null`.

		Note: This is a macro and cannot be called from within other macros. Refer
		to `haxe.macro.Context.definedValue` to obtain defined values in macro context.

		@see https://haxe.org/manual/lf-condition-compilation.html
	**/
	macro /* <-- ! */ static public function getDefine(key:String) {
		return macro $v{haxe.macro.Context.definedValue(key)};
	}

	#if (neko || (macro && hl) || (macro && eval))
	static var ident = ~/^[A-Za-z_][A-Za-z0-9_]*$/;
	static var path = ~/^[A-Za-z_][A-Za-z0-9_.]*$/;

	public static function allowPackage(v:String) {
		#if (neko || eval)
		load("allow_package", 1)(v);
		#end
	}

	/**
		Set a conditional compiler flag.

		Usage of this function outside of initialization macros is deprecated and may cause compilation server issues.
	**/
	public static function define(flag:String, ?value:String) {
		#if (neko || eval)
		load("define", 2)(flag, value);
		#end
	}

	#if (!neko && !eval)
	private static function typePatch(cl:String, f:String, stat:Bool, t:String) {}

	private static function metaPatch(meta:String, cl:String, f:String, stat:Bool) {}

	private static function addGlobalMetadataImpl(pathFilter:String, meta:String, recursive:Bool, toTypes:Bool, toFields:Bool) {}
	#end

	/**
		Removes a (static) field from a given class by name.
		An error is thrown when `className` or `field` is invalid.
	**/
	public static function removeField(className:String, field:String, ?isStatic:Bool) {
		if (!path.match(className))
			throw "Invalid " + className;
		if (!ident.match(field))
			throw "Invalid " + field;
		#if (neko || eval)
		load("type_patch", 4)(className, field, isStatic == true, null);
		#else
		typePatch(className, field, isStatic == true, null);
		#end
	}

	/**
		Set the type of a (static) field at a given class by name.
		An error is thrown when `className` or `field` is invalid.
	**/
	public static function setFieldType(className:String, field:String, type:String, ?isStatic:Bool) {
		if (!path.match(className))
			throw "Invalid " + className;
		if (!ident.match((field.charAt(0) == "$") ? field.substr(1) : field))
			throw "Invalid " + field;
		#if (neko || eval)
		load("type_patch", 4)(className, field, isStatic == true, type);
		#else
		typePatch(className, field, isStatic == true, type);
		#end
	}

	/**
		Add metadata to a (static) field or class by name.
		An error is thrown when `className` or `field` is invalid.
	**/
	public static function addMetadata(meta:String, className:String, ?field:String, ?isStatic:Bool) {
		if (!path.match(className))
			throw "Invalid " + className;
		if (field != null && !ident.match(field))
			throw "Invalid " + field;
		#if (neko || eval)
		load("meta_patch", 4)(meta, className, field, isStatic == true);
		#else
		metaPatch(meta, className, field, isStatic == true);
		#end
	}

	/**
		Add a class path where ".hx" source files or packages (sub-directories) can be found.

		Usage of this function outside of initialization macros is deprecated and may cause compilation server issues.
	**/
	public static function addClassPath(path:String) {
		#if (neko || eval)
		load("add_class_path", 1)(path);
		#end
	}

	public static function getOutput():String {
		#if (neko || eval)
		return load("get_output", 0)();
		#else
		return null;
		#end
	}

	public static function setOutput(fileOrDir:String) {
		#if (neko || eval)
		load("set_output", 1)(fileOrDir);
		#end
	}

	public static function getDisplayPos():Null<{file:String, pos:Int}> {
		#if (neko || eval)
		return load("get_display_pos", 0)();
		#else
		return null;
		#end
	}

	/**
		Adds a native library depending on the platform (e.g. `-swf-lib` for Flash).

		Usage of this function outside of initialization macros is deprecated and may cause compilation server issues.
	**/
	public static function addNativeLib(name:String) {
		#if (neko || eval)
		load("add_native_lib", 1)(name);
		#end
	}

	/**
		Adds an argument to be passed to the native compiler (e.g. `-javac-arg` for Java).
	**/
	public static function addNativeArg(argument:String) {
		#if (neko || eval)
		load("add_native_arg", 1)(argument);
		#end
	}

	/**
		Includes all modules in package `pack` in the compilation.

		In order to include single modules, their paths can be listed directly
		on command line: `haxe ... ModuleName pack.ModuleName`.

		By default `Compiler.include` will search for modules in the directories defined with `-cp`.
		If you want to specify a different set of paths to search for modules, you can use the optional
		argument `classPath`.

		@param pack The package dot-path as String. Use `''` to include the root package.
		@param rec If true, recursively adds all sub-packages.
		@param ignore Array of module names to ignore for inclusion.
			   You can use `module*` with a * at the end for Wildcard matching
		@param classPaths An alternative array of paths (directory names) to use to search for modules to include.
			   Note that if you pass this argument, only the specified paths will be used for inclusion.
		@param strict If true and given package wasn't found in any of class paths, fail with an error.
	**/
	public static function include(pack:String, ?rec = true, ?ignore:Array<String>, ?classPaths:Array<String>, strict = false) {
		var ignoreWildcard:Array<String> = [];
		var ignoreString:Array<String> = [];
		if (ignore != null) {
			for (ignoreRule in ignore) {
				if (StringTools.endsWith(ignoreRule, "*")) {
					ignoreWildcard.push(ignoreRule.substr(0, ignoreRule.length - 1));
				} else {
					ignoreString.push(ignoreRule);
				}
			}
		}
		var skip = if (ignore == null) {
			function(c) return false;
		} else {
			function(c:String) {
				if (Lambda.has(ignoreString, c))
					return true;
				for (ignoreRule in ignoreWildcard)
					if (StringTools.startsWith(c, ignoreRule))
						return true;
				return false;
			}
		}
		var displayValue = Context.definedValue("display");
		if (classPaths == null) {
			classPaths = Context.getClassPath();
			// do not force inclusion when using completion
			switch (displayValue) {
				case null:
				case "usage":
				case _:
					return;
			}
			// normalize class path
			for (i in 0...classPaths.length) {
				var cp = StringTools.replace(classPaths[i], "\\", "/");
				if (StringTools.endsWith(cp, "/"))
					cp = cp.substr(0, -1);
				if (cp == "")
					cp = ".";
				classPaths[i] = cp;
			}
		}
		var prefix = pack == '' ? '' : pack + '.';
		var found = false;
		for (cp in classPaths) {
			var path = pack == '' ? cp : cp + "/" + pack.split(".").join("/");
			if (!sys.FileSystem.exists(path) || !sys.FileSystem.isDirectory(path))
				continue;
			found = true;
			for (file in sys.FileSystem.readDirectory(path)) {
				if (StringTools.endsWith(file, ".hx") && file.substr(0, file.length - 3).indexOf(".") < 0) {
					if( file == "import.hx" ) continue;
					var cl = prefix + file.substr(0, file.length - 3);
					if (skip(cl))
						continue;
					Context.getModule(cl);
				} else if (rec && sys.FileSystem.isDirectory(path + "/" + file) && !skip(prefix + file))
					include(prefix + file, true, ignore, classPaths);
			}
		}
		if (strict && !found)
			Context.error('Package "$pack" was not found in any of class paths', Context.currentPos());
	}

	/**
		Exclude a class or an enum without changing it to `@:nativeGen`.
	**/
	static function excludeBaseType(baseType:Type.BaseType):Void {
		if (!baseType.isExtern) {
			var meta = baseType.meta;
			if (!meta.has(":nativeGen")) {
				meta.add(":hxGen", [], baseType.pos);
			}
			baseType.exclude();
		}
	}

	/**
		Exclude a specific class, enum, or all classes and enums in a
		package from being generated. Excluded types become `extern`.

		@param pack The package dot-path as String. Use `''` to exclude the root package.
		@param rec If true, recursively excludes all sub-packages.
	**/
	public static function exclude(pack:String, ?rec = true) {
		Context.onGenerate(function(types) {
			for (t in types) {
				var b:Type.BaseType, name;
				switch (t) {
					case TInst(c, _):
						name = c.toString();
						b = c.get();
					case TEnum(e, _):
						name = e.toString();
						b = e.get();
					default:
						continue;
				}
				var p = b.pack.join(".");
				if ((p == pack || name == pack) || (rec && StringTools.startsWith(p, pack + ".")))
					excludeBaseType(b);
			}
		}, false);
	}

	/**
		Exclude classes and enums listed in an extern file (one per line) from being generated.
	**/
	public static function excludeFile(fileName:String) {
		fileName = Context.resolvePath(fileName);
		var f = sys.io.File.read(fileName, true);
		var classes = new haxe.ds.StringMap();
		try {
			while (true) {
				var l = StringTools.trim(f.readLine());
				if (l == "" || !~/[A-Za-z0-9._]/.match(l))
					continue;
				classes.set(l, true);
			}
		} catch (e:haxe.io.Eof) {}
		Context.onGenerate(function(types) {
			for (t in types) {
				switch (t) {
					case TInst(c, _):
						if (classes.exists(c.toString()))
							excludeBaseType(c.get());
					case TEnum(e, _):
						if (classes.exists(e.toString()))
							excludeBaseType(e.get());
					default:
				}
			}
		});
	}

	/**
		Load a type patch file that can modify the field types within declared classes and enums.
	**/
	public static function patchTypes(file:String):Void {
		var file = Context.resolvePath(file);
		var f = sys.io.File.read(file, true);
		try {
			while (true) {
				var r = StringTools.trim(f.readLine());
				if (r == "" || r.substr(0, 2) == "//")
					continue;
				if (StringTools.endsWith(r, ";"))
					r = r.substr(0, -1);
				if (r.charAt(0) == "-") {
					r = r.substr(1);
					var isStatic = StringTools.startsWith(r, "static ");
					if (isStatic)
						r = r.substr(7);
					var p = r.split(".");
					var field = p.pop();
					removeField(p.join("."), field, isStatic);
					continue;
				}
				if (r.charAt(0) == "@") {
					var rp = r.split(" ");
					var type = rp.pop();
					var isStatic = rp[rp.length - 1] == "static";
					if (isStatic)
						rp.pop();
					var meta = rp.join(" ");
					var p = type.split(".");
					var field = if (p.length > 1 && p[p.length - 2].charAt(0) >= "a") null else p.pop();
					addMetadata(meta, p.join("."), field, isStatic);
					continue;
				}
				if (StringTools.startsWith(r, "enum ")) {
					define("enumAbstract:" + r.substr(5));
					continue;
				}
				var rp = r.split(" : ");
				if (rp.length > 1) {
					r = rp.shift();
					var isStatic = StringTools.startsWith(r, "static ");
					if (isStatic)
						r = r.substr(7);
					var p = r.split(".");
					var field = p.pop();
					setFieldType(p.join("."), field, rp.join(" : "), isStatic);
					continue;
				}
				throw "Invalid type patch " + r;
			}
		} catch (e:haxe.io.Eof) {}
	}

	/**
		Marks types or packages to be kept by DCE.

		This also extends to the sub-types of resolved modules.

		In order to include module sub-types directly, their full dot path
		including the containing module has to be used
		(e.g. `msignal.Signal.Signal0`).

		This operation has no effect if the type has already been loaded, e.g.
		through `Context.getType`.

		@param path A package, module or sub-type dot path to keep.
		@param paths An Array of package, module or sub-type dot paths to keep.
		@param recursive If true, recurses into sub-packages for package paths.
	**/
	public static function keep(?path:String, ?paths:Array<String>, ?recursive:Bool = true) {
		if (null == paths)
			paths = [];
		if (null != path)
			paths.push(path);
		for (path in paths) {
			addGlobalMetadata(path, "@:keep", recursive, true, true);
		}
	}

	/**
		Enables null safety for a type or a package.

		@param path A package, module or sub-type dot path to enable null safety for.
		@param recursive If true, recurses into sub-packages for package paths.
	**/
	public static function nullSafety(path:String, mode:NullSafetyMode = Loose, recursive:Bool = true) {
		addGlobalMetadata(path, '@:nullSafety($mode)', recursive);
	}

	/**
		Adds metadata `meta` to all types (if `toTypes = true`) or fields (if
		`toFields = true`) whose dot-path matches `pathFilter`.

		If `recursive` is true a dot-path is considered matched if it starts
		with `pathFilter`. This automatically applies to path filters of
		packages. Otherwise an exact match is required.

		If `pathFilter` is the empty String `""` it matches everything (if
		`recursive = true`) or only top-level types (if `recursive = false`).

		This operation has no effect if the type has already been loaded, e.g.
		through `Context.getType`.
	**/
	public static function addGlobalMetadata(pathFilter:String, meta:String, ?recursive:Bool = true, ?toTypes:Bool = true, ?toFields:Bool = false) {
		#if (neko || eval)
		load("add_global_metadata_impl", 5)(pathFilter, meta, recursive, toTypes, toFields);
		#else
		addGlobalMetadataImpl(pathFilter, meta, recursive, toTypes, toFields);
		#end
	}

	/**
		Change the default JS output by using a custom generator callback
	**/
	public static function setCustomJSGenerator(callb:JSGenApi->Void) {
		#if (neko || eval)
		load("set_custom_js_generator", 1)(callb);
		#end
	}

	#if (neko || eval)
	static inline function load(f, nargs):Dynamic {
		return @:privateAccess Context.load(f, nargs);
	}
	#end

	/**
		Clears cached results of file lookups
	**/
	public static function flushDiskCache() {
		#if (neko || eval)
		load("flush_disk_cache", 0)();
		#end
	}

	#end

	#if (js || lua || macro)
	/**
		Embed a JavaScript or Lua file at compile time (can be called by `--macro` or within an `__init__` method).
	**/
	public static #if !macro macro #end function includeFile(file:String, position:IncludePosition = Top) {
		return switch ((position : String).toLowerCase()) {
			case Inline:
				if (Context.getLocalModule() == "")
					Context.error("Cannot use inline mode when includeFile is called by `--macro`", Context.currentPos());

				var f = try sys.io.File.getContent(Context.resolvePath(file)) catch (e:Dynamic) Context.error(Std.string(e), Context.currentPos());
				var p = Context.currentPos();
				if(Context.defined("js")) {
					macro @:pos(p) js.Syntax.plainCode($v{f});
				} else {
					macro @:pos(p) untyped __lua__($v{f});
				}
			case Top | Closure:
				@:privateAccess Context.includeFile(file, position);
				macro {};
			case _:
				Context.error("unknown includeFile position: " + position, Context.currentPos());
		}
	}
	#end
}

enum abstract IncludePosition(String) from String to String {
	/**
		Prepend the file content to the output file.
	**/
	var Top = "top";

	/**
		Prepend the file content to the body of the top-level closure.

		Since the closure is in strict-mode, there may be run-time error if the input is not strict-mode-compatible.
	**/
	var Closure = "closure";

	/**
		Directly inject the file content at the call site.
	**/
	var Inline = "inline";
}

enum abstract NullSafetyMode(String) to String {
	/**
		Disable null safety.
	**/
	var Off;

	/**
		Loose safety.
		If an expression is checked `!= null`, then it's considered safe even if it could be modified after the check.
		E.g.
		```haxe
		function example(o:{field:Null<String>}) {
			if(o.field != null) {
				mutate(o);
				var notNullable:String = o.field; //no error
			}
		}

		function mutate(o:{field:Null<String>}) {
			o.field = null;
		}
		```
	**/
	var Loose;

	/**
		Full scale null safety.
		If a field is checked `!= null` it stays safe until a call is made or any field of any object is reassigned,
		because that could potentially alter an object of the checked field.
		E.g.
		```haxe
		function example(o:{field:Null<String>}, b:{o:{field:Null<String>}}) {
			if(o.field != null) {
				var notNullable:String = o.field; //no error
				someCall();
				var notNullable:String = o.field; // Error!
			}
			if(o.field != null) {
				var notNullable:String = o.field; //no error
				b.o = {field:null};
				var notNullable:String = o.field; // Error!
			}
		}
		```
	**/
	var Strict;

	/**
		Full scale null safety for a multi-threaded environment.
		With this mode checking a field `!= null` does not make it safe, because it could be changed from another thread
		at the same time or immediately after the check.
		The only nullable thing could be safe are local variables.
	**/
	var StrictThreaded;
}
