/**
 **      ==============================
 **       O           O      O   OOOO
 **       O           O     O O  O   O
 **       O           O     O O  O   O
 **       OOOO   OOOO O     OOO  OOOO
 **       O   O       O    O   O O   O
 **       O   O       O    O   O O   O
 **       OOOO        OOOO O   O OOOO
 **      ==============================
 **      Dr. Stefan Bosse http://www.bsslab.de
 **
 **      COPYRIGHT: THIS SOFTWARE, EXECUTABLE AND SOURCE CODE IS OWNED
 **                 BY THE AUTHOR(S).
 **                 THIS SOURCE CODE MAY NOT BE COPIED, EXTRACTED,
 **                 MODIFIED, OR OTHERWISE USED IN A CONTEXT
 **                 OUTSIDE OF THE SOFTWARE SYSTEM.
 **
 **    $AUTHORS:     Stefan Bosse
 **    $INITIAL:     (C) 2006-2024 bLAB
 **    $CREATED:     01-02-17 by sbosse.
 **    $VERSION:     1.18.21
 **
 **    $INFO:
 **
 **  SEJAM: JAM Agent Simluator
 **         + WEB/nw.js webix+ Anychart graphics.js GUI
 **         + Chat Dialog API (botui)
 **
 **    $ENDOFINFO
 */


var Comp = Require('com/compat');
var Io = Require('com/io');
var current=none;
var Aios = none;

var Papa    = Require('parser/papaparse.js');
var Esprima = Require('parser/esprima');
var Name    = Require('com/pwgen');
var Json    = Require('jam/jsonfn');
var util    = Require('util');
var JamAnal = Require('jam/analyzer');
var Db      = Require('db/dbS');
var DbQ     = Require('db/dbQ');
var SQLJSON = Require('db/sqljson');
var RTree   = Require('rtree/rtree');
var AiosXnet = Require('simu/aiosXnet');

var ml      = Require('ml/ml')
var nn      = Require('nn/nn')
var csp     = Require('csp/csp')
var sat     = Require('logic/sat')
var numerics = Require('numerics/numerics');

var nameopts = {
  world:{length:8, memorable:true, uppercase:true},
  node: {length:8, memorable:true, lowercase:true}
}

// Geometric Utiliy Functions
function sind(x) { return Math.sin(x/360*(2*Math.PI)) }
function cosd(x) { return Math.cos(x/360*(2*Math.PI)) }
function rotate(d,a) {
  return [
    int(d[0]*cosd(a)-d[1]*sind(a)),
    int(d[1]*sind(a)+d[0]*sind(a))
  ]
}
function distance2Rect (pos,bbox,scale) {
  if (!scale) scale={x:1,y:1};
  var px = pos.x,
      py = pos.y,
      x0 = bbox.x+bbox.w/2,
      y0 = bbox.y+bbox.h/2,
      dx = (Math.max(Math.abs(px - x0) - bbox.w / 2, 0))/scale.x,
      dy = (Math.max(Math.abs(py - y0) - bbox.h / 2, 0))/scale.y;

  return Math.sqrt(Math.pow(dx,2)+Math.pow(dy,2))
}
function distance (pos1,pos2,scale) {
  if (!scale) scale={x:1,y:1};
  var 
      dx = Math.abs(pos1.x - pos2.x) / scale.x,
      dy = Math.abs(pos1.y - pos2.y) / scale.y;

  return Math.sqrt(Math.pow(dx,2)+Math.pow(dy,2))
}

var hrtime;

if (global.TARGET != "browser") hrtime = process.hrtime;
else {
  // polyfil for window.performance.now
  var performance = global.performance || {}
  var performanceNow =
    performance.now        ||
    performance.mozNow     ||
    performance.msNow      ||
    performance.oNow       ||
    performance.webkitNow  ||
    function(){ return (new Date()).getTime() }

  // generate timestamp or delta
  // see http://nodejs.org/api/process.html#process_process_hrtime
  hrtime = function (previousTimestamp){
    var clocktime = performanceNow.call(performance)*1e-3
    var seconds = Math.floor(clocktime)
    var nanoseconds = Math.floor((clocktime%1)*1e9)
    if (previousTimestamp) {
      seconds = seconds - previousTimestamp[0]
      nanoseconds = nanoseconds - previousTimestamp[1]
      if (nanoseconds<0) {
        seconds--
        nanoseconds += 1e9
      }
    }
    return [seconds,nanoseconds]
  }
}

/* construct bbox {x,y,w,h} from geometric data 
  {x,y,x0,y0,x1,y1,dx,dy,w,h,dir} relative
  to current position {x,y}
  optional bounds {x0,y0,x1,y1}
 
  +----->  x
  |      N            x,y--+
  |    W X E          |    |
  v      S            +--w,h
  
  y
 
*/

function makeBbox (pos,geo,bounds) {
  bbox={x:pos.x,y:pos.y,w:0,h:0} // {x,y,w,h}
  if (typeof geo == 'number') // radius around center pos
    return {x:pos.x-geo,y:pos.y-geo,w:2*geo+1,h:2*geo+1};
 
  if (geo.x)  bbox.x=geo.x;
  if (geo.y)  bbox.y=geo.y;
  if (geo.x0) bbox.x=geo.x0;
  if (geo.y0) bbox.x=geo.y0;
  if (geo.dx) bbox.x=pos.x+geo.dx;
  if (geo.dy) bbox.y=pos.y+geo.dy;
  if (geo.w)  bbox.w=geo.w;
  if (geo.h)  bbox.w=geo.h;
  if (geo.x1) bbox.w=geo.x1-bbox.x+1;
  if (geo.y1) bbox.h=geo.y1-bbox.y+1;
  if (geo.r) return {x:bbox.x-geo.r,y:bbox.y-geo.r,w:2*geo.r+1,h:2*geo.r+1};  
  if (geo.dir) switch (geo.dir) {
    // including current position X
    // Ex. WEST:
    // ****
    // ***X
    // ****
    case Aios.DIR.NORTH: 
      if (geo.distance) bbox.w=geo.spread||1,bbox.h=geo.distance+1;
      bbox.x -= int(bbox.w/2); bbox.y -= (bbox.h-1);  
      break;
    case Aios.DIR.SOUTH:
      if (geo.distance) bbox.w=geo.spread||1,bbox.h=geo.distance+1;
      bbox.x -= int(bbox.w/2);
      break;
    case Aios.DIR.WEST: 
      if (geo.distance) bbox.h=geo.spread||1,bbox.w=geo.distance+1;
      bbox.y -= int(bbox.h/2); bbox.x -= (bbox.w-1);  
      break;
    case Aios.DIR.EAST:
      if (geo.distance) bbox.h=geo.spread||1,bbox.w=geo.distance+1;
      bbox.y -= int(bbox.h/2);  
      break;
  }
  return bbox;
}

function bbox2pp(bbox) {
  return {x0:bbox.x,y0:bbox.y,x1:bbox.x+bbox.w-1,y1:bbox.y+bbox.h-1,
          dir:bbox.dir,distance:bbox.distance}
}
function pp2bbox(pp) {
  return {x:pp.x0,y:pp.y0,w:pp.x1-pp.x0+1,h:pp.y1-pp.y0+1}
}
/** Create a simulation world with visualization
 *
 * options: {config:object,id:string?,
 *           nolimits:boolean is a disable flag for agent check poininting,
 *           connection:{random:number?},markings?,
 *           fastcopy?,
 *           gui,
 *           log:function,
 *           msg:function,
 *           units? : {},
 *           format? : {node,agent,class,...} is logging format setting,
 *           classes:{node:{..},ac1:{..},..}}
 * with config: <geometrical world configuration>
 *
 */
