/**
* creates a new zoom behavior
*/
var zoom = d3.zoom().on("zoom", zoomHandler);

/**
* creates svg object and associated attributes
* applies the zoom behavior to svg
*/
var svg = d3.select("svg.graph")
    .call(zoom),
width = svg.attr("width"),
height = svg.attr("height");
var textinfo='';
var textabstract='';
var perc;

/**
* creates node object and associated attributes
*/
var node,
r=10,
color = d3.scaleOrdinal()
    .domain(["height", "input", "depth"])
    .range([' #01d7c0', ' #8b90fe ', '  #a15eb2 ']),
yscale = d3.scaleOrdinal()
    .domain(["height", "input", "depth"])
    .range([0, 200, 400]),
linetype=d3.scaleOrdinal()
    .domain(["line","dotted"])
    .range([("8,0"),("8,8")]),
toRemove;

/**
* creates link object
*/
var link;

/**
* creates a background
* creates a click functionality of the background
*/
var rect = svg.append("rect")
    .attr("x", 0)
    .attr("y", 0)
    .attr("height", height)
    .attr("width", width)
    .style("fill", 'white')
    .on('click', clickRect);

/**
* creates svg object (legend) and associated attributes
*/
var svglegend = d3.select("svg.legendsvg"),
legendposition = [65,95,125],
arrowlegendposition = [0,25],
arrowgroupnames = ["citation","self-citation"],
groupnames = ["cited by","input","reference"];
    
var legend = svglegend.selectAll(".legend")
    .data(legendposition)
    .enter()
    .append("g")
    .attr("class","legend")
    .attr("transform", function(d,i) {return "translate(0," + d  + ")"; });
    
legend.append("text")
    .attr("x", 80)
    .attr("y", 0)
    .attr("dy", ".35em")
    .style("text-anchor", "start")
    .text(function(d,i){return groupnames[i]});
    
legend.append("circle")
    .attr("r", r)
    .attr("cx",30-r)
    .style("fill", color);
        
var legendarrow = svglegend.selectAll(".legendarr")
    .data(arrowlegendposition)
    .enter()
    .append("g")
    .attr("class","legendarr")
    .attr("transform", function(d) { return "translate(0," + d  + ")"; });
    
legendarrow.append("line")
    .attr("x1", 10)
    .attr("x2", 50)
    .attr("y1", 10)
    .attr("y2", 10)
    .style("stroke-dasharray",linetype)
    .style("stroke", '#999')
    .style("stroke-width", "1px")
    .style('pointer-events', 'none')
    .attr('marker-end',updateMarker('#999',this));

legendarrow.append("text")
    .attr("x", 80)
    .attr("y", 10)
    .attr("dy", ".35em")
    .style("text-anchor", "start")
    .text(function(d,i){return arrowgroupnames[i]});

/**
* creates a new simulation
* updates the positions of the links and nodes when the 
  state of the layout has changed (simulation has advanced by a tick)
*/
var simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id(function(d) {return d.doi;}).distance(50).strength(function(d) {
            if (d.group == "input") {return 0;}
            else {return 5;}
        }))
        .force("collide", d3.forceCollide(function(d) {
            if (d.group == "input") {return 100;}
            else {return 65;}
        }).strength(0.5))
    .force("charge", d3.forceManyBody().strength(0.001))
    .force("center", d3.forceCenter(width/2, height/2+20))
    .force("yscale", d3.forceY().strength(function(d) {
        if (d.group == "input") {return 1000;}
        else {return 50;}
    }).y(function(d) {return yscale(d.group)}))
    .alpha(0.005)
    .on("end",  zoomTo);

/**
* creates group element
*/
var g = svg.append("g")
    .attr("class", "everything")

/**
* loads JSON data and calls the update function
*/
d3.json("json_text.json").then(function(graph) {
    update(graph.links, graph.nodes);
})

