package schema;
import model.Obj;

typedef MarkerDesc = {
	var id : String;
	var height : Int;
	var width : Int;
	var path : String;
	var style : String;
}

class Svg {
	public static var coordRE : EReg = new EReg("^translate\\(([0-9]+,[0-9]+)\\)$", "");

	public static inline var PADDING : Int = 5;
	public static inline var MARGIN : Int = 50;
	public static inline var IMARGIN : Int = 10;
	public static inline var RXY : Int = 20;

	public static inline var LETTER_HEIGHT : Int = 16;
	#if (sys || nodejs)
	public static inline var TEXT_STYLE : String = "font-family:'Dejavu Sans Mono', monospace;font-size:16px";
	public static inline var LETTER_WIDTH : Float = 10;
	#else
	public static inline var TEXT_STYLE : String = "font-family:'Roboto Mono';font-size:16px";
	public static inline var LETTER_WIDTH : Float = 9.5;
	#end

	public static inline var UNDERLINE_STYLE : String = "text-decoration:underline";
	public static inline var RECT_STYLE : String = "stroke:black";
	public static inline var LINE_STYLE : String = "fill:none;stroke:black";
	public static inline var WHITE_STYLE : String = "fill:white;stroke:black";
	public static inline var DOTTED_STYLE : String = "2,2";
	public static var MARKERS : Array<MarkerDesc> = [
		{	id : "arrowMarker", height : PADDING * 2, width : PADDING,
			path : 'M 0 0 L $PADDING $PADDING L 0 ${PADDING * 2} Z',
			style : "fill:black;stroke:black" },
		{	id : "inheritMarker", height : PADDING * 3, width : PADDING * 3,
			path : 'M 0 0 L ${PADDING * 3} ${PADDING * 1.5} L 0 ${PADDING * 3} Z',
			style : WHITE_STYLE },
		{	id : "aggregationMarker", height : PADDING * 3, width : PADDING * 4,
			path : 'M 0 ${PADDING * 1.5} L ${PADDING * 2} 0 L ${PADDING * 4} ${PADDING * 1.5} L ${PADDING * 2} ${PADDING * 3} Z',
			style : WHITE_STYLE },
		{	id : "compositionMarker", height : PADDING * 3, width : PADDING * 4,
			path : 'M 0 ${PADDING * 1.5} L ${PADDING * 2} 0 L ${PADDING * 4} ${PADDING * 1.5} L ${PADDING * 2} ${PADDING * 3} Z',
			style : "fill:black;stroke:black" }
	];
	public static inline var FILL_WHITE_STYLE : String = "fill:white";
	public static inline var FILL_GREY_STYLE : String = "fill:lightgrey";

	public static function initSvg(doc : Xml) : Xml {
		var svg : Xml = Xml.createElement("svg");
		doc.addChild(svg);
		svg.set("xmlns", "http://www.w3.org/2000/svg");
		var defs : Xml = Xml.createElement("defs");
		svg.addChild(defs);
		#if (js && !nodejs)
		var fontStyle : Xml = Xml.createElement("style");
		defs.addChild(fontStyle);
		fontStyle.addChild(Xml.createPCData("@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap')"));
		#end
		var css : Xml = Xml.createElement("style");
		defs.addChild(css);
		css.addChild(Xml.createPCData("text,text>tspan{" + TEXT_STYLE + "} rect{" + RECT_STYLE + "} line,path{" + LINE_STYLE + "}"));

		for (desc in MARKERS) {
			var marker : Xml = Xml.createElement("marker");
			marker.set("id", desc.id);
			marker.set("orient", "auto");
			marker.set("refX", '${desc.width + 1}');
			marker.set("refY", '${desc.height / 2}');
			marker.set("markerHeight", '${desc.height}');
			marker.set("markerWidth", '${desc.width}');
			var path : Xml = Xml.createElement("path");
			path.set("style", desc.style);
			path.set("d", desc.path);
			marker.addChild(path);
			defs.addChild(marker);
		}
		return svg;
	}