var simu = function (options) {
  var self=this;
  var node1,node2,row,row1,row2,i,j,p;
  
  this.options=options||{};
  
  if (this.options.id==_) this.options.id=Name.generate(nameopts.world);
  if (this.options.connections==_) this.options.connections={};
  if (this.options.markings) this.options.nodebar=true;
  if (this.options.nolimits) Aios.config({nolimits:true});
  
  this.classes=this.options.classes||{};
  delete this.options.classes; 
  this.run=false;       // Simulator running
  this.stopped=false;   // Forced stop
  this.step=0;          // Step counter
  this.stepped=0;       // Remaining steps before stopping
  this.loop=none;
  this.time=0;          // Current simulation time
  this.time0=0;
  this.lag=0;           // Time lag between simulation runs
  this.events=[];
  this.parameter=options.parameter||{};
  this.verbose = options.verbose||0;
  Aios.config({verbose:this.verbose});
  
  this.cleanWorld=false;
  
  this.log=options.log; // Simulation messaging
  this.msg=options.msg; // Agent messaging
  Aios.config({print:options.log});
  // Aios.World.options.verbose=1;
  
  this.UI       = options.gui.UI;
  this.webix    = options.gui.webix;
  this.gui      = options.gui;
  this.simuPhy  = options.simuPhy;
  this.CANNON   = options.CANNON;
  
  this.stats = {
    agents:{},
    custom:{},
    cpu:0
  };
  
  // Caches
  this.cache = {
    agent2node:{}
  }
  
  // Region tree for resources
  this.rtree = RTree(10);
  
  this.err=function (msg,err) {
    self.log('Error: '+msg);
    throw (err||'[SIM] Error');
  }
  
  this.warn=function (msg) {
    self.log('Warning: '+msg);
  }
  
  this.out=function (msg) {
    self.log(msg);    
  }

  
  if (options.time!='real') 
    Aios.config({time:function () {
      return self.step;
    }}),this.log('Setting simulation clock to step time!') ;
  if (!options.format) this.options.format={}; // Agent logging format
  this.options.format.forEach(function (v,a) { Aios.config(v?{'log+':a}:{'log-':a})});
  this.out('[SIM] Logging flags: '+util.inspect(this.options.format));
  
  Aios.off('agent+');
  Aios.on('agent+',function (desc) {
    var aid,nid='node['+desc.node.id+']',
        visual,xn,yn,level=desc.node.position.z||0;
    if (desc.agent.ac && self.world.classes[desc.agent.ac]) {
      if (!self.stats.agents[desc.agent.ac]) self.stats.agents[desc.agent.ac]=0;
      self.stats.agents[desc.agent.ac]++; 
      aid='agent['+desc.agent.ac+':'+desc.agent.id+':'+desc.node.id+']';
      visual=self.world.classes[desc.agent.ac].visual;
      //self.out('Event register '+desc.agent.id+' of class '+desc.agent.ac+' on node '+desc.node.id+
      //         '('+self.gui.objects[nid].visual.x+','+self.gui.objects[nid].visual.y+')');
      if (visual) {
        xn=self.gui.objects[nid].visual.x+self.gui.objects[nid].visual.w/2;
        yn=self.gui.objects[nid].visual.y+self.gui.objects[nid].visual.h/2;
        if (visual.x!=undefined && visual.y!=undefined) {
          xn=xn+visual.x;
          yn=yn+visual.y;
        }
        if (visual.center && visual.center.x!=undefined && visual.center.y!=undefined) {
          xn=xn+visual.center.x;
          yn=yn+visual.center.y;
        }
        self.gui.objects[aid] = { 
          visual:{
            x:xn,
            y:yn,
            shape:visual.shape||'circle',
            w:visual.width,
            h:visual.height,
            fill:visual.fill,
            line:visual.line,
            level:level
          },
          obj:desc.agent,
          id:aid      
        };
        self.gui.drawObject(aid);        
      }
    } else {
    }
  });

  Aios.off('agent-');
  Aios.on('agent-',function (desc) {
    var aid='agent['+desc.agent.ac+':'+desc.agent.id+':'+desc.node.id+']';
    //self.out('Event unregister '+proc.agent.id+' on node '+node.id);
    self.gui.destroyObject(aid);
    if (self.cache.agent2node[desc.agent.id]) delete self.cache.agent2node[desc.agent.id];
    if (desc.proc.type=='physical' && desc.node.type=='physical' && self.world.getNode(desc.node.id)) {
      self.deleteNode(desc.node.id);
    }
  });

  Aios.off('signal+');
  Aios.on('signal+',function (proc,node,sig,arg,from) {
    var aid='agent['+proc.agent.ac+':'+proc.agent.id+':'+node.id+']',
        sid='signal['+proc.agent.ac+':'+proc.agent.id+':'+node.id+']',
        nid='node['+node.id+']',
        visual,level=node.position.z||0;
    // self.log(aid);
    if (self.model.classes[proc.agent.ac] && self.model.classes[proc.agent.ac].on) 
      visual=self.model.classes[proc.agent.ac].on[sig];
    if (visual) {
      xn=self.gui.objects[nid].visual.x+self.gui.objects[nid].visual.w/2;
      yn=self.gui.objects[nid].visual.y+self.gui.objects[nid].visual.h/2;
      if (visual.x && visual.y) {
        xn=xn+visual.x;
        yn=yn+visual.y;
      }
      self.gui.objects[sid] = { 
        visual:{
          x:xn,
          y:yn,
          shape:visual.shape||'circle',
          w:visual.width,
          h:visual.height,
          fill:visual.fill,
          line:visual.line,
          level:level,
          timeout:visual.time
        },
        obj:{sig:sig,arg:arg,from:from},
        id:sid      
      };
      self.gui.drawObject(sid);
      self.cleanWorld=true;
    }
    
  });
 
  // PRINT: Smart print function
  var print = function (msg,header,depth,nolog) {
    var lines=[];
    var line='';
    if (depth==_) depth=1;
    if (msg==undefined) msg='undefined';
    if (msg==null) msg='null';
    function isvec(obj) {return(Comp.obj.isArray(obj) && (obj.length == 0 || !Comp.obj.isArray(obj[0])))}
    function ismat(obj) {return(Comp.obj.isArray(obj) && obj.length > 0 && Comp.obj.isArray(obj[0]))}
    function mat(o,depth) {
        // matrix
        var lines=[];
        var line = '';
        if (header) {line=header; header=_};
        for (var j in o) {
          var row=o[j];
          line += Comp.printf.list(row,function (v) {
            return (Comp.obj.isArray(v)?(depth>0?'['+vec(v,depth-1)+']':'[..]'):
                                         Comp.obj.isObj(v)?(depth>0?obj(v,depth-1):'{..}'):v);
          });
          lines.push(line);
          line='';
        }    
        return lines;
    }
    function vec(v,depth) {
        // vector
        var lines=[];
        var line = '';
        if (header) {line=header; header=_};
        if (v.length==0) return(line+'[]');
        else {
          // can still contain matrix elements that must bes separated
          var sep='',sepi='';
          for (var p in v) {
            if (ismat(v[p])) {              
              //self.log.log(line); line='  ';
              if (depth>0) {
                lines = mat(v[p],depth-1);
                line += sep+'['; sepi='';
                Comp.array.iter(lines,function (line2) {
                  line += sepi+'['+line2+']';
                  sepi=',';
                });
                line += ']';
                sep=',';
              } else {
                line += sep+'[[..]]';
                sep=',';
              }
            }
            else if (isvec(v[p])) {
              //self.log.log(line); line='  ';
              line += sep+vec(v[p],depth-1);
              sep=',';
            }
            else {
              line += sep+(Comp.obj.isArray(v[p])?(depth>0?vec(v[p],depth-1):'[..]'):
                                                  Comp.obj.isObj(v[p])?(depth>0?obj(v[p],depth-1):'{..}'):v[p]);
              sep=',';
            }
          }
          if (line!='') return line;
        }
    }
    function obj(o,depth) {
      var line='';
      var sep='';
      if (header) {line=header; header=_};
      line += '{';
      for (var p in o) {
        if (!Comp.obj.isFunction(o[p])) {
          line += sep + p+':'+
            (Comp.obj.isArray(o[p])?(depth>0?vec(o[p],depth-1):'[..]'):
                                    Comp.obj.isObj(o[p])?(depth>0?obj(o[p],depth-1):'{..}'):o[p]);
          sep=',';
        } else {
          line += sep + p+':'+'function()';
          sep=',';
        }
      }      
      return line+'}';
    }
    
    function str(s) {
      var line='';
      var lines=[];
      var lines2 = Comp.string.split('\n',msg);
      if (header) {line=header; header=_};
      if (lines2.length==1)
        lines.push(line+msg);
      else {
        Comp.array.iter(lines2,function (line2,i) {
          if (i==0) lines.push(line+line2);
          else lines.push(line2);
        });
      } 
      return lines;
    }
    
    if (Comp.obj.isError(msg)) {
      if (header) {line=header; header=_};
      line += (/Error/.test(msg.toString())?msg.toString():'Error: '+msg.toString());
      lines.push(line);      
    } else if (ismat(msg)) lines = Comp.array.concat(lines,
                                              Comp.array.map(mat(msg,depth-1),function (line){
                                                  return '    '+line}));
    else if (Comp.obj.isString(msg)) lines = Comp.array.concat(lines,str(msg));
    else if (isvec(msg)) lines.push(vec(msg,depth-1));
    else if (Comp.obj.isObj(msg)) lines.push(obj(msg,depth-1));
    else {
      if (header) {line=header; header=_};
      line += msg;
      lines.push(line);      
    }
 
    if (nolog) return lines; else Comp.array.iter(lines,function (line) {self.msg(line)});    
  };
  
  var msg = function(){
    var header='';
    if (!current.node || !current.process) {
      print('[SIM] '+arguments[0]);
    }
    else if (arguments.length==1) {
      header='[';
      if (self.options.format.time) header += (Aios.time()+':');
      if (self.options.format.node) header += (current.node.id+':');
      header += (current.process.agent.id+':');
      if (self.options.format.pid) header += (current.process.pid+':');
      if (self.options.format.class) header += (current.process.agent.ac+'');
      header += '] ';
      print(arguments[0],header);
    } else {
      header='[';
      if (self.options.format.time) header += (Aios.time()+':');
      if (self.options.format.node) header += (current.node.id+':');
      header += (current.process.agent.id+':');
      if (self.options.format.pid) header += (current.process.pid+':');
      if (self.options.format.class) header += (current.process.agent.ac+'');
      header += '] ';
      for (var i in arguments) {
        if (i==0) 
          print(arguments[i],'['+current.node.id+':'+current.process.agent.id+':'+current.process.pid+':'+current.process.agent.ac+'] ');
        else
          print(arguments[i],_,2);
      }
    }
  };
  
  // Module extensions
  Aios.ml = ml.agent;
  Aios.aios1.ml = ml.agent;
  Aios.aios2.ml = ml.agent;
  Aios.aios3.ml = ml.agent;
  Aios.nn = nn.agent;
  Aios.aios1.nn = nn.agent;
  Aios.aios2.nn = nn.agent;
  Aios.aios3.nn = nn.agent;
  Aios.sat = sat.agent;
  Aios.aios1.sat = sat.agent;
  Aios.aios2.sat = sat.agent;
  Aios.aios3.sat = sat.agent;
  Aios.csp = csp.agent;
  Aios.aios1.csp = csp.agent;
  Aios.aios2.csp = csp.agent;
  Aios.aios3.csp = csp.agent;
  Aios.numerics = numerics.agent;
  Aios.aios1.numerics = numerics.agent;
  Aios.aios2.numerics = numerics.agent;
  Aios.aios3.numerics = numerics.agent;
  
  // Extended sandbox environment available for all agents
  Aios.aios0.log=msg;
  Aios.aios1.log=msg;
  Aios.aios2.log=msg;
  Aios.aios3.log=msg;

  Aios.err=function(_msg) {msg('[AIOS] Error: '+_msg)};
  Aios.aios0.print=function(msg,depth) {return print(msg,_,depth,false)};
  Aios.aios1.print=function(msg,depth) {return print(msg,_,depth,false)};
  Aios.aios2.print=function(msg,depth) {return print(msg,_,depth,false)};
  Aios.aios3.print=function(msg,depth) {return print(msg,_,depth,false)};
  Aios.aios0.sprint=function(msg,depth) {return print(msg,_,depth,true)};
  Aios.aios1.sprint=function(msg,depth) {return print(msg,_,depth,true)};
  Aios.aios2.sprint=function(msg,depth) {return print(msg,_,depth,true)};
  Aios.aios3.sprint=function(msg,depth) {return print(msg,_,depth,true)};
  //Aios.aios0.inspect=function(msg,depth) {return util.inspect(msg)};
  //Aios.aios1.inspect=function(msg,depth) {return util.inspect(msg)};
  //Aios.aios2.inspect=function(msg,depth) {return util.inspect(msg)};
  //Aios.aios3.inspect=function(msg,depth) {return util.inspect(msg)};
  Aios.aios3.sensors=function () {
    return Aios.aios0.copy(current.node.sensors);
  };
  Aios.aios0.Vec3=function(x,y,z) {return new self.CANNON.Vec3(x,y,z)};
  Aios.aios1.Vec3=function(x,y,z) {return new self.CANNON.Vec3(x,y,z)};
  Aios.aios2.Vec3=function(x,y,z) {return new self.CANNON.Vec3(x,y,z)};
  Aios.aios3.Vec3=function(x,y,z) {return new self.CANNON.Vec3(x,y,z)};
  
  Aios.config({iterations:1,fastcopy:options.fastcopy||false});
  
  if (!this.options.nolimits) Aios.config({TIMESCHED:1000});

  // Create the simulation world
  this.world = Aios.World.World([],{id:this.options.id,classes:this.classes});
  
  /* optional geographical (gps) or geometric (geo) mapping functions ({x,y,z}) -> {latitude,longitude,height}
  ** { gps2px, px2gps, geo2px, px2geo, gps2dist }
  ** Can be used for absolute or relative coordinate transformations ({delta:true}).
  */
  this.world.units = options.units||{gps2dist:function () {}};

  // old channel-based SQL DB connection
  this.db = {
    init: function (path,channel,callback) {
      var proc=current.process,stat;
      if (!self.Db) self.Db={};
      if (!self.Db[path]) {
        if (channel==undefined) {
          // embedded SQL
          self.Db[path]=DbQ.Sql(path);
          stat=self.Db[path].open();
          if (callback) proc.callback(callback,[stat]);
        } else {
          // remote SQL
          self.Db[path]=Db.Sqlc(path,channel);
          self.Db[path].setLog(function (msg) {self.log(msg)});
          self.Db[path].init(function (stat,err) {
            self.log('[DB] '+path+' Initialized: '+stat);            
            if (callback) proc.callback(callback,[stat,err]);
          });
        }
      } 
    },
    // function (path:string,matname:string,header:[$var:$type]|[$type], callback?)
    createMatrix: function (path,matname,header,callback) {
      var proc=current.process;
      if (callback) self.Db[path].createMatrix(matname,header,function (stat,err) {
        if (callback) proc.callback(callback,[stat,err]);       
      }); else return self.Db[path].createMatrix(matname,header);
    },
    // function (path:string,matname:string,header:{$var:$type}, callback?)
    createTable: function (path,tblname,header,callback) {
      var proc=current.process;
      if (callback) self.Db[path].createTable(tblname,header,function (stat,err) {
        if (callback) proc.callback(callback,[stat,err]);       
      }); else return self.Db[path].createTable(tblname,header);
    },
    drop: function (path,tbl,callback) {
      var proc=current.process;
      self.Db[path].drop(tbl,function (stat,err) {
        if (callback) proc.callback(callback,[stat,err]);
      });
    },
    error: function (path) {
      return self.Db[path].error
    },
    exec: function (path,cmd,callback) {
      var proc=current.process;
      if (callback) self.Db[path].exec(cmd,function (stat,err) {
        if (callback) proc.callback(callback,[stat,err]);
      }); else return self.Db[path].exec(cmd);
    },
    get: function (path,callback) {
      var proc=current.process;
      self.Db[path].get(undefined,function (row,err) {
        if (callback) proc.callback(callback,[row,err]);
      });
    },
    insert: function (path,tbl,row,callback) {
      var proc=current.process;
      self.Db[path].insert(tbl,row,function (stat,err) {
        if (callback) proc.callback(callback,[stat,err]);
      });
    },
    insertMatrix: function (path,matname,row,callback) {
      var proc=current.process;
      if (callback) self.Db[path].insertMatrix(matname,row,function (stat,err) {
        if (callback) proc.callback(callback,[stat,err]);
      }); else return self.Db[path].insertMatrix(matname,row);
    },
    insertTable: function (path,tblname,row,callback) {
      var proc=current.process;
      if (callback) self.Db[path].insertTable(tblname,row,function (stat,err) {
        if (callback) proc.callback(callback,[stat,err]);
      }); else return self.Db[path].insertTable(tblname,row);
    },
    readMatrix: function (path,matname,callback) {
      var proc=current.process;
      if (callback) self.Db[path].readMatrix(matname,function (mat,err) {
        if (callback) proc.callback(callback,[mat,err]);
      }); else return self.Db[path].readMatrix(matname);
    },
    readTable: function (path,tblname,callback) {
      var proc=current.process;
      if (callback) self.Db[path].readTable(tblname,function (tbl,err) {
        if (callback) proc.callback(callback,[tbl,err]);
      }); else return self.Db[path].readTable(tblname);
    },
    select: function (path,tbl,vars,cond,callback) {
      var proc=current.process;
      if (!callback && Comp.obj.isFunction(cond)) callback=cond,cond=undefined;
      self.Db[path].select(tbl,vars,cond,function (stat,err) {
        if (callback) proc.callback(callback,[stat,err]);
      });
    },
    writeMatrix: function (path,matname,matrix,callback) {
      var proc=current.process;
      if (callback) self.Db[path].writeMatrix(matname,matrix,function (stat,err) {
        if (callback) proc.callback(callback,[stat,err]);
       }); else return self.Db[path].writeMatrix(matname,matrix);
    },
    writeTable: function (path,tblname,table,callback) {
      var proc=current.process;
      if (callback) self.Db[path].writeTable(tblname,table,function (stat,err) {
        if (callback) proc.callback(callback,[stat,err]);
       }); else return self.Db[path].writeTable(tblname,table);
    },
  }
  // New SQLJSOn RPC API
  this.sql = {
    init : function (url) {
      return SQLJSON.sql(url)
    },
  }
  
  // CSV file import/export
  this.csv = {
    read: function (file,callback,verbose) {
      var text,data,header,convert,proc=current.process;
      if (callback == true) convert=true,callback=null;
      if (verbose) self.log('CSV: Reading from '+file);
      try {
        text=Io.read_file(file);
        if (!text) throw 'CSV File read error: '+file;          
        if (verbose) self.log('CSV: Parsing '+file);
        Papa.parse(text,{
          skipEmptyLines: true,
          dynamicTyping: true,          
          complete: function(results) {
            if (verbose) self.log('CSV parsed with DEL="'+results.meta.delimiter+'" TRUNC='+results.meta.truncated+
                                  ' ABORT='+results.meta.aborted);
            if (callback) proc.callback(callback,[results.data]);
            data=results.data;
          }
        });
        if (convert) { // first line must be header
          header=data.shift();
          data=data.map(function (row) {
            var r={};
            header.forEach(function (col,i) { r[col]=row[i] });
            return r; 
          }) 
        }
        return data;
      } catch (e) {
        if (callback) callback(e);
        return e;
      }
    },
    write: function (file,header,data,callback,verbose) {
      if (file[0]!='/') file=self.gui.workdir+'/'+file;
      var d1=false,fd,i,convert=!Comp.obj.isArray(data[0])&&Comp.obj.isObj(data[0]),
          sep=',',proc=current.process;
      if (typeof callback == 'string') sep=callback,callback=null;
      d1 = typeof data[0] != 'object';
      if (!header || header.length==0) {
        if (!convert)
          header=d1?['0']:data[0].map(function (x,i) { return String(i) });
        else {
          header=[];
          for (var p in data[0]) {
            header.push(p);
          }
        }
      }
      try {
        if (verbose) self.log('CSV: Writing to '+file);
        fd=Io.open(file,'w+');
        Io.write_line(fd,header.join(sep));
        if (!d1) 
          for(i in data) {
            if (!convert)
              Io.write_line(fd,data[i].join(sep));
            else
              Io.write_line(fd,header.map(function (col) { return data[i][col]}).join(sep));
          }
        else
          for(i in data) {
            if (!convert)
              Io.write_line(fd,data[i]);
            else
              Io.write_line(fd,data[i][header[0]]);
          };
        
        Io.close(fd);
        if (callback) proc.callback(callback,[data.length]);
        return data.length
      } catch (e) {
        if (callback) proc.callback(callback,[e]); 
        return e;
      }      
    }
  };



  /* utils */
  function within(p1,p2,r) {
    return (p1.x>= p2.x-r && p1.x <= p2.x+2) &&
           (p1.y>= p2.y-r && p1.y <= p2.y+2)
  } 
  function whatType(what) {
    // agent-twin => agent
    var tokens = what.match(/([a-z]+)(-.+)/)
    return tokens?tokens[1]:what;
  }  
  function whatName(what) {
    // agent-twin => twin
    var tokens = what.match(/[a-z]+-(.+)/)
    return tokens?tokens[1]:null;
  }
  // Generic simulator interface
  var aiosXsimu = {
    changeVisual: function (id,visual) {
      self.gui.changeObject(id,visual);
    },
    
    chat : {
      agent : self.gui.chatAgent,
      message: self.gui.chatMessage,
      question : function (id, question, action, timeout, callback) {
        var process = Aios.current.process;
        self.gui.chatQuestion (id, question, action, function (res) {
          process.callback(callback,res?[res.value]:null);
          process.wakeup();
        },timeout);
        process.suspend(Aios.timeout(timeout));
      },
      script: self.gui.chatScript,
    },
    
    clear: function (msg,log) {
      if (msg) {
        self.gui.clear('msgWin');
        self.gui.messages=[];
      }
      if (log) self.gui.clear('logWin');
    },
    
    /*
    ** typeof arguments = {nodeclass:string|function,arg1,arg2,...}
    ** arguments arg1,.. are node constructor function arguments
    ** if nodeclass is a string then a function of model.nodes.<nodeclass>(arg1,arg2,..) or
    ** model.world.nodes is used
    ** returns node identifier
    */
    createNode: function () {
      var nodes = (self.model.world && self.model.world.nodes) ||
                   self.model.nodes,res,
                   cls=typeof arguments[0] == 'string'?arguments[0]:'function';
      function shift(args) {
        var i=0,p;
        for(p in args) {
          if (p=='0') continue;
          args[i]=args[p];
          i++;
        }
        return args;
      }
      if (nodes) {
        var constrFun;
        if (Comp.obj.isFunction(arguments[0])) constrFun=arguments[0];
        else constrFun=nodes[arguments[0]];
        if (constrFun && Comp.obj.isFunction(constrFun)) {
          var desc=constrFun.apply(null,shift(arguments));
          if (desc) {
            if (res=self.checkVisual(desc.visual)) throw 'Invalid node ('+cls+') visual: '+res;
            return self.createNode(desc,true);
          }
        } else self.log('createNode: unknown node class '+arguments[0])
      } else self.log('createNode: no node constructors defined (expected model.world.nodes or model.nodes')
    },
    
    /*
    ** Create an agent on specified node
    */
    createOn:function (nid,ac,args,level) {      
      var node=self.world.getNode(nid),      
          proc=none;
      if (!node) return none;
      if (level==undefined) level=1;
      if (args==undefined) args=[{}];
      if (self.world.classes[ac])
        agent = Aios.Code.createOn(node,self.world.classes[ac][level],args,level,ac);
      else 
        self.err('createOn: no such class '+ac);
      if (agent) 
        return agent.agent.id; 
      else return none;
    },
    
    deleteNode: function (nodeid) {
      self.deleteNode(nodeid);
    },
    
    event: {
      add: function (ev) {
        self.events.push({step:self.step,node:current.node.id,event:ev});
      },
      get: function () {
        var evl = self.events;
        self.events=[];
        return evl;
      }
    },
    
    get : function (p) {
      switch (p) {
        case 'delay': return self.delay;
      }
    },
    
    // Return node object (if id is a regular expression, an array of matching nodes)
    getNode:function (id) {
      return self.world.getNode(id);
    },
    
    getVisual: function (id) {
      return self.gui.getObject(id).visual;
    },
    
    getStats: function (target,arg) {
      var i,j,proc,node,p,
          stats;
      switch (target) {
        case 'node':
          node=self.world.getNode(arg);
          if (node) stats=node.stats;
          break;
        default:
          stats={steps:self.step,time:Aios.time(),agents:{},agentsN:self.stats.agents,total:0,nodes:0,
                 create:0,migrate:0,fork:0,fastcopy:0,signal:0,
                 send:0,
                 cpu:self.stats.cpu,
                 custom:{}}
          for(p in self.stats.custom) stats.custom[p]=self.stats.custom[p];
          if (!self.world || !self.world.nodes) return {};
          for (i in self.world.nodes) {        
            node=self.world.nodes[i];  
            if (!node) continue;
            stats.nodes++;
            for (p in node.stats) stats[p] += node.stats[p];
            for(j in node.processes.table) {
              proc=node.processes.table[j];
              if (proc && proc.agent.ac) {
                if (stats.agents[proc.agent.ac]==undefined) stats.agents[proc.agent.ac]=0;
                stats.agents[proc.agent.ac]++;
                stats.total++;
              }
            }
            for(p in node.connections) {
              if (node.connections[p] && node.connections[p].count)
                stats.send += node.connections[p].count();
            }
          }
          break;
      }
      return stats;
    },
    
    getSteps:function () {return self.step},
    
    // Load JSON object from file
    load : function (file) {
      var json;
      if (global.config.os=='win32' && global.config.win) {
        json = Io.read_file('Z:'+self.gui.lastdir+'\\'+file);
        self.gui.log('Z:'+self.gui.lastdir+'\\'+file) 
      } else if (global.config.os=='win32') {
        json = Io.read_file(self.gui.lastdir+'\\'+file);
        self.gui.log(self.gui.lastdir+'\\'+file) 
      } else if (global.TARGET != 'browser') {
        json = Io.read_file(self.gui.lastdir+'/'+file);
        self.gui.log(self.gui.lastdir+'/'+file) 
      } else {
        json = loadFile(file);
        self.gui.log(file)       
      }
      if (json) return aiosXsimu.ofJSON(json);      
    },
    
    log: function (msg) {
      self.gui.log(msg);
    },
    
    inspect : util.inspect,
    
    model : function (name) {
      return self.model;
    }, 
    message:this.gui.message,
    // relative move dx,dy,dz with|w/o units m | \B0 ..
    
    move: self.moveObject.bind(this),
    // absolute move to x,y,z with|w/o units m | \B0 ..
    
    moveTo: self.moveObjectTo.bind(this),
   
    network: {
      x:this.options.x,
      y:this.options.y,
      z:this.options.z
    },
    
    ofJSON: function (s) {
      return Aios.Code.Jsonf.parse(s,{})
    },
    
    options:this.options,
    
    parameter: function (name) {
      return name?self.parameter[name]:self.parameter;
    }, 
    
    position: function () { return current.node.position },
    
    print:print,
    
    // Save object to  JSON file
    save : function (file,o) {
      var json = aiosXsimu.toJSON(o),res;
      if (global.config.os=='win32' && global.config.win) 
        res = Io.write_file('Z:'+self.gui.lastdir+'\\'+file,json);
      else if (global.config.os=='win32')
        res = Io.write_file(self.gui.lastdir+'\\'+file,json);      
      else
        res = Io.write_file(self.gui.lastdir+'/'+file,json);

      return res
    },
    
    // Set simulation control parameter and GUI control
    set : function (p,v) {
      var old;
      switch (p) {
        case 'delay': old=self.delay; self.delay=v; break;
        case 'window':
          if (Comp.obj.isObj(v)) for(var o in v) {
            console.log(o,v[o])
            switch (o) {
              case 'layout':
                if (v[o]=='auto') {
                  var _locked=self.gui.toolbar.locked;            
                  self.gui.toolbar.locked=false;
                  self.gui.autoLayout();
                  self.gui.toolbar.locked=_locked;
                }
                break;
              case 'Inspector': 
              case 'inspector': 
                if (self.gui.inspectorWin[v[o]]) self.gui.inspectorWin[v[o]]();
                break;
              case 'Chat': 
              case 'chat': 
                if (self.gui.chatWin[v[o]]) self.gui.chatWin[v[o]]();
                if (v[o]=='show') self.gui.chatInit();
                break;
              case 'World':
              case 'world':
                if (self.gui.worldWin.container) self.gui.worldWin.container.resume()
                if (v[o]['zoom'] && v[o]['zoom']=='fit') self.gui.worldWin.zoomFit();
                break;
            }
          } 
          break;
      }
      return old;
    },
    
    start: function (steps) { self.start(steps,true)},
    
    stat: function (p,v) {
      self.stats.custom[p]=v;
    },
    
    stop: function () { return self.stop(true) },
    // Use simulation time (steps) instead of system time
    
    toJSON: function (o) {
      return Aios.Code.Jsonf.stringify(o)
    },
    
    time : function () {return self.step},
    
    utime: function () { 
      hr=hrtime(); return hr[0]*1E9+hr[1] 
    },
  };

  aiosXsimu.csv = this.csv;
  aiosXsimu.db = this.db;
  aiosXsimu.sql = this.sql;
  aiosXsimu.units = this.world.units;
  
  // Simulation extension for phyiscal (behaviorual) agents
  var aiosXnet = AiosXnet.call(self,aiosXsimu,Aios);
  
  // if (this.simuPhy) ..
  // always set-up API for optional physical simulation
  //  -> simuPhy will be usually created AFTER simuWeb!
  aiosXsimu.simuPhy = aiosXsimu.phy = {
      changeScene: function (scene,options) {
        if (!self.simuPhy) return;
        self.simuPhy.changeScene(scene,options);        
      },
      get : function (id) {
        var tokens,
            obj;
        if (!self.simuPhy) return;
        tokens =  (id||Aios.current.node.id).split(',');
        obj = self.simuPhy.get(tokens.map(function (s) {return Number(s)}));
        return obj;
      },
      refresh: function () {
        if (!self.simuPhy) return;
        self.simuPhy.refresh();        
      },
      step: function (n,callback) {
        if (!self.simuPhy) return;
        return self.simuPhy.step(n,callback);
      },
      stepPhyOnly: function (n,callback) {
        if (!self.simuPhy) return;
        self.simuPhy.stepPhyOnly(n,callback);
      }
    };
    
  // Extend corefuncs in JAM analyzer ...
  // MUST BE COMPLETE - else analyzer fails!
  JamAnal.extend({
    // keep consistent with aiosXnet.js API
    net : {
      obj : {
        ask:{argn:[2,3]},    
        create:{argn:[2,3]},
        die:{argn:[0,1]},
        forward:{ argn:[0,1]},
        get:{argn:1},
        globals:{argn:0},
        group : { obj : {
          add: {argn:[2,3] },
          rm: {argn:2 }
        }},
        set:{argn:2},
        setxy:{argn:2},
        turn:{ argn:1},
        within:{ argn:2},
      }
    },
    simu:{
      obj:{
        chat : {
          obj : {
            agent:{argn:1},
            message:{argn:2},
            question:{argn:5},
            script:{obj:{
              init:{argn:2},
              next:{argn:0},
              cancel:{argn:0},
              reset:{argn:0}
            }}
          }
        },
        changeVisual:{argn:2},
        clear:{argn:[0,1,2]},
        createNode:{argn:[1,2,3,4,5,6,7,8,9]},
        createOn:{argn:[2,3,4]},
        csv:{ obj:{
          read:{argn:[1,2,3]},
          write:{argn:[2,3,4]},
        }},
        db: { obj :{
          error:{argn:1},
          init:{argn:3},
          createMatrix:{argn:[2,3]},
          createTable:{argn:[2,3]},
          drop:{argn:[2,3]},
         insert:{argn:[3,4]},
        }},
        deleteNode:{argn:1},
        event:{obj:{add:{argn:1},get:{argn:0}}},
        get:{argn:1},
        getNode:{argn:1},
        getStats:{argn:0},
        getSteps:{argn:0},
        inspect:{argn:[1,2,3,4,5,6,7,8,9]},
        load:{argn:1},
        log:{argn:1},
        message:{argn:1},
        model:{argn:0},
        move: {argn:[3,4]},
        moveTo: {argn:[3,4]},
        ofJSON: {argn:1},
        parameter:{argn:0},
        position : {argn:0},
        phy : {
          obj : {
            changeScene:{argn:2},
            get:{argn:1},
            refresh:{argn:0},
            step:{argn:[1,2]},
            stepPhyOnly:{argn:[1,2]},
          }
        },
        save:{argn:2},
        set: {argn:2},
        simuPhy : {
          obj : {
            changeScene:{argn:2},
            get:{argn:1},
            refresh:{argn:0},
            step:{argn:[1,2]},
            stepPhyOnly:{argn:[1,2]},
          }
        },
        sql : {
          init : {argn:1},
        },
        start: {argn:1},
        stat: {argn:2},
        stop: {argn:0},
        toJSON: {argn:1},
        time: {argn:0},
        units: {obj:{gps2dist:{argn:2}}},
        utime:{argn:0},
      }
    }
  });
  
  // Extended sandbox environment available for all agents in simulation
  this.aiosX = aiosXsimu;
  // Aios.aios0.simu=this.aiosX;
  // Aios.aios1.simu=this.aiosX;
  Aios.aios2.simu=this.aiosX;
  Aios.aios3.simu=this.aiosX;
  // only immobile level-3 phyiscal agents (on mobile nodes) can use the net API
  Aios.aios3.net=aiosXnet;

  if (options.aios) {
    // Extend AIOS environment(s)
    for (p in options.aios) {
      if (!Aios.aios0[p]) Aios.aios0[p]=options.aios[p];
      if (!Aios.aios1[p]) Aios.aios1[p]=options.aios[p];
      if (!Aios.aios2[p]) Aios.aios2[p]=options.aios[p];
      if (!Aios.aios3[p]) Aios.aios3[p]=options.aios[p];
    }
  }
  if (options.aios0) {
    // Extend AIOS environment(s)
    for (p in options.aios0) {
      if (!Aios.aios0[p]) Aios.aios0[p]=options.aios[p];
    }
  }
  if (options.aios1) {
    // Extend AIOS environment(s)
    for (p in options.aios1) {
      if (!Aios.aios1[p]) Aios.aios1[p]=options.aios[p];
    }
  }
  if (options.aios2) {
    // Extend AIOS environment(s)
    for (p in options.aios2) {
      if (!Aios.aios2[p]) Aios.aios2[p]=options.aios[p];
    }
  }
  if (options.aios3) {
    // Extend AIOS environment(s)
    for (p in options.aios3) {
      if (!Aios.aios3[p]) Aios.aios3[p]=options.aios[p];
    }
  }
  this.delay=0;
   
  this.out('[SIM] Simulation created.');
}