/**
* calls update functions for links and nodes
* adds the nodes and links to the simulation
* @param {object} nodes - nodes
* @param {object} links - links
*/
function update(links, nodes) {
    updateLinks(links);
    updateNodes(nodes);
    
    simulation
        .nodes(nodes)
        .on("tick", tickHandler);
    simulation.force("link")
        .links(links);
    
    link.attr('marker-end', function(d) {return updateMarker("#999", d.target);})
        .style("stroke-dasharray",function(d){return self_cit(d.source,d.target)? ("8,8"): ("1,0")});
}

/**
* initializes and shows links
* @param {object} links - links
*/
function updateLinks(links) {
    link = g.append("g")
        .selectAll(".link")
        .data(links)
        .enter()
        .append("line")
        .style("stroke-width", "1px")
        .style("stroke", "#999")
        .attr("class", "link");
}

/**
* initializes and shows nodes with circles and texts
* creates a new drag behavior and applies it to the circles
* creates a click functionality of the circles and texts
* @param {object} nodes - nodes
*/
function updateNodes(nodes) {
    node = g.selectAll(".node")
        .data(nodes)
        .enter()
        .append("g")
        .attr("class", "node")
        .call(d3.drag()
            .on("start", dragstarted)
            .on("drag", dragged)
        );

    node.append("circle")
        .attr("class", "circle")
        .attr("r", function(d) {return 1.5*r+d.citations*0.05})
        .style("fill", function(d){ return color(d.group)})
        .on('click', clickNode);

    node.append("text")
        .attr("class", "text") 
        .style("font-size", "15px")
        .style('pointer-events', 'auto')
        .text(function (d) {const firstauthor=d.author[0].split(" ")
        return firstauthor[firstauthor.length-1];})
        .on('click', clickNode);
}

/**
* creates arrowhead and returns its url
* @param {string} color - color of arrowhead
* @param {string} target - target-node
*/
function updateMarker(color, target) {
    var radius=1.5*r+target.citations*0.05
    svg.append('defs').append('marker')//arrowhead
        .attr('id',color.replace("#", "")+radius)
        .attr('viewBox','-0 -5 10 10')
        .attr('refX',radius+9.5)
        .attr('refY',0)
        .attr('orient','auto')
        .attr('markerWidth',10)
        .attr('markerHeight',15)
        .attr('xoverflow','visible')
        .append('svg:path')
        .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
        .attr('fill', color)//arrowhead color
        .style('stroke','none');
    return "url(" + color + radius + ")";
};

/**
* colors the circle and its links black and removes the previous markings
* @param {object} node - node
*/
function clickNode(node) {
    d3.select(this.parentNode).raise();
    fix_nodes(node);
    if(toRemove){
        d3.select(toRemove).selectAll(".circle").style("stroke","none")
    }
    toRemove = this.parentNode;
    d3.select(this.parentNode).selectAll(".circle").style("stroke","black")
    marklink(node)
    textfunc(node)
    resetbuttonhighlight()
    highlightbutton("overview")
}

/**
* removes the markings of the circles and their links
*/
function clickRect() {
    fix_nodes(node);
    d3.selectAll(".circle").style("stroke", "none")
    d3.selectAll(".link")
        .style("stroke", "#999")
        .attr('marker-end', function(d) {return updateMarker('#999', d.target);})
    textabstract='';
    textinfo='';
    resetbuttonhighlight()
    document.getElementById('textbox').innerHTML = "Click node";
}

function create_author_array(authors){
    authorarray = authors.split(",")
    authorarray = authorarray.map(elem =>{return elem.trim();})
    
    return authorarray
}

function self_cit(source,target){
    return source.author.some(item=>target.author.includes(item))
}

