/**
 **      ==============================
 **       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-2017 bLAB
 **    $CREATED:     29-3-16 by sbosse.
 **    $VERSION:     1.5,1
 **
 **    $INFO:
 **
 **  JAM Standalone Node VM with DOS: WEB edition
 **
 **    $ENDOFINFO
 */

global.config={
  simulation:false,
  dos:true
};

var Io = Require('com/io');
var Comp = Require('com/compat');
var Name = Require('com/pwgen');

var Net = Require('dos/network');
var Buf = Require('dos/buf');
var Sch = Require('dos/scheduler');
var Conn = Require('dos/connection');
var Rpc = Require('dos/rpc');
var Std = Require('dos/std');
var Router = Require('dos/router');
var assert = Comp.assert;
var String = Comp.string;
var Array = Comp.array;
var Perv = Comp.pervasives;
var Printf = Comp.printf;
var Filename = Comp.filename;
var Obj = Comp.obj;
var Args = Comp.args;
var Status = Net.Status;
var Command = Net.Command;
var Dns = Require('dos/dns');
var Cs = Require('dos/capset');
var Getenv = Require('com/getenv');
var HostSrv = Require('dos/hostsrv');
var Run = Require('dos/run');
var RunSrv = Require('dos/runsrv');
var Esprima = Require('parser/esprima');
var Json = Require('jam/jsonfn');
var satelize = Require('dos/ext/satelize');
//var FileReader = Require('os/FileReader');
//var FileSaver = Require('os/FileSaver');

var Db = Require('db/db');
var Aios = Require('jam/aios');
var Dios = Require('dos/dios');

var nameopts = {length:8, memorable:true, lowercase:true};

var out = function (msg) { Io.out('[JAM] '+msg)};

/* ----------------------- */

var jamnet = function (_options) {
  var self=this;
  this.options = {
    bip:'localhost',
    bport:3001,
    broker:false,
    default:true,
    env:{},
    geo:undefined,
    http:false,
    links:[],
    myip:'localhost',
    monitor:0,
    out:function (msg) {Io.out(msg)},
    err:function (msg) {Io.out(msg)},
    warn:function (msg) {Io.out(msg)},
    verbose:0
  };
  for (p in _options) this.options[p]=_options[p];
  
  this.world = this.options.world; // name only!
  this.out=function (msg) { (self.options.out||Io.out)('[NET] '+msg)};
  this.err=function (msg) { throw ('[NET] Error: '+msg)};
  this.warn=function (msg) { (self.options.out||Io.out)('[NET] Warning: '+msg)};

  this.broker=this.options.http;
  this.bport=this.options.bport||3001;
  this.bip=this.options.bip;

  this.scheduler=Sch.TaskScheduler();
  
  this.todo=[];
  this.exit = [];
  
  this.env=this.options.env;

  satelize.satelize({}, function(err, geoData) {
      // process err
      if (err != undefined) {
          self.out('GEO Location failed: '+err)
      } else if (geoData) {
        try {
          var obj = JSON.parse(geoData);
          self.out('GEO Location (lati=' + obj.lat + ', long=' + obj.lon + ')');
          self.options.geo=obj;
        } catch (e) {
          if (self.options.verbose>1) myJam.out('GEO Location failed: '+e+',\n'+geoData);
          else self.out('GEO Location not available: '+e);
        }
      }
    });
  
  this.out('Network with host port '+Net.Print.port(this.options.pubhostport)+
           ' created for world '+this.world+'.');
  this.run=false;

  
}

jamnet.prototype.init=function () {
  var self=this;    
  this.out('Initializing ..');
  if (this.broker) {
    this.network  = Conn.setup(this.options);
    this.router   = this.network.router;
    this.rpc      = this.network.rpc;
    this.std      = Std.StdInt(this.rpc,this.env);
    this.dns      = Dns.DnsInt(this.rpc,this.env);
    this.cs       = Cs.CsInt(this.rpc,this.env);
    this.hostsrv  = none; // requires router init., created on initialization
    Aios.current.network = this.network;
  }
  this.scheduler.Init();
}

jamnet.prototype.start=function () {
  this.run=true;
  this.out('Starting ..');
  // Start up the network ..
  Sch.ScheduleBlock([this.network.init]);
  // this.out('Starting ..');
  this.scheduler.Run();
}