simu.prototype.analyze=JamAnal.jamc.prototype.analyze;
simu.prototype.syntax=JamAnal.jamc.prototype.syntax;

/** Add an agent template class
 *
 */
simu.prototype.addClass = function (constructor,name,visual) {
  var self=this,
      modu, 
      content,
      off,
      syntax,
      regex1,
      env={},
      interface,
      p,      
      text;
        
  try {
    content = 'var ac = '+constructor;
    this.out('Parsing agent class template "'+name+'" ...');
    syntax = Esprima.parse(content, { tolerant: true, loc:true });
    if (syntax.errors && syntax.errors.length>0) {
      throw syntax.errors[0];
    }
    interface=this.analyze(syntax,{classname:name,level:3,verbose:1,err:function (msg) { self.out(msg); throw msg}});
    this.out('Agent class template "'+name+'" analyzed successfully.');
    text=Json.stringify(constructor);
    // regex1= /this\.next=([a-zA-Z0-9_]+)/;
    // text=text.replace(regex1,"this.next='$1'");
    // console.log(text);
    for (p in interface.activities) env[p]=p;
    with (env) { eval('constructor='+text) };
    this.out('Agent class template "'+name+'" compiled successfully.');
    this.out('Adding agent class constructor "'+name+'" ...');
    this.world.classes[name]=[
      Aios.Code.makeSandbox(constructor,0,env),
      Aios.Code.makeSandbox(constructor,1,env),
      Aios.Code.makeSandbox(constructor,2,env),
      Aios.Code.makeSandbox(constructor,3,env)
    ];
    this.world.classes[name].visual=visual;
  } catch (e) {  
     this.out('Agent class template "'+name+'" not compiled successfully: '+e);
     this.gui.message(e);
     if (this.verbose>1) this.out(Io.sprintstack(e));
  }
  
};