/**
* sets color of link (line and arrowhead) to black if it is directly connected to node
* and to grey otherwise
* @param {object} node - node
*/
function marklink(node){
    d3.selectAll(".link")
        .style("stroke", function(o) {
            return isLinkForNode(node, o) ? "black" : "#999";})
        .attr('marker-end', function(o) {
            return isLinkForNode(node, o) ? updateMarker('#000000', o.target) : updateMarker('#999', o.target);})
}

/**
* returns true if link is directly connected to node and false if it is not
* @param {object} node - node
* @param {object} link - link
*/
function isLinkForNode(node, link){
    return link.source.index == node.index || link.target.index == node.index;
}

/**
* outputs node info to textbox
* @param {object} node - data of current node
*/
function textfunc(node) {
    textinfo="Title:" + '</br>' + node.name +
    '</br>' +'</br>'+"Author:"+ '</br>' +node.author+'</br>'+'</br>'+"Date:"+'</br>'
    +node.year+'</br>'+'</br>'+"doi:"+'</br>'+'<a href="'+node.doi+ '">'+node.doi
    +'</a>'+'</br>'+'</br>'+"Citations:"+'</br>'+node.citations;
    textabstract=node.abstract;
    document.getElementById('textbox').innerHTML = textinfo;
}

function highlightbutton(btn) {
    resetbuttonhighlight();
    document.getElementById(btn).style.background="#CACACA";
}

function resetbuttonhighlight() {
    document.getElementById("overview").style.background='';
    document.getElementById("abstract").style.background='';
}

function displayabstract(a){
    if (textabstract=='' && textinfo=='') {
        document.getElementById('textbox').innerHTML="Click node";
    }
    else {
        if (a==true) {
            document.getElementById('textbox').innerHTML =textabstract;
        }
        else {
            document.getElementById('textbox').innerHTML =textinfo;
        }
    }   
}

/**
* updates the positions of the links and nodes
*/
function tickHandler() {
    link.attr("x1", function (d) {return d.source.x;})
        .attr("y1", function (d) {return d.source.y;})
        .attr("x2", function (d) {return d.target.x;})
        .attr("y2", function (d) {return d.target.y;});
    node.attr("transform", function (d) {return "translate(" + d.x + ", " + d.y + ")";});
}

/**
* initializes the dragging of the node
* @param {object} node - data of current node
*/
function dragstarted(node) {
    d3.select(this).raise();
    if (!d3.event.active) 
        simulation.alphaTarget(0.3).restart()
    node.fx = node.x;
    node.fy = node.y;
    fix_nodes(node);
}

/**
* applies the dragging to the node
* @param {object} node - data of current node
*/
function dragged(node) {
    node.fx = d3.event.x;
    node.fy = d3.event.y;
    fix_nodes(node);
}

function fix_nodes(this_node) {
    node.each(function(d) {
      if (this_node != d) {
        d.fx = d.x;
        d.fy = d.y;
      }
    });
}

/**
* applies the transformation (zooming or dragging) to the g element
*/
function zoomHandler() {
    d3.select('g').attr("transform", d3.event.transform);
}

d3.selection.prototype.moveToFront = function() {
    return this.each(function(){
    this.parentNode.appendChild(this);
    });
  };

/**
* transforms svg so that the zoom is adapted to the size of the graph
*/
function zoomTo() {
    node_bounds = d3.selectAll("svg.graph").node().getBBox();
    svg_bounds = d3.select("rect").node().getBBox();

    perc_x = width/(node_bounds.width+100);
    perc_y = height/(node_bounds.height+100);
    perc = d3.min([perc_x, perc_y])
    
    d3.select('svg')
		.call(zoom.scaleBy, perc);
}


/**
* transforms svg so that the zoom is reset
*/
function resetView() {
    d3.select('svg')
        .call(zoom.scaleTo, 1)
    d3.select('svg')
        .call(zoom.translateTo, 0.5 * width, 0.5 * height);
    d3.select('svg')
		.call(zoom.scaleBy, perc);
    
}