	public static function placeObjects(objects : Iterable<Obj>, svg : Xml, height : Int, width : Int, marginFactor : Float = 1) {
		//calculate max lines heights and columns widths:
		var heights : Array<Int> = [ for (i in 0...height) 0 ];
		var widths : Array<Int> = [ for (i in 0...width) 0 ];
		for (o in objects) {
			if (o.svg != null) {
				var w : Int = Std.parseInt(o.svg.firstChild().get("width"));
				var h : Int = Std.parseInt(o.svg.firstChild().get("height"));
				if (w > widths[o.col]) widths[o.col] = w;
				if (h > heights[o.row]) heights[o.row] = h;
			}
		}
		//calculate lines and columns offsets
		var th : Int = PADDING;
		var lineOffsets : Array<Int> = new Array();
		for (i in 0...height) {
			lineOffsets[i] = th;
			th = th + heights[i] + (heights[i] != 0 ? Std.int(MARGIN * marginFactor) : 0);
		}
		var tw : Int = PADDING;
		var columnOffsets : Array<Int> = new Array();
		for (i in 0...width) {
			columnOffsets[i] = tw;
			tw = tw + widths[i] + (widths[i] != 0 ? Std.int(MARGIN * marginFactor) : 0);
		}

		//translate objects:
		for (o in objects) {
			if (o.svg != null) {
				var w : Int = Std.int(columnOffsets[o.col] + (widths[o.col] - Std.parseInt(o.svg.firstChild().get("width"))) / 2);
				var h : Int = Std.int(lineOffsets[o.row] + (heights[o.row] - Std.parseInt(o.svg.firstChild().get("height"))) / 2);
				o.svg.set("transform", 'translate($w,$h)');
			}
		}

		th = th - Std.int(MARGIN * marginFactor) + PADDING;
		tw = tw - Std.int(MARGIN * marginFactor) + MARGIN;
		svg.set("width", Std.string(tw));
		svg.set("height", Std.string(th));
	}

	@SuppressWarnings("checkstyle:CyclomaticComplexity") //I know this is complex ;-)
	public static function getLine(obj1 : Obj, obj2 : Obj, card1 : String = "", card2 : String = "", bend : Int = 0) : Line {
		var coords1 : Array<String> = Svg.coordRE.replace(obj1.svg.get("transform"), "$1").split(","); //"translate(x,y)" -> [x, y]
		var x1 : Int = Std.parseInt(coords1[0]);
		var y1 : Int = Std.parseInt(coords1[1]);
		var w1 : Int = Std.parseInt(obj1.svg.firstChild().get("width"));
		var h1 : Int = Std.parseInt(obj1.svg.firstChild().get("height"));
		var coords2 : Array<String> = Svg.coordRE.replace(obj2.svg.get("transform"), "$1").split(",");
		var x2 : Int = Std.parseInt(coords2[0]);
		var y2 : Int = Std.parseInt(coords2[1]);
		var w2 : Int = Std.parseInt(obj2.svg.firstChild().get("width"));
		var h2 : Int = Std.parseInt(obj2.svg.firstChild().get("height"));
		var hOrientation : Int = obj1.col == obj2.col ? 0 : obj1.col < obj2.col ? -1 : 1;
		var vOrientation : Int = obj1.row == obj2.row ? 0 : obj1.row < obj2.row ? -1 : 1;
		var line : Line = new Line(x1 + Std.int(w1 / 2), y1 + Std.int(h1 / 2), x2 + Std.int(w2 / 2), y2 + Std.int(h2 / 2), hOrientation, vOrientation);

		var dx : Int = 0;
		var dy : Int = 0;
		//set coordinates to sides facing each other
		x1 += x1 < x2 ? w1 : 0;
		y1 += y1 < y2 ? h1 : 0;
		x2 += x2 < x1 ? w2 : 0;
		y2 += y2 < y1 ? h2 : 0;
		if (line.isVertical()) {
			line.y1 = y1;
			line.y2 = y2;
			if (bend != 0) {
				line.x1 = line.x1 + Std.int(w1 * bend / 3);
				line.x2 = line.x2 + Std.int(w2 * bend / 3);
				dx = bend * PADDING;
			}
		} else if (line.isHorizontal()) {
			line.x2 = x2;
			if (bend == 0) {
				line.x1 = x1;
			} else {
				line.y1 = y1 + (bend == 1 ? h1 : 0);
				line.y2 = line.y2 + Std.int(h2 * bend / 3);
				dy = bend * PADDING;
			}
		} else { //diagonal
			if (bend == 1) {
				line.x1 = x1;
				line.y2 = y2;
				line.x2 -= Std.int((line.x2 - x2) / 2);
			} else if (bend == -1) {
				line.y1 = y1;
				line.x2 = x2;
				line.y2 -= Std.int((line.y2 - y2) / 2);
			} else { //calculation of the intersection point between the box side and the line
				var segStart : Int;
				var steering : Float = line.getSteering();
				var offset : Float = line.getOffset();
				segStart = y1 < y2 ? y1 - h1 : y1;
				var tx1 : Int = Math.round((y1 - offset) / steering); //given y
				var ty1 : Int = Math.round(x1 * steering + offset); //given x
				if (ty1 >= segStart && ty1 <= segStart + h1) { //if the line intersects the box left or right side
					line.x1 = x1;
					line.y1 = ty1;
				} else { //else, it intersects the top or bottom side
					line.x1 = tx1;
					line.y1 = y1;
				}
				segStart = y2 < y1 ? y2 - h2 : y2;
				var tx2 : Int = Math.round((y2 - offset) / steering);
				var ty2 : Int = Math.round(x2 * steering + offset);
				if (ty2 >= segStart && ty2 <= segStart + h2) { //if the line intersects the box left or right side
					line.x2 = x2;
					line.y2 = ty2;
				} else { //else, it intersects the top or bottom side
					line.x2 = tx2;
					line.y2 = y2;
				}
			}
		}

		//calculation of cardinality coordinates
		line.cx1 = line.x1 + PADDING;
		line.cy1 = line.y1 + LETTER_HEIGHT + PADDING;
		line.cx2 = line.x2 + PADDING + dx;
		line.cy2 = line.y2 + LETTER_HEIGHT + PADDING + dy;
		if (line.hOrientation == 1) line.cx1 = line.x1 - Std.int(LETTER_WIDTH * card1.length + PADDING * 1.5);
		if (line.hOrientation == -1 || dx > 0) line.cx2 = line.x2 - Std.int(LETTER_WIDTH * card2.length + PADDING * 1.5) + dx;
		if (line.vOrientation == 1) line.cy1 = line.y1 - Std.int(PADDING * 1.5);
		if (line.vOrientation == -1 || dy > 0) line.cy2 = line.y2 - Std.int(PADDING * 1.5) + dy;
		return line;
	}