/** Check coordinates in boundaries of the current world
 *
 */
simu.prototype.checkBounds = function (x,y,z) {
  if (x < 0 || y  < 0) return false;
  if (this.options.x && x >= this.options.x) return false;
  if (this.options.y && y >= this.options.y) return false;
  return true;
}

simu.prototype.checkVisual =  function (v) {
  if (!v) return 'missing visual';
  if (['circle','rect','icon'].indexOf(v.shape)<0) return 'invalid shape';
}

/** Convert coordinates from one unit system to another
 *
 */
simu.prototype.convertUnit = function (p,from,to) {
  if (Comp.obj.isString(p)) {
    // Convert to px (delta only)
    if (Comp.string.endsWith(p,'m')) 
      return this.world.units.geo2px({delta:true,X:Comp.string.prefix(p,'m')}).x;
    else if (Comp.string.endsWith(p,'\B0')) 
      return this.world.units.gps2px({delta:true,longitude:Comp.string.prefix(p,'\B0')}).x
    else if (Comp.string.endsWith(p,'px')) 
      return Number(Comp.string.prefix(p,'px'));
    else Number(p);
  } else switch (from) {
    case 'px':
      switch (to) {
        case 'px':
          return p;
        case 'm':
          if (this.world.units.px2geo) return this.world.units.px2geo(p);
          break;
        case '\B0':
          if (this.world.units.px2gps) return this.world.units.px2gps(p);
          break;
      }
      break;
    case 'm':
      switch (to) {
        case 'px':
          if (this.world.units.geo2px) return this.world.units.geo2px(p);
          break;
        case 'm':
          return p;
        case '\B0':
          if (this.world.units.geo2gps) return this.world.units.geo2gps(p);
          break;
      }
      break;
    case '\B0':
      switch (to) {
        case 'px':
          if (this.world.units.gps2px) return this.world.units.gps2px(p);
          break;
        case 'm':
          if (this.world.units.gps2geo) return this.world.units.gps2geo(p);
          break;
        case '\B0':
          return p;
      }
      break;
  }
}

/** Create a regular (or irregular) 2d mesh grid of nodes and connections.
 *  type of options = {width: number as node width in pixel,
                       height: number as node heigth in pixel,
                       rows:number,cols:number,level?:number,
                       node : { visual },
                       port?, link?,
                       map?,
                       connections?}
 */
