

// config
var VECTOR_N = 2;
var CONNECT_MAX = 3;
var ACCEPT_MAX = 4;
var NODE_LIST_MAX = 50;
var NODE_K = 10;
var PENALTY_RATE = 15.0;
var DIRECTION_PENALTY = 0.6;
var INIT_NODE_NUM = 2;
var REMOVE_NODE_DISTANCE = 100.0;
var NEXT_CONNECT_LIMIT = 0;

if (!NET) {
    NET = new VNetwork();
};

// commands
var NODE_LIST_COMMAND_MAX = 20;
NodeInfo = function(address, fv)
{
    this.address = address;
    this.fv = fv;
};

NodeCommand = function (command, argv)
{
    this.command = command;
    this.argv = argv;
};
NodeCommand.Command = {
  NEGOTIATION: 0,
  NODE_LIST: 1,
  SEARCH_REQ: 2,
  SEARCH_RES: 2
};

NegotiationCommand = function(address, fv)
{
    this.address = address;
    this.fv = fv;
};

NodeListCommand = function(node_list)
{
    this.node_list = node_list;
};

function Node()
{
    this.id = NET.newId();
    this.fv = new Vector(VECTOR_N, [Math.random(), Math.random()]);
    this.pos = {x: Math.random(), y: Math.random() };
    this.connect_node = new Object();
    this.accept_node = new Object();
    this.node_list = new Array();
    this.clock = 0;
    this.is_dead = false;
    
    // server port
    this.server = new VSocket(this.id);
    this.server.listen(randInt(1024, 0xffff));
};

