/**
 * Parse a relational tree and visualize using arcs.
 *
 * @author Nils Diewald
 */
"use strict";

define([], function () {

  const svgNS = "http://www.w3.org/2000/svg";
  const _TermRE = new RegExp("^(?:([^\/]+?)\/)?([^:]+?):(.+?)$");
  const d = document;

  return {
    create : function (snippet) {
      return Object.create(this)._init(snippet);
    },

    // Initialize the state of the object
    _init : function (snippet) {

      const t = this;

      // Predefine some values
      t._tokens  = [];
      t._arcs    = [];
      t._tokenElements = [];
      t._y = 0;
      t._currentInFocus = undefined;

      // Some configurations
      t.maxArc      = 200; // maximum height of the bezier control point
      t.overlapDiff = 40;  // Difference on overlaps and minimum height for self-refernces
      t.arcDiff     = 15;
      t.anchorDiff  = 8;
      t.anchorStart = 15;
      t.tokenSep    = 30;
      t.xPadding    = 10;
      t.yPadding    = 5;

      // No snippet to process
      if (snippet == undefined || snippet == null)
        return t;

      // Parse the snippet
      const html = d.createElement("div");
      html.innerHTML = snippet;

      // Establish temporary parsing memory
      t.temp = {
        target : {}, // Remember the map id => pos        
        edges  : [], // Remember edge definitions
        pos    : 0   // Keep track of the current token position
      };

      // Start parsing from root
      t._parse(0, html.childNodes, undefined);

      // Establish edge list
      const targetMap = t.temp['target'];
      const edges = t.temp['edges'];

      let targetID, target, relation;
      
      // Iterate over edge lists
      // TODO:
      //   Support spans for anchors!
      edges.forEach(function(edge) {

        // Check the target identifier
        targetID = edge.targetID;
        target = targetMap[targetID];

        if (target != undefined) {

          // Check if the source is a span anchor
          /*
          var start = edge.srcStart;
          if (start !== edge.srcEnd) {
            start = [start, edge.srcEnd];
          };
          */
          
          // Add relation
          relation = {
            start : [edge.srcStart, edge.srcEnd],
            end : target,
            direction : 'uni',
            label : edge.label
          };
          // console.log(relation);
          this.addRel(relation);
        };
      }, t);

      // Reset parsing memory
      delete t["temp"];

      return this;
    },

    // Parse a node of the tree snippet
    _parse : function (parent, children, mark) {

      // Iterate over all child nodes
      Array.from(children).forEach(function(c) {

        // Element node
        if (c.nodeType == 1) {

          let xmlid, target, start, end;

          // Node is an identifier
          if (c.hasAttribute('xml:id')) {

            // Remember that pos has this identifier
            xmlid = c.getAttribute('xml:id');
            // this.temp['target'][xmlid] =
            start = this.temp['pos'];
            end = this.temp['pos'];
          }

          // Node is a link
          // Stricter: && c.hasAttribute('xlink:show')
          else if (c.hasAttribute('xlink:href')) {

            // Node is a continuation
            if (c.getAttribute('xlink:show') == "other" &&
                c.hasAttribute('data-action') &&
                c.getAttribute('data-action') == "join"
               ) {
              xmlid = c.getAttribute('xlink:href').replace(/^#/, "");
              start = this.temp['pos'];
              end = this.temp['pos'];

              // this.temp['target'][xmlid][1] = this.temp['pos'] -1;
              // console.log("Here");
            }

            // Node is a relation
            // Stricter: c.getAttribute('xlink:show') == "none"
            else {
              let label;

              // Get target id
              target = c.getAttribute('xlink:href').replace(/^#/, "");

              if (c.hasAttribute('xlink:title')) {
                label = this._clean(c.getAttribute('xlink:title'));
              };

              // Remember the defined edge
// WRONG!
              var edge = {
                label    : label,
                srcStart : this.temp['pos'],
                targetID : target
              };

              // TEMP: Hot-fix for root relations
              if (!label.match(/--$/) && !label.match(/ROOT$/)) {
                this.temp['edges'].push(edge);
              };

            };
          };

          // Go on with child nodes
          if (c.hasChildNodes()) {
            this._parse(0, c.childNodes, mark);
          };


          // The element is a defined anchor
          if (xmlid !== undefined) {

            // this.temp['target'][xmlid][1] = this.temp['pos'] -1;

            // Element already defined
            if (this.temp['target'][xmlid] !== undefined) {
              const newtarget = this.temp['target'][xmlid];
              end = this.temp['pos'] - 1;
              newtarget[0] = start < newtarget[0] ? start : newtarget[0];
              newtarget[1] = end > newtarget[1] ? end : newtarget[1];
            }

            // Element not yet defined
            else {
              end = this.temp['pos'] - 1;
              this.temp['target'][xmlid] = [start, end];
            };

            /*
            console.log('Target ' + xmlid + ' spans from ' +
                        this.temp['target'][xmlid][0] +
                        ' to ' +
                        this.temp['target'][xmlid][1]
                       );
            */
            xmlid = undefined;
          }

          // Current element describes an arc
          else if (target !== undefined) {

            // TODO: This does not work yet!
            edge["srcEnd"] = this.temp['pos'] -1;
            // console.log('Here');

            /*
            console.log('Source spans from ' +
                        edge["srcStart"] +
                        ' to ' +
                        edge["srcEnd"]
                       );
            */
            target = undefined;
          };
        }

        // Text node
        else if (c.nodeType == 3) {

          // Check, if there is a non-whitespace token
          if (c.nodeValue !== undefined) {

            const str = c.nodeValue.trim();

            if (str !== undefined && str.length > 0) {

              // Add token to token list
              this.addToken(str);

              // Move token position
              this.temp['pos']++;
            };
          };
        };
      }, this);

      // Todo: define edges here!
    },

    
    // Remove foundry and layer for labels
    _clean : function (title) {
      return title.replace(_TermRE, "$3");
    },

    
    // Return the number of leaf nodes
    // (not necessarily part of a relation).
    // Consecutive nodes that are not part of any
    // relation are summarized in one node.
    size : function () {
      return this._tokens.length;
    },

    
    // This is a shorthand for SVG element creation
    _c : function (tag) {
      return d.createElementNS(svgNS, tag);
    },


    // Get bounding box - with workaround for text nodes
    _rect : function (node) {
      if (node.tagName == "tspan" && !navigator.userAgent.match(/Edge/)) {
        const range = d.createRange();
        range.selectNode(node);
        const rect = range.getBoundingClientRect();
        range.detach();
        return rect;
      };
      return node.getBoundingClientRect();
    },


    // Returns the center point of the requesting token
    _tokenPoint : function (node) {
	    const box = this._rect(node);
	    return box.left + (box.width / 2);
    },


    // Draws an anchor
    _drawAnchor : function (anchor) {

      // Calculate the span of the first and last token, the anchor spans
      const firstBox = this._rect(this._tokenElements[anchor.first]);
      const lastBox  = this._rect(this._tokenElements[anchor.last]);
	
      const y = this._y + (anchor.overlaps * this.anchorDiff) - this.anchorStart;
      const l = this._c('path');
      
	    this._arcsElement.appendChild(l);

      const pathStr = "M " +
          (firstBox.left - this.offsetLeft) +
          "," +
          y +
          " L " +
          (lastBox.right - this.offsetLeft) +
          "," + y;

      l.setAttribute("d", pathStr);
      l.setAttribute("class", "anchor");
      anchor.element = l;
      anchor.y = y;
      return l;
    },
    

    // Create an arc with an optional label
    // Potentially needs a height parameter for stacks
    _drawArc : function (arc) {

      const t = this;
      let startPos, endPos;
      let startY = this._y;
      let endY = this._y;
	
      if (arc.startAnchor !== undefined) {
        startPos = t._tokenPoint(arc.startAnchor.element);
        startY = arc.startAnchor.y;
      }
      else {
        startPos = t._tokenPoint(t._tokenElements[arc.first]);
      };

      if (arc.endAnchor !== undefined) {
        endPos = t._tokenPoint(arc.endAnchor.element)
        endY = arc.endAnchor.y;
      }
      else {
        endPos = t._tokenPoint(t._tokenElements[arc.last]);
      };

      startPos -= t.offsetLeft;
      endPos -= t.offsetLeft;

	    // Special treatment for self-references
      var overlaps = arc.overlaps;
      if (startPos == endPos) {
        startPos -= t.overlapDiff / 3;
        endPos   += t.overlapDiff / 3;
        overlaps += .5;
      };

      const g = t._c("g");
      g.setAttribute("class", "arc");
      const p = g.appendChild(t._c("path"));
      p.setAttribute('class', 'edge');
      
      // Attach the new arc before drawing, so computed values are available
      t._arcsElement.appendChild(g);

      // Create arc
      let middle = Math.abs(endPos - startPos) / 2;

      // TODO:
      //   take the number of tokens into account!
      let cHeight = t.arcDiff + (overlaps * t.overlapDiff) + (middle / 2);

      // Respect the maximum height
      cHeight = cHeight < t.maxArc ? cHeight : t.maxArc;

      var x = Math.min(startPos, endPos);

      //var controlY = (startY + endY - cHeight);
      let controlY = (endY - cHeight);
      
      const arcE = "M "+ startPos + "," + startY +
          " C " + startPos + "," + controlY +
          " " + endPos + "," + controlY +
          " " + endPos + "," + endY;

      p.setAttribute("d", arcE);

      if (arc.direction !== undefined) {
        p.setAttribute("marker-end", "url(#arr)");
        if (arc.direction === 'bi') {
          p.setAttribute("marker-start", "url(#arr)");
        };
      };

      if (arc.label === undefined)
        return g;
      
      /*
       * Calculate the top point of the arc for labeling using
       * de Casteljau's algorithm, see e.g.
       * http://blog.sklambert.com/finding-the-control-points-of-a-bezier-curve/
       * of course simplified to symmetric arcs ...
       */
      // Interpolate one side of the control polygon
      let middleY = (((startY + controlY) / 2) + controlY) / 2;

      // Create a boxed label
      const label = this._c("g");
      label.setAttribute("class", "label");
      t._labelsElement.appendChild(label);

      // Set arc reference
      label.arcRef = g;

      const that = t;
      label.addEventListener('mouseenter', function () {
        that.inFocus(this);
      });

      const labelE = label.appendChild(t._c("text"));
      labelE.setAttribute("x", x + middle);
      labelE.setAttribute("y", middleY + 3);
      labelE.setAttribute("text-anchor", "middle");
      const textNode = d.createTextNode(arc.label);
      labelE.appendChild(textNode);

      const labelBox   = labelE.getBBox();

      /*
      if (!labelBox)
        console.log("----");
      */
      
      const textWidth  = labelBox.width; // labelE.getComputedTextLength();
      const textHeight = labelBox.height; // labelE.getComputedTextLength();

      // Add box with padding to left and right
      const labelR = label.insertBefore(t._c("rect"), labelE);
      const boxWidth = textWidth + 2 * t.xPadding;
      labelR.setAttribute("x", x + middle - (boxWidth / 2));
      labelR.setAttribute("ry", 5);
      labelR.setAttribute("y", labelBox.y - t.yPadding);
      labelR.setAttribute("width", boxWidth);
      labelR.setAttribute("height", textHeight + 2 * t.yPadding);
    },


    /**
     * Get the svg element
     */
    element : function () {
      if (this._el !== undefined)
        return this._el;

      // Create svg
      const svg = this._c("svg");

      window.addEventListener("resize", function () {
        // TODO:
        //   Only if text-size changed!
        // TODO:
        //   This is currently untested
        this.show();
      }.bind(this));

      // Define marker arrows
      const defs = svg.appendChild(this._c("defs"));
      const marker = defs.appendChild(this._c("marker"));
      marker.setAttribute("refX", 9);
      marker.setAttribute("id", "arr");
      marker.setAttribute("orient", "auto-start-reverse");
      marker.setAttribute("markerUnits","userSpaceOnUse");

      const arrow = this._c("path");
      arrow.setAttribute("transform", "scale(0.8)");
      arrow.setAttribute("d", "M 0,-5 0,5 10,0 Z");
      marker.appendChild(arrow);

      this._el = svg;
      return this._el;
    },


    // Add a relation with a start, an end,
    // a direction value and an optional label text
    addRel : function (rel) {
      this._arcs.push(rel);
      return this;
    },


    // Add a token to the list (this will mostly be a word)
    addToken : function(token) {
      this._tokens.push(token);
      return this;
    },


    // Move label and arc in focus
    inFocus : function (element) {
      let cif;

      if (this._currentInFocus) {

        // Already in focus
        if (this._currentInFocus === element)
          return;

        cif = this._currentInFocus;
        cif.classList.remove('infocus');
        cif.arcRef.classList.remove('infocus');
      };

      cif = this._currentInFocus = element;
      this._labelsElement.appendChild(cif);
      this._arcsElement.appendChild(cif.arcRef);
      cif.classList.add('infocus');
      cif.arcRef.classList.add('infocus');
    },
    

    /*
     * All arcs need to be sorted before shown,
     * to avoid nesting.
     */
    _sortArcs : function () {

      // TODO:
      //   Keep in mind that the arcs may have long anchors!
      //   1. Iterate over all arcs
      //   2. Sort all multi
      let anchors = {};
      
      // 1. Sort by length
      // 2. Tag all spans with the number of overlaps before
      //    a) Iterate over all spans
      //    b) check the latest preceeding overlapping span (lpos)
      //       -> not found: tag with 0
      //       -> found: Add +1 to the level of the (lpos)
      //    c) If the new tag is smaller than the previous element,
      //       reorder

      // Normalize start and end
      const sortedArcs = this._arcs.map(function (v) {

        // Check for long anchors
        if (v.start instanceof Array) {

          if (v.start[0] == v.start[1]) {
            v.start = v.start[0];
          }

          else {
          
            const middle = Math.ceil(Math.abs(v.start[1] - v.start[0]) / 2) + v.start[0];

            // Calculate signature to avoid multiple anchors
            let anchorSig = "#" + v.start[0] + "_" + v.start[1];

            // Reverse signature
            if (v.start[0] > v.start[1]) {
              anchorSig = "#" + v.start[1] + "_" + v.start[0];
            };
            
            // Check if the anchor already exist
            let anchor = anchors[anchorSig];
            if (anchor === undefined) {
              anchor = {
                "first":   v.start[0],
                "last" :   v.start[1],
                "length" : v.start[1] - v.start[0]
              };
              anchors[anchorSig] = anchor;
            };

            v.startAnchor = anchor;

            // Add to anchors list
            v.start = middle;
          };
        };

        if (v.end instanceof Array) {

          if (v.end[0] == v.end[1]) {
            v.end = v.end[0];
          }

          else {

            const middle = Math.abs(v.end[0] - v.end[1]) + v.end[0];

            // Calculate signature to avoid multiple anchors
            let anchorSig = "#" + v.end[0] + "_" + v.end[1];

            // Reverse signature
            if (v.end[0] > v.end[1]) {
              anchorSig = "#" + v.end[1] + "_" + v.end[0];
            };

            // Check if the anchor already exist
            var anchor = anchors[anchorSig];
            if (anchor === undefined) {
              anchor = {
                "first":   v.end[0],
                "last" :   v.end[1],
                "length" : v.end[1] - v.end[0]
              };
              anchors[anchorSig] = anchor;
            };
            
            v.endAnchor = anchor;

            // Add to anchors list
            v.end = middle;
          };
        };

        v.first = v.start;
        v.last  = v.end;

        // calculate the arch length
        if (v.start < v.end) {
          v.length = v.end - v.start;
        }

        else {
          v.length = v.start - v.end;
        };

        return v;
      });

      // Sort based on length
      sortedArcs.sort(function (a, b) {
        if (a.length < b.length)
          return -1;
        else
          return 1;
      });

      // Add sorted arcs and anchors
      this._sortedArcs    = lengthSort(sortedArcs, false);
      this._sortedAnchors = lengthSort(Object.values(anchors), true);
    },


    /**
     * Center the viewport of the canvas
     * TODO:
     *   This is identical to treehierarchy
     */
    center : function () {
      if (this._el === undefined)
       return;

      const treeDiv = this._el.parentNode;

      const cWidth = parseFloat(window.getComputedStyle(this._el).width);
      const treeWidth = parseFloat(window.getComputedStyle(treeDiv).width);
      // Reposition:
      if (cWidth > treeWidth) {
       var scrollValue = (cWidth - treeWidth) / 2;
       treeDiv.scrollLeft = scrollValue;
      };
    },


    // Show the element
    show : function () {
      const t = this;
      const svg = this._el;
      const height = this.maxArc;

      // Delete old group
      if (svg.getElementsByTagName("g")[0] !== undefined) {
        svg.removeChild(
          svg.getElementsByTagName("g")[0]
        );
        t._tokenElements = [];
      };

      const g = svg.appendChild(t._c("g"));

      // Draw token list
      const text = g.appendChild(t._c("text"));
      text.setAttribute('class', 'leaf');
      text.setAttribute("text-anchor", "start");
      text.setAttribute("y", height);

      // Calculate the start position
      t._y = height - (t.anchorStart);

      // Introduce some prepending whitespace (yeah - I know ...)
      const ws = text.appendChild(t._c("tspan"));
      ws.appendChild(d.createTextNode('\u00A0'));
      ws.style.textAnchor = "start";
      
      t._tokens.forEach(function(node_i) {
        // Append svg
        // var x = text.appendChild(this._c("text"));
        const tspan = text.appendChild(this._c("tspan"));
        tspan.appendChild(d.createTextNode(node_i));
        tspan.setAttribute("text-anchor", "middle");
        
        this._tokenElements.push(tspan);

        // Add whitespace!
        tspan.setAttribute("dx", this.tokenSep);
      }, t);

      // Get some global position data that may change on resize
      t.offsetLeft = t._rect(g).left;

      // The group of arcs
      const arcs = g.appendChild(t._c("g"));
      t._arcsElement = arcs;
      arcs.classList.add("arcs");

      const labels = g.appendChild(t._c("g"));
      t._labelsElement = labels;
      labels.classList.add("labels");

      // Sort arcs if not sorted yet
      if (t._sortedArcs === undefined)
        t._sortArcs();

      // 1. Draw all anchors
      t._sortedAnchors.forEach(
        i => t._drawAnchor(i)
      );

      // 2. Draw all arcs
      t._sortedArcs.forEach(
        i => t._drawArc(i)
      );

      // Resize the svg with some reasonable margins
      svg.setAttribute("width", t._rect(text).width + 20);
      svg.setAttribute("height", height + 20);
      svg.setAttribute("class", "relTree");
    }
  };

  // Sort relations regarding their span
  function lengthSort (list, inclusive) {

    /*
     * The "inclusive" flag allows to
     * modify the behaviour for inclusivity check,
     * e.g. if identical start or endpoints mean overlap or not.
     */
    
    let stack = [];

    // Iterate over all definitions
    list.forEach(function(current) {

      // Check the stack order
      let overlaps = 0;
      let check;
      for (let j = (stack.length - 1); j >= 0; j--) {
        check = stack[j];

        // (a..(b..b)..a)
        if (current.first <= check.first && current.last >= check.last) {
          overlaps = check.overlaps + 1;
          break;
        }

        // (a..(b..a)..b)
        else if (current.first <= check.first && current.last >= check.first) {

          if (inclusive || (current.first != check.first && current.last != check.first)) {
            overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
          };
        }
        
        // (b..(a..b)..a)
        else if (current.first <= check.last && current.last >= check.last) {

          if (inclusive || (current.first != check.last && current.last != check.last)) {
            overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
          };
        };
      };

      // Set overlaps
      current.overlaps = overlaps;

      stack.push(current);

      // Although it is already sorted,
      // the new item has to be put at the correct place
      // TODO:
      //   Use something like splice() instead
      stack.sort(function (a,b) {
        b.overlaps - a.overlaps
      });
    });

    return stack;
  };
});
