package transform;
import model.*;

@:expose
class DiagramToMLD {
	public static function transform (diagram : Diagram) : MLD {
		var mld = new MLD();
		setsToRelations(diagram, mld);
		subsetsToRelations(diagram, mld);
		componentsToRelations(diagram, mld);
		associationsToRelations(diagram, mld);
		supersetToChildren(diagram, mld);
		subsetsToParent(diagram, mld);
		copyLayout(diagram, mld);

		var unneeded : Array<String> = new Array();
		Lambda.iter(mld.relations, r -> if (r.foreignKeys.length == 0 && r.key.length == r.fields.length) unneeded.push(r.name));
		Lambda.iter(unneeded, rName -> mld.relations.remove(rName));
		for (rName in unneeded) { //remove external keys targeting removed relations
			Lambda.iter(mld.relations, r -> r.foreignKeys = Lambda.filter(r.foreignKeys, fk -> fk.relation.name != rName));
		}

		return mld;
	}

	static function setsToRelations(diagram : Diagram, mld : MLD) {
		for (set in diagram.sets) {
			var eName : String = set.dbName;
			var r = new Relation(eName);
			for (f in set.fields) {
				var fName : String = f.className;
				if (!f.isStatic) {
					r.fields.push(new Field(fName, f.type));
					if (set.id.indexOf(f.name) != -1) {
						r.key.push(fName);
					}
				}
			}
			mld.relations.set(eName, r);
		}
	}

	static function subsetsToRelations(diagram : Diagram, mld : MLD) {
		for (set in diagram.sets) {
			if (set.superset != null) {
				var r : Relation = mld.relations.get(set.dbName);
				var pr : Relation = mld.relations.get(set.superset.dbName);
				var hasProperKey : Bool = r.key.length != 0;
				var  pos : Int = 0;
				for (fName in pr.key) {
					if (!hasProperKey) {
						r.key.push(fName);
					}
					var pField : Field = pr.getField(fName);
					r.fields.insert(pos, new Field(fName, pField.dbType));
					pos++; //to superset keys before and ordered
					r.foreignKeys.push({ name : fName, relation : pr, key : fName });
				}
			}
		}
	}

	static function componentsToRelations(diagram : Diagram, mld : MLD) {
		for (set in diagram.sets) {
			if (set.compound != null) { //compound superset key becomes key (field will be added by association)
				var r : Relation = mld.relations.get(set.dbName);
				var pos : Int = 0;
				var cr : Relation = mld.relations.get(set.compound.dbName);
				for (fName in cr.key) {
					r.key.insert(pos, fName);
					var cField : Field = cr.getField(fName);
					var cType : String = cField.dbType;
					r.fields.insert(pos, new Field(fName, cType));
					r.foreignKeys.push({ name : fName, relation : cr, key : fName }); //FIXME: already in foreignKeys
					pos++; //to insert compound keys before and ordered
				}
			}
		}
	}

	static function associationsToRelations(diagram : Diagram, mld : MLD) {
		var associations : Array<Association> = new Array();
		for (assoc in diagram.associations) {
			associations.push(assoc);
		}
		while (associations.length != 0) { //aggregations requires that target relation is created first
			var assoc : Association = associations.pop();
			if (Lambda.exists(assoc.links, l -> mld.relations.get(l.set.dbName) == null)) {
				associations.insert(0, assoc);
				continue;
			}
			if (assoc.isRelation()) {
				associationNNtoRelation(diagram, mld, assoc);
			} else {
				association1NtoRelation(diagram, mld, assoc);
			}
		}
	}

	static function association1NtoRelation(diagram : Diagram, mld : MLD, assoc : Association) {
		var l1 : Link = null;
		var ln : Link = null;
		for (l in assoc.links) {
			if (l.max == "1") {
				if (l1 == null) { //01 - 11 associations
					l1 = l;
				} else {
					if (l.min == "1") {
						ln = l1;
						l1 = l;
					} else {
						ln = l;
					}
				}
			} else {
				ln = l;
			}
		}
		if (l1 != null && ln != null) {
			var r1 : Relation = mld.relations.get(l1.set.dbName);
			if (!l1.isWeak) {
				addForeignKey(diagram, mld, ln, r1, false);
			}
		}
	}

	static function associationNNtoRelation(diagram : Diagram, mld : MLD, assoc : Association) {
		var r = new Relation(assoc.dbName);
		var nnAssoc : Bool = assoc.isNN();
		for (link in assoc.links) {
			addForeignKey(diagram, mld, link, r, (nnAssoc || link.max == "1"));
		}
		for (field in assoc.fields) {
			if (!field.isStatic) {
				r.fields.push(new Field(field.className, field.type));
			}
		}
		mld.relations.set(assoc.dbName, r);
	}

	static function addForeignKey(diagram : Diagram, mld : MLD, l : Link, r : Relation, pk : Bool) {
		var rl : Relation = mld.relations.get(l.set.dbName);
		if (rl == null) { //then it is a subsets entity with SUBSET_OPT_PARENT, so, get the superset relation instead
			rl = mld.relations.get(l.set.superset.dbName);
		}
		for (k in rl.key) {
			var fName : String = constructFieldName(r, k, l.classRole, rl.dbName, rl.key.length == 1);
			var rField : Field = rl.getField(k);
			var fType : String = rField.dbType;
			r.fields.push(new Field(fName, fType));
			r.foreignKeys.push({ name : fName, relation : rl, key : k });
			if (pk) {
				r.key.push(fName);
			}
		}
	}