simu.prototype.createMeshNetwork = function (model) {
  var self=this,
      options=model.world.meshgrid;
  if (!options.node) return this.out('Incomplete meshgrid model!');
  var margin_x = options.node.visual.width/2,
      margin_y = options.node.visual.height/2,
      space_x = options.node.visual.width/2,
      space_y = options.node.visual.height/2,      
      off_x = options.off_x||5,
      off_y = options.off_y||5,
      xn,yn,x,y,w,h,p,obj,obj2,link,
      node,world,node1,node2,place,port,ports={},
      id,i,j,k,level=0,dim=2,
      xMoff=0,yMoff=0;

  if (options.levels) dim=3;
  if (!options.matrix) this.gui.level=this.gui.level.map(function (x,i) {
    return i==0?1:0;
  }); else this.gui.level=this.gui.level.map(function (x,i) {
    return 1;
  }); 
  this.gui.level[-1]=1;
  
  this.out('Creating meshgrid network with '+options.rows+
           ' rows and '+options.cols+
           ' columns on '+(options.levels?options.levels:1)+
           ' levels.'); 

  this.options.x=options.cols;
  this.options.y=options.rows||0;
  this.options.z=options.levels||0;

  function makeRef(o) {return o};
  function vec2str(vec) { 
    if (self.options.z) return vec.join(',');
    else if (self.options.y) return vec.slice(0,2).join(',');
    else return vec[0] }
  
  for(level=0;level<(options.levels?options.levels:1);level++) {
    if (options.matrix && options.matrix[level]) {
      xMoff=options.matrix[level][0];
      yMoff=options.matrix[level][1];
    }           
    // 1. Nodes     
    for(j=0;j<options.rows;j++) {
      for(i=0;i<options.cols;i++) {
        if (options.node.filter && !options.node.filter([i,j,level])) continue; 
        id=vec2str([i,j,level]);
        node=Aios.Node.Node({id:id,
                             position:{x:i,y:j,z:(dim==3?level:undefined)},
                             TMO:self.options.TMO||1000000
                            });
        id='node['+id+']';
        this.world.addNode(node);
        if (this.verbose>1) this.log('[SIM] Created node '+node.id);
        xn=off_x+margin_x+i*(options.node.visual.width+space_x)+xMoff;
        yn=off_y+margin_y+j*(options.node.visual.height+space_y)+yMoff;
        this.gui.objects[id] = { visual:{
            x:xn,
            y:yn,
            shape:options.node.visual.shape||'rect',
            w:options.node.visual.width,
            h:options.node.visual.height,
            fill:options.node.visual.fill,
            line:options.node.visual.line,
            level:level    
          },
          obj:makeRef(node),
          id:id
        }
      }
    }
    
    // 2. Link ports of nodes
    if (options.port && options.port.place) {
      for(i in this.world.nodes) {
        node=this.world.nodes[i];
        id='node['+node.id+']';
        obj=this.gui.objects[id];
        place=options.port.place(node);
        if (place && obj) {
            for(j in place) {
              xn=obj.visual.x+place[j].x-options.port.visual.width/2+obj.visual.w/2;
              yn=obj.visual.y+place[j].y-options.port.visual.height/2+obj.visual.h/2;
              id='port['+node.id+':'+place[j].id+']';
              if (!ports[node.id]) ports[node.id]={};
              obj2={node:makeRef(node)};
              ports[node.id]['DIR.'+place[j].id]=obj2;
              this.gui.objects[id] = { visual:{
                  x:xn,
                  y:yn,
                  shape:options.port.visual.shape||'rect',
                  w:options.port.visual.width,
                  h:options.port.visual.height,
                  fill:options.port.visual.fill,
                  line:options.port.visual.line,
                  level:obj.visual.level
                },
                obj:obj2,
                id:id
              }
            }

        }
      }
    }
    // 3. Links between nodes
    // Only nodes having ports can be connected by links!
    if (options.link) {
      function port(i,j,k,dir) {
        var p=ports[vec2str([i,j,k])];
        if (!p) return false;
        return p[dir];
      }
      function setPort(i,j,k,dir,link) {
        var p=ports[vec2str([i,j,k])];
        if (!p) return ;
        p[dir].link=link;
      }
      for(k=0;k<(options.levels?options.levels:1);k++) {           
        if (options.matrix && options.matrix[k]) {
          xMoff=options.matrix[k][0];
          yMoff=options.matrix[k][1];
        }           
        for(j=0;j<options.rows;j++) {
          for(i=0;i<options.cols;i++) {      
            if (options.node.filter && !options.node.filter([i,j,k])) continue; 
            xn=off_x+xMoff+margin_x+i*(options.node.visual.width+space_x);
            yn=off_y+yMoff+margin_y+j*(options.node.visual.height+space_y);
            if ((i+1)<options.cols) {
              if (!options.link.connect || 
                   options.link.connect([i,j,k],[i+1,j,k])) {
                node1=self.world.getNode(vec2str([i,j,k]));
                node2=self.world.getNode(vec2str([i+1,j,k]));
                if (port(i,j,k,Aios.DIR.EAST) && port(i+1,j,k,Aios.DIR.WEST) && node2) {
                  link=this.world.connect(Aios.DIR.EAST,node1,node2);
                  setPort(i,j,k,Aios.DIR.EAST,link);
                  setPort(i+1,j,k,Aios.DIR.WEST,link);
                  id='link['+vec2str([i,j,k])+','+vec2str([i+1,j,k])+']';
                  w=margin_x;
                  h=options.link.visual.width||2;   
                  x=xn+options.node.visual.width;
                  y=yn+space_y-h/2;
                  this.gui.objects[id] = {visual:{
                    x:x,
                    y:y,
                    shape:options.link.visual.shape||'rect',
                    w:w,
                    h:h,
                    fill:options.link.visual.fill,
                    line:options.link.visual.line,
                    level:k    
                  },obj:{node1:node1,node2:node2},id:id}
                }
              }
            } 
            if ((j+1)<options.rows) {
              if (!options.link.connect || 
                   options.link.connect([i,j,k],[i,j+1,k])) {
                node1=self.world.getNode(vec2str([i,j,k]));
                node2=self.world.getNode(vec2str([i,j+1,k]));
                if (port(i,j,k,Aios.DIR.SOUTH) && port(i,j+1,k,Aios.DIR.NORTH) && node2) {
                  link=this.world.connect(Aios.DIR.SOUTH,node1,node2);
                  setPort(i,j,k,Aios.DIR.SOUTH,link);
                  setPort(i,j+1,k,Aios.DIR.NORTH,link);
                  id='link['+vec2str([i,j,k])+','+vec2str([i,j+1,k])+']';
                  w=options.link.visual.width||2;          
                  h=margin_x;
                  x=xn+space_x-w/2;
                  y=yn+options.node.visual.height;
                  this.gui.objects[id] = {visual:{
                    x:x,
                    y:y,
                    shape:options.link.visual.shape,
                    w:w,
                    h:h,
                    fill:options.link.visual.fill,
                    line:options.link.visual.line,
                    level:k    
                  },obj:{node1:node1,node2:node2},id:id}
                }
              }
            } 
          }
        }
      }
      for(k=0;k<(options.levels?options.levels-1:0);k++) {           
        for(j=0;j<options.rows;j++) {
          for(i=0;i<options.cols;i++) {      
            if (options.node.filter && !options.node.filter([i,j,k])) continue; 
            xn=off_x+margin_x+i*(options.node.visual.width+space_x);
            yn=off_y+margin_y+j*(options.node.visual.height+space_y);
            if (!options.link.connect || 
                 options.link.connect([i,j,k],[i,j,k+1])) {
                if (!port(i,j,k,Aios.DIR.UP) || !port(i,j,k+1,Aios.DIR.DOWN)) continue;
                node1=self.world.getNode(vec2str([i,j,k]));
                node2=self.world.getNode(vec2str([i,j,k+1]));
                if (!node2) continue;
                link=this.world.connect(Aios.DIR.UP,node1,node2);
            }
          }
        }
      }
    }
  }
  // 4. World
  if (!model.world.map && !model.world.nodes) {
    id='world';
    node=Aios.Node.Node({id:id,
                         position:{},
                         TMO:self.options.TMO||1000000
                        });
    this.world.addNode(node);
    if (this.verbose>1) this.log('[SIM] Created node '+node.id);
    id='node['+id+']';
    this.gui.objects[id] = {visual:{
              x:margin_x+off_x+xn+space_x*1.5-4,
              y:margin_y+off_y+yn+space_y*1.5-4,
              shape:'rect',
              w:margin_x,
              h:margin_y,
              fill:(options.world&&options.world.visual&&options.world.visual.fill)||{color:'black'},
              line:(options.world&&options.world.visual&&options.world.visual.line)||{width:2}
            },obj:this.world,id:id};
    id='world-anchor';
    this.gui.objects[id] = {visual:{
              x:off_x+1,
              y:off_y+1,
              shape:'rect',
              w:margin_x+xn+space_x*1.5,
              h:margin_y+yn+space_y*1.5,
              line:(options.world&&options.world.visual&&options.world.visual.line)||{width:1,color:'#BBB'}
            },obj:this.world,id:id};
  }
}

/** Create a node in the simulation world
 *
 */