/**
* transforms svg so that it is centered
*/
function center() {
    d3.select('svg')
        .call(zoom.translateTo, 0.5 * width, 0.5 * height);
}

function savesvg(){
	var svgString = getSVGString(svg.node());
	svgString2Image( svgString, 2*width, 2*height, 'png', save ); // passes Blob and filesize String to the callback

	function save( dataBlob, filesize ){
		saveAs( dataBlob, 'D3 vis exported to PNG.png' ); // FileSaver.js function
	}
};

// Below are the functions that handle actual exporting:
// getSVGString ( svgNode ) and svgString2Image( svgString, width, height, format, callback )
function getSVGString( svgNode ) {
	svgNode.setAttribute('xlink', 'http://www.w3.org/1999/xlink');
	var cssStyleText = getCSSStyles( svgNode );
	appendCSS( cssStyleText, svgNode );

	var serializer = new XMLSerializer();
	var svgString = serializer.serializeToString(svgNode);
	svgString = svgString.replace(/(\w+)?:?xlink=/g, 'xmlns:xlink='); // Fix root xlink without namespace
	svgString = svgString.replace(/NS\d+:href/g, 'xlink:href'); // Safari NS namespace fix

	return svgString;

	function getCSSStyles( parentElement ) {
		var selectorTextArr = [];

		// Add Parent element Id and Classes to the list
		selectorTextArr.push( '#'+parentElement.id );
		for (var c = 0; c < parentElement.classList.length; c++)
				if ( !contains('.'+parentElement.classList[c], selectorTextArr) )
					selectorTextArr.push( '.'+parentElement.classList[c] );

		// Add Children element Ids and Classes to the list
		var nodes = parentElement.getElementsByTagName("*");
		for (var i = 0; i < nodes.length; i++) {
			var id = nodes[i].id;
			if ( !contains('#'+id, selectorTextArr) )
				selectorTextArr.push( '#'+id );

			var classes = nodes[i].classList;
			for (var c = 0; c < classes.length; c++)
				if ( !contains('.'+classes[c], selectorTextArr) )
					selectorTextArr.push( '.'+classes[c] );
		}

		// Extract CSS Rules
		var extractedCSSText = "";
		for (var i = 0; i < document.styleSheets.length; i++) {
			var s = document.styleSheets[i];
			
			try {
			    if(!s.cssRules) continue;
			} catch( e ) {
		    		if(e.name !== 'SecurityError') throw e; // for Firefox
		    		continue;
		    	}

			var cssRules = s.cssRules;
			for (var r = 0; r < cssRules.length; r++) {
				if ( contains( cssRules[r].selectorText, selectorTextArr ) )
					extractedCSSText += cssRules[r].cssText;
			}
		}
		

		return extractedCSSText;

		function contains(str,arr) {
			return arr.indexOf( str ) === -1 ? false : true;
		}

	}

	function appendCSS( cssText, element ) {
		var styleElement = document.createElement("style");
		styleElement.setAttribute("type","text/css"); 
		styleElement.innerHTML = cssText;
		var refNode = element.hasChildNodes() ? element.children[0] : null;
		element.insertBefore( styleElement, refNode );
	}
}


function svgString2Image( svgString, width, height, format, callback ) {
	var format = format ? format : 'png';

	var imgsrc = 'data:image/svg+xml;base64,'+ btoa( unescape( encodeURIComponent( svgString ) ) ); // Convert SVG string to data URL

	var canvas = document.createElement("canvas");
	var context = canvas.getContext("2d");

	canvas.width = width;
	canvas.height = height;

	var image = new Image();
	image.onload = function() {
		context.clearRect ( 0, 0, width, height );
		context.drawImage(image, 0, 0, width, height);

		canvas.toBlob( function(blob) {
			var filesize = Math.round( blob.length/1024 ) + ' KB';
			if ( callback ) callback( blob, filesize );
		});

		
	};

	image.src = imgsrc;
}