package schema;

import model.*;

@:expose
class UMLToSVG {
	static inline var N_A_ASSOC : Int = 5;
	/**
	 * 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.sets) { //draw boxes
			set.svg = generateSetSVG(set, classDiagram, showPrivate);
			svg.addChild(set.svg);
		}
		for (assoc in diagram.associations) { //draw boxes
			if (assoc.isClass()) {
				assoc.svg = generateSetSVG(assoc, classDiagram, showPrivate);
				svg.addChild(assoc.svg);
			} else { //ensure each association has at least an empty SVG
				assoc.svg = Xml.createElement("g");
				assoc.svg.addChild(Xml.createElement("g"));
				assoc.svg.firstChild().set("height", assoc.isReflexive() ? Std.string(Svg.RXY) : "0");
				assoc.svg.firstChild().set("width", Std.string(Svg.RXY));
				svg.addChild(assoc.svg);
			}
		}
		Svg.placeObjects(diagram, svg, diagram.height, diagram.width);
		drawInheritance(diagram, svg);

		for (assoc in diagram.associations) {
			drawLinks (svg, assoc, 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 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);
		titleBG.set("style", Svg.FILL_WHITE_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++;
				}
			}
		}

		if (classDiagram) { //methods
			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 * 3));
			line.set("y2", Std.string(Svg.LETTER_HEIGHT * offset - Svg.PADDING * 3));
			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) {
				var line : Line = Box.getLine(Svg.getBox(set.svg), Svg.getBox(set.superset.svg));
				svg.addChild(Svg.getSvgLine(line, null, null, Svg.MARKERS[1].id));
			}
		}
	}


	static function drawLinks(svg : Xml, assoc : Association, classDiagram : Bool) {
		if (assoc.isClass()) { //add losange + dotted line
			var box = Svg.getBox(assoc.svg);
			var linkBox = Svg.getBox(assoc.links[0].set.svg);
			var lx = box.center.x;
			var ly = box.center.y;
			if (!Box.areSameLine(box, linkBox) && !Box.areSameColumn(box, linkBox)) {
				lx = Box.leftToRight(box, linkBox) ? box.topleft.x + box.width + Svg.RXY : box.topleft.x - Svg.RXY;
				ly = Box.topToBottom(box, linkBox) ? box.topleft.y + box.height + Svg.RXY : box.topleft.y - Svg.RXY;
			} else if (Box.areSameLine(box, linkBox)) {
				lx = Box.leftToRight(box, linkBox) ? box.topleft.x + box.width + Svg.RXY : box.topleft.x - Svg.RXY;
			} else {
				ly = Box.topToBottom(box, linkBox) ? box.topleft.y + box.height + Svg.RXY : box.topleft.y - Svg.RXY;
			}
			var losangeBox = new Box({ x : lx, y : ly }, 0, 0);
			var losange = Xml.createElement("path");
			losange.set("transform", 'translate(${losangeBox.center.x},${losangeBox.center.y})');
			losange.set("style", Svg.WHITE_STYLE);
			losange.set("d", 'M ${-N_A_ASSOC * 2} 0 L 0 ${-N_A_ASSOC * 2} L ${N_A_ASSOC * 2} 0 L 0 ${N_A_ASSOC * 2} Z');
			for (link in assoc.links) {
				var line = Box.getLine(Svg.getBox(link.set.svg), losangeBox);
				var svgLine = Svg.getSvgLine(line, null, null);
				if (!classDiagram) Svg.addLineLabels(line, getLabel(null, link), null, svgLine, 1);
				svg.addChild(svgLine);
			}
			var dotted = Svg.getSvgLine(Box.getLine(Svg.getBox(assoc.svg), losangeBox), null, null);
			dotted.set("stroke-dasharray",  Svg.DOTTED_STYLE);
			svg.addChild(dotted);
			svg.addChild(losange); //above
		} else { //binary association
			var link1 = assoc.links[0];
			var link2 = assoc.links[1];
			if (link1 != null && link2 != null) { //to prevent errors while typing
				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 box1 = Svg.getBox(link1.set.svg);
				var box2 = Svg.getBox(link2.set.svg);
				var line = Box.getLine(box1, box2);
				var card1 = link1.getUMLCards();
				if (link1.isWeak || link1.isAgregate) { //add space arround
					card1 = " " + card1 + " "; //NBSP : <MAJ>+<CTRL>+<U>,A0,<ENTER>
				}
				var svgLine : Xml = null;
				if (link1.set.name == link2.set.name) { //reflexive
					var qc = getQuadCurve(assoc, link1);
					line = qc;
					svgLine = Svg.getQuadCurve(qc, link2.getUMLCards(), card1, markerId);
				} else {
					svgLine = Svg.getSvgLine(line, link2.getUMLCards(), card1, markerId);
				}
				if (!classDiagram) {
					Svg.addLineLabels(line, getLabel(link2, link1), getLabel(link1, link2), svgLine, 2);
				}
				svg.addChild(svgLine);
			}
		}
	}

	static function getQuadCurve(assoc : Association, link : Link) : QuadCurve {
		var assocBox = Svg.getBox(assoc.svg);
		var linkBox = Svg.getBox(link.set.svg);
		var start : Point = null;
		var end : Point = null;
		var xDst = 0;
		var yDst = 0;
		if (Box.areSameLine(assocBox, linkBox)) { //vertical
			start = { x : linkBox.topleft.x + linkBox.width, y : linkBox.topleft.y + Svg.LETTER_HEIGHT + Svg.PADDING };
			end = { x : linkBox.topleft.x + linkBox.width, y : linkBox.topleft.y + linkBox.height - Svg.LETTER_HEIGHT - Svg.PADDING };
			xDst = Std.int(2 * Svg.RXY);
		} else { //horizontal
			start = { x : Math.ceil(linkBox.topleft.x + Svg.LETTER_WIDTH), y : linkBox.topleft.y + linkBox.height };
			end = { x : Math.ceil(linkBox.topleft.x + linkBox.width - Svg.LETTER_WIDTH), y : linkBox.topleft.y + linkBox.height };
			yDst = Std.int(2 * Svg.RXY);
		}
		return new QuadCurve(start, end, xDst, yDst);
	}

	static function getLabel(link : Link, oppositeLink : Link) : String {
		var label = "";
		if (link == null || !link.hasArrow) {
			label = oppositeLink.classRole;
			if (label == "") {
				label = link == null || link.max == "1" ? oppositeLink.set.className : oppositeLink.set.getPluralClassName();
			}
			label = label.charAt(0).toLowerCase() + label.substr(1);
		}
		return label;
	}
}
