package source;

import model.*;
using StringTools;

@:expose
class Mocodo {
	static final DEFAULT_TYPE : String = "VARCHAR(50)";

	static var typeRE : EReg = new EReg("([^\\[]*)\\[(.*)\\]", "");
	static var entityRE : EReg = new EReg("^[^/,:(]{2,}:", "");
		// nomEnt: id, prop1, prop2
	static var associationRE : EReg = new EReg("^[^/,:]{2,},", "");
		// nomAssoc, 11 nomEnt, 0N nomEnt: prop1, prop2
	static var legacySubsetRE : EReg = new EReg("([^/,:]{2,}( U [^,:]{2,})*) C( ?\\[(X?T?)\\])? ([^,:]{2,})$", "i");
		// child1 U child2 C [XT] superset
	static var subsetRE : EReg = new EReg("^/([XT]{0,2}|P)?\\\\ * ([^-><]{2,}) *([-><]) *(\\[.*\\])? *(.*$)", "i");
		/**
		 * /XT\ superset - child1, child2
		 * /X\ superset > child1, child2
		 * /\ superset < child1, child2
		 */
	static var linkRE : EReg = new EReg("[^01NX]*[01NX]{2}>? *(\\[.*\\])?(.*)$", "");
	static var methodRE : EReg = new EReg("^[^/,:(]{2,}\\(\\):", "");

	public static function deComment(line : String) : String { //comments begin with "//"
		var posComment = line.indexOf("//");
		if (posComment != -1) {
			line = line.substring(0, posComment);
		}
		var startComment = line.indexOf("/*");
		var endComment = line.indexOf("*/", startComment + 2);
		while (startComment != -1 && endComment != -1) {
			line = line.substring(0, startComment) + line.substring(endComment + 2);
			startComment = line.indexOf("/*");
			endComment = line.indexOf("*/", startComment + 2);
		}
		return line;
	}

	/**
	 * charge un Diagram à partir de sa description textuelle au format Mocodo
	 * @param mocodoText le code source Mocodo
	 * @return un objet de la classe Diagram
	 */
	public static function load(mocodoText : String) : Diagram {
		var lines : Array<String> = mocodoText.split("\n");
		var diagram : Diagram = new Diagram();
		try {
			loadSets(diagram, lines); //must be done before associations and subsets
			loadAssociations(diagram, lines);
			loadSubsets(diagram, lines);
			loadMethods(diagram, lines); //UML
			loadLayout(diagram, lines);
		} catch (msg : String) {
			#if (sys || nodejs)
			Sys.println(msg);
			#end
			diagram = null;
		} catch (e : haxe.Exception) {
			#if (sys || nodejs)
			Sys.println(e.message);
			#else
			js.Browser.console.log(e.message);
			#end
		}
		if (diagram != null && !diagram.sets.iterator().hasNext()) {
			diagram = null;
		}
		return diagram;
	}

	static function loadAssociations(diagram : Diagram, lines : Array<String>) {
		for (lineIndex in 0...lines.length) { //first load all associations (in case of aggregations)
			var line : String = deComment(lines[lineIndex]);
			if (associationRE.match(line)) {
				var name = line.substring(0, line.indexOf(",")).trim();
				var assoc : Association = new Association(name);
				var posColumn = line.indexOf(":");
				if (posColumn == -1) posColumn = line.length;
				var fields = line.substring(posColumn + 1).split(","); //if (posColumn + 1) index exceeds line.length, "" is returned
				for (i in 0...fields.length) {
					var field : String = fields[i].trim();
					if (field != "") {
						loadField(assoc, field);
					}
				}
				diagram.associations.set(name, assoc);
			}
		}
		for (lineIndex in 0...lines.length) { //then links
			var line : String = lines[lineIndex];
			if (associationRE.match(line)) {
				var name = line.substring(0, line.indexOf(",")).trim();
				var assoc : Association = diagram.associations.get(name);
				var posColumn = line.indexOf(":");
				if (posColumn == -1) posColumn = line.length;
				var links : Array<String> = line.substring(line.indexOf(",") + 1, posColumn).split(",");
				loadAssociationLinks(diagram, lineIndex, assoc, links);
			}
		}
	}

