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

using Lambda;
using StringTools;

/**
	This class provides some utility methods to convert elements from the
	macro context to a human-readable String representation.
 */
class Printer {
	var tabs:String;
	var tabString:String;

	public function new(?tabString = "\t") {
		tabs = "";
		this.tabString = tabString;
	}

	public function printUnop(op:Unop)
		return switch (op) {
			case OpIncrement: "++";
			case OpDecrement: "--";
			case OpNot: "!";
			case OpNeg: "-";
			case OpNegBits: "~";
			case OpSpread: "...";
		}

	public function printBinop(op:Binop)
		return switch (op) {
			case OpAdd: "+";
			case OpMult: "*";
			case OpDiv: "/";
			case OpSub: "-";
			case OpAssign: "=";
			case OpEq: "==";
			case OpNotEq: "!=";
			case OpGt: ">";
			case OpGte: ">=";
			case OpLt: "<";
			case OpLte: "<=";
			case OpAnd: "&";
			case OpOr: "|";
			case OpXor: "^";
			case OpBoolAnd: "&&";
			case OpBoolOr: "||";
			case OpShl: "<<";
			case OpShr: ">>";
			case OpUShr: ">>>";
			case OpMod: "%";
			case OpInterval: "...";
			case OpArrow: "=>";
			case OpIn: "in";
			case OpAssignOp(op):
				printBinop(op) + "=";
		}

	function escapeString(s:String, delim:String) {
		return delim
			+ s.replace("\n", "\\n")
				.replace("\t", "\\t")
				.replace("\r", "\\r")
				.replace("'", "\\'")
				.replace('"', "\\\"") #if sys .replace("\x00", "\\x00") #end + delim;
	}

	public function printFormatString(s:String) {
		return escapeString(s, "'");
	}

	public function printString(s:String) {
		return escapeString(s, '"');
	}

	public function printConstant(c:Constant)
		return switch (c) {
			case CString(s, SingleQuotes): printFormatString(s);
			case CString(s, _): printString(s);
			case CIdent(s), CInt(s), CFloat(s):
				s;
			case CRegexp(s, opt): '~/$s/$opt';
		}

	public function printTypeParam(param:TypeParam)
		return switch (param) {
			case TPType(ct): printComplexType(ct);
			case TPExpr(e): printExpr(e);
		}

	public function printTypePath(tp:TypePath)
		return (tp.pack.length > 0 ? tp.pack.join(".") + "." : "")
			+ tp.name
			+ (tp.sub != null ? '.${tp.sub}' : "")
			+ (tp.params == null ? "" : tp.params.length > 0 ? "<" + tp.params.map(printTypeParam).join(", ") + ">" : "");

	// TODO: check if this can cause loops
	public function printComplexType(ct:ComplexType)
		return switch (ct) {
			case TPath(tp): printTypePath(tp);
			case TFunction(args, ret):
				var wrapArgumentsInParentheses = switch args {
					// type `:(a:X) -> Y` has args as [TParent(TNamed(...))], i.e `a:X` gets wrapped in `TParent()`. We don't add parentheses to avoid printing `:((a:X)) -> Y`
					case [TParent(t)]: false;
					// this case catches a single argument that's a type-path, so that `X -> Y` prints `X -> Y` not `(X) -> Y`
					case [TPath(_) | TOptional(TPath(_))]: false;
					default: true;
				}
				var argStr = args.map(printComplexType).join(", ");
				(wrapArgumentsInParentheses ? '($argStr)' : argStr) + " -> " + (switch ret {
					// wrap return type in parentheses if it's also a function
					case TFunction(_): '(${printComplexType(ret)})';
					default: (printComplexType(ret): String);
				});
			case TAnonymous(fields): "{ " + [for (f in fields) printField(f) + "; "].join("") + "}";
			case TParent(ct): "(" + printComplexType(ct) + ")";
			case TOptional(ct): "?" + printComplexType(ct);
			case TNamed(n, ct): n + ":" + printComplexType(ct);
			case TExtend(tpl, fields):
				var types = [for (t in tpl) "> " + printTypePath(t) + ", "].join("");
				var fields = [for (f in fields) printField(f) + "; "].join("");
				'{${types}${fields}}';
			case TIntersection(tl): tl.map(printComplexType).join(" & ");
		}

	public function printMetadata(meta:MetadataEntry)
		return '@${meta.name}' + ((meta.params != null && meta.params.length > 0) ? '(${printExprs(meta.params, ", ")})' : "");