Node.prototype = {
  // Visualization
  speed: function(distance)
  {
      if (distance < 0.05) {
	  return -0.01;
      } else {
	  return 0.1 * distance * distance;
      };
  },
  
  moveNode: function(ctx)
  {
      var i, n;
      var avg = { x: 0.01 * (this.fv.vec[0] - this.pos.x), y: 0.01 * (this.fv.vec[1] - this.pos.y) };
      var id;
      n = 0;
      for (id in this.connect_node) {
	  var node = GNODE.get(this.connect_node[id].address.id);
	  if (!node) {
	      continue;
	  }
	  avg.x += sign(node.pos.x - this.pos.x) * this.speed(Math.abs(node.pos.x - this.pos.x));
	  avg.y += sign(node.pos.y - this.pos.y) * this.speed(Math.abs(node.pos.y - this.pos.y));;
      }
      for (id in this.accept_node) {
	  var node = GNODE.get(this.accept_node[id].address.id);
	  if (!node) {
	      continue;
	  }
	  avg.x += 0.1 * sign(node.pos.x - this.pos.x) * this.speed(Math.abs(node.pos.x - this.pos.x));
	  avg.y += 0.1 * sign(node.pos.y - this.pos.y) * this.speed(Math.abs(node.pos.y - this.pos.y));;
      }
      this.pos.x += avg.x;
      this.pos.y += avg.y;
      
      if (this.pos.x < 0.0) {
	  this.pos.x = 0.01;
      }
      if (this.pos.y < 0.0) {
	  this.pos.y = 0.01;
      }
      if (this.pos.x > 1.0) {
	  this.pos.x = 1.0 - 0.01;
      }
      if (this.pos.y > 1.0) {
	  this.pos.y = 1.0 - 0.01;
      }
  },
  
  drawLink: function(ctx)
  {
      var id;
      var this_x = Math.floor(this.pos.x * ctx.canvas.width);
      var this_y = Math.floor(this.pos.y * ctx.canvas.width);
      
      ctx.strokeStyle = rgb(100, 100, 255);
      ctx.beginPath();
      for (id in this.connect_node) {
	  var node = GNODE.get(this.connect_node[id].address.id);
	  if (!node) {
	      continue;
	  }
	  var node_x = Math.floor(node.pos.x * ctx.canvas.width);
	  var node_y = Math.floor(node.pos.y * ctx.canvas.width);
	  
	  ctx.moveTo(this_x, this_y);
	  ctx.lineTo(node_x, node_y);
      }
      ctx.stroke();
      
      ctx.strokeStyle = rgb(100, 255, 255);
      ctx.beginPath();
      for (id in this.accept_node) {
	  var node = GNODE.get(this.accept_node[id].address.id);
	  if (!node) {
	      continue;
	  }
	  var node_x = Math.floor(node.pos.x * ctx.canvas.width);
	  var node_y = Math.floor(node.pos.y * ctx.canvas.width);
	  
	  ctx.moveTo(this_x, this_y);
	  ctx.lineTo(node_x, node_y);
      }
      ctx.stroke();
  },
  
  drawNode: function(ctx)
  {
      var x = this.pos.x;
      ctx.fillStyle = rgb(this.fv.vec[0] * 255, this.fv.vec[1] * 255, 0x33);
      ctx.beginPath();
      ctx.rect(max(Math.floor(this.pos.x * ctx.canvas.width) - 3, 0), 
	       max(Math.floor(this.pos.y * ctx.canvas.width) - 3, 0),
	       6, 6);
      ctx.fill();
  },
  
  // Network
  step: function()
  {
      ++this.clock; // overflow?
      this.receiveCommand();
      this.acceptClient();
      this.checkTimeLimit();
      this.linkNN();
  },
  
  addInitialNode: function(init_node)
  {
      this.addNodeList(init_node);
  },
  
  die: function()
  {
      var id;
      var client;
      
      // close
      
      while (client = this.server.accept()) {
	  client.close();
      }
      this.server.close();
      
      for (id in this.connect_node) {
	  this.connect_node[id].socket.close();
      }
      for (id in this.accpet_node) {
	  this.accept_node[id].socket.close();
      }
      
      // free
      delete this.fv;
      delete this.pos;
      delete this.connect_node;
      delete this.accept_node;
      delete this.node_list;
      
      // dead
      this.is_dead = true;
  },
  
  hasNode: function(node_info)
  {
      for (var i = 0; i < this.node_list.length; ++i) {
	  if (this.node_list[i].address.id == node_info.address.id) {
	      return true;
	  }
      }
      return false;
  },
  
  addNodeList: function(node_list)
  {
      var i;
      var my_norm = this.fv.norm();
      for (i = 0; i < node_list.length; ++i) {
	  var node_info = node_list[i];
	  if (node_info.address.id != this.id
	      && !this.hasNode(node_info)) 
	  {
	      var node_norm = node_info.fv.norm();
	      this.node_list.push({
		address: node_info.address,
		fv: node_info.fv,
		distance: this.fv.distance(node_info.fv) + (my_norm < node_norm ? 0.0:DIRECTION_PENALTY)
	      });
	  }
      }
      this.sortNodeList();
      if (this.node_list.length > NODE_LIST_MAX) {
	  this.node_list.length = NODE_LIST_MAX;
      }
  },
  
  updateNodeInfo: function(node_info)
  {
      var my_norm = this.fv.norm();
      
      if (this.connect_node[node_info.address.id]) {
	  var node = this.connect_node[node_info.address.id];
	  var node_norm = node_info.fv.norm();
	  
	  node.fv = node_info.fv;
	  node.distance = this.fv.distance(node_info.fv) + (my_norm < node_norm ? 0.0 : DIRECTION_PENALTY);
      }
      if (this.accept_node[node_info.address.id]) {
	  var node = this.accept_node[node_info.address.id];
	  var node_norm = node_info.fv.norm();
	  
	  node.fv = node_info.fv;
	  node.distance = this.fv.distance(node_info.fv) + (my_norm < node_norm ? DIRECTION_PENALTY : 0.0);
      }
      for (var i = 0; i < this.node_list.length; ++i) {
	  var node = this.node_list[i];
	  if (node.address.id == node_info.address.id) {
	      var node_norm = node_info.fv.norm();
	      node.fv = node_info.fv;
	      node.distance = this.fv.distance(node_info.fv) + (my_norm < node_norm ? 0.0 : DIRECTION_PENALTY);
	  }
      }
  },
  
  processCommand: function(packet)
  {
      switch (packet.command) {
      case NodeCommand.Command.NEGOTIATION:
	  this.updateNodeInfo(packet.argv);
	  break;
      case NodeCommand.Command.NODE_LIST:
	  this.addNodeList(packet.argv.node_list);
	  break
      default:
	  break;
      }
  },
  
  receiveCommand: function()
  {
      var id;
      var limit = 0;
      var disconnect_nodes = new Array();
      for (id in this.connect_node) {
	  var node = this.connect_node[id];
	  if (node.socket.poll()) {
	      var packet = node.socket.receive();
	      if (packet) {
		  this.processCommand(packet);
	      } else {
		  // disconnect
		  disconnect_nodes.push({'id': id, 'node': node});
		  break;
	      }
	  }
      }
      for (i = 0; i < disconnect_nodes.length; ++i) {
	  var id = disconnect_nodes[i].id;
	  var node = disconnect_nodes[i].node;
	  node.socket.close();
	  delete this.connect_node[id];
      }
      
      disconnect_nodes.length = 0;
      for (id in this.accept_node) {
	  var node = this.accept_node[id];
	  if (node.socket.poll()) {
	      var packet = node.socket.receive();
	      if (packet) {
		  this.processCommand(packet);
	      } else {
		  // disconnect
		  disconnect_nodes.push({'id': id, 'node':node});
		  break;
	      }
	  }
      }
      for (i = 0; i < disconnect_nodes.length; ++i) {
	  var id = disconnect_nodes[i].id;
	  var node = disconnect_nodes[i].node;
	  node.socket.close();
	  delete this.accept_node[id];
      }
  },
  
  acceptClient: function()
  {
      var client = null;
      var n = hashCount(this.accept_node);
      var my_norm = this.fv.norm();
      
      while (client = this.server.accept()) {
	  var close_flag = false;
	  var client_fv = null;
	  var packet = client.receive();
	  
	  if (!packet
	      || packet.command != NodeCommand.Command.NEGOTIATION)
	  {
	      continue;
	  }
	  var negotiation = packet.argv;
	  if (!this.accept_node[negotiation.address.id]
	      && !this.connect_node[negotiation.address.id]
	      && n < ACCEPT_MAX)
	  {
	      // accept
	      var node_norm = negotiation.fv.norm();
	      var fv_distance = this.fv.distance(negotiation.fv);
	      this.accept_node[negotiation.address.id] = {
		socket: client,
		connect_time: this.clock,
		address: negotiation.address,
		fv: negotiation.fv,
		distance: fv_distance + (my_norm < node_norm ? DIRECTION_PENALTY : 0.0)
	      };
	      this.addNodeList([{
		address: negotiation.address,
		fv: negotiation.fv,
		distance: fv_distance + (my_norm < node_norm ? 0.0 : DIRECTION_PENALTY)
	      }]);
      	      ++n;
	  } else {
	      // deny
	      close_flag = true;
	  }
	  
	  // negotiation
	  var my_negotiation = new NegotiationCommand(this.server.bind_address, this.fv);
	  client.send(new NodeCommand(NodeCommand.Command.NEGOTIATION, my_negotiation));
	  
	  // recommend
	  var recommend_node_list = this.getRecommendNodeList(negotiation.fv);
	  var node_list = new NodeListCommand(recommend_node_list);
	  client.send(new NodeCommand(NodeCommand.Command.NODE_LIST, node_list));
	  
	  if (close_flag) {
	      client.close();
	  }
      }
  },
  
  compareNodeInfo: function(a, b)
  {
      return a.distance - b.distance;
  },
  
  sortNodeList: function()
  {
      this.node_list = this.node_list.sort(this.compareNodeInfo);
  },
  
  getRecommendNodeList: function(fv)
  {
      var fv_norm = fv.norm();
      var comp = (function(fv,fv_norm){
	  return function (a, b) { 
	      var a_norm = a.fv.norm();
	      var b_norm = b.fv.norm();
	      var a_dist = fv.distance(a.fv);
	      var b_dist = fv.distance(b.fv);
	      a_dist += (fv_norm < a_norm ? 0.0:DIRECTION_PENALTY);
	      b_dist += (fv_norm < b_norm ? 0.0:DIRECTION_PENALTY);
	      
	      return a_dist - b_dist;
	  }}
      )(fv, fv_norm);
      var recm = this.node_list.sort(comp);
      
      if (recm.length > NODE_LIST_COMMAND_MAX) {
	  recm.length = NODE_LIST_COMMAND_MAX;
      }
      
      return recm;
  },
  
  checkTimeLimit: function()
  {
      var id;
      
      //var count = this.predictNetworkNodes();
      //var axis_per_node = 1.0 / Math.pow(count, 1.0 / this.fv.n);
      //var k_nn_dist_connect = Math.pow(ACCEPT_MAX, 1.0 / this.fv.n) * axis_per_node;
      //var k_nn_dist_accpept = Math.pow(ACCEPT_MAX, 1.0 / this.fv.n) * axis_per_node;
      
      for (id in this.connect_node) {
	  // ((d-kd)^2)^1/2
	  var node = this.connect_node[id];
	  var penalty = 0.5 + (2.0 - node.distance);//k_nn_dist_connect / node.distance;
	  if ((this.clock - node.connect_time) > penalty * PENALTY_RATE) {
	      node.socket.close();
       	      delete this.connect_node[id];
	  }
      }
      for (id in this.accept_node) {
	  // ((d-kd)^2)^1/2
	  var node = this.accept_node[id];
	  var penalty = 0.5 + (2.0 - node.distance);//k_nn_dist_accept / node.distance;
	  if ((this.clock - node.connect_time) > penalty * PENALTY_RATE) {
	      node.socket.close();
       	      delete this.accept_node[id];
	  }
      }
  },
  
  predictNetworkNodes: function()
  {
      var r;
      var n = this.node_list.length > NODE_K ? NODE_K : this.node_list.length;
      if (n == 0) {
	  return 1;
      }
      r_node = this.node_list[n - 1];
      return 0.5 * n / Math.pow(this.fv.distance(r_node.fv), r_node.fv.n);
  },
  
  linkNN: function()
  {
      var n = CONNECT_MAX - hashCount(this.connect_node);
      var remove_nodes = new Array();
      var connect_error = false;
      for (i = 0; i < n  && i < this.node_list.length; ++i) {
	  var node = this.node_list[i];
	  if (this.connect_node[node.address.id]
	      || this.accept_node[node.address.id])
	  {
	      ++n;
	      continue;
	  }
	  
	  node.last_connect = this.clock;
	  var server = new VSocket(this.id);
	  if (server.connect(node.address)) {
	      this.connect_node[node.address.id] = {
		  socket: server,
		  address: node.address,
		  fv: node.fv,
		  connect_time: this.clock,
		  distance: node.distance
	      };
	      // negotiation
	      var negotiation = new NegotiationCommand(this.server.bind_address, this.fv);
	      server.send(new NodeCommand(NodeCommand.Command.NEGOTIATION, negotiation));
	      
	      // recommend
	      /*
	      var recommend_node_list = this.getRecommendNodeList(node.fv);
	      var node_list = new NodeListCommand(recommend_node_list);
	      server.send(new NodeCommand(NodeCommand.Command.NODE_LIST, node_list));
	      */
	      //++n;
	  } else {
	      node.distance = REMOVE_NODE_DISTANCE;
	      connect_error = true;
	  }
      }
      if (connect_error) {
	  this.sortNodeList();
      }
  }
};