jamnet.prototype.status=function () {
  return this.router?this.router.status():'Not initialized.';
}

jamnet.prototype.stop=function () {
  this.run=true;
  this.out('Stopping ..');
  this.network.stop();
}


var JamNet = function(options) {
  var jamNet;
  jamNet = new jamnet(options);
  return jamNet;
};


/* ----------------------- */

var jam = function (_options) {
  var main=this,
  options = {
    default:true,
    domain:'default',
    env:{},
    geo:undefined,
    hostname:'localhost',
    hostport:undefined,
    nodename:Name.generate(nameopts),   // pre-guess
    onexit:false,
    out:function (msg) {Io.out(msg)},
    start:false,
    verbose:0
  };
  for (p in _options) options[p]=_options[p];
  
  this.options = options;
  this.env=this.options.env;
  this.verbose=options.verbose||0;

  this.out=function (msg) { (options.out||Io.out)('[JAM '+main.nodename+'] '+msg)};
  this.err=function (msg) { throw ('[JAM '+main.nodename+'] Error: '+msg)};
  this.warn=function (msg) { (options.out||Io.out)('[JAM '+main.nodename+'] Warning: '+msg)};
  
  if (!this.options.id) this.id=Aios.aidgen();
  else this.id=options.id;

  this.world = options.world||Aios.World.World([],{
    id:this.options.id,
    classes:options.classes||[]
  });
  this.node = Aios.Node.Node({
    id:this.options.id,
    out:this.out,
    position:{x:0,y:0}
  });
  this.world.add(this.node);

  this.run=false;
  this.looprun=none;
  
  this.network=options.network;
  if (this.network) this.rpc=this.network.rpc;
  if (this.network) this.dios = Dios.Dios(this.network.rpc,this.network.env);
  if (options.scheduler) this.scheduler=options.scheduler;

  // Register a DOS link-connection for agent migration  
  if (this.network) this.node.connections.dos = {
    send: function (text,dest,context) {
      main.node.connections.dos.count += text.length;
      if (Obj.isObject(dest)) // cap
      {
        var stat;
        // This schedule block must by passed to the global (DOS) scheduler!!
        Sch.B([
          function () {
            main.network.run.ps_migrate(dest,text,function (_stat) {              
              stat=_stat;
            });
          },
          function () {          
            if (stat!=Net.Status.STD_OK) {
              // context???
              context.error='Migration to server '+Net.Print.capability(dest)+' failed: '+Net.Print.status(stat);
              // We're still in the agent process context! Throw an error for this agent ..
              throw 'MOVE';              
            };
            
            // kill ghost agent
            context.process.finalize();
          }  
        ]);      
      } else if (Obj.isString(dest)) { // path
      
      }
    },
    status: function () {
      // TODO
      return main.network.status()==Net.Status.STD_OK;
    },
    count:0
  }
  
  
  this.options.privhostport= Net.uniqport();
  this.options.pubhostport = Net.prv2pub(this.options.privhostport);
  
  
  this.todo=[];

  this.exit = [];
  
  this.hostname=options.hostname;
  this.nodename=options.nodename;
  this.domain=options.domain;

  /* Install HOST/DNS tuple provider
  ** Expected patterns: 
  **  DNS,path,?
  **  DOMAIN,?
  **  HOSTNAME,?
  */
  
  this.node.ts.register(function (pat) {
    var stat,tuple;
    // Caching?
    if (pat.length<2) return none;
    switch (pat[0]) {
      case 'DNS':
        if (pat.length<3) return none; else Sch.B([
          function () {
            main.dios.dir(pat[1],function (rows,_stat) {
              stat=_stat;
              if (stat==Status.STD_OK) tuple=[pat[0],pat[1],Comp.array.filtermap(rows,function (row) {
                if (row.stat==Status.STD_OK) return {name:row.name,cap:Net.Print.capability(row.cap)};
                else return none;
              })];
            })
          },
          function () {
            // console.log('>> '+Status.print(stat));
            if (stat==Status.STD_OK)
              main.node.ts.checkwaiter(tuple);
          } 
        ]);
        break;
      case 'HOSTNAME': return ['HOSTNAME',main.hostname];
      case 'NODENAME': return ['NODENAME',main.nodename];
      case 'DOMAIN': return ['DOMAIN','default'];
    }
    // console.log(Sch.GetCurrent())
    return none;
  });  
  
  this.out('JAM created in world '+this.world.id+'.');
}

