package schema;

import model.*;

@:expose
class UMLToSVG {
	/**
	 * génère un diagramme au format SVG à partir d'un MCD
	 * @param diagram le MCD
	 * @return le diagramme SVG (sous la forme d'un objet XML)
	 */
	public static function generateSVG(diagram : Diagram, classDiagram : Bool, showPrivate : Bool) : Xml {
		var doc : Xml = Xml.createDocument();
		var svg : Xml = Svg.initSvg(doc);

		for (set in diagram) {
			if (Std.isOfType(set, Association)) {
				var assoc : Association = cast(set, Association);
				if (assoc.isClass()) {
					set.svg = generateAssociationSVG(diagram, assoc, classDiagram, showPrivate);
					svg.addChild(set.svg);
				} else {
					set.svg = Xml.createElement("g");
					set.svg.addChild(Xml.createElement("g"));
					set.svg.firstChild().set("height", "0");
					set.svg.firstChild().set("width", "0");
				}
			} else {
				set.svg = generateSetSVG(set, classDiagram, showPrivate);
				svg.addChild(set.svg);
			}
		}

		Svg.placeObjects(diagram, svg, diagram.height, diagram.width, classDiagram ? 2 : 4);
		drawInheritance(diagram, svg);

		placeClassAssociations(diagram, svg);
		for (assoc in diagram.associations) {
			if (!assoc.isClass()) {
				var w : Int = Std.parseInt(assoc.svg.firstChild().get("width"));
				var h : Int = Std.parseInt(assoc.svg.firstChild().get("height"));
				var coords : Array<String> = Svg.coordRE.replace(assoc.svg.get("transform"), "$1").split(","); //"translate(x,y)" -> [x, y]
				var x : Int = Std.parseInt(coords[0]);
				var y : Int = Std.parseInt(coords[1]);
				svg.addChild(drawLinks(assoc, x, y, w, h, !classDiagram));
			}
		}
		return doc;
	}

	static function useField(field : Field, classDiagram : Bool, showPrivate : Bool) : Bool {
		return (!field.isStatic || classDiagram) && (field.visibility == Field.ACCESS_PUBLIC || showPrivate);
	}

	static function getFieldMaxWidth(field : Field, classDiagram : Bool, showPrivate : Bool, isKey : Bool, max : Int) : Int {
		var maxWidth : Int = max;
		if (useField(field, classDiagram, showPrivate)) {
			var width : Int = classDiagram ? field.toUMLString().length : field.toDomainString().length + (isKey ? 7 : 0);
			if (width > maxWidth) {
				maxWidth = width;
			}
		}
		return maxWidth;
	}

	static function getMethodMaxWidth(method : Method, classDiagram : Bool, showPrivate : Bool, max : Int) : Int {
		var maxWidth : Int = max;
		if (useField(method, classDiagram, showPrivate)) {
			var width : Int = method.toUMLString().length;
			if (width > maxWidth) {
				maxWidth = width;
			}
		}
		return maxWidth;
	}

	static function getFieldsCount(set, classDiagram : Bool, showPrivate : Bool) : Int {
		var fieldsCount : Int = 1; //class name
		fieldsCount += Lambda.count(set.fields, field -> useField(field, classDiagram, showPrivate));
		if (classDiagram) {
			fieldsCount += Lambda.count(set.navFields, field -> useField(field, classDiagram, showPrivate));
			fieldsCount += Lambda.count(set.constructors, field -> useField(field, classDiagram, showPrivate));
			fieldsCount += Lambda.count(set.accessors, field -> useField(field, classDiagram, showPrivate));
			fieldsCount += Lambda.count(set.methods, field -> useField(field, classDiagram, showPrivate));
		}
		return fieldsCount;
	}

	static function getMaxWidth(set : Set, classDiagram : Bool, showPrivate : Bool) : Int {
		var chrWidth : Int = set.className.length;
		Lambda.iter(set.fields, field -> chrWidth = getFieldMaxWidth(field, classDiagram, showPrivate, set.id.contains(field.name), chrWidth));
		Lambda.iter(set.navFields, field -> chrWidth = getFieldMaxWidth(field, classDiagram, showPrivate, set.id.contains(field.name), chrWidth));
		if (classDiagram) {
			Lambda.iter(set.constructors, method -> chrWidth = getMethodMaxWidth(method, classDiagram, showPrivate, chrWidth));
			Lambda.iter(set.accessors, method -> chrWidth = getMethodMaxWidth(method, classDiagram, showPrivate, chrWidth));
			Lambda.iter(set.methods, method -> chrWidth = getMethodMaxWidth(method, classDiagram, showPrivate, chrWidth));
		}
		return chrWidth;
	}