	public function printAccess(access:Access)
		return switch (access) {
			case AStatic: "static";
			case APublic: "public";
			case APrivate: "private";
			case AOverride: "override";
			case AInline: "inline";
			case ADynamic: "dynamic";
			case AMacro: "macro";
			case AFinal: "final";
			case AExtern: "extern";
			case AAbstract: "abstract";
			case AOverload: "overload";
		}

	public function printField(field:Field) {
		inline function orderAccess(access: Array<Access>) {
			// final should always be printed last
			// (does not modify input array)
			return access.has(AFinal) ? access.filter(a -> !a.match(AFinal)).concat([AFinal]) : access;
		}
		return (field.doc != null
			&& field.doc != "" ? "/**\n"
				+ tabs
				+ tabString
				+ StringTools.replace(field.doc, "\n", "\n" + tabs + tabString)
				+ "\n"
				+ tabs
				+ "**/\n"
				+ tabs : "")
			+ (field.meta != null && field.meta.length > 0 ? field.meta.map(printMetadata).join('\n$tabs') + '\n$tabs' : "")
			+ (field.access != null && field.access.length > 0 ? orderAccess(field.access).map(printAccess).join(" ") + " " : "")
			+ switch (field.kind) {
				case FVar(t, eo): ((field.access != null && field.access.has(AFinal)) ? '' : 'var ') + '${field.name}' + opt(t, printComplexType, " : ") + opt(eo, printExpr, " = ");
				case FProp(get, set, t, eo): 'var ${field.name}($get, $set)' + opt(t, printComplexType, " : ") + opt(eo, printExpr, " = ");
				case FFun(func): 'function ${field.name}' + printFunction(func);
			}
	}

	public function printTypeParamDecl(tpd:TypeParamDecl)
		return (tpd.meta != null && tpd.meta.length > 0 ? tpd.meta.map(printMetadata).join(" ") + " " : "")
			+ tpd.name
			+ (tpd.params != null && tpd.params.length > 0 ? "<" + tpd.params.map(printTypeParamDecl).join(", ") + ">" : "")
			+ (tpd.constraints != null && tpd.constraints.length > 0 ? ":(" + tpd.constraints.map(printComplexType).join(", ") + ")" : "");

	public function printFunctionArg(arg:FunctionArg)
		return (arg.opt ? "?" : "") + arg.name + opt(arg.type, printComplexType, ":") + opt(arg.value, printExpr, " = ");

	public function printFunction(func:Function, ?kind:FunctionKind) {
		var skipParentheses = switch func.args {
			case [{ type:null }]: kind == FArrow;
			case _: false;
		}
		return (func.params == null ? "" : func.params.length > 0 ? "<" + func.params.map(printTypeParamDecl).join(", ") + ">" : "")
			+ (skipParentheses ? "" : "(")
			+ func.args.map(printFunctionArg).join(", ")
			+ (skipParentheses ? "" : ")")
			+ (kind == FArrow ? " ->" : "")
			+ opt(func.ret, printComplexType, ":")
			+ opt(func.expr, printExpr, " ");
	}

	public function printVar(v:Var) {
		var s = v.name + opt(v.type, printComplexType, ":") + opt(v.expr, printExpr, " = ");
		return switch v.meta {
			case null|[]: s;
			case meta: meta.map(printMetadata).join(" ") + " " + s;
		}
	}

	public function printObjectFieldKey(of:ObjectField) {
		return switch (of.quotes) {
			case null | Unquoted: of.field;
			case Quoted: '"${of.field}"'; // TODO: Have to escape that?
		}
	}

	public function printObjectField(of:ObjectField) {
		return '${printObjectFieldKey(of)} : ${printExpr(of.expr)}';
	}