// Import analyzer class...
var JamAnal = Require('jam/analyzer');
JamAnal.current(Aios);
jam.prototype.analyze=JamAnal.jamc.prototype.analyze;
jam.prototype.syntax=JamAnal.jamc.prototype.syntax;

// Run server extension
RunSrv.current(Aios);
jam.prototype.request=RunSrv.run.prototype.request;

/** Add an agent class template {<ac name>:<ac constructor fun>} to the JAM world
 *
 */
jam.prototype.addclass = function (ac) {
  for (var p in ac) {
    if (this.options.verbose>=0) this.out((this.world.classes[p]?'Updating':'Adding')+' agent class template '+p+'.');
    this.world.classes[p]=ac[p];
  }
};


/** Compile agent class templates from text. 
 *  Expected text format: var classes = {ac1: function () {}, ac2: function ..}
 *
 */
jam.prototype.compile = function (text,modu) {
  var ac,
      ast,
      p,
      regex1,
      ast=null,
      off=null,
      code,
      content,
      syntax,
      more;

  if (this.options.verbose>0) this.out('Compiling agent class template(s)');

  if (text!=undefined) {
    code=text+' classes';
    try {
      ast=Esprima.parse(code, { tolerant: true, loc:true });
      if (ast.errors && ast.errors.length>0) throw ast.errors[0];
      modu=eval(code);
    } catch (e) {      
      if (e.lineNumber) more = ", in line "+e.lineNumber;
      throw e;
    }
  }

  for (p in modu) {
    ac={};
    ac[p]=modu[p];
    if (ast) off=this.syntax.find(ast,'VariableDeclarator',p);
    if (off && off.loc) this.syntax.offset=off.loc.start.line-1;
    content = 'var ac = '+modu[p];
    try {
      syntax = Esprima.parse(content, { tolerant: true, loc:true })
      if (syntax.errors && syntax.errors.length>0) throw syntax.errors[0];
    } catch (e) {
      if (e.lineNumber) more = ", in line "+e.lineNumber;
      throw e;
    };
    
    this.analyze(syntax,{classname:p,level:2,verbose:1});
    text=Json.stringify(ac);
    regex1= /this\.next=([a-zA-Z0-9_]+)/;
    text=text.replace(regex1,"this.next='$1'");
    // console.log(text);
    ac=Json.parse(text,{});    
    this.addclass(ac);
    this.syntax.offset=0;
  }
};

/** Create and start an agent from class ac with arguments.
 *
 */
jam.prototype.create = function (ac,args) {
  var node=this.node;
  var agent=none;
  if (this.world.classes[ac])
    agent = Aios.Code.createOn(node,this.world.classes[ac],args);
  else this.out('create: Cannot find agent class '+ac);
  if (agent) {
    if (this.options.verbose>0) this.out('Created agent '+agent.agent.id+' from class template(s) '+ac);
    agent.agent.ac=ac;
    return agent.agent.id; 
  } else return none;
}

/** Initialize JAMDOS and create/start host server
 *
 */