	static function generateAssociationSVG(diagram : Diagram, assoc : Association, classDiagram : Bool, showPrivate : Bool) : Xml {
		return generateSetSVG(assoc, classDiagram, showPrivate);
	}

	static function generateSetSVG(set : Set, classDiagram : Bool, showPrivate : Bool) : Xml {
		var fieldsCount : Int = getFieldsCount(set, classDiagram, showPrivate);
		var chrWidth : Int = getMaxWidth(set, classDiagram, showPrivate);

		var g : Xml = Xml.createElement("g");
		var rect : Xml = Xml.createElement("rect");
		g.addChild(rect);
		rect.set("height", Std.string(fieldsCount * Svg.LETTER_HEIGHT + Svg.PADDING * (classDiagram ? 3 : 2)));
		rect.set("width", Std.string(chrWidth * Svg.LETTER_WIDTH + Svg.PADDING * 2));
		rect.set("style", Svg.FILL_WHITE_STYLE);

		var titleBG : Xml = Xml.createElement("rect");
		g.addChild(titleBG);
		titleBG.set("height", Std.string(Svg.LETTER_HEIGHT + Svg.PADDING));
		titleBG.set("width", Std.string(chrWidth * Svg.LETTER_WIDTH + Svg.PADDING * 2));
		titleBG.set("style", classDiagram ? Svg.FILL_WHITE_STYLE : Svg.FILL_GREY_STYLE);

		var title : Xml = Xml.createElement("text");
		g.addChild(title);
		title.addChild(Xml.createPCData(set.className));
		title.set("x", Std.string(Svg.PADDING + (chrWidth - set.className.length) * Svg.LETTER_WIDTH / 2));
		title.set("y", Std.string(Svg.LETTER_HEIGHT));

		var text : Xml = Xml.createElement("text");
		g.addChild(text);
		var offset : Int = 2;
		var fieldSets : Array<Array<Field>> = [ set.fields ];
		if (classDiagram) fieldSets.push(set.navFields);
		for (fieldSet in fieldSets) { //fields
			for (field in fieldSet) {
				if (useField(field, classDiagram, showPrivate)) {
					var tspan : Xml = Xml.createElement("tspan");
					text.addChild(tspan);
					tspan.set("x", Std.string(Svg.PADDING / 2));
					tspan.set("y", Std.string(Svg.LETTER_HEIGHT * offset + Svg.PADDING));
					if (field.isStatic) {
						tspan.set("style", Svg.UNDERLINE_STYLE);
					}
					var visibility : String = switch (field.visibility) {
						case Field.ACCESS_PRIVATE : "- ";
						case Field.ACCESS_PROTECTED : "# ";
						case Field.ACCESS_PUBLIC : "+ ";
						default : "  ";
					}
					tspan.addChild(Xml.createPCData(classDiagram ? field.toUMLString() : field.toDomainString() + (set.id.contains(field.name) ? " <<id>>" : "")));
					offset++;
				}
			}
		}

		var line : Xml = Xml.createElement("line");
		g.addChild(line);
		line.set("x1", "0");
		line.set("x2", Std.string(Svg.PADDING * 2 + chrWidth * Svg.LETTER_WIDTH));
		line.set("y1", Std.string(Svg.LETTER_HEIGHT * offset - Svg.PADDING));
		line.set("y2", Std.string(Svg.LETTER_HEIGHT * offset - Svg.PADDING));
		if (classDiagram) { //methods
			for (methodSet in [ set.constructors, set.accessors, set.methods ]) {
				for (method in methodSet) {
					if (useField(method, classDiagram, showPrivate)) {
						var tspan : Xml = Xml.createElement("tspan");
						text.addChild(tspan);
						tspan.set("x", Std.string(Svg.PADDING / 2));
						tspan.set("y", Std.string(Svg.LETTER_HEIGHT * offset + Svg.PADDING * 2));
						if (method.isStatic) {
							tspan.set("style", Svg.UNDERLINE_STYLE);
						}
						tspan.addChild(Xml.createPCData(method.toUMLString()));
						offset++;
					}
				}
			}
		}
		return g;
	}

