import { SVG, G } from "./svg.js/src/main.js"

export default {newNode, mecaOnOff, toDot}

var link_tmp = {node1: null, node2:null}
var line_tmp = null

var draw = SVG().addTo('#svg').size("100%", 5000)
draw.mousemove(function(ev){
    if (linking){
	var p = $("#svg").offset()
	if (line_tmp){
	    line_tmp.remove()
	    line_tmp = null
	}
	var x1 = link_tmp.node1.anchor.cx()
	var y1 = link_tmp.node1.anchor.cy()
	var x2 = ev.clientX - p.left
	var y2 = ev.clientY - p.top + $(document).scrollTop() + $("#svg").scrollTop()
	line_tmp = draw.line(
	    x1, y1, 0.1 * x1 + 0.9 * x2, 0.1 * y1 + 0.9 * y2
	).stroke({color:"black", width: 2}).attr("z-axis", -10)
    }
})

const centreX = draw.node.clientWidth/2
const centreY = draw.node.clientHeight/2

var meca =  [] // liste des objets mécaniques soumis à des forces
var links = [] // list de liens

const size = {w: 230, h: 36} // taille des étiquettes
const margin = {x: 20, y: 4} // marges des étiquettes

/**
 * on détermine une origine décalée (coordonnées négatives)
 * équivalente au centre du canvas modulo 'step', légèrement décalée pour
 * centrer les étiquettes. Celle-ci sert à calculer une force de rappel
 * périodique pour que les étiquettes s'alignent dans des "cases"
 **/
const msize = 1   // multiplicateur pour la taille des cases
const step = {    // dimension des cases de la grille
    x: (size.w + 2 * margin.x) * msize,
    y: (size.h + 2 * margin.y) * msize
}
var ox = centreX - (size.w + margin.x) /2
var oy = centreY - (size.h + margin.y) /2
while (ox > 0) ox -= step.x
while (oy > 0) oy -= step.y
const origin = { x:ox, y:oy }

var dirty = true;    // on ne lance pas la mécanique quand dirty est faux
var frozen = false   // pas de gel de la mécanique au commencement
var linking = false; // linking devient vrai quand on est à créer un lien
const seuil = 1.3    // en dessous de ce déplacement, dirty devient faux
const dt = 0.02      // durée en seconde entre deux « tours de mecanique »

var _id = 0           // index pour les identifiants
var _la = 0           // compteur pour les nœuds créés avec anchor="left"
var _ra = 0           // compteur pour les nœuds créés avec anchor="right"

/**
 * Noeud du graphe ; seul le premier paramètre est obligatoire, le deuxième
 * est recommandé.
 *
 * Le constructeur place un noeud dans une image SVG, sous forme d'un texte,
 * sur un rectangle coloré, sur un rectangle transparent un peu plus grand
 *
 * Paramètres du constructeur :
 *
 * @param svg une instance de SVG
 * @param meca liste de noeuds participant à une simulation "mécanique"
 * @param data un objet avec des propriétés facultatives :
 *  - label: une étiquette (texte), No title" par défaut
 *  - anchor lieu d'attache des liens (top, left, right, ou bottom),
 *    aléatoire par défaut; si anchor est "left" ou "right", le valeurs
 *    de target, color, position peuvent être déduits automatiquement
 *  - target: coordonnées {x, y} du centre d'attraction,
 *    centre du canevas par défaut
 *  - color une couleur (texte), aléatoire par défaut
 *  - position du noeud {x: abscisse, y: ordonnée}, aléatoire par défaut
 **/