	public function printExpr(e:Expr)
		return e == null ? "#NULL" : switch (e.expr) {
			case EConst(c): printConstant(c);
			case EArray(e1, e2): '${printExpr(e1)}[${printExpr(e2)}]';
			case EBinop(op, e1, e2): '${printExpr(e1)} ${printBinop(op)} ${printExpr(e2)}';
			case EField(e1, n): '${printExpr(e1)}.$n';
			case EParenthesis(e1): '(${printExpr(e1)})';
			case EObjectDecl(fl):
				"{ " + fl.map(function(fld) return printObjectField(fld)).join(", ") + " }";
			case EArrayDecl(el): '[${printExprs(el, ", ")}]';
			case ECall(e1, el): '${printExpr(e1)}(${printExprs(el, ", ")})';
			case ENew(tp, el): 'new ${printTypePath(tp)}(${printExprs(el, ", ")})';
			case EUnop(op, true, e1): printExpr(e1) + printUnop(op);
			case EUnop(op, false, e1): printUnop(op) + printExpr(e1);
			case EFunction(FNamed(no,inlined), func): (inlined ? 'inline ' : '') + 'function $no' + printFunction(func);
			case EFunction(kind, func): (kind != FArrow ? "function" : "") + printFunction(func, kind);
			case EVars(vl): "var " + vl.map(printVar).join(", ");
			case EBlock([]): '{ }';
			case EBlock(el):
				var old = tabs;
				tabs += tabString;
				var s = '{\n$tabs' + printExprs(el, ';\n$tabs');
				tabs = old;
				s + ';\n$tabs}';
			case EFor(e1, e2): 'for (${printExpr(e1)}) ${printExpr(e2)}';
			case EIf(econd, eif, null): 'if (${printExpr(econd)}) ${printExpr(eif)}';
			case EIf(econd, eif, eelse): 'if (${printExpr(econd)}) ${printExpr(eif)} else ${printExpr(eelse)}';
			case EWhile(econd, e1, true): 'while (${printExpr(econd)}) ${printExpr(e1)}';
			case EWhile(econd, e1, false): 'do ${printExpr(e1)} while (${printExpr(econd)})';
			case ESwitch(e1, cl, edef):
				var old = tabs;
				tabs += tabString;
				var s = 'switch ${printExpr(e1)} {\n$tabs'
					+ cl.map(function(c) return 'case ${printExprs(c.values, ", ")}' + (c.guard != null ? ' if (${printExpr(c.guard)}):' : ":")
						+ (c.expr != null ? (opt(c.expr, printExpr)) + ";" : ""))
						.join('\n$tabs');
				if (edef != null)
					s += '\n${tabs}default:' + (edef.expr == null ? "" : printExpr(edef) + ";");
				tabs = old;
				s + '\n$tabs}';
			case ETry(e1, cl):
				'try ${printExpr(e1)}' + cl.map(function(c) return ' catch(${c.name}${c.type == null ? '' : (':' + printComplexType(c.type))}) ${printExpr(c.expr)}').join("");
			case EReturn(eo): "return" + opt(eo, printExpr, " ");
			case EBreak: "break";
			case EContinue: "continue";
			case EUntyped(e1): "untyped " + printExpr(e1);
			case EThrow(e1): "throw " + printExpr(e1);
			case ECast(e1, cto) if (cto != null): 'cast(${printExpr(e1)}, ${printComplexType(cto)})';
			case ECast(e1, _): "cast " + printExpr(e1);
			case EIs(e1, ct): '${printExpr(e1)} is ${printComplexType(ct)}';
			case EDisplay(e1, _): '#DISPLAY(${printExpr(e1)})';
			case EDisplayNew(tp): '#DISPLAY(${printTypePath(tp)})';
			case ETernary(econd, eif, eelse): '${printExpr(econd)} ? ${printExpr(eif)} : ${printExpr(eelse)}';
			case ECheckType(e1, ct): '(${printExpr(e1)} : ${printComplexType(ct)})';
			case EMeta({ name:":implicitReturn" }, { expr:EReturn(e1) }): printExpr(e1);
			case EMeta(meta, e1): printMetadata(meta) + " " + printExpr(e1);
		}

	public function printExprs(el:Array<Expr>, sep:String) {
		return el.map(printExpr).join(sep);
	}

	function printExtension(tpl:Array<TypePath>, fields:Array<Field>) {
		return '{\n$tabs>'
			+ tpl.map(printTypePath).join(',\n$tabs>')
			+ ","
			+ (fields.length > 0 ? ('\n$tabs' + fields.map(printField).join(';\n$tabs') + ";\n}") : ("\n}"));
	}

	function printStructure(fields:Array<Field>) {
		return fields.length == 0 ? "{ }" : '{\n$tabs' + fields.map(printField).join(';\n$tabs') + ";\n}";
	}