	static function drawInheritance(diagram : Diagram, svg : Xml) {
		for (set in diagram.sets) {
			if (set.superset != null) {
				svg.addChild(Svg.getSvgLine(Svg.getLine(set, set.superset), Svg.MARKERS[1].id));
			}
		}
	}

	static function placeClassAssociations(diagram : Diagram, svg : Xml) {
		for (assoc in diagram.associations) {
			if (assoc.isClass()) {
				var w : Int = Std.parseInt(assoc.svg.firstChild().get("width"));
				var h : Int = Std.parseInt(assoc.svg.firstChild().get("height"));
				var coords : Array<String> = Svg.coordRE.replace(assoc.svg.get("transform"), "$1").split(","); //"translate(x,y)" -> [x, y]
				var x : Int = Std.parseInt(coords[0]);
				var y : Int = Std.parseInt(coords[1]);
				var g : Xml = drawLinks(assoc, x, y, w, h, false);
				var cX : Int = Std.int(x + w / 2);
				var aCx : Int = cX;
				var cY : Int = Std.int(y + h / 2);
				var aCy : Int = cY;
				var aX : Int = x;
				var aY : Int = y;
				if (assoc.links.length == 2) {
					if (assoc.row == assoc.links[0].set.row && assoc.row == assoc.links[1].set.row) {
						aY += Std.int(h / 2 + Svg.PADDING * 4);
						aCy = aY;
					} else if (assoc.col == assoc.links[0].set.col && assoc.col == assoc.links[1].set.col) {
						aX += Std.int(w / 2 + Svg.PADDING * 4);
						aCx = aX;
					}
					var line : Line = Svg.getLine(assoc.links[0].set, assoc.links[1].set);
					cX = Std.int((line.x1 + line.x2) / 2);
					cY = Std.int((line.y1 + line.y2) / 2);
				} else {
					aX += Std.int(w / 2 + Svg.PADDING * 4);
					aY += Std.int(h / 2 + Svg.PADDING * 4);
				}
				if (assoc.col == diagram.width - 1) {
					svg.set("width", Std.string(Std.parseInt(svg.get("width")) + aX - x));
				}
				if (assoc.row == diagram.height - 1) {
					svg.set("height", Std.string(Std.parseInt(svg.get("height")) + aY - y));
				}
				assoc.svg.set("transform", 'translate($aX,$aY)');
				var dotted : Xml = Xml.createElement("line");
				dotted.set("x1", Std.string(cX));
				dotted.set("x2", Std.string(aCx));
				dotted.set("y1", Std.string(cY));
				dotted.set("y2", Std.string(aCy));
				dotted.set("stroke-dasharray",  Svg.DOTTED_STYLE);
				g.insertChild(dotted, 0);
				svg.insertChild(g, 0);
			}
		}
	}