class Node {
    constructor (svg, meca, data){
	var l = data.label || "No title"
	this.label = l
	var a = data.anchor || ["top", "left", "right", "bottom"][Math.floor(4 * Math.random())]
	
	var p, c, h, t // position, couleur, hue, centre d'attraction(target)
	var ax = 0, ay = 0 // abscisse/ordonnée de l'ancre
	var delx = 0, dely = 0 // abscisse/ordonnée de l'icône de suppression
	
	switch(a){
	    /*
	case "top":
	    ax = size.w / 2 + margin.x
	    ay = margin.y
	    break;
	case "bottom":
	    ax = size.w / 2 + margin.x
	    ay = size.h + margin.y
	    break;
	    */
	case "left":
	    ax = margin.x
	    ay = size.h / 2 + margin.y
	    delx = size.w + margin.x
	    dely = size.h / 2 + margin.y
	    h = 5 * _id
	    p = {x: centreX * 1.3 - (size.w + margin.x) /2,
		 y: 100 + 0.9 * step.y * _la++}
	    t = {x: centreX * 1.3 - (size.w + margin.x) /2,
		 y: centreY}
	    break;
	case "right":
	    ax = size.w + margin.x
	    ay = size.h / 2 + margin.y
	    delx = margin.x
	    dely = size.h / 2 + margin.y
	    h = 5 * _id + 180
	    p = {x: centreX * 0.7 - (size.w + margin.x) /2,
		 y: 100 + 0.9 * step.y * _ra++}
	    t = {x: centreX * 0.7 - (size.w + margin.x) /2,
		 y: centreY}
	    break;
	default:
	    alert("the value of anchor is " + a + " this should not happen")
	    h = 360 * Math.random()
	    p = {x: centreX * (0.5 + Math.random()),
		 y : centreY * (0.5 + Math.random())}
	    t = {x: centreX, y: centreY}
	}
	p = data.position || p
	c = data.color  || 'hsl(' + h + ', 50%, 75%)'
	t = data.target || t

	this.id = "node" + _id++
	this.vx = 0
	this.vy = 0
	this.rect0 = svg.rect(size.w + 2 * margin.x, size.h + 2 * margin.y ).move(p.x, p.y).fill('rgba(255,255,255,0)')
	this.rect = svg.rect(size.w, size.h).move(p.x + margin.x, p.y + margin.y).fill(c).stroke('black')
	this.text = svg.text(l).move(p.x + margin.x + 17, p.y + margin.y + 6)
	this.target = {x: t.x - size.w / 2 - margin.x,
		       y: t.y - size.h / 2 - margin.y}
	var d = 20 // diamètre de l'ancre à liens
	this.anchor = svg.circle(d).move(p.x + ax - d/2, p.y + ay - d/2).fill("cyan").stroke("black").attr({
	    cursor: "pointer",
	})
	this.anchor.parent = this // lien veurs le noeud
	this.anchor.mouseover(function(){
	    this.fill("red")
	})
	this.anchor.mouseout(function(){
	    this.fill("cyan")
	})
	this.anchor.click(function(ev){
	    if (! linking){
		dirty = false // arrêt des mécaniques
		linking = true
		link_tmp.node1 = this.parent
		$("#linking").show();
		$("#not_linking").hide();
	    } else {
		linking = false
		link_tmp.node2 = this.parent
		$("#linking").hide();
		$("#not_linking").show();
		line_tmp.remove()
		line_tmp = null
		this.parent.vx = 2 * seuil / dt // on bouscule le noeud
		dirty = true; mecanique()       // reprise des mécaniques
		new Link(draw, link_tmp.node1, link_tmp.node2)
	    }
	})
	/* widget pour supprimer le nœud */
	const ddel = 42 // diamètre du widget de suppression
	this.delbg = svg.circle(ddel).fill("rgba(255,255,255,0.8)") // fond
	this.del = svg.path(                                      // dessin
	    "M 24.,3.5 C 12.7,3.54 3.5,12.7 3.5,24 C 3.5,35.3 12.7,44.5 24,44.5 C 35.3,44.5 44.5,35.3 44.5,24 C 44.5,12.7 35.3,3.5 24.,3.5 z M 24.,8.5 C 27.4,8.5 30.5,9.6 33,11.4 L 11.43,33 C 9.6,30.5 8.5,27.4 8.5,24 C 8.55,15.4 15.4,8.5 24,8.5 z M 36.6,15 C 38.4,17.5 39.5,20.6 39.5,24 C 39.5,32.6 32.6,39.5 24,39.5 C 20.6,39.5 17.5,38.4 15,36.6 L 36.6,15 z"
	).fill('red').stroke('darkred')
	this.deltitle = svg.element("title").words("Suppr. " + l)
	this.gdel = svg.group().attr({
	    cursor: "pointer",
	    "data-label": l,
	    "class": "delbutton",
	})
	// groupement du fond et du dessin
	this.gdel.add(this.delbg).add(this.del).add(this.deltitle).move(p.x + delx - ddel/2, p.y + dely - ddel/2).scale(0.7, 0.7)
	// interactivité
	const _delnode = function(gdel){
	    $(gdel).parent().remove()
	    meca.forEach(function(obj, index){
		var delbutton = obj.gdel.node
		if (delbutton == gdel) {
		    var dli = [] // indices des liens à supprimer
		    links.forEach(function(objl, indexl){
			if(objl.node1 == obj || objl.node2 == obj){
			    $(objl.line.node).remove()
			    $(objl.group.node).remove()
			    dli.push(indexl)
			}
		    })
		    dli.sort()
		    dli.reverse()
		    dli.forEach(function(i){
			links.splice(i,1)

		    })
		    meca.splice(index, 1)
		}
	    })
	}
	
	this.gdel.click(function(){
	    var elt = $(this).attr("node")
	    $("#node-dialog").html(
		"<p>Supprimer le nœud « " + $(elt).data("label") + " » et tous les liens qui le concernent ?</p>"
	    ).dialog({
		title: "Supprimer un nœud du graphe",
		width: 350,
		buttons: [
		    {
			text: "Échap",
			click: function() {
			    $( this ).dialog( "close" );
			}
		    },
		    {
			text: "OK",
			click: function() {
			    _delnode(elt)
			    $( this ).dialog( "close" );
			}
		    },
		],
	    })
	})

	// groupement de tous les composants du nœud pour le rendre dragable
	this.group = svg.group()
	this.group.add(this.rect0).add(this.rect).add(this.text).add(this.gdel)
	this.group.add(this.anchor)
	
	this.group.draggable().on('dragend', function(e) {
	    // relance les « tours de mecanique » après un déplacement
	    // forcé, si on n'est pas en train de tirer un lien
	    if (linking) return
	    dirty = true
	    mecanique()
	})
	this.group.draggable().on('dragstart', function(e) {
	    // stoppe les « tours de mecanique » en début de déplacement
	    dirty = false
	})
	
	if (typeof meca !== 'undefined'){
	    meca.push(this)
	}
    }