simu.prototype.createNode = function (desc,drawnow) {
  var self=this,id,index,
      node,nodeid,port,pos,drawpos,xn,yn,x,y,p,options,chan,addr;
  
  desc.id=desc.id.replace(/\$WORLD/,this.world.id);
  
  function makeRef(x) {return x};
  if (desc.id == undefined) 
    throw 'createNpde: Invalid node descriptor, missing id attribute';
  if (desc.gps) {
    pos=self.world.units.gps2px(desc.gps);
    desc.x=pos.x; desc.y=pos.y; desc.z=pos.z;
  } else if (desc.geo) {
    pos=self.world.units.m2px(desc.geo);
    desc.x=pos.x; desc.y=pos.y; desc.z=pos.z;
  }
  if (desc.x == undefined || desc.y == undefined) 
    throw 'createNode: Invalid node descriptor, missing coordinates';
  
  id = 'node['+desc.id+']';
  if (this.gui.objects[id]) 
    throw 'createNode: Node '+desc.id+' exists already!';
  pos = {x:desc.x,y:desc.y};
  // Patch world model?
  if (desc.gps) pos.gps=desc.gps;
  if (desc.geo) pos.geo=desc.geo;

  node=Aios.Node.Node({id:desc.id,position:pos,parameter:desc.parameter||{}});

  nodeid=node.id;
  if (desc.location) node.location=desc.location;
  this.world.addNode(node);
  if (this.verbose>1) this.log('[SIM] Created node '+node.id);
  xn=this.world2draw(desc).x+checkOption(desc.visual.center && desc.visual.center.x,0);
  yn=this.world2draw(desc).y+checkOption(desc.visual.center && desc.visual.center.y,0);
  
  if (Comp.obj.isString(desc.visual.width)) desc.visual.width=this.convertUnit(desc.visual.width);
  if (Comp.obj.isString(desc.visual.height)) desc.visual.height=this.convertUnit(desc.visual.height);
  if (Comp.obj.isString(desc.visual.radius)) desc.visual.width=desc.visual.height=this.convertUnit(desc.visual.radius)*2;
  this.gui.objects[id] = { 
    visual:{
      x:xn,
      y:yn,
      shape:desc.visual.shape||'rect',
      icon:desc.visual.icon,
      w:desc.visual.width,
      h:desc.visual.height,
      fill:desc.visual.fill,
      line:desc.visual.line,
      align:desc.visual.shape=='circle'?'center':desc.visual.align,
    },
    obj:makeRef(node),
    id:id  
  };
  
  if (desc.visual.label) {
    self.gui.objects['label['+desc.id+']'] = {
      visual:{
        x:xn,
        y:yn-5-(desc.visual.label.fontSize||24),
        shape:'text',
        fontSize:desc.visual.label.fontSize||24,
        color:desc.visual.label.color,
        text:desc.visual.label.text||desc.visual.label,
      },id:'label['+desc.id+']'
    }
    if (drawnow) this.gui.drawObject('label['+desc.id+']');
  }
  if (drawnow) this.gui.drawObject(id);
  
  
  // Node virtual and physical communication ports
  if (desc.port) {
    // backward compatibility
    if (Comp.obj.isArray(desc.port))
      desc.ports = desc.port; 
    else 
      desc.ports = [desc.port];  
  }
  if (desc.ports) for (index in desc.ports) {  
    port=desc.ports[index];
    if (!port) continue;
    // Create a virtual link port object
    switch (port.type) {
      case 'multicast':
        chan = (function (port) {
          var chan = {
            _connected:[],
            _count:0,
            _state:true,
            
            count:function () {return chan._count},
            /** Send agent snapshot to destination node.
             *  'dest' must be a node identifier!
             */
            /* OLDCOMM send: function (data,dest,current) { */
            /* NEWCOMM */
            send: function (msg) {
              var node;
              //console.debug('chan.vc.send',msg)
              if (!chan.status()) return;
              if (Comp.array.contains(chan._connected,msg.to.node /*dest*/)) {
                node=self.world.getNode(msg.to.node /*dest*/);
                if (!node) return;
                //console.debug('chan.vc.send',msg,node)
                if (msg.agent)
                  node.receive(msg.agent /*data*/);
                else if (msg.signal)
                  node.handle(msg.signal);
                chan._count += ((msg.agent||msg.signal).length||1);
                if (msg.context) msg.context.kill=true;                
              } else {
                // Throw exception??
                if (msg.context) msg.context.kill=true;  
              }
            },
            // Enable or disable communication port (default: on)
            getState: function () { return chan._state },
            label:desc.id+(port.id?(':'+port.id):''),
            setState: function (on) { chan._state=on },
            /** Check connection status and/or return available connected or reachable nodes (links).
             *
             */
            status: function (path) {
              // self.log('status state='+chan._state);
              if (!chan._state) {
                chan._connected=[];
                return none;
              }
              // Find all overlapping connections
              var port1='port['+node.id+']',objs;
              // self.log(port1);
              if (port1) {  
                objs=self.gui.overlapObjects(port1);
              }
              // self.log(objs);
              if (port.status) objs=port.status(objs);
              if (objs && objs.length>0) {
                // Return all connected nodes/links
                chan._connected=Comp.array.map(objs,function (o) {
                  return o.replace(/port\[([^\]]+)\]/,'$1');
                });
                return chan._connected;
              } else {
                chan._connected=[];
                return none;
              }
            },
            virtual:true
          }; return chan})(port);
        // TODO multiple ports
        node.connections.path=chan;
        x=xn; y=yn;
        id='port['+chan.label+']';
        if (!port.visual) port.visual={
          w:desc.visual.width*2,
          h:desc.visual.height*2              
        };
        if (port.visual.line && !port.visual.line.width)
          port.visual.line.width=2;
        port.visual.x=x+desc.visual.width/2;
        port.visual.y=y+desc.visual.height/2;
        if (Comp.obj.isString(port.visual.width)) 
          port.visual.width=self.convertUnit(port.visual.width);
        if (Comp.obj.isString(port.visual.height)) 
          port.visual.height=self.convertUnit(port.visual.height);
        if (Comp.obj.isString(port.visual.radius)) 
          port.visual.width=
          port.visual.height=self.convertUnit(port.visual.radius)*2;
        if (Comp.obj.isNumber(port.visual.radius)) 
          port.visual.width=
          port.visual.height=port.visual.radius*2;
        self.gui.objects[id] = {visual:{
            x:port.visual.x,
            y:port.visual.y,
            shape:port.visual.shape||'circle',
            w:port.visual.width,
            h:port.visual.height,
            fill:port.visual.fill,
            line:port.visual.line
          },obj:chan,id:id}
        if (drawnow) this.gui.drawObject(id);
        break;
        
      case 'physical':
        // Physical communication ports
        options={verbose:1,proto:'http',multicast:true};
        if (port.verbose!=undefined) options.verbose=port.verbose;
        if (port.proto != undefined) options.proto=port.proto;
        if (port.multicast != undefined) options.multicast=port.multicast;
        if (port.broker) options.broker=port.broker;
        if (options.broker) options.multicast=false;

        if (port.port) addr=port.ip+':'+port.port; else addr=port.ip;
        options.from=addr;
        this.log('[SIM] Creating physical port IP('+addr+') '+Io.inspect(options)+' on node '+node.id);
        chan=this.world.connectPhy(
                Aios.DIR.IP(addr),
                node,
                {
                  broker:options.broker,
                  multicast:options.multicast,
                  name:options.name,
                  on:options.on,
                  oneway:options.oneway,
                  proto:options.proto,
                  rcv:options.from,
                  snd:options.to,
                  verbose:options.verbose
                });
        chan.init();
        chan.start();
        node.connections.ip.chan=chan;
        if (port.to) this.world.connectTo(Aios.DIR.IP(port.to),node);
        break;
    }
    
  }
  if (this.verbose>1) this.log('[SIM] Created node '+node.id);
  return nodeid; 
}
/** Create a regular (or irregular) 2d patch grid of nodes and connections.
 *  type of options = {
                       width: number as node width in pixel,
                       height: number as node heigth in pixel,
                       visual?,  // if not set, model.patches.default is required
                       rows:number,cols:number,
                       floating?:boolean,  // if false/undef. grid consists of logical nodes, else resources
                       }
 */
simu.prototype.createPatchWorld = function (model,resources) {
  var self=this,
      i,j,row,
      options=model.world.patchgrid,
      patch = options.visual || (model.patches && model.patches.default && model.patches.default.visual);
  if (!model.nodes || !model.nodes.world)
    this.err('Incomplete patchgrid model (requires model.nodes.world); no world node found'); 
 
  if (patch) options.visual = patch;
   
  var width     = (options.visual&&options.visual.width)||options.width||10,
      height    = (options.visual&&options.visual.height)||options.height||10,
      margin_x  = 0,
      margin_y  = 0,
      off_x     = 0,
      off_y     = 0,
      xn,yn,x,y,w,h,p,obj,obj2,link,
      node,world,node1,node2,place,port,ports={},
      id,i,j,k,level=0,dim=2,
      xMoff=0,yMoff=0;

  function makeRef(o) {return o};
  function vec2str(vec) { return vec.join(',') }
  
  this.out('Creating patchgrid network with '+options.rows+
           ' rows and '+options.cols+
           ' columns');

  this.options.x=options.cols;
  this.options.y=options.rows;
  
  // A world map for fast search of physical agents(nodes)
  this.agentMap = [];
  // A world map for fast search of physical resources 
  this.resourceMap = [];
  
  for (j=0;j<options.rows*2;j++) {
    row=[];
    for(i=0;i<options.cols*2;i++)
      row.push({})
    this.agentMap.push(row);
  }  
  for (j=0;j<options.rows*2;j++) {
    row=[];
    for(i=0;i<options.cols*2;i++)
      row.push({})
    this.resourceMap.push(row);
  }  
  // Patches parameter variables
  this.patches = [];
  for (j=0;j<options.rows;j++) {
    row=[];
    for(i=0;i<options.cols;i++)
      row.push({x:i,y:j})
    this.patches.push(row);
  }  
  // Draw resources before any other objects
  // Resources have currently patch grid coordinates needing transformation!
  resources.forEach(function (r) {
      if (r.visual.shape == 'circle') r.visual.x += r.visual.width/2, r.visual.y += r.visual.height/2
      r.visual.x *= width;  r.visual.y *= height; 
      if (r.visual.width)   r.visual.width *= width;
      if (r.visual.height)  r.visual.height *= height;
  })
  this.options.patch = {rows:options.rows, cols:options.cols, width:width, height:height}

  // 1. Patches   
  if (patch) {
    this.createResources(resources);
    
    for(j=0;j<options.rows;j++) {
      for(i=0;i<options.cols;i++) {
        id=vec2str([i,j]);
        if (!options.floating) {
          // patch == node
          node=Aios.Node.Node({id:id,
                               position:{x:i,y:j},
                               TMO:self.options.TMO||1000000
                              });
          if (this.verbose>1) this.log('[SIM] Created node '+node.id);
          this.world.addNode(node);
        }  // else patch == resource
        if (options.floating) id = 'patch['+id+']'; else id='node['+id+']';
        xn=off_x+margin_x+i*width+xMoff;
        yn=off_y+margin_y+j*height+yMoff;
        this.gui.objects[id] = { visual:{
            x:xn,
            y:yn,
            shape:options.visual.shape||'rect',
            w:width,
            h:height,
            fill:options.visual.fill,
            line:options.visual.line,
          },
          obj:{parameter:{}},
          id:id
        }
      }
    }
  } else {
    // Default patchgrid visual: frame boundary + grid pattern
    xn=off_x+margin_x+xMoff;
    yn=off_y+margin_y+yMoff;
    id='patch[world]';
    this.gui.objects[id] = { visual:{
        x:xn,
        y:yn,
        shape:'rect',
        pattern:{
          shape:'rect',
          w:options.width,
          h:options.width,
          color:"1 #ddd 0.9"
        },
        w:width*options.cols,
        h:height*options.rows,
        fill:null,
        line:{color:'#888'},
      },
      obj:{parameter:{},patches:self.patches},
      id:id
    }
    // Draw resources before any other objects
    this.createResources(resources);
    
  }

}


/** Create a physical communication port
 *
 */
simu.prototype.createPort = function (dir,options,nodeid) {
  if (!options) options={};
  var multicast=options.multicast||true;
  if (dir.tag != Aios.DIR.tag.IP)  {}
  if (options.from==undefined && dir.ip) options.from=dir.ip.toString();
  var  chan=this.world.connectPhy(
            dir,
            this.getNode(nodeid),
            {
              broker:options.broker,
              multicast:multicast,
              name:options.name,
              on:options.on,
              oneway:options.oneway,
              proto:options.proto||'udp',
              rcv:options.from,
              snd:options.to,
              verbose:options.verbose||this.verbose
            });
  chan.init();
  chan.start();
  return chan;
}

/** Create unit conversion functions from a map object
 *
 */