	static function loadAssociationLinks(diagram : Diagram, lineIndex : Int, assoc : Association, links : Array<String>) {
		var compoundSet : Set = null;
		var componentSet : Set = null;
		for (i in 0...links.length) {
			var link : String = links[i].trim();
			var role : String = linkRE.replace(link, "$1");
			if (role != "") role = role.substr(1, role.length - 2);
			var setName : String = linkRE.replace(link, "$2").trim();
			var isWeak : Bool = false;
			var isAgregate : Bool = false;
			var hasArrow : Bool = false;
			var visibility : Int = Field.ACCESS_UNSET;
			var set : Set = diagram.sets.get(setName);
			if (set == null) {
				set = diagram.associations.get(setName);
				if (set != null) cast(set, Association).isAgregation = true;
			}
			if (set != null) {
				while (link.charAt(0) == "_" || link.charAt(0) == "*" || link.charAt(0) == "o" ||
						link.charAt(0) == "-" || link.charAt(0) == "+" || link.charAt(0) == "#") {
					switch (link.charAt(0)) {
						case "o" : isAgregate = true;
						case "_" | "*" : componentSet = set; isWeak = true;
						case "-" : visibility = Field.ACCESS_PRIVATE;
						case "#" : visibility = Field.ACCESS_PROTECTED;
						case "+" : visibility = Field.ACCESS_PUBLIC;
					}
					link = link.substring(1);
				}
				if (!isWeak) compoundSet = set;
				var min : String = link.charAt(0);
				var max : String = link.charAt(1);
				if (link.charAt(2) == ">") {
					hasArrow = true;
				}
				assoc.links.push(new Link(set, role, min, max, isWeak, isAgregate, hasArrow, visibility));
			} else {
				#if (sys || nodejs)
				throw "error line " + Std.string(lineIndex + 1) + " : no entity or association named '" + setName + "'";
				#end
			}
		}
		if (componentSet != null) {
			componentSet.compound = compoundSet;
		}
	}

	static function loadSets(diagram : Diagram, lines : Array<String>) {
		for (line in lines) {
			line = deComment(line);
			if (entityRE.match(line)) {
				var name = line.substring(0, line.indexOf(":")).trim();
				var isInterface : Bool = false;

				var set : Set = new Set(name, false, isInterface);
				var lineSubStr : String = line.substring(line.indexOf(":") + 1).trim();
				if (lineSubStr != "") {
					var fields : Array<String> = lineSubStr.split(",");
					for (i in 0...fields.length) {
						loadField(set, i, fields[i].trim());
					}
				}
				diagram.sets.set(name, set);
			}
		}
	}

	static function loadField(set : Set, fieldIndex : Int = -1, name : String) {
		var type : String = DEFAULT_TYPE;
		var visibility : Int = Field.ACCESS_UNSET;
		var isStatic : Bool = false;
		var idSwap : Bool = false;
		if (typeRE.match(name)) {
			type = typeRE.replace(name, "$2").trim();
			name = typeRE.replace(name, "$1").trim();
		}
		while (name.charAt(0) == "_" || name.charAt(0) == "~" || name.charAt(0) == "-" || name.charAt(0) == "+" || name.charAt(0) == "#") {
			switch (name.charAt(0)) {
				case "~" : isStatic = true;
				case "-" : visibility = Field.ACCESS_PRIVATE;
				case "#" : visibility = Field.ACCESS_PROTECTED;
				case "+" : visibility = Field.ACCESS_PUBLIC;
				case "_" : idSwap = true;
			}
			name = name.substr(1);
		}
		if (!Std.isOfType(set, Association)) {
			if (fieldIndex == 0 && !idSwap || fieldIndex != 0 && idSwap) { //i==0 XOR idSwap
				set.id.push(name);
			}
		}
		set.fields.push(new Field(name, type, visibility, isStatic));
	}