    get bbox() {return this.group.bbox}
    get x() {return this.group.x()}
    get y() {return this.group.y()}

    dmove = function(dx, dy){
	this.group.dmove(dx, dy)
    }

    
    /**
     * fonction qui calcule la somme des forces reçues par un noeud,
     * puis affecte son mouvement.
     **/
    force = function(){
	const Kc = 6      // force centripète générale
	const Kv = 5      // frottement fluide
	const Khc = 2.5   // répulsion « hard core »
	const Kp = 0.08   // force périodique pour faire des cases en tableau

	var fx = 0, fy = 0
	
	// force centripète
	var dx = this.x - this.target.x, dy = this.y - this.target.y
	var r = Math.sqrt(dx ** 2 + dy ** 2)
	fx += - Kc * step.x * dx /r
	fy += - Kc * step.y * dy /r

	// frottement fluide
	fx += - Kv * this.vx
	fy += - Kv * this.vy
	
	// répulsion "hardcore" ;
	// direction : d'un rectangle à l'autre
	// intensité : croissante avec l'aire d'intersection
	var self = this
	meca.forEach(function(other){
	    if (other != self) {
		var ddx = self.x - other.x
		var ddy = self.y - other.y
		var area = interArea(self.rect0, other.rect0)
		fx += Khc / step.x * ddx * area
		fy += Khc / step.y * ddy * area
	    }
	})
	
	/**
	 * force périodique de rappel vers les cases de la grille
	 * on calcule un écart compris entre -step/2 et step/2,
	 * avec des positions d'équilibre périodiques,
	 * pour en déduire une force de rappel
	 **/
	fx += Kp * step.x * ((this.x - origin.x) % step.x - step.x / 2)
	fy += Kp * step.y * ((this.y - origin.y) % step.y - step.y / 2)
	
	return {x: fx, y: fy}
    }

    /**
     * calcul cinématique : change la position et la vitesse
     * @param f objet {x, y}
     * @param dt intervalle de temps
     **/
    cinematique = function(f,dt){
	// calcul des nouvelles vitesses
	this.vx += f.x * dt // chaque objet possède la masse 1 kg
	this.vy += f.y * dt
	// déplacement de l'objet
	this.dmove(this.vx * dt, this.vy * dt)

	// ajustement des liens
	links.forEach(function(l){
	    adjust(l)
	})
    }
}

/**
 * Lien du graphe ; les trois premiers paramètres sont obligatoires
 * le constructeur crée une ligne entre deux noeuds, et une cible
 * circulaire pour interagir avec le lien
 *
 * Paramètres du constructeur :
 *
 * @param svg une instance de SVG
 * @param node1 premier noeud
 * @param node2 deuxième noeud
 * @param data un objet avec des propriétés facultatives :
 *  - color :  couleur de la cible (jaune par défaut)
 *  - stroke : couleur du tour de cible (bleu marine par défault)
 *  - label :  texte de la cible ("" par défaut)
 **/