simu.prototype.createUnits = function (map) {
  /*
  **  0---x   latitude           Y
  **  | PX    |  GPS             | GEO
  **  | GUI   |                  | 
  **  y       +----- longitude   0-----X 
  */
  // Geographic units using map parameters {offset,scale}
  this.world.units.gps2px = function (gps) {
    if (!gps.delta) {
      if (!map.area) area={x:0,y:0};
      else if (!map.area.selector) area={x:map.area.x||0,y:map.area.y||0};
      else area=map.area[map.area.selector(gps)];
      return {
          x:area.x+((gps.longitude-map.longitude.offset)*map.longitude.scale)|0,
          y:area.y-((gps.latitude-map.latitude.offset)*map.latitude.scale)|0, 
          z:gps.height*(map.height?map.height.scale:1)
      };
    } else
      // delta coord.
      return {
       x:(gps.longitude*map.longitude.scale)|0,
       y:-(gps.latitude*map.latitude.scale)|0,
       z:gps.height*(map.height?map.height.scale:1)
      };
  };
  this.world.units.px2gps = function (px) {
    if (!px.delta) {
      if (!map.area) area={x:0,y:0};
      else if (!map.area.selector) area={x:map.area.x||0,y:map.area.y||0};
      else area=map.area[map.area.selector(px)];
      return {
        latitude:  -(px.y-area.y)/map.longitude.scale+map.longitude.offset,
        longitude: (px.x-area.x)/map.latitude.scale+map.latitude.offset            
      }
    } else
      // delta;
      return {
        latitude: -px.y/map.longitude.scale,
        longitude: px.x/map.latitude.scale
      }
  };
  this.world.units.gps2geo = function (gps) {
    // Only for delta vectors
    if (gps.delta) return {
      X:gps.longitude*111320,
      Y:gps.latitude*111320,
      Z:gps.height
    }
  },
  this.world.units.geo2gps = function (geo) {
    // Only for delta vectors
    if (geo.delta) return {
      latitude: geo.Y/113200,
      longitude: geo.X/113200,
      height: geo.Z
    }
  };

  // Geometric units using map parameters {offset,scale}
  this.world.units.geo2px = function (geo) {
    if (geo.delta) return {
      x:(geo.X)*(map.X.scale||1),
      y:(geo.Y)*(map.Y.scale||1),
      z:(geo.Z)*(map.Z.scale||1)
    }; else return {
      x:(geo.X-map.X.offset||0)*(map.X.scale||1),
      y:(geo.Y-map.Y.offset||0)*(map.Y.scale||1),
      z:(geo.Z-map.Z.offset||0)*(map.Z.scale||1)
    } 
  };
  this.world.units.px2geo = function (px) {
    if (px.delta) return {
      X:px.x/(map.X.scale||1),
      Y:px.y/(map.Y.scale||1),
      Z:px.z/(map.Z.scale||1)
    }; else return {
      X:px.x/(map.X.scale||1)+(map.X.offset||0),
      Y:px.y/(map.Y.scale||1)+(map.Y.offset||0),
      Z:px.z/(map.Z.scale||1)+(map.Z.offset||0)
    }
  };
  function rad2deg (rad) { return rad/Math.PI*180.0 };
  function deg2rad (deg) { return deg/180.0*Math.PI };
  
  // Computer distance and angle direction between two GPS locations
  this.world.units.gps2dist = function (gps1,gps2) {
    var dy=(gps2.latitude-gps1.latitude)*111320,
        dx=(gps2.longitude-gps1.longitude)*111320,
        dz=(gps2.height-gps1.height),
        angle = rad2deg(Math.atan2(dy, dx));
    // Convert to 0\B0: N, 90\B0 : E, 180\B0 : S, 270\B0 : W 
    if (angle >=0 && angle <= 180) angle=270-angle;
    else if(angle > -90) angle=-angle+270;
    else angle=-angle-90;
     
    return {dx:dx,dy:dy,dz:dz,angle:angle}
  }
  
  this.world.units.rad2deg = rad2deg;
  this.world.units.deg2rad = deg2rad;
  this.aiosX.units=this.world.units;
}

// Draw resources
simu.prototype.createResources = function (resources) {
  var self=this,xi,yi,wi,hi;
  if (resources.length) this.log('Creating '+resources.length+' resource(s)..');
  Comp.array.iter(resources,function (r) {      
    if (r.id != undefined && r.visual) {
      id='resource['+r.id+']';
      self.gui.objects[id] = {
        visual:{
          x:r.visual.x,
          y:r.visual.y,
          shape:r.visual.shape||'circle',
          w:r.visual.width,
          h:r.visual.height,
          fill:r.visual.fill,
          line:r.visual.line
        },obj:r.data,id:id
      }
      if (r.data) self.gui.objects[id].data=r.data;
      if (r.class) self.gui.objects[id].class=r.class;
      if (self.options.patch) {
        xi=int(r.visual.x/self.options.patch.width);
        yi=int(r.visual.y/self.options.patch.height);
        wi=int(r.visual.width/self.options.patch.width);
        hi=int(r.visual.height/self.options.patch.height);
        if (yi>self.options.patch.rows || xi>self.options.patch.cols) 
          throw Error('createResources: invalid visual patch coordinates ('+xi+','+yi+')');
        self.resourceMap[yi][xi][id]=self.gui.objects[id];
        self.rtree.insert({x:xi, y:yi, w:wi, h:hi},id);
      }
      id='label['+r.id+']';
      if (r.visual.label) self.gui.objects[id] = {
        visual:{
          x:r.visual.x+10,
          y:r.visual.y+10,
          shape:'text',
          fontSize:r.visual.label.fontSize||24,
          color:r.visual.label.color,
          text:r.visual.label.text||r.visual.label,
        },id:id
      }
    }      
  });    
}


/** Create the simulation world
 *
 * type of model = { name, classes, resources?, world}
 * type of world = { init?, map?, nodes?, meshgrid?, patchgrid?, resources?, units? }
 * type of units = { map?, gps2px?, px2gps?, geo2px?, px2gep? }
 *
 *
 *
 */
simu.prototype.createWorld = function (model) {
  var margin_x = 5,
      margin_y = 5,
      off_x = 5,
      off_y = 5,      
      node,
      self=this,
      id,i,j,p,res,pos,
      map,area;
  self.model=model;
  try {
    // Check model ..
    if (model.agents) {
      for (p in model.agents) {
        if (!model.agents[p].visual) throw "Invalid model.agents."+p+": missing visual";
        if (res=this.checkVisual(model.agents[p].visual)) throw "Invalid model.agents."+p+".visual: "+res+")";
      }
    } 
    
    
    
    if (model.world.units) this.world.units=model.world.units;
    if (model.world.units && model.world.units.map) this.createUnits(model.world.units.map);

    // Draw resources before any other objects (lowest z level)
    // model.world.resources returns array of simulation object descriptors
    var resources = model.world.resources?model.world.resources(model):[];

    if (!Comp.obj.isArray(resources))   
      throw ('odel.world.resources returned '+(typeof resources)+', expected array');
    
    if (!model.world.patchgrid) this.createResources(resources);
    if (model.world.meshgrid)   this.createMeshNetwork(model);
    if (model.world.patchgrid)  this.createPatchWorld(model,resources);
    if (model.world.nodes && typeof model.world.nodes == "function") 
      model.world.map=model.world.nodes; // synonym
    if (model.world.map) {
      // use a mapping function and node descriptors {id,x,y,link,visual}
      Comp.array.iter(model.world.map(model), function (desc,i) {
          self.createNode(desc);
      });
    }
  } catch (e) {
    this.log('Failure in createWorld: '+e);
    if (this.gui.options.verbose) this.log(e.stack);
  }
}

// Delete a JAM node and visuals
simu.prototype.deleteNode = function (nodeid) { try {
  var objid = 'node['+nodeid+']',
      node = this.world.getNode(nodeid),
      objs,p,port;

  this.gui.destroyObject(objid);
  // Remove ports of deleted node
  if (node) {
    for(p in {NORTH:1,SOUTH:1,WEST:1,EAST:1,UP:1,DOWN:1}) {
      if (node.connections[p.toLowerCase()])
        this.gui.destroyObject('port['+nodeid+':'+p+']');
    }
    if (node.connections.path) {
      this.gui.destroyObject('port['+node.connections.path.label+']');
    }  
    if (node.connections.ip && node.connections.ip.chann) {
      // Stop physical link
      node.connections.ip.chann.stop();
    } 
    this.world.removeNode(nodeid);
    node.destroy();
  } else if (this.gui.options.verbose) this.log('deleteNode: No node found for id '+nodeid);
  
  // Remove links to deleted node
  objs=this.gui.findObjects('link',nodeid);
  // console.debug('deleteNode',objid,node,objs)
  for(p in objs) {
    this.gui.destroyObject(p);
  }
  // Cleanup links of ports currently linked with other nodes
  objs=this.gui.findObjects('port','.+');
  for(p in objs) {
    port=objs[p].obj;
    if (port.link && 
        ((port.link.node1 && port.link.node1.id==nodeid)||
         (port.link.node2 && port.link.node2.id==nodeid))) port.link=undefined;
  }
  if (this.verbose>1) this.log('[SIM] Deleted node '+nodeid);
  } catch (e) { console.log(e) }
}

/** Destroy the simulation world: Destroy agents, AIOS nodes, and AIOS world.
 *
 */
simu.prototype.destroy = function () {
  var n,p;
  for(n in this.world.nodes) {
    if (!this.world.nodes[n]) continue; // ??
    this.gui.objects[this.world.nodes[n].id]=undefined;
    if (this.world.nodes[n].connections.ip) this.world.nodes[n].connections.ip.chan.stop(); 
    this.world.nodes[n].destroy();
  }
  if (this.model.world && this.model.world.init && this.model.world.init.physics) {
    if (this.simuPhy) 
      this.gui.destroySimuPhy();
    this.simuPhy=none;
  }
  this.world.nodes=none;
  this.world=none;
  this.model=none;
  if (this.Db) for(p in this.Db) this.Db[p].close();
  this.Db=none;
  this.log('[SIM] Simulation world destroyed');  
}
/** Return agent object (or array if ais is a regex) referenced by logical node number, position, or name,
 ** and agent identifier.
 *  If @id is undefined return current node object.
 */
simu.prototype.getAgent = function (id,aid) {
  var node = this.getNode(id);
  if (node) return node.getAgent(aid);
}
simu.prototype.getAgentProcess = function (id,aid) {
  var node = this.getNode(id);
  if (node) return node.getAgentProcess(aid);
}


/** Return node object referenced by logical node number, position, or name
 *  If @id is undefined return current node object.
 */
simu.prototype.getNode = function (id) {
  var node;
  if (id==undefined) return this.world.nodes[0];
  if (typeof id == 'number') 
    node=this.world.nodes[id];
  else if (typeof id == 'string') {
    // Search node identifier;
    node = this.world.getNode(id);
  } else if (id.x != undefined && 
             id.y != undefined) {
    // Search node position;
    loop: for(var i in this.world.nodes) {
      if (this.world.nodes[i] && Comp.obj.equal(this.world.nodes[i].position,id)) {
        node = this.world.nodes[i];
        break loop;
      } 
    }
  }
  
  return node;
} 

/** Return process object referenced by logical node number, position, or name,
 ** and agent identifier.
 *  If @id is undefined return current node object.
 */
simu.prototype.getProcess = function (id,aid) {
  var node = this.getNode(id);
  if (node) return node.getAgentProcess(aid);
}

/** Return statistics
 *  type stats = {steps,time,agents:{},agentsN:{},all;number}
 */
 
simu.prototype.getStats = function () {
  return this.aiosX.getStats();
}
/** Initialize and create simulation world
 *
 */
simu.prototype.init = function (model) {  
  var a,n,c,params,proc,count;
 try {  
  this.out('Initializing ...');
  
  if (model.parameter) this.parameter=model.parameter;
  // 1. AC COMPILING
  if (model.agents) model.classes = model.agents; // synonym for classes
  else if (model.classes) model.agents = model.classes;
  if (model.classes) 
    for(c in model.classes) {
      if (!model.classes[c].behaviour) 
        this.out('Error: Agent class "'+c+'" has no behaviour!');
      else {
        this.out('Compiling agent class template "'+c+'"...');
        this.addClass(model.classes[c].behaviour,c,model.classes[c].visual);
      }
    }
  // 2. CREATE World
  
  this.createWorld(model);
  
  // 3. START AGENTS
  if (model.world && model.world.init && model.world.init.agents) {
    for (a in model.world.init.agents) {
      if (!Comp.obj.isFunction(model.world.init.agents[a]))
        this.out('Error: Init: "'+a+'" is not a function!');
      else if (!this.world.classes[a])
        this.out('Error: Init: Agent class "'+a+'" is not registered!');      
      else {
        this.out('Creating agents of class "'+a+'" ..');
        count=0;
        for(n in this.world.nodes) {
          params=model.world.init.agents[a](this.world.nodes[n].id,
                                            this.world.nodes[n].position,
                                            // can be defined in model.world.nodes(desc)
                                            this.world.nodes[n].options.parameter);
          if (params) {
            if (params.level == undefined) params.level=1;
            else if (params.level<0 || params.level>3)
              this.out('Error: Init: Invalid agent level '+params.level+' for agent class "'+a+'".');
            else {
              proc=Aios.Code.createOn(this.world.nodes[n],
                                      this.world.classes[a][params.level],
                                      params.args,params.level,a);
              count++;
            }
          }
        }        
        this.out('Created '+count+' '+a+' agents.');
      }
    }
  }

  // 4. CREATE PHYSICAL WOLRD - IF ANY  
  if (model.world.init && model.world.init.physics) {
    if (!this.simuPhy) 
      this.simuPhy=this.gui.createSimuPhy();
    model.world.init.physics(this.simuPhy);
  }
  this.out('Ready.');
 } catch (e) {
    this.out('Error: '+e);
    if (this.verbose) this.out(Io.sprintstack(e));  
 }
}