jam.prototype.init=function () {
  var self=this,
      stat,
      cs,
      csrow,
      csdir,
      i,
      names=[];
      
  this.out('Initializing ..');
  for(i=0;i<10;i++) names.push(Name.generate(nameopts));
  
  this.todo.push([
    // TODO: GEO location from broker using satellize ...
    function () {
    },
    function () {
      // Append default DNS and root directory
      var dnscs;
      if (self.env.rootdir) {
        dnscs = self.env.rootdir;
        self.hostsrv.append('/dns/default', dnscs, function (_stat) {stat=_stat});
        if (stat != Status.STD_OK) self.out('Cannot append ' + 'dns/default' + ': ' + Status.print(stat));
        self.hostsrv.append('/root', dnscs, function (_stat) {stat=_stat});
        if (stat != Status.STD_OK) self.out('Cannot append ' + 'root' + ': ' + Status.print(stat));
      }
    },
    function () {
      // Check for default domain ...
      self.hostsrv.lookup('/root/domains/'+self.domain, function (_stat,_cs) {
        if (self.options.verbose>0) self.out('lookup /root/domains/'+self.domain+': '+Status.print(_stat))
        stat=_stat;
        csdir=_cs;
      });  
    },
    function () {
      if (stat==Status.STD_OK) 
      L(
        function () {
          return names.length>0;
        },
        [
          function () {
            // Check for node in default domain ...
            self.hostsrv.lookup('/root/domains/'+self.domain+'/'+self.nodename, function (_stat,_cs) {
              if (self.options.verbose>0) self.out('lookup /root/domains/'+self.domain+'/'+self.nodename+': '+Status.print(_stat))
              stat=_stat;
              cs=_cs;
            });  
          },
          function () {
            // Try to remove remains of node in default domain (2)
            if (stat==Status.RPC_FAILURE) {
              self.hostsrv.delete('/root/domains/'+self.domain+'/'+self.nodename,
                function (_stat) {
                  if (self.options.verbose>0)  self.out('delete /root/domains/'+self.domain+'/'+self.nodename+': '+Status.print(_stat))
                  stat=_stat;
                }
              ); 
            }
          },
          function () {
            // Try to register node in default domain (3)
            
            if (stat==Status.STD_NOTFOUND || stat==Status.STD_OK) {
              self.hostsrv.append('/root/domains/'+self.domain+'/'+self.nodename, self.hostsrv.hostcap,
                function (_stat) {
                  self.out('Publish /domains/'+self.domain+'/'+self.nodename+': '+Status.print(_stat))
                  stat=_stat;
                }
              ); 
            } 
          },
          function () {
            if (stat==Status.STD_OK) names=[];  // Done
            else {
              self.nodename=Array.head(names);
              names=Array.tail(names);
            }
          }
        ], 
        [
          function () {
          self.out('My final host name is '+self.hostname+'.'+self.nodename);
          }
        ]
      );
    }
  ]);
  B(Comp.array.flatten(this.todo));
  this.exit.push([
    function () {
      self.hostsrv.delete('/root/domains/'+self.domain+'/'+self.nodename,
         function (_stat) {
           self.out('Unpublish /domains/'+self.domain+'/'+self.nodename+': '+Status.print(_stat))
           stat=_stat;
         }
       );     
    }
  ]);
  this.env.rootdir=this.network.env.rootdir;
  this.hostsrv=HostSrv.HostServer(this.scheduler,
                               this.rpc,
                               this.options,
                               this.hostname+'.'+this.nodename,
                               this.env);
  // Register our request handler and make a link back to our class handler
  this.hostsrv.register(this.request);
  this.hostsrv.addclass=function (ac) {
      self.addclass(ac);
    };
  this.hostsrv.getclass=function (classname) {
      return self.world.classes[classname];
    };
  this.hostsrv.receive=function (code,start) {
      self.node.receive(code,start);
    };
  if (this.options.geo) myJam.hostsrv.set_geo(this.options.geo);
}


/** Read and compile agent class templates from file
 *  Format: module.exports = {ac1: function, ac2: function , ...]
 *
 */
jam.prototype.readclass = function (file) {
  var text,
      modu;

  if (this.options.verbose>0) this.out('Looking up agent class template(s) from '+file);
  modu=Require(file);
  this.compile(text,modu);
};


/** Start the JAM scheduler
 *
 */
jam.prototype.start=function () {
  this.run=true;
  // this.out('Starting ..');
}



/** Stop the JAM scheduler
 * 
 */
jam.prototype.stop=function () {
  this.run=false;
  this.out('Stopping ..');
  if (this.looprun)
    clearTimeout(this.looprun);
  B(Comp.array.flatten(this.exit));
}


var Jam = function(options) {
  var myJam,
  myJam = new jam(options);
  return myJam;
};

out('Main module compiled.');

module.exports={
  Aios:Aios,
  Cs:Cs,
  Dns:Dns,
  Dios:Dios,
//  FileReader:FileReader,
//  FileSaver:FileSaver,
  Jam:Jam,
  Io:Io,
  Name:Name,
  Net:Net,
  JamNet: JamNet,
  Sch: Sch
};
