package code;
import haxe.ds.StringMap;
import model.Association;
import model.Method;
import model.Diagram;
import model.Field;
import model.Set;

@:expose
class PythonCode {
	public static function generate(diagram : Diagram) : StringMap<String> {
		var files : StringMap<String> = new StringMap();
		for (set in diagram) {
			if (!Std.isOfType(set, Association) || cast(set, Association).isClass()) {
				files.set(set.className.toLowerCase() + ".py", generateClass(set));
			}
		}
		return files;
	}

	public static function generateClass(set : Set) : String {
		var content : String = "";
		content += addImports(set);
		if (!set.isInterface) {
			content += "class " + set.className + (set.superset != null ? "(" + set.superset.className + ")" : "") + ":\n";
		}
		content += generateFields(set);
		content += generateConstructor(set);
		content += generateMethods(set);
		content += "\n";
		return content;
	}

	static function addImports(set : Set) : String {
		var imports : Array<String> = new Array();
		imports.push("from __future__ import annotations");
		for (fields in [set.fields, set.navFields]) {
			for (field in fields) {
				if (field.classType.substr(0, 5) == "Array") {
					if (!Lambda.has(imports, "from typing import List")) {
						imports.push("from typing import List");
					}
					var classType : String = field.classType.substring(6, field.classType.length - 1);
					if (!Lambda.has(imports, "import " + classType.toLowerCase())) { //'from file import Class' cause circular imports
						imports.push("import " + classType.toLowerCase());
					}
				} else if (field.classType.substr(0, 4) == "Date" && !Lambda.has(imports, "from datetime import date")) {
					imports.push("from datetime import date");
				} else if (field.classType.substr(0, 4) == "Time" && !Lambda.has(imports, "from datetime import time")) {
					imports.push("from datetime import time");
				} else if (field.classType.substr(0, 8) == "DateTime" && !Lambda.has(imports, "from datetime import datetime")) {
					imports.push("from datetime import datetime");
				} else if (field.classType != "T" && getType(field.classType).charCodeAt(0) <= 90 && field.classType != set.className && //A-Z -> 65-90
							!Lambda.has(imports, "import " + field.classType.toLowerCase())) {
					imports.push("import " + field.classType.toLowerCase());
				}
			}
		}
		if (set.superset != null && !Lambda.has(imports, "from " + set.superset.className.toLowerCase() + " import " + set.superset.className)) {
			imports.push("from " + set.superset.className.toLowerCase() + " import " + set.superset.className);
		}
		if (Lambda.exists(set.fields, f -> f.classType.indexOf("<T>") != -1 || f.classType == "T") || Lambda.exists(set.methods, m -> m.isGeneric())) {
			imports.push("from typing import TypeVar");
			imports.push("T = TypeVar(\"T\")");
		}
		var content : String = imports.join("\n") + "\n\n";
		return content;
	}

	static function generateFields(set : Set) : String {
		var content : String = "";
		for (fields in [set.fields, set.navFields]) {
			for (field in fields) {
				content += "\t" + (field.visibility == Field.ACCESS_PRIVATE || field.visibility == Field.ACCESS_PROTECTED ? "_" : "") +
					field.className + ": " + getType(field.classType) + (field.isStatic ? " #static" : "") + "\n";
			}
		}
		return content;
	}

	@SuppressWarnings("checkstyle:CyclomaticComplexity")
	static function generateMethods(set : Set) : String {
		var content : String = "";
		for (methods in [set.accessors, set.methods]) {
			for (method in methods) {
				var params : Array<String> = Lambda.map(method.params, p -> p.className + ": " + getType(p.classType));
				if (!method.isStatic) {
					params.insert(0, "self");
				}
				content += "\n\t" + (method.visibility == Field.ACCESS_PRIVATE || method.visibility == Field.ACCESS_PROTECTED ? "_" : "") +
					"def " + method.className + "(" + params.join(", ") + ")" + (method.classType == "" ? "" : " -> " + getType(method.classType)) + ":\n";
				if (method.className.substr(0, 3) == "get" || method.className.substr(0, 3) == "set") {
					var field : Field = set.getFieldCode(Code.extractFieldName(method.className));
					if (field != null) {
						if (method.className.substr(0, 3) == "set") {
							content += "\t\tself." + (field.visibility == Field.ACCESS_PRIVATE || field.visibility == Field.ACCESS_PROTECTED ? "_" : "") +
								field.className + " = " + field.className + "\n";
						}
						content += "\t\treturn self." + (field.visibility == Field.ACCESS_PRIVATE || field.visibility == Field.ACCESS_PROTECTED ? "_" : "") +
							field.className + (field.classType.substr(0, 5) == "Array" ? ".copy()" : "") + "\n";
					}
				} else if (method.params.length == 1 && (method.className.substr(0, 3) == "add" || method.className.substr(0, 6) == "remove")) {
					var field : Field  = set.getFieldFromArrayType(method.params[0].classType);
					if (field != null) {
						content += "\t\tself." + (field.visibility == Field.ACCESS_PRIVATE || field.visibility == Field.ACCESS_PROTECTED ? "_" : "") +
							field.className + (method.className.substr(0, 3) == "add" ? ".append(" : ".remove(") + method.params[0].className + ")\n";
						content += "\t\treturn self;\n";
					}
				} else if (method.classType != "") {
					content += "\t\treturn " + Code.getDefaultValue(method.classType, "None") + "\n";
				} else {
					content += "\t\tpass\n";
				}
			}
		}
		return content;
	}

	static function generateConstructor(set : Set) : String {
		var content : String = "";
		if (set.constructors.length != 0) { //only one allowed
			var emptyConstructor : Bool = true;
			var method : Method = set.constructors[0];
			var params : Array<String> = Lambda.map(method.params, p -> p.className + ": " + getType(p.classType));
			params.insert(0, "self");
			content += "\n\tdef __init__(" + params.join(", ") + "):\n";
			if (set.superset != null) {
				var parentFields : Array<Field> = Lambda.filter(method.params, p -> set.getFieldCode(p.className) == null);
				content += "\t\tsuper().__init__(" + Lambda.map(parentFields, p -> p.className).join(", ") + ")\n";
				emptyConstructor = false;
			}
			for (param in method.params) {
				var field : Field = set.getFieldCode(param.className);
				if (field != null) {
					content += "\t\tself." + (field.visibility == Field.ACCESS_PRIVATE || field.visibility == Field.ACCESS_PROTECTED ? "_" : "") +
						field.className + " = " + param.className + "\n";
						emptyConstructor = false;
				}
			}
			for (fields in [set.fields, set.navFields]) {
				for (field in fields) {
					if (!field.isStatic && !Lambda.exists(method.params, p -> field.className == p.className)) {
						content += "\t\tself." + (field.visibility == Field.ACCESS_PRIVATE || field.visibility == Field.ACCESS_PROTECTED ? "_" : "") +
							field.className + " = " + Code.getDefaultValue(field.classType, "[]", "None") + "\n";
						emptyConstructor = false;
					}
				}
			}
			if (emptyConstructor) content += "\t\tpass\n";
		}
		return content;
	}

	static function getType(classType : String) : String {
		var languageType : String = classType;
		if (classType.substr(0, 5) == "Array") {
			languageType = "List[" + classType.substr(6, classType.length - 7) + "]";
		} else if (classType == "String") {
			languageType = "str";
		} else if (classType == "Int" || classType == "Float" || classType == "Double" ||
				classType == "Date" || classType == "Time" || classType == "DateTime") {
			languageType = classType.toLowerCase();
		}
		return languageType;
	}
}