	public static function getSvgLine(coords : Line, bend : Int = 0, ?endMarkerId : String) : Xml {
		var line : Xml;
		if (bend == 0) {
			line = Xml.createElement("line");
			line.set("x1", Std.string(coords.x1));
			line.set("x2", Std.string(coords.x2));
			line.set("y1", Std.string(coords.y1));
			line.set("y2", Std.string(coords.y2));
		} else {
			line = Xml.createElement("path");
			var xlm : Int = Std.int((coords.x2 + coords.x1) / 2);
			var ylm : Int = Std.int((coords.y2 + coords.y1) / 2);
			var distance : Float = Math.sqrt(Math.pow(coords.x2 - coords.x1, 2) + Math.pow(coords.y2 - coords.y1, 2)) / 3;
			var steering : Float = -1 / coords.getSteering();
			var angle : Float = Math.atan(steering);
			var xlq : Int = xlm;
			var ylq : Int = ylm;
			if (coords.hOrientation == 0) { //vertical
				xlq += Std.int(Math.cos(angle) * distance) * bend;
				ylq += Std.int(Math.sin(angle) * distance) * bend;
			} else if (coords.vOrientation == 0) { //horizontal
				xlq += Std.int(Math.cos(angle) * distance) * bend;
				ylq += Std.int(Math.sin(angle) * distance) * coords.hOrientation;
			} else { //diagonal (no bend)
				xlq += Std.int(Math.cos(angle) * distance) * bend * -coords.hOrientation;
				ylq += Std.int(Math.sin(angle) * distance) * bend * -coords.hOrientation;
			}
			line.set("d", 'M ${coords.x1} ${coords.y1} Q $xlq $ylq ${coords.x2} ${coords.y2}');
		}
		if (endMarkerId != null) {
			line.set("marker-end", 'url(#$endMarkerId)');
		}
		return line;
	}
}