	static function loadSubsets(diagram : Diagram, lines : Array<String>) {
		for (lineIndex in 0...lines.length) {
			var line : String = deComment(lines[lineIndex]);
			if (legacySubsetRE.match(line)) { //<legacy code>
				var type : String = legacySubsetRE.replace(line, "$4");
				var supersetName : String = legacySubsetRE.replace(line, "$5").trim();
				var superset : Set = diagram.sets.get(supersetName);
				if (superset != null) {
					superset.subsetType = type;
					if (type == "XT" || type == "TX" || type == "P") {
						superset.isAbstract = true;
					}
					var children = legacySubsetRE.replace(line, "$1").split(" U ");
					for (i in 0...children.length) {
						var childName : String = children[i].trim();
						var child = diagram.sets.get(childName);
						if (child != null) {
							superset.subsets.push(child);
							child.id = new Array();
							child.superset = superset;
						} else {
							#if (sys || nodejs)
							throw "error line " + Std.string(lineIndex + 1) + " : no entity named '" + childName + "'";
							#end
						}
					}
				} else {
					#if (sys || nodejs)
					throw "error line " + Std.string(lineIndex + 1) + " : no entity named '" + supersetName + "'";
					#end
				}
			} else if (subsetRE.match(line)) { //</legacy code>
				var type : String = subsetRE.replace(line, "$1");
				var supersetName : String = subsetRE.replace(line, "$2").rtrim();
				var superset : Set = diagram.sets.get(supersetName);
				if (superset != null) {
					superset.subsetType = type;
					superset.subsetOption = switch (subsetRE.replace(line, "$3")) {
						case "-" : Set.SUBSET_OPT_BOTH;
						case ">" : Set.SUBSET_OPT_CHILDREN;
						case "<" : Set.SUBSET_OPT_PARENT;
						default : -1; //needed only to compile (unmatched patterns)
					};
					if (type == "XT" || type == "TX" || type == "P") {
						superset.isAbstract = true;
					}
					var classType = subsetRE.replace(line, "$4");
					if (classType != "") {
						superset.subsetCode = classType.substr(1, classType.length - 2);
					}
					var children = subsetRE.replace(line, "$5").split(",");
					for (i in 0...children.length) {
						var childName : String = children[i].trim();
						var child = diagram.sets.get(childName);
						if (child != null) {
							superset.subsets.push(child);
							child.id.shift();
							child.superset = superset;
						} else {
							#if (sys || nodejs)
							throw "error line " + Std.string(lineIndex + 1) + " : no entity named '" + childName + "'";
							#end
						}
					}
				} else {
					#if (sys || nodejs)
					throw "error line " + Std.string(lineIndex + 1) + " : no entity named '" + supersetName + "'";
					#end
				}
			}
		}
	}

	static function loadMethods(diagram : Diagram, lines : Array<String>) {
		for (lineIndex in 0...lines.length) {
			var line : String = deComment(lines[lineIndex]);
			if (methodRE.match(line)) {
				var className = line.substring(0, line.indexOf(":") - 2).trim();
				var set : Set = diagram.sets.get(className);
				if (set == null) {
					set = new Set(className, null, false, true, true);
					diagram.sets.set(className, set);
				}
				var lineSubStr : String = line.substring(line.indexOf(":") + 1);
				var start : Int = 0;
				var end : Int = lineSubStr.indexOf(")", start);
				while (end != -1) {
					var next : Int = lineSubStr.indexOf(",", end);
					var lob : Int = lineSubStr.indexOf("[", end);
					var lcb : Int = lineSubStr.indexOf("]", end);
					if (lob != -1 && next > lob && next < lcb) { //allows: [Type, Type]
						next = lineSubStr.indexOf(",", lcb);
					}
					var methodStr : String;
					if (next != -1) {
						methodStr = lineSubStr.substring(start, next);
						start = next + 1; //next loop setup
						end = lineSubStr.indexOf(")", start);
					} else {
						methodStr = lineSubStr.substring(start); //the rest of the line
						end = -1;
					}
					loadMethod(set, methodStr.trim());
				}
			}
		}
	}

