// 1. Graph Drawing by Force-directed Placement
// 2. http://webatlas.fr/tempshare/ForceAtlas2_Paper.pdf
module.exports = function __echartsForceLayoutWorker(require) {
  'use strict';

  var vec2; // In web worker

  var inWorker = typeof window === 'undefined' && typeof require === 'undefined';

  if (inWorker) {
    vec2 = {
      create: function (x, y) {
        var out = new Float32Array(2);
        out[0] = x || 0;
        out[1] = y || 0;
        return out;
      },
      dist: function (a, b) {
        var x = b[0] - a[0];
        var y = b[1] - a[1];
        return Math.sqrt(x * x + y * y);
      },
      len: function (a) {
        var x = a[0];
        var y = a[1];
        return Math.sqrt(x * x + y * y);
      },
      scaleAndAdd: function (out, a, b, scale) {
        out[0] = a[0] + b[0] * scale;
        out[1] = a[1] + b[1] * scale;
        return out;
      },
      scale: function (out, a, b) {
        out[0] = a[0] * b;
        out[1] = a[1] * b;
        return out;
      },
      add: function (out, a, b) {
        out[0] = a[0] + b[0];
        out[1] = a[1] + b[1];
        return out;
      },
      sub: function (out, a, b) {
        out[0] = a[0] - b[0];
        out[1] = a[1] - b[1];
        return out;
      },
      dot: function (v1, v2) {
        return v1[0] * v2[0] + v1[1] * v2[1];
      },
      normalize: function (out, a) {
        var x = a[0];
        var y = a[1];
        var len = x * x + y * y;

        if (len > 0) {
          //TODO: evaluate use of glm_invsqrt here?
          len = 1 / Math.sqrt(len);
          out[0] = a[0] * len;
          out[1] = a[1] * len;
        }

        return out;
      },
      negate: function (out, a) {
        out[0] = -a[0];
        out[1] = -a[1];
        return out;
      },
      copy: function (out, a) {
        out[0] = a[0];
        out[1] = a[1];
        return out;
      },
      set: function (out, x, y) {
        out[0] = x;
        out[1] = y;
        return out;
      }
    };
  } else {
    vec2 = require('zrender/tool/vector');
  }

  var ArrayCtor = typeof Float32Array == 'undefined' ? Array : Float32Array;
  /****************************
   * Class: Region
   ***************************/

  function Region() {
    this.subRegions = [];
    this.nSubRegions = 0;
    this.node = null;
    this.mass = 0;
    this.centerOfMass = null;
    this.bbox = new ArrayCtor(4);
    this.size = 0;
  } // Reset before update


  Region.prototype.beforeUpdate = function () {
    for (var i = 0; i < this.nSubRegions; i++) {
      this.subRegions[i].beforeUpdate();
    }

    this.mass = 0;

    if (this.centerOfMass) {
      this.centerOfMass[0] = 0;
      this.centerOfMass[1] = 0;
    }

    this.nSubRegions = 0;
    this.node = null;
  }; // Clear after update


  Region.prototype.afterUpdate = function () {
    this.subRegions.length = this.nSubRegions;

    for (var i = 0; i < this.nSubRegions; i++) {
      this.subRegions[i].afterUpdate();
    }
  };

  Region.prototype.addNode = function (node) {
    if (this.nSubRegions === 0) {
      if (this.node == null) {
        this.node = node;
        return;
      } else {
        this._addNodeToSubRegion(this.node);

        this.node = null;
      }
    }

    this._addNodeToSubRegion(node);

    this._updateCenterOfMass(node);
  };

  Region.prototype.findSubRegion = function (x, y) {
    for (var i = 0; i < this.nSubRegions; i++) {
      var region = this.subRegions[i];

      if (region.contain(x, y)) {
        return region;
      }
    }
  };

  Region.prototype.contain = function (x, y) {
    return this.bbox[0] <= x && this.bbox[2] >= x && this.bbox[1] <= y && this.bbox[3] >= y;
  };

  Region.prototype.setBBox = function (minX, minY, maxX, maxY) {
    // Min
    this.bbox[0] = minX;
    this.bbox[1] = minY; // Max

    this.bbox[2] = maxX;
    this.bbox[3] = maxY;
    this.size = (maxX - minX + maxY - minY) / 2;
  };

  Region.prototype._newSubRegion = function () {
    var subRegion = this.subRegions[this.nSubRegions];

    if (!subRegion) {
      subRegion = new Region();
      this.subRegions[this.nSubRegions] = subRegion;
    }

    this.nSubRegions++;
    return subRegion;
  };

  Region.prototype._addNodeToSubRegion = function (node) {
    var subRegion = this.findSubRegion(node.position[0], node.position[1]);
    var bbox = this.bbox;

    if (!subRegion) {
      var cx = (bbox[0] + bbox[2]) / 2;
      var cy = (bbox[1] + bbox[3]) / 2;
      var w = (bbox[2] - bbox[0]) / 2;
      var h = (bbox[3] - bbox[1]) / 2;
      var xi = node.position[0] >= cx ? 1 : 0;
      var yi = node.position[1] >= cy ? 1 : 0;

      var subRegion = this._newSubRegion(); // Min


      subRegion.setBBox( // Min
      xi * w + bbox[0], yi * h + bbox[1], // Max
      (xi + 1) * w + bbox[0], (yi + 1) * h + bbox[1]);
    }

    subRegion.addNode(node);
  };

  Region.prototype._updateCenterOfMass = function (node) {
    // Incrementally update
    if (this.centerOfMass == null) {
      this.centerOfMass = vec2.create();
    }

    var x = this.centerOfMass[0] * this.mass;
    var y = this.centerOfMass[1] * this.mass;
    x += node.position[0] * node.mass;
    y += node.position[1] * node.mass;
    this.mass += node.mass;
    this.centerOfMass[0] = x / this.mass;
    this.centerOfMass[1] = y / this.mass;
  };
  /****************************
   * Class: Graph Node
   ***************************/


  function GraphNode() {
    this.position = vec2.create();
    this.force = vec2.create();
    this.forcePrev = vec2.create();
    this.speed = vec2.create();
    this.speedPrev = vec2.create(); // If repulsionByDegree is true
    //  mass = inDegree + outDegree + 1
    // Else
    //  mass is manually set

    this.mass = 1;
    this.inDegree = 0;
    this.outDegree = 0;
  }
  /****************************
   * Class: Graph Edge
   ***************************/


  function GraphEdge(node1, node2) {
    this.node1 = node1;
    this.node2 = node2;
    this.weight = 1;
  }
  /****************************
   * Class: ForceLayout
   ***************************/


  function ForceLayout() {
    this.barnesHutOptimize = false;
    this.barnesHutTheta = 1.5;
    this.repulsionByDegree = false;
    this.preventNodeOverlap = false;
    this.preventNodeEdgeOverlap = false;
    this.strongGravity = true;
    this.gravity = 1.0;
    this.scaling = 1.0;
    this.edgeWeightInfluence = 1.0;
    this.center = [0, 0];
    this.width = 500;
    this.height = 500;
    this.maxSpeedIncrease = 1;
    this.nodes = [];
    this.edges = [];
    this.bbox = new ArrayCtor(4);
    this._rootRegion = new Region();
    this._rootRegion.centerOfMass = vec2.create();
    this._massArr = null;
    this._k = 0;
  }

  ForceLayout.prototype.nodeToNodeRepulsionFactor = function (mass, d, k) {
    return k * k * mass / d;
  };

  ForceLayout.prototype.edgeToNodeRepulsionFactor = function (mass, d, k) {
    return k * mass / d;
  };

  ForceLayout.prototype.attractionFactor = function (w, d, k) {
    return w * d / k;
  };

  ForceLayout.prototype.initNodes = function (positionArr, massArr, sizeArr) {
    this.temperature = 1.0;
    var nNodes = positionArr.length / 2;
    this.nodes.length = 0;
    var haveSize = typeof sizeArr !== 'undefined';

    for (var i = 0; i < nNodes; i++) {
      var node = new GraphNode();
      node.position[0] = positionArr[i * 2];
      node.position[1] = positionArr[i * 2 + 1];
      node.mass = massArr[i];

      if (haveSize) {
        node.size = sizeArr[i];
      }

      this.nodes.push(node);
    }

    this._massArr = massArr;

    if (haveSize) {
      this._sizeArr = sizeArr;
    }
  };

  ForceLayout.prototype.initEdges = function (edgeArr, edgeWeightArr) {
    var nEdges = edgeArr.length / 2;
    this.edges.length = 0;
    var edgeHaveWeight = typeof edgeWeightArr !== 'undefined';

    for (var i = 0; i < nEdges; i++) {
      var sIdx = edgeArr[i * 2];
      var tIdx = edgeArr[i * 2 + 1];
      var sNode = this.nodes[sIdx];
      var tNode = this.nodes[tIdx];

      if (!sNode || !tNode) {
        continue;
      }

      sNode.outDegree++;
      tNode.inDegree++;
      var edge = new GraphEdge(sNode, tNode);

      if (edgeHaveWeight) {
        edge.weight = edgeWeightArr[i];
      }

      this.edges.push(edge);
    }
  };

  ForceLayout.prototype.update = function () {
    var nNodes = this.nodes.length;
    this.updateBBox();
    this._k = 0.4 * this.scaling * Math.sqrt(this.width * this.height / nNodes);

    if (this.barnesHutOptimize) {
      this._rootRegion.setBBox(this.bbox[0], this.bbox[1], this.bbox[2], this.bbox[3]);

      this._rootRegion.beforeUpdate();

      for (var i = 0; i < nNodes; i++) {
        this._rootRegion.addNode(this.nodes[i]);
      }

      this._rootRegion.afterUpdate();
    } else {
      // Update center of mass of whole graph
      var mass = 0;
      var centerOfMass = this._rootRegion.centerOfMass;
      vec2.set(centerOfMass, 0, 0);

      for (var i = 0; i < nNodes; i++) {
        var node = this.nodes[i];
        mass += node.mass;
        vec2.scaleAndAdd(centerOfMass, centerOfMass, node.position, node.mass);
      }

      if (mass > 0) {
        vec2.scale(centerOfMass, centerOfMass, 1 / mass);
      }
    }

    this.updateForce();
    this.updatePosition();
  };

  ForceLayout.prototype.updateForce = function () {
    var nNodes = this.nodes.length; // Reset forces

    for (var i = 0; i < nNodes; i++) {
      var node = this.nodes[i];
      vec2.copy(node.forcePrev, node.force);
      vec2.copy(node.speedPrev, node.speed);
      vec2.set(node.force, 0, 0);
    }

    this.updateNodeNodeForce();

    if (this.gravity > 0) {
      this.updateGravityForce();
    }

    this.updateEdgeForce();

    if (this.preventNodeEdgeOverlap) {
      this.updateNodeEdgeForce();
    }
  };

  ForceLayout.prototype.updatePosition = function () {
    var nNodes = this.nodes.length; // Apply forces
    // var speed = vec2.create();

    var v = vec2.create();

    for (var i = 0; i < nNodes; i++) {
      var node = this.nodes[i];
      var speed = node.speed; // var swing = vec2.dist(node.force, node.forcePrev);
      // // var swing = 30;
      // vec2.scale(node.force, node.force, 1 / (1 + Math.sqrt(swing)));

      vec2.scale(node.force, node.force, 1 / 30); // contraint force

      var df = vec2.len(node.force) + 0.1;
      var scale = Math.min(df, 500.0) / df;
      vec2.scale(node.force, node.force, scale);
      vec2.add(speed, speed, node.force);
      vec2.scale(speed, speed, this.temperature); // Prevent swinging
      // Limited the increase of speed up to 100% each step
      // TODO adjust by nodes number
      // TODO First iterate speed control

      vec2.sub(v, speed, node.speedPrev);
      var swing = vec2.len(v);

      if (swing > 0) {
        vec2.scale(v, v, 1 / swing);
        var base = vec2.len(node.speedPrev);

        if (base > 0) {
          swing = Math.min(swing / base, this.maxSpeedIncrease) * base;
          vec2.scaleAndAdd(speed, node.speedPrev, v, swing);
        }
      } // constraint speed


      var ds = vec2.len(speed);
      var scale = Math.min(ds, 100.0) / (ds + 0.1);
      vec2.scale(speed, speed, scale);
      vec2.add(node.position, node.position, speed);
    }
  };

  ForceLayout.prototype.updateNodeNodeForce = function () {
    var nNodes = this.nodes.length; // Compute forces
    // Repulsion

    for (var i = 0; i < nNodes; i++) {
      var na = this.nodes[i];

      if (this.barnesHutOptimize) {
        this.applyRegionToNodeRepulsion(this._rootRegion, na);
      } else {
        for (var j = i + 1; j < nNodes; j++) {
          var nb = this.nodes[j];
          this.applyNodeToNodeRepulsion(na, nb, false);
        }
      }
    }
  };

  ForceLayout.prototype.updateGravityForce = function () {
    for (var i = 0; i < this.nodes.length; i++) {
      this.applyNodeGravity(this.nodes[i]);
    }
  };

  ForceLayout.prototype.updateEdgeForce = function () {
    // Attraction
    for (var i = 0; i < this.edges.length; i++) {
      this.applyEdgeAttraction(this.edges[i]);
    }
  };

  ForceLayout.prototype.updateNodeEdgeForce = function () {
    for (var i = 0; i < this.nodes.length; i++) {
      for (var j = 0; j < this.edges.length; j++) {
        this.applyEdgeToNodeRepulsion(this.edges[j], this.nodes[i]);
      }
    }
  };

  ForceLayout.prototype.applyRegionToNodeRepulsion = function () {
    var v = vec2.create();
    return function applyRegionToNodeRepulsion(region, node) {
      if (region.node) {
        // Region is a leaf 
        this.applyNodeToNodeRepulsion(region.node, node, true);
      } else {
        // Static region and node
        if (region.mass === 0 && node.mass === 0) {
          return;
        }

        vec2.sub(v, node.position, region.centerOfMass);
        var d2 = v[0] * v[0] + v[1] * v[1];

        if (d2 > this.barnesHutTheta * region.size * region.size) {
          var factor = this._k * this._k * (node.mass + region.mass) / (d2 + 1);
          vec2.scaleAndAdd(node.force, node.force, v, factor * 2);
        } else {
          for (var i = 0; i < region.nSubRegions; i++) {
            this.applyRegionToNodeRepulsion(region.subRegions[i], node);
          }
        }
      }
    };
  }();

  ForceLayout.prototype.applyNodeToNodeRepulsion = function () {
    var v = vec2.create();
    return function applyNodeToNodeRepulsion(na, nb, oneWay) {
      if (na === nb) {
        return;
      } // Two static node


      if (na.mass === 0 && nb.mass === 0) {
        return;
      }

      vec2.sub(v, na.position, nb.position);
      var d2 = v[0] * v[0] + v[1] * v[1]; // PENDING

      if (d2 === 0) {
        return;
      }

      var factor;
      var mass = na.mass + nb.mass;
      var d = Math.sqrt(d2); // Normalize v

      vec2.scale(v, v, 1 / d);

      if (this.preventNodeOverlap) {
        d = d - na.size - nb.size;

        if (d > 0) {
          factor = this.nodeToNodeRepulsionFactor(mass, d, this._k);
        } else if (d <= 0) {
          // A stronger repulsion if overlap
          factor = this._k * this._k * 10 * mass;
        }
      } else {
        factor = this.nodeToNodeRepulsionFactor(mass, d, this._k);
      }

      if (!oneWay) {
        vec2.scaleAndAdd(na.force, na.force, v, factor * 2);
      }

      vec2.scaleAndAdd(nb.force, nb.force, v, -factor * 2);
    };
  }();

  ForceLayout.prototype.applyEdgeAttraction = function () {
    var v = vec2.create();
    return function applyEdgeAttraction(edge) {
      var na = edge.node1;
      var nb = edge.node2;
      vec2.sub(v, na.position, nb.position);
      var d = vec2.len(v);
      var w;

      if (this.edgeWeightInfluence === 0) {
        w = 1;
      } else if (this.edgeWeightInfluence == 1) {
        w = edge.weight;
      } else {
        w = Math.pow(edge.weight, this.edgeWeightInfluence);
      }

      var factor;

      if (this.preventOverlap) {
        d = d - na.size - nb.size;

        if (d <= 0) {
          // No attraction
          return;
        }
      }

      var factor = this.attractionFactor(w, d, this._k);
      vec2.scaleAndAdd(na.force, na.force, v, -factor);
      vec2.scaleAndAdd(nb.force, nb.force, v, factor);
    };
  }();

  ForceLayout.prototype.applyNodeGravity = function () {
    var v = vec2.create();
    return function (node) {
      // PENDING Move to centerOfMass or [0, 0] ?
      // vec2.sub(v, this._rootRegion.centerOfMass, node.position);
      // vec2.negate(v, node.position);
      vec2.sub(v, this.center, node.position);

      if (this.width > this.height) {
        // Stronger gravity on y axis
        v[1] *= this.width / this.height;
      } else {
        // Stronger gravity on x axis
        v[0] *= this.height / this.width;
      }

      var d = vec2.len(v) / 100;

      if (this.strongGravity) {
        vec2.scaleAndAdd(node.force, node.force, v, d * this.gravity * node.mass);
      } else {
        vec2.scaleAndAdd(node.force, node.force, v, this.gravity * node.mass / (d + 1));
      }
    };
  }();

  ForceLayout.prototype.applyEdgeToNodeRepulsion = function () {
    var v12 = vec2.create();
    var v13 = vec2.create();
    var p = vec2.create();
    return function (e, n3) {
      var n1 = e.node1;
      var n2 = e.node2;

      if (n1 === n3 || n2 === n3) {
        return;
      }

      vec2.sub(v12, n2.position, n1.position);
      vec2.sub(v13, n3.position, n1.position);
      var len12 = vec2.len(v12);
      vec2.scale(v12, v12, 1 / len12);
      var len = vec2.dot(v12, v13); // n3 can't project on line n1-n2

      if (len < 0 || len > len12) {
        return;
      } // Project point


      vec2.scaleAndAdd(p, n1.position, v12, len); // n3 distance to line n1-n2

      var dist = vec2.dist(p, n3.position) - n3.size;
      var factor = this.edgeToNodeRepulsionFactor(n3.mass, Math.max(dist, 0.1), 100); // Use v12 as normal vector

      vec2.sub(v12, n3.position, p);
      vec2.normalize(v12, v12);
      vec2.scaleAndAdd(n3.force, n3.force, v12, factor); // PENDING

      vec2.scaleAndAdd(n1.force, n1.force, v12, -factor);
      vec2.scaleAndAdd(n2.force, n2.force, v12, -factor);
    };
  }();

  ForceLayout.prototype.updateBBox = function () {
    var minX = Infinity;
    var minY = Infinity;
    var maxX = -Infinity;
    var maxY = -Infinity;

    for (var i = 0; i < this.nodes.length; i++) {
      var pos = this.nodes[i].position;
      minX = Math.min(minX, pos[0]);
      minY = Math.min(minY, pos[1]);
      maxX = Math.max(maxX, pos[0]);
      maxY = Math.max(maxY, pos[1]);
    }

    this.bbox[0] = minX;
    this.bbox[1] = minY;
    this.bbox[2] = maxX;
    this.bbox[3] = maxY;
  };

  ForceLayout.getWorkerCode = function () {
    var str = __echartsForceLayoutWorker.toString();

    return str.slice(str.indexOf('{') + 1, str.lastIndexOf('return'));
  };
  /****************************
   * Main process
   ***************************/

  /* jshint ignore:start */


  if (inWorker) {
    var forceLayout = null;

    self.onmessage = function (e) {
      // Position read back
      if (e.data instanceof ArrayBuffer) {
        if (!forceLayout) return;
        var positionArr = new Float32Array(e.data);
        var nNodes = positionArr.length / 2;

        for (var i = 0; i < nNodes; i++) {
          var node = forceLayout.nodes[i];
          node.position[0] = positionArr[i * 2];
          node.position[1] = positionArr[i * 2 + 1];
        }

        return;
      }

      switch (e.data.cmd) {
        case 'init':
          if (!forceLayout) {
            forceLayout = new ForceLayout();
          }

          forceLayout.initNodes(e.data.nodesPosition, e.data.nodesMass, e.data.nodesSize);
          forceLayout.initEdges(e.data.edges, e.data.edgesWeight);
          break;

        case 'updateConfig':
          if (forceLayout) {
            for (var name in e.data.config) {
              forceLayout[name] = e.data.config[name];
            }
          }

          break;

        case 'update':
          var steps = e.data.steps;

          if (forceLayout) {
            var nNodes = forceLayout.nodes.length;
            var positionArr = new Float32Array(nNodes * 2);
            forceLayout.temperature = e.data.temperature;

            for (var i = 0; i < steps; i++) {
              forceLayout.update();
              forceLayout.temperature *= e.data.coolDown;
            } // Callback


            for (var i = 0; i < nNodes; i++) {
              var node = forceLayout.nodes[i];
              positionArr[i * 2] = node.position[0];
              positionArr[i * 2 + 1] = node.position[1];
            }

            self.postMessage(positionArr.buffer, [positionArr.buffer]);
          } else {
            // Not initialzied yet
            var emptyArr = new Float32Array(); // Post transfer object

            self.postMessage(emptyArr.buffer, [emptyArr.buffer]);
          }

          break;
      }
    };
  }
  /* jshint ignore:end */


  return ForceLayout;
}(require);