	@SuppressWarnings("checkstyle:ParameterNumber")
	static function drawLinks(assoc : Association, x : Int, y : Int, w : Int, h : Int, showNav : Bool) : Xml {
		var g : Xml = Xml.createElement("g");
		if (assoc.links.length > 2) { //n-ary associations (n >= 3)
			x = Std.int(x + w / 2);
			y = Std.int(y + h / 2);
			for (link in assoc.links) {
				var line : Line = Svg.getLine(link.set, assoc);
				line.x2 = x;
				line.y2 = y;
				g.addChild(Svg.getSvgLine(line));
			}
			var path = Xml.createElement("path");
			path.set("transform", 'translate($x,$y)');
			path.set("style", Svg.WHITE_STYLE);
			path.set("d", 'M ${-Svg.PADDING * 2} 0 L 0 ${-Svg.PADDING * 2} L ${Svg.PADDING * 2} 0 L 0 ${Svg.PADDING * 2} Z');
			g.addChild(path);
		} else if (assoc.links.length == 2) { //binary associations (check to avoid exception)
			var link1 : Link = assoc.links[0];
			var link2 : Link = assoc.links[1];
			if (link2.isWeak || link2.isAgregate || link1.hasArrow) {
				link1 = assoc.links[1];
				link2 = assoc.links[0];
			}
			var markerId : String = link1.isWeak ? Svg.MARKERS[3].id : link1.isAgregate ? Svg.MARKERS[2].id : link2.hasArrow ? Svg.MARKERS[0].id : null;
			var mm1 : Xml = Xml.createElement("text");
			mm1.addChild(Xml.createPCData(link2.getUMLCards())); //inverted
			var mm2 : Xml = Xml.createElement("text");
			mm2.addChild(Xml.createPCData(link1.isWeak || link1.isAgregate ? "" : link1.getUMLCards()));
			if (link1.set.className == link2.set.className) { //reflexive
				var line1 : Line = Svg.getLine(assoc, link2.set, "", link1.isWeak || link1.isAgregate ? "" : link1.getUMLCards(), -1);
				var line2 : Line = Svg.getLine(assoc, link1.set, "", link2.getUMLCards(), 1);
				var bezier : Xml = Xml.createElement("path");
				var xlq : Int = line1.x1 + (line1.x2 == line2.x2 ? (line1.x1 - line1.x2) : 0);
				var ylq : Int = line1.y1 + (line1.y2 == line2.y2 ? (line1.y1 - line1.y2) : 0);
				bezier.set("d", 'M ${line2.x2} ${line2.y2} Q $xlq $ylq ${line1.x2} ${line1.y2}');
				if (markerId != null) {
					bezier.set("marker-end", 'url(#$markerId)');
				}
				g.addChild(bezier);
				mm1.set("x", Std.string(line2.cx2));
				mm1.set("y", Std.string(line2.cy2));
				mm2.set("x", Std.string(line1.cx2));
				mm2.set("y", Std.string(line1.cy2));
				if (showNav) {
					addNavLabel(link1, link2, line1, 2, g);
					addNavLabel(link2, link1, line2, 2, g);
				}
			} else {
				var line : Line = Svg.getLine(link1.set, link2.set, link2.getUMLCards(), link1.isWeak || link1.isAgregate ? "" : link1.getUMLCards());
				g.addChild(Svg.getSvgLine(line, 0, markerId));
				mm1.set("x", Std.string(line.cx1));
				mm1.set("y", Std.string(line.cy1));
				mm2.set("x", Std.string(line.cx2));
				mm2.set("y", Std.string(line.cy2));
				if (showNav) {
					addNavLabel(link1, link2, line, 2, g);
					addNavLabel(link2, link1, line, 1, g);
				}
			}
			g.addChild(mm1);
			g.addChild(mm2);
		}
		return g;
	}

	static function addNavLabel(link : Link, oppositeLink : Link, line : Line, end : Int, g : Xml) {
		if (!link.hasArrow) {
			var fName : String = oppositeLink.classRole;
			if (oppositeLink.role == "") {
				fName = link.max == "N" ? oppositeLink.set.getPluralClassName() : oppositeLink.set.className;
			}
			var visibility : String = switch (oppositeLink.visibility) {
				case Field.ACCESS_PRIVATE : "-";
				case Field.ACCESS_PROTECTED : "#";
				case Field.ACCESS_PUBLIC : "+";
				default : ""; //unset
			}
			fName = visibility + fName.charAt(0).toLowerCase() + fName.substr(1);
			var text : Xml = Xml.createElement("text");
			text.addChild(Xml.createPCData(fName));
			var x : Int = end == 1 ? line.x1 : line.x2;
			var y : Int = end == 1 ? line.y1 : line.y2;
			var cx : Int = end == 1 ? line.cx1 : line.cx2;
			var cy : Int = end == 1 ? line.cy1 : line.cy2;
			text.set("x", Std.string(x < cx ? cx : cx - Svg.LETTER_WIDTH * (fName.length - link.getUMLCards().length)));
			if (link.isWeak || link.isAgregate) {
				text.set("y", Std.string(cy));
			} else {
				text.set("y", Std.string(y < cy ? cy + Svg.LETTER_HEIGHT : cy - Svg.LETTER_HEIGHT));
			}
			g.addChild(text);
		}
	}
}