class Link{
    constructor (svg, node1, node2, data){
	data = data || {}
	this.id = "link" + _id++
	this.node1 = node1
	this.node2 = node2
	this.fill = data.color || "yellow"
	this.stroke = data.stroke || "navy"
	this.label = data.label || "no label"
	this.line = svg.line(0,0,1,1).stroke({color: this.stroke, width: 2})
	this.target = svg.circle(30).fill(this.fill).stroke({color: this.stroke})
	this.text = null
	this.title = svg.element("title")
	this.group = svg.group()
	this.group.add(this.target).add(this.title)	
	adjust(this) // gère le placement, crée le texte de deux lettres
	var _del_link = function(l){
	    return function(){
		del_link(l.id)
		$("#link-dialog").dialog('close')
	    }
	} (this)
	var _update_label = function(l){
	    return function update_label(){
		l.label = $("#input-label").val()
		adjust(l)
		$("#link-dialog").dialog('close')
	    }
	} (this)
	var _validate = function(ev){
	    if (ev.which == 13){
		event.preventDefault()
		$("#link-dialog .label_button").click()
	    }
	}
	var self = this
	this.group.click(function (){
	    var d = $("#link-dialog")
	    d.find("span.ident").text(self.id)
	    $("#input-label").val(self.label)
	    $("#input-label").off("keypress").on("keypress", _validate )
	    d.find(".del_button").off("click").on("click", _del_link)
	    d.find(".label_button").off("click").on("click", _update_label)
	    d.dialog({width: 500})
	})
	links.push(this)
    }
    
    /**
     * Getter pour montrer le lien
     **/
    get toDot() {
	return this.node1.label + " -> " + this.node2.label
    }
}

/**
 * Produit un graphe à notre mode, à partir d'un graphe déjà généré par
 * `dot -T svg`
 * @param svg_elts une suite d'éléments SVG enrobés dans des balises <svg></svg>
 **/
function fromDot(svg_elts){
}

/**
 * réajuste l'affichage d'un lien quand les noeuds ont bougé
 * param l le lien à ajuster
 */
function adjust(l){
    var x1 = l.node1.anchor.cx()
    var y1 = l.node1.anchor.cy()
    var x2 = l.node2.anchor.cx()
    var y2 = l.node2.anchor.cy()
    l.line.attr({x1: x1, y1: y1, x2: x2, y2: y2})
    l.target.attr({cx: (x1 + x2) / 2, cy: (y1 + y2) / 2})
    if (l.text) l.text.remove()
    l.title.node.innerHTML = l.label
    l.text = draw.text(l.label.substring(0,2).toUpperCase())
    l.text.move(l.target.cx() - 11, l.target.cy()-11)
    l.group.add(l.text)

}

/**
 * aire d'intersection de deux rectangles
 * @param r1 premier rectangle
 * @param r2 deuxième rectangle
 * @return aire de l'intersection
 **/
function interArea(r1,r2){
    var top    = Math.max(r1.y(), r2.y())
    var bottom = Math.min(r1.y() + r1.height(), r2.y() + r2.height())
    var left   = Math.max(r1.x(), r2.x())
    var right  = Math.min(r1.x() + r1.width(), r2.x() + r2.width())
    if (bottom > top && right > left){
	return (bottom -top) * (right - left)
    }
    return 0
}

/**
 * fonction qui donne un « tour de mecanique »,
 * et modifie donc par effet de bord les positions et vitesses des objets
 **/
function mecanique(){
    if (! dirty || frozen) return
    var vmax = 0
    meca.forEach(function (node){
	node.cinematique(node.force(), dt)
	var v = Math.sqrt(node.vx ** 2 + node.vy ** 2)
	vmax = Math.max(v, vmax)
    })
    if (vmax * dt < seuil) {
	dirty = false
    } else {
	setTimeout(mecanique, 1000*dt)
    }
}

/**
 * suppression d'un lien
 * @param id l'identifiant du lien
 **/
function del_link(id){
    links.forEach(function(l,i){
	if (l.id == id){
	    l.line.remove()
	    l.target.remove()
	    l.text.remove()
	    links.splice(i, 1)
	    l = null
	}
    })
}

/**
 * exporte les nœuds et les liens vers une chaîne au format dot
 **/
export function toDot(){
    var debug = $("#debug")
    var text = "digraph mon_graphe {\n"
    $("svg .edge").each( function (i,e){
	title = $(e).find("title").text()
	text += title.replace(/(.*)-&gt;(.*)/, '"$1" -> "$2"\n')
    })
    text += "}\n"
    debug.text(text)
}

/**
 * crée un nouveau nœud
 */
export function newNode(event){
    var button = $(event.target)
    var anchor = button.data("anchor")
    var selections = button.parent().find(":selected")
    selections.each(function(i, sel){
	var label = $(sel).val()
	new Node(draw, meca, {label: label, anchor: anchor,})
    })
}

/**
 * active ou désactive la simulation mécanique
 **/
export function mecaOnOff(event){
    var b = $(event.target)
    var mode = b.data("meca")
    if (mode == "on") {
	frozen = false
	dirty = true
	setTimeout(mecanique, 0)
    } else {
	frozen = true
	dirty = false
    }
}