/** Move an object (node or resource) relative to its current position.
 *  type of dx,dy,dz = number is normalized pixel units | string (number + 'm' | '\B0')
 *
 *
 */
simu.prototype.moveObject = function(objid,dx,dy,dz) {
  var id,obj,_dx,_dy,_dz,isnode=false;
  if (dx && Comp.obj.isString(dx)) {
    if (Comp.string.endsWith(dx,'\B0') && this.world.units.gps2px)
      _dx=this.world.units.gps2px({delta:true,longitude:Number(Comp.string.prefix(dx,'\B0'))}).x;
    else if (Comp.string.endsWith(dx,'m') && this.world.units.geo2px)  
      _dx=this.world.units.geo2px({delta:true,X:Number(Comp.string.prefix(dx,'m'))}).x;
    else 
      _dx=Number(dx);
  } else _dx=dx;
  if (dy && Comp.obj.isString(dy)) {
    if (Comp.string.endsWith(dy,'\B0') && this.world.units.gps2px) 
      _dy=this.world.units.gps2px({delta:true,latitude:Number(Comp.string.prefix(dy,'\B0'))}).y;
    else if (Comp.string.endsWith(dy,'m') && this.world.units.geo2px) 
      _dy=this.world.units.geo2px({delta:true,Y:Number(Comp.string.prefix(dx,'m'))}).y;
    else 
      _dy=Number(dy);  
  } else _dy=dy;
  if (dz && Comp.obj.isString(dz)) {
    if (Comp.string.endsWith(dz,'m') && this.world.units.geo2px) 
      _dz=this.world.units.geo2px({delta:true,Z:Number(Comp.string.prefix(dz,'m'))}).z;
    else 
      _dz=Number(dz);  
  } else _dz=dz;
  
  if (Comp.string.startsWith(objid,'node')) {
    id=objid.replace(/node\[([^\]]+)\]/,'$1');
    obj=this.world.getNode(id);
    isnode=true;
  } else if (Comp.string.startsWith(objid,'resource')) {
    // TODO, use .data
  }
  if (!obj) return;
  
  if (obj && obj.position) {    
    // Assumption: x/y/z are normalized pixel and linear coordinates!
    if (obj.position.x != undefined && _dx) obj.position.x += _dx;
    if (obj.position.y != undefined && _dy) obj.position.y += _dy;
    if (obj.position.z != undefined && _dz) obj.position.z += _dz;
    if (obj.position.gps && this.world.units.px2gps) {
      if (_dx) obj.position.gps.latitude += this.world.units.px2gps({delta:true,y:_dy}).latitude;
      if (_dy) obj.position.gps.longitude += this.world.units.px2gps({delta:true,x:_dx}).longitude;
      if (_dz) obj.position.gps.height += this.world.units.px2gps({delta:true,z:_dz}).height;
    }
  } 
  if (isnode) this.gui.moveObject(objid,_dx,_dy,'agent','port');
  else this.gui.moveObject(objid,_dx,_dy);
}

/** Move an object (node or resource) to new absolute position.
 *  type of x,y,z = number is normalized pixel unit | string (number + 'm' | '\B0')
 *
 *
 */
simu.prototype.moveObjectTo = function(objid,x,y,z) {
  var id,obj,_px,_gps,isnode=false;
  if (Comp.obj.isString(x) &&  Comp.obj.isString(y)) {
    if (Comp.string.endsWith(x,'\B0') && Comp.string.endsWith(x,'\B0') && this.world.units.gps2px) {
      _gps={latitude:Number(Comp.string.prefix(x,'\B0')),longitude:Number(Comp.string.prefix(y,'\B0')),height:Number(z)};
      _px=this.world.units.gps2px(_gps);      
    } else if (Comp.string.endsWith(x,'m') && Comp.string.endsWith(y,'m') && this.world.units.geo2px)
      _px=this.world.units.geo2px({X:Number(Comp.string.prefix(x,'m')),Y:Number(Comp.string.prefix(y,'m')),Z:Number(z)});
    else 
      _px={x:Number(x),y:Number(y),z:Number(z)};
  } else _px={x:x,y:y,z:z};
  
  if (Comp.string.startsWith(objid,'node')) {
    id=objid.replace(/node\[([^\]]+)\]/,'$1');
    obj=this.world.getNode(id);
    isnode=true;
  } else if (Comp.string.startsWith(objid,'resource')) {
    // TODO, use .data
  }
  if (!obj) return;
  
  if (obj && obj.position && !this.options.patch) {
    if (obj.position.x != undefined) obj.position.x = _px.x;
    if (obj.position.y != undefined) obj.position.y = _px.y;
    if (obj.position.z != undefined) obj.position.z = _px.z;
    if (obj.position.gps && _gps) {
      obj.position.gps.latitude = _gps.latitude;
      obj.position.gps.longitude = _gps.longitude;
      obj.position.gps.height = _gps.height;
    }
  } 
  if (isnode) this.gui.moveObjectTo(objid,_px.x,_px.y,'agent','port');
  else this.gui.moveObjectTo(objid,_px.x,_px.y);
}

// Set simulation delay between two steps
simu.prototype.setDelay = function (ms) {
  this.delay=ms;
}

/** Set simulation parameter (accessible by agents using simu.paramater(name))
 *
 */
simu.prototype.setParameter = function (name,value) {
  this.parameter[name]=value;
}

/** Run or step simulation
 */
simu.prototype.simulate = function (steps,callback) {
  var self=this,
      realtime=this.options.time=='real',
      milliTime=function () {return Math.ceil(Date.now())},
      start,stop,timeReal,
      lazy=this.gui.options.display.lazy,suspend=false,
      curtime=realtime?milliTime():Aios.time();
  var lasttime=curtime;
  this.stepped=steps;
  
  start=curtime;
  if (realtime && this.time>0) current.world.lag=current.world.lag+(start-this.time);
  else if (this.time>0) this.lag=this.lag+(start-this.time);
  this.time0=start;
  this.time=start;
  this.run=true;
  if (this.verbose) this.log('[SIM] Stepping simulation ('+steps+') at '+this.step+ ' .. (t '+(start-current.world.lag-this.lag)+' ms)');
  function starting () {
    if (self.model && self.model.world && self.model.world.start) try { self.model.world.start({
      log:self.log.bind(self),
      time:Date.now(),
      step:self.step,
      simtime:curtime,
      model:self.model,
      cpu:self.stats.cpu,
    }) }  catch (e) { self.gui.log('model.world.start: '+e.toString())};
  }
  function stopping () {
    if (self.gui.worldWin.container && suspend) { // enable world win updates
        suspend=false; self.gui.worldWin.container.resume() };
    self.gui.updateGui(true);
    if (self.model && self.model.world && self.model.world.stop) try { self.model.world.stop({
      log:self.log.bind(self),
      time:Date.now(),
      step:self.step,
      simtime:curtime,
      model:self.model,
      cpu:self.stats.cpu,
    }) }  catch (e) { self.gui.log('model.world.stop:'+e.toString())};
  }
  // self.but2.setStyle({bg:'red'});
  Aios.config({iterations:1});
  var loop = function () {
        if (self.gui.worldWin.container && !suspend) { // disable world win updates
          suspend=true; self.gui.worldWin.container.suspend() };
          
        // Execute JAM scheduler
        timeReal=Date.now();

        var nexttime=Aios.scheduler();

        self.stats.cpu += (Date.now()-timeReal);
        
        if (self.gui.worldWin.container && !lazy && suspend) { // enable world win updates
          suspend=false; self.gui.worldWin.container.resume() };
          
        if (self.gui.logging) self.gui.logTable[self.step]=self.getStats();
        if (self.cleanWorld) self.cleanWorld=self.gui.cleanWorld()>0;
        self.step++;
        if (self.stepped>0) self.stepped--;
        
        self.time=curtime=Aios.time();
        if (!self.run) {
          stop=realtime?milliTime():Aios.time();
          //self.log(stop+','+current.world.lag+','+self.lag+','+curtime+','+milliTime());
          if (self.verbose) self.log('[SIM] Stopping simulation at '+self.step+ ' .. (d '+(stop-self.time0)+' ms / t '+(stop-current.world.lag-self.lag)+' ms): Stopped.');
          self.time=curtime;
          if (callback) callback();
          stopping();
          return;
        }
        if (self.stepped==0) {
          stop=realtime?milliTime():Aios.time();
          self.run=false;
          //self.but2.setStyle({bg:'blue'});
          if (self.verbose) self.log('[SIM] Stopping simulation at '+self.step+ ' .. (d '+(stop-self.time0)+' ms / t '+(stop-current.world.lag-self.lag)+' ms): No more steps.');
          self.time=stop;
          if (callback) callback();
          stopping();
          return;      
        }
        if (nexttime>0) self.loop=setTimeout(loop,(self.options.time=='real'?nexttime:self.delay));   // TODO: resonable timeout if nexttime is in steps!!!
        else if (nexttime<0 && !self.delay) self.loop=setImmediate(loop);
        else if (nexttime<0) self.loop=setTimeout(loop,self.delay);
        else {
          // self.but2.setStyle({bg:'blue'});
          stop=realtime?milliTime():Aios.time();
          self.run=false;
          if (self.verbose) self.log('[SIM] Stopping simulation at '+self.step+ ' .. (d '+(stop-self.time0)+' ms / t '+(stop-current.world.lag-self.lag)+' ms): Nothing to run.');
          self.time=curtime;
          if (callback) callback();
          stopping();
          return;
        }
        
        if ((curtime-lasttime)>100) {
          if (self.gui.worldWin.container && lazy && suspend) { // enable world win updates
              suspend=false; self.gui.worldWin.container.resume() };
          self.gui.updateGui(true);
          lasttime=curtime;
        } else 
          self.gui.updateGui(false);
  }
  starting();
  this.loop = setTimeout(loop,0);
}

/** Start simulation 
 *  steps: number of simulation steps
 *  temp: temporary step
 *
 */
simu.prototype.start = function (steps,temp) {
  var self=this;
  if (steps<=0 || (temp && this.stopped)) return;
  if (!temp) {
    this.stopped=false;
    this.gui.UI('simulationWinButStep').disable();
    this.gui.UI('simulationWinButPlay').disable();
    this.gui.UI('simulationWinButStop').enable();
  } 
  setImmediate(function () {
    self.simulate(self.stepped|steps,function () {
      if (self.stepped==0) {
        this.stopped=true;
        self.gui.UI('simulationWinButStep').enable();
        self.gui.UI('simulationWinButPlay').enable();
        self.gui.UI('simulationWinButStop').disable(); 
      } 
    });
  });
}

/** Stop simulation, return remaining steps (only in temporary mode)
 *
 */
simu.prototype.stop = function (temp) {
  this.run=false;
  if (this.loop) clearTimeout(this.loop);
  this.loop=null;
  if (this.gui.worldWin.container) this.gui.worldWin.container.resume()
  if (temp && !this.stopped)
    return this.stepped-1;
  else {
    this.stepped=0;
    this.stopped=true;
    return 0;
  }
}

/** convert simulation world to graphics coordinates 
 */
simu.prototype.world2draw =  function (p) {
  if (this.options.patch) return {x:p.x*this.options.patch.width,
                                  y:p.y*this.options.patch.height};
  else return p;
}

var Simu = function(options) {
  var obj=none;
  obj = new simu(options);
  return obj;
}

module.exports = {
  simu:simu,
  Simu:Simu,
  current:function (module) { current=module.current; Aios=module; JamAnal.current(module)}
}