	public function printTypeDefinition(t:TypeDefinition, printPackage = true):String {
		var old = tabs;
		tabs = tabString;

		var str = t == null ? "#NULL" : (printPackage && t.pack.length > 0 && t.pack[0] != "" ? "package " + t.pack.join(".") + ";\n" : "")
			+ (t.doc != null && t.doc != "" ? "/**\n" + tabString + StringTools.replace(t.doc, "\n", "\n" + tabString) + "\n**/\n" : "")
			+ (t.meta != null && t.meta.length > 0 ? t.meta.map(printMetadata).join(" ") + " " : "")
			+ (t.isExtern ? "extern " : "")
			+ switch (t.kind) {
				case TDEnum:
					"enum "
					+ t.name
					+ ((t.params != null && t.params.length > 0) ? "<" + t.params.map(printTypeParamDecl).join(", ") + ">" : "")
					+ " {\n"
					+ [
						for (field in t.fields)
							tabs
							+ (field.doc != null
								&& field.doc != "" ? "/**\n"
									+ tabs
									+ tabString
									+ StringTools.replace(field.doc, "\n", "\n" + tabs + tabString)
									+ "\n"
									+ tabs
									+ "**/\n"
									+ tabs : "")
							+ (field.meta != null && field.meta.length > 0 ? field.meta.map(printMetadata).join(" ") + " " : "")
							+ (switch (field.kind) {
								case FVar(t, _): field.name + opt(t, printComplexType, ":");
								case FProp(_, _, _, _): throw "FProp is invalid for TDEnum.";
								case FFun(func): field.name + printFunction(func);
							})
							+ ";"].join("\n")
					+ "\n}";
				case TDStructure:
					"typedef "
					+ t.name
					+ ((t.params != null && t.params.length > 0) ? "<" + t.params.map(printTypeParamDecl).join(", ") + ">" : "")
					+ " = {\n"
					+ [
						for (f in t.fields) {
							tabs + printField(f) + ";";
						}
					].join("\n")
					+ "\n}";
				case TDClass(superClass, interfaces, isInterface, isFinal, isAbstract):
					(isFinal ? "final " : "")
						+ (isAbstract ? "abstract " : "")
						+ (isInterface ? "interface " : "class ")
						+ t.name
						+ (t.params != null && t.params.length > 0 ? "<" + t.params.map(printTypeParamDecl).join(", ") + ">" : "")
						+ (superClass != null ? " extends " + printTypePath(superClass) : "")
						+ (interfaces != null ? (isInterface ? [for (tp in interfaces) " extends " + printTypePath(tp)] : [
							for (tp in interfaces)
								" implements " + printTypePath(tp)
						]).join("") : "")
						+ " {\n"
						+ [
							for (f in t.fields) {
								tabs + printFieldWithDelimiter(f);
							}
						].join("\n")
						+ "\n}";
				case TDAlias(ct):
					"typedef "
					+ t.name
					+ ((t.params != null && t.params.length > 0) ? "<" + t.params.map(printTypeParamDecl).join(", ") + ">" : "")
					+ " = "
					+ (switch (ct) {
						case TExtend(tpl, fields): printExtension(tpl, fields);
						case TAnonymous(fields): printStructure(fields);
						case _: printComplexType(ct);
					})
					+ ";";
				case TDAbstract(tthis, from, to):
					"abstract "
					+ t.name
					+ ((t.params != null && t.params.length > 0) ? "<" + t.params.map(printTypeParamDecl).join(", ") + ">" : "")
					+ (tthis == null ? "" : "(" + printComplexType(tthis) + ")")
					+ (from == null ? "" : [for (f in from) " from " + printComplexType(f)].join(""))
					+ (to == null ? "" : [for (t in to) " to " + printComplexType(t)].join(""))
					+ " {\n"
					+ [
						for (f in t.fields) {
							tabs + printFieldWithDelimiter(f);
						}
					].join("\n")
					+ "\n}";
				case TDField(kind, access):
					tabs = old;
					(access != null && access.length > 0 ? access.map(printAccess).join(" ") + " " : "")
					+ switch (kind) {
						case FVar(type, eo): ((access != null && access.has(AFinal)) ? '' : 'var ') + '${t.name}' + opt(type, printComplexType, " : ") + opt(eo, printExpr, " = ") + ";";
						case FProp(get, set, type, eo): 'var ${t.name}($get, $set)' + opt(type, printComplexType, " : ") + opt(eo, printExpr, " = ") + ";";
						case FFun(func): 'function ${t.name}' + printFunction(func) + switch func.expr { case {expr: EBlock(_)}: ""; case _: ";"; };
					}
			} tabs = old;

		return str;
	}