GlobalNode = function() {
    this.table = new Object();
    this.list = new Array();
};

GlobalNode.prototype = {
    getRandomNode: function()
    {
	return this.list[Math.floor(Math.random() * (this.list.length-1))];
    },
    get: function(id)
    {
	return this.table[id];
    },
    add: function()
    {
	var new_node = new Node();
	var init_nodes = new Array();
	for (var i = 0; i < INIT_NODE_NUM; ++i) {
	    var node = this.getRandomNode();
	    if (node) {
		init_nodes.push({
		  address: node.server.bind_address,
		  fv: node.fv,
		  distance: 0
		});
	    }
	}
	new_node.addInitialNode(init_nodes);
	this.list.push(new_node);
	this.table[new_node.id] = new_node;
    },
    remove: function(id)
    {
	var remove_node = null;
	if (id) {
	    remove_node = this.table[id];
	} else {
	    remove_node = this.getRandomNode();
	}
	
	if (remove_node) {
	    var new_list = new Array();
	    remove_node.die();
	    delete this.table[remove_node.id];
	    for (id in this.table) {
		new_list.push(this.table[id]);
	    }
	    delete this.list;
	    this.list = new_list;
	}
    },
    step: function()
    {
	for (var i = 0; i < this.list.length; ++i) {
	    this.list[i].step();
	    this.list[i].moveNode();
	}
    },
    
    draw: function(ctx)
    {
	var i;
	for (i = 0; i < this.list.length; ++i) {
	    this.list[i].drawLink(ctx);
	}
	for (i = 0; i < this.list.length; ++i) {
	    this.list[i].drawNode(ctx);
	}
    },
    
    count: function()
    {
	return this.list.length;
    },

    clear: function()
    {
    	this.list = new Array();
	this.table = new Object();
    }
};