	static function constructFieldName(r : Relation, k : String, role : String, rName : String, isSingleField : Bool) : String {
		var fName : String = "";
		if (role != "") {
			if (isSingleField) {
				fName = role;
			} else {
				fName = k + (k.charCodeAt(k.length - 1) >= 97 && role.charCodeAt(0) <= 90 ? role.charAt(0).toUpperCase() + role.substring(1) : "_" + role);
			}
		} else {
			if (k.toLowerCase().indexOf(rName.toLowerCase()) == -1 && k.length < 5) {
				fName = k + (k.charCodeAt(k.length - 1) >= 97 && rName.charCodeAt(0) <= 90 ? rName.charAt(0).toUpperCase() + rName.substring(1) : "_" + rName);
			} else {
				fName = k;
			}
		}
		var usedFields : Array<String> = r.fields.map(f -> f.name);
		var tryName : String = fName;
		var num : Int = 0;
		while (usedFields.indexOf(tryName) != -1) {
			num++;
			tryName = fName + Std.string(num);
		}
		fName = tryName;
		return fName;
	}

		static function subsetsToParent (diagram : Diagram, mld : MLD) {
		for (set in diagram.sets) {
			if (set.subsetOption == Set.SUBSET_OPT_PARENT) { //subsets > superset attributes migration asked
				var pr : Relation = mld.relations.get(set.dbName);
				if (set.subsetCode != "") {
					pr.fields.push(new Field(set.subsetCode, "INT"));
				}
				for (c in set.subsets) {
					var cr = mld.relations.get(c.dbName);
					//insert child fields but the superset key members one into superset relation:
					Lambda.iter(cr.fields, f -> if (pr.key.indexOf(f.name) == -1) pr.fields.push(f));
					//insert child foreign keys not pointing to superset into the superset relation:
					Lambda.iter(cr.foreignKeys, fk -> if (fk.relation.name != pr.name) pr.foreignKeys.push(fk));
					//make other relations foreign keys referencing child point to the superset instead:
					Lambda.iter(mld.relations, r -> Lambda.iter(r.foreignKeys, fk -> if (fk.relation.name == cr.name) fk.relation = pr)); //TODO: write tests
					mld.relations.remove(cr.name); //remove child relation
				}
			}
		}
	}

	static function supersetToChildren(diagram : Diagram, mld : MLD) {
		for (set in diagram.sets) {
			var pr : Relation = mld.relations.get(set.dbName);
			var subsetsNames : Array<String> = Lambda.map(set.subsets, c -> c.dbName);
			var subsetType : String = set.subsetType.toUpperCase();
			var refCount : Int = Lambda.count(mld.relations, r -> subsetsNames.indexOf(r.name) == -1 &&
				Lambda.exists(r.foreignKeys, fk -> fk.relation.name == pr.name));
			if (set.subsetOption == Set.SUBSET_OPT_CHILDREN && refCount == 0 && //superset > subsets attributes migration requires no references on parent,
					(set.subsets.length == 1 || subsetType == "X" || subsetType == "XT" ||
												subsetType == "TX" || subsetType == "P")) { //only one subset or no subsets intersection
				for (c in set.subsets) {
					var cr = mld.relations.get(c.dbName);
					var pos : Int = cr.key.length - 1;
					for (f in pr.fields) {
						if (pr.key.indexOf(f.name) == -1) {
							cr.fields.insert(pos, f);
						}
						pos++; //to insert superset fields before child one
					}
					Lambda.iter(pr.foreignKeys, fk -> cr.foreignKeys.push(fk));
					if (set.subsets.length == 1 || subsetType == "XT" || subsetType == "TX" || subsetType == "P") {
						var supersetFK : Array<ForeignKey> = new Array();
						Lambda.iter(cr.foreignKeys, fk -> if (fk.relation.name == pr.name) supersetFK.push(fk));
						Lambda.iter(supersetFK, fk -> cr.foreignKeys.remove(fk)); //remove references to superset
					}
				}
				if (set.subsets.length == 1 || subsetType == "XT" || subsetType == "TX" || subsetType == "P") {
					mld.relations.remove(pr.name); //remove superset relation
				}
			}
		}
	}

	static function copyLayout(diagram : Diagram, mld : MLD) {
		mld.height = diagram.height;
		mld.width = diagram.width;
		var rows : Array<Int> = [ for (i in 0...mld.height) 0 ];
		var columns : Array<Int> = [ for (i in 0...mld.width) 0 ];
		for (o in diagram) {
			var r : Relation = mld.relations.get(o.dbName);
			if (r != null) {
				r.row = o.row;
				r.col = o.col;
				rows[r.row]++;
				columns[r.col]++;
			}
		}
		//detect empty rows and colums, then compact layout
		var empty : Int = 0;
		for (i in 0...rows.length) {
			if (rows[i] == 0) {
				empty++;
			} else {
				rows[i] = i - empty;
			}
		}
		mld.height -= empty;
		empty = 0;
		for (i in 0...columns.length) {
			if (columns[i] == 0) {
				empty++;
			} else {
				columns[i] = i - empty;
			}
		}
		mld.width -= empty;
		for (r in mld.relations) {
			r.row = rows[r.row];
			r.col = columns[r.col];
		}
	}
}