	function printFieldWithDelimiter(f:Field):String {
		return printField(f) + switch (f.kind) {
			case FVar(_, _), FProp(_, _, _, _): ";";
			case FFun({expr: null}): ";";
			case FFun({expr: {expr: EBlock(_)}}): "";
			case FFun(_): ";";
			case _: "";
		};
	}

	function opt<T>(v:T, f:T->String, prefix = "")
		return v == null ? "" : (prefix + f(v));

	public function printExprWithPositions(e:Expr) {
		var buffer = new StringBuf();
		function format4(i:Int) {
			return StringTools.lpad(Std.string(i), " ", 4);
		}
		function loop(tabs:String, e:Expr) {
			function add(s:String, ?p = null) {
				if (p == null) {
					p = e.pos;
				}
				var p = #if macro haxe.macro.Context.getPosInfos(p) #else e.pos #end;
				buffer.add('${format4(p.min)}-${format4(p.max)} $tabs$s\n');
			}
			function loopI(e:Expr)
				loop(tabs + tabString, e);
			switch (e.expr) {
				case EConst(c):
					add(printConstant(c));
				case EArray(e1, e2):
					add("EArray");
					loopI(e1);
					loopI(e2);
				case EBinop(op, e1, e2):
					add("EBinop " + printBinop(op));
					loopI(e1);
					loopI(e2);
				case EField(e, field):
					add("EField " + field);
					loopI(e);
				case EParenthesis(e):
					add("EParenthesis");
					loopI(e);
				case EObjectDecl(fields):
					add("EObjectDecl");
					for (field in fields) {
						add(field.field); // TODO: we don't have the field pos?
						loopI(field.expr);
					}
				case EArrayDecl(values):
					add("EArrayDecl");
					values.iter(loopI);
				case ECall(e, params):
					add("ECall");
					loopI(e);
					params.iter(loopI);
				case ENew(tp, params):
					add("ENew " + printTypePath(tp));
					params.iter(loopI);
				case EUnop(op, postFix, e):
					add("EUnop " + printUnop(op));
					loopI(e);
				case EVars(vars):
					add("EVars");
					for (v in vars) {
						if (v.expr != null) {
							add(v.name);
							loopI(v.expr);
						}
					}
				case EFunction(_, f):
					add("EFunction");
					if (f.expr != null) {
						loopI(f.expr);
					}
				case EBlock(exprs):
					add("EBlock");
					exprs.iter(loopI);
				case EFor(it, expr):
					add("EFor");
					loopI(it);
					loopI(expr);
				case EIf(econd, eif, eelse):
					add("EIf");
					loopI(econd);
					loopI(eif);
					if (eelse != null) {
						loopI(eelse);
					}
				case EWhile(econd, e, normalWhile):
					add("EWhile");
					loopI(econd);
					loopI(e);
				case ESwitch(e, cases, edef):
					add("ESwitch");
					loopI(e);
					for (c in cases) {
						for (pat in c.values) {
							loop(tabs + tabString + tabString, pat);
						}
						if (c.expr != null) {
							loop(tabs + tabString + tabString + tabString, c.expr);
						}
					}
					if (edef != null) {
						loop(tabs + tabString + tabString + tabString, edef);
					}
				case ETry(e, catches):
					add("ETry");
					loopI(e);
					for (c in catches) {
						loop(tabs + tabString + tabString, c.expr);
					}
				case EReturn(e):
					add("EReturn");
					if (e != null) {
						loopI(e);
					}
				case EBreak:
					add("EBreak");
				case EContinue:
					add("EContinue");
				case EUntyped(e):
					add("EUntyped");
					loopI(e);
				case EThrow(e):
					add("EThrow");
					loopI(e);
				case ECast(e, t):
					add("ECast");
					loopI(e);
				case EIs(e, t):
					add("EIs");
					loopI(e);
				case EDisplay(e, displayKind):
					add("EDisplay");
					loopI(e);
				case EDisplayNew(t):
					add("EDisplayNew");
				case ETernary(econd, eif, eelse):
					add("ETernary");
					loopI(econd);
					loopI(eif);
					loopI(eelse);
				case ECheckType(e, t):
					add("ECheckType");
					loopI(e);
				case EMeta(s, e):
					add("EMeta " + printMetadata(s));
					loopI(e);
			}
		}
		loop("", e);
		return buffer.toString();
	}
}