	static function loadMethod(set : Set, methodStr : String) {
		var visibility : Int = Field.ACCESS_PUBLIC;
		var isStatic : Bool = false;
		var name : String = methodStr.substring(0, methodStr.indexOf("(")).trim();
		var type : String = methodStr.substring(methodStr.indexOf(")") + 1);
		type = type.substring(type.indexOf("[") + 1, type.indexOf("]")).trim();
		var firstChar : String = name.charAt(0);
		while (firstChar == "~" || firstChar == "-" || firstChar == "+" || firstChar == "#") {
			switch (firstChar) {
				case "~" : isStatic = true;
				case "-" : visibility = Field.ACCESS_PRIVATE;
				case "#" : visibility = Field.ACCESS_PROTECTED;
				case "+" : visibility = Field.ACCESS_PUBLIC;
			}
			name = name.substr(1);
			firstChar = name.charAt(0);
		}
		if (name == "") {
			name = set.name;
		}
		var method : Method = new Method(name, type, visibility, isStatic);
		var paramsStr : String = methodStr.substring(methodStr.indexOf("(") + 1, methodStr.indexOf(")")).trim();
		if (paramsStr != "") {
			loadParams(set, method, paramsStr);
		}
		if (name == set.name) {
			set.constructors.push(method);
		} else {
			set.methods.push(method);
		}
	}

	static function loadParams(set : Set, method : Method, paramsStr : String) {
		for (paramSubStr in paramsStr.split(",")) {
			var name : String = paramSubStr.trim();
			var type : String = "";
			if (paramSubStr.indexOf("[") != -1) {
				name = paramSubStr.substring(0, paramSubStr.indexOf("[")).trim();
				if (paramSubStr.indexOf("]", paramSubStr.indexOf("[")) != -1) {
					type = paramSubStr.substring(paramSubStr.indexOf("[") + 1, paramSubStr.indexOf("]")).trim();
				}
			}
			if (type == "") {
				var field : Field = set.getField(name);
				if (field != null) { //if a field has the same name as parameter, copy its type
					type = field.dbType;
				} else {
					type = DEFAULT_TYPE;
				}
			}
			method.params.push(new Field(name, type));
		}
	}

	static function loadLayout(diagram : Diagram, lines : Array<String>) {
		var maxCol : Int = 0;
		var col : Int = 0;
		var row : Int = 0;
		for (line in lines) {
			line = deComment(line);
			if (line.trim() == "") { //empty line
				if (col != 0) {
					if (col > maxCol) {
						maxCol = col;
					}
					col = 0;
					row++;
				}
			} else {
				var set : Set = null;
				if (line.trim() == ":") {
					col++;
				} else if (entityRE.match(line)) {
					set = diagram.sets.get(line.substring(0, line.indexOf(":")).trim());
					set.row = row;
					set.col = col;
					col++;
				} else if (associationRE.match(line)) {
					set = diagram.associations.get(line.substring(0, line.indexOf(",")).trim());
					set.row = row;
					set.col = col;
					col++;
				}
			}
		}
		diagram.height = row; //last line is empty
		diagram.width = maxCol;
	}

	public static function save(diagram : Diagram) : String { //TODO : UML (visibility, static, methods)
		var mocodoText : String = "";
		var grid : Array<Array<Set>> = diagram.getGrid();
		for (lineIndex in 0...grid.length) {
			for (set in grid[lineIndex]) {
				if (set == null) {
					mocodoText += ":";
				} else {
					if (Std.isOfType(set, Association)) {
						mocodoText += saveAssociation(cast(set, Association));
					} else {
						mocodoText += saveSet(set);
					}
				}
				mocodoText += "\n";
			}
			if (lineIndex != grid.length - 1) {
				mocodoText += "\n";
			}
		}
		return mocodoText;
	}

	static function saveAssociation(assoc : Association) : String {
		var mocodoText : String = assoc.name;
		for (link in assoc.links) {
			mocodoText += ", " + (link.isWeak ? "_" : "") + link.min + link.max + " " + link.set.name;
		}
		for (fieldIndex in 0...assoc.fields.length) {
			if (fieldIndex != 0) {
				mocodoText += ", ";
			} else {
				mocodoText += ": ";
			}
			mocodoText += assoc.fields[fieldIndex].name + " [" + assoc.fields[fieldIndex].type + "]";
		}
		return mocodoText;
	}

	static function saveSet(set : Set) : String {
		var mocodoText : String = set.name + ": ";
		mocodoText += Lambda.map(set.fields, field -> field.name + " [" + field.type + "]").join(", ");
		if (set.subsets.length != 0) {
			mocodoText += "\n/\\ " + set.name + " - " + Lambda.map(set.subsets, subset -> subset.name).join(", ");
		}
		return mocodoText;
	}
}
