/**
 **      ==============================
 **       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:     31-10-16 by sbosse.
 **    $VERSION:     1.3.2
 **
 **    $INFO:
 **
 * ====================================================================
 * DOS: Broker connection module 
 * Client, FS FIFO connection
 *
 * Data transfer: JSON
 * ====================================================================
 *
 * Two unidirectional FIFOs are used for bidirectional client-server communication
 * There is one shared public channel request link, and multiple private P2P client-server links (FIFO pair).
 * Clients writing initially to the request channel to get a free private channel from the server.
 * Note: All clients simultaneously requesting a channel will receive multiple replies for different clients!
 * A FIFO channel is shared by all participants!
 *
 * Notes:
 * Superfluous? All response callback computations wrapped by scheduler callbacks to avoid main thread preemption!
 * Is event queuing always guaranteed in JS?
 **
 **
 ** Message format:
 *
 *  status:'EOK'|'ENOENTR'|'EWOULDBLOCK'|'STD_OK'
 *  EABC: ASCII Hexadecimal Code
 *  XX: Hexadecimal Number Code
 *  BB: Binary String Code
 *  SS: String Code
 *
 * TRANSREQ (send RPC message to broker)
 * --------
 *
 * { rpc:'trans',hostport:BB,tid:NN, ipport:NN,
 *   data: {
 *     header:EABC
 *     data:EABC
 *   }
 * }
 *
 * REPLY:
 * 
 * {status} |
 * { status,
 *   data: { header:EABC, data:EABC}
 * }
 *
 * REQUEST (get RPC messages from broker)
 * -------
 *    
 * {rpc:'request',hostport:BB,ipport:NN}
 *
 * REPLY:
 *
 * { status,
 *   data: [
 *     { hostport: BB,
 *       sendport: BB,
 *       operation : Rpc.Operation,
 *       tid: NN,
 *       header:EABC, data:EABC},
 *     ..
 *   ]}
 *
 * REPLY
 * -----
 * {rpc:'reply',hostport:BB,sendport:BB,
 *  tid:NN, ipport:NN,
 *  data:{header:EABC,data:EABC}
 * }
 *
 * IAMHERE
 * ------- 
 * {type:'iamhere',hostport:BB,srvport:BB}  
 *
 * ALIVE
 * -----
 * {type:'alive',hostport:BB,srvport:BB}  
 *
 * ASK
 * ---
 * {type:'ask',hostport:BB,xname:SS}  
 *
 * REPLY:
 * 
 * {status, xname:SS, xvalue:SS}
 *
 * NOTIFY
 * ---
 * {type:'notify',hostport:BB,xname:SS,xvalue:SS}  
 * =>
 *
 * REPLY:
 * 
 * {status}
 *
 **    $ENDOFINFO
 */
"use strict";
var log = 0;


var util = Require('util');
var net = Require('net');
var Fs = Require('fs');

var Io = Require('com/io');
var Net = Require('dos/network');
var Buf = Require('dos/buf');
var Rpc = Require('dos/rpc');
var Conn = Require('dos/connutils');
var xmldoc = Require('dos/ext/xmldoc');
var Sch = Require('dos/scheduler');
var Comp = Require('com/compat');
var Perv = Comp.pervasives;
var Hashtbl = Comp.hashtbl;
var String = Comp.string;
var Rand = Comp.random;
var Array = Comp.array;
var Obj = Comp.obj;
var trace = Io.tracing;
var div = Perv.div;
var Status = Net.Status;

var Mode = Conn.Mode;

/** Client-side Appl. Connection object.
 *
 *
 * @param hostport
 * @param srv_url
 * @param srv_ipport
 * @param [my_url]
 * @param [my_ipport]
 * @constructor
 */

// tyoeof options = {hostport,path,channel,router,verbose?}
var netConnection = function(options) {
    /*
    ** Broker
     */
    var self=this;
    this.path=options.path;                 // FIFO FS path [optionally including channel number]
    this.channel=options.channel;           // FIFO channel [optional]
    this.srv_port=undefined;            // Broker host server RPC port (== host node port), returned by ALIVE request
    this.hostport=options.hostport;             // Public communication Net.Port == Host port
    this._status = Net.Status.STD_UNKNOWN;
    this.verbose=options.verbose||0;
    this.mode=Mode.TWOCHAN;
    this.keepalive=true;
    this.enabled=false;
    /*
     ** Pending broker request?
     */
    this.pending=0;
    this.waiting=false;

    this.log = options.log || Io.out;

    this.conn_port = Net.uniqport();
    this.rpccon=Rpc.RpcConn(
        self.conn_port,
        /*
        ** send: Connection Forward and Deliver Operation
        ** ONECHAN client connectivity: collect
        ** TWOCHAN client connectivity: forward
        */
        function(rpcio,callback) {
          // TODO rpcio -> msg
        },
        /*
         ** alive: Connection status
         */
        function() {
            return self._status==Net.Status.STD_OK;
        }
    );
    this.requests=[];
    this.last=undefined;
    
    // Service replies from broker
    this.receiver=undefined;
    this.socket=undefined;
    
    this.warned=0;
    this.ready=false;
    this.router=options.router;
    // Transfer of multi-part messages?
    this.multipart=true;
    this.rid=0;
    this.request_id=function (){var i=this.rid;this.rid=(this.rid+1)%65536;return i};
};



/** Send the broker server an ALIVE message and wait for response
 ** to check the connection status.
 *
 * @param callback
 */
netConnection.prototype.alive = function (callback) {
    var self=this;
    var msg= {
      type:'alive',
      hostport:this.hostport,
    };
    Io.log(((log+this.verbose)<2)||('[BFIC] ALIVE, current status: '+this._status));
    this.rpccon.stats.op_alive++;

    this.request(msg,
      function(reply) {
        Io.log(((log+self.verbose)<2)||('[BFIC] ALIVE status: STATUS: ' + reply.status));
        Io.log(((log+self.verbose)<2)||('[BFIC] ALIVE data: ' + reply.data));
        if (reply.status!='EOK')  {
          if (self.verbose>=0 && (self._status==Net.Status.STD_OK||self._status==Net.Status.STD_UNKNOWN))
            Io.out('[BFIC] ALIVE! Not connected to broker '+self.path+':'+self.channel+' ['+
                 Net.Print.port(self.srv_port)+ ']');                
          self._status=Net.Status.STD_IOERR;
          Io.log(((log+self.verbose)<2)||('[BFIC] ALIVE problem with request ['+
                  self.path+':'+self.channel+']: ' + e.message));
        }
        /*
        ** Reply must contain the broker host server port.
        */
        if (reply.data && String.length(reply.data)==(Net.PORT_SIZE*2)) {
          var buf=Buf.Buffer(reply.data);
          self.srv_port=Buf.buf_get_port(buf);
          if (self.verbose>0 && (self._status!=Net.Status.STD_OK || self.waiting))
            Io.out('[BFIC] ALIVE! Connected to broker '+self.path+':'+self.channel+' ['+
                            Net.Print.port(self.srv_port)+ '] in mode '+
                            self.mode+
                            (self.keepalive?' KEEPALIVE':''));
          self._status=Net.Status.STD_OK;
          self.waiting=false;
        }
        else {
            if (self.verbose>0 && (self._status==Net.Status.STD_OK||self._status==Net.Status.STD_UNKNOWN)) 
                Io.out('[BFIC] ALIVE! Not connected to broker '+self.path+':'+self.channel+' ['+
                            Net.Print.port(self.srv_port)+ ']');                
            Io.log(((log+self.verbose)<1)||('[BFIC] ALIVE returned invalid data: '+reply.data+', '+
                                            self.path+':'+self.channel));
            self._status=Net.Status.STD_IOERR;
       }
       if (callback) {
         Sch.ScheduleCallback([callback,self._status]);
       }
    });    
};

/** Ask the broker server for a value (e.g., a capability)..
 *
 * @param {string} xname
 * @param {function(string)} callback
 */
netConnection.prototype.ask = function (xname,callback) {
    var self=this;
    var msg= {
      type:'ask',
      hostport:this.hostport,
      xname:xname
    };
    Io.log(((log+this.verbose)<2)||('[BFIC] ASK: ' + xname));

    this.rpccon.stats.op_ask++;

    this.request(msg,
      function(reply) {
        Io.log(((log+self.verbose)<2)||('[BFIC] ASK status: '+xname+' STATUS: ' + reply.status));
        if (reply.status!='EOK') {
          self._status=Net.Status.STD_IOERR;
          Io.log(((log+self.verbose)<2)||('[BFIC] ASK problem with request ['+
                  self.path+':'+self.channel+']: ' + e.status));   
        } else {   
          var buf=Buf.Buffer();
          Io.log(((log+self.verbose)<2)||('[BFIC] ASK data: ' + reply.data));
          Buf.buf_of_hex(buf,reply.data);
          Sch.ScheduleCallback([callback,Buf.buf_get_string(buf)]);
        }
    });
};


/** Initialize connection module
 *
 */
netConnection.prototype.init = function (callback) {
    var self=this;
    this.enabled=true;
    function receiver (i) {
      var chI, chO,
          pathI=self.path+i+'O',
          pathO=self.path+i+'I';
      if (!Fs.existsSync(pathI)) {
        Io.out('[BFIC] Cannot open ' + pathO+': not existing!');          
        return;
      }
      if (!Fs.existsSync(pathI)) {
        Io.out('[BFIC] Cannot open ' + pathI+': not existing!');          
        return;
      }
      chO=Fs.createWriteStream(pathO);
      self.socket=chO;
      Io.log(((log+self.verbose)<2)||('[BFIC] Connected on ' + pathO));
//process.exit();
      if (callback) callback();

      function listen () {            
        Io.out('[BFIC] Listening on ' + pathI+' in mode '+self.mode+' '+(self.keepalive?'KEEPALIVE':''));
        chI=Fs.createReadStream(pathI);
        self.receiver=chI;
        chI.on('open',function () {
          Io.log(((log+self.verbose)<2)||('[BFIC] Connected on ' + pathI));
        });
        chI.on('close',function () {
          Io.log(((log+self.verbose)<2)||('[BFIC] Disconnect on ' + pathI));
          self.receiver=undefined;
        });
        chI.on('data',function (chunk) {
          var part,parts,data,callback,reply;
          Io.log(((log+self.verbose)<2)||('[BFIC] Received: ' + chunk.length));
          data=chunk.toString('ascii', 0, chunk.length);

          Io.log(((log+self.verbose)<2)||('[BFIC] Received: ' + data));
          parts=Conn.splitData(data);
          if (self.last) parts[0]=self.last+parts[0];
          if (Array.last(parts) != '') self.last=Array.last(parts); else self.last=undefined;

          // console.log(parts)

          for (part in parts) {
            if (parts[part]=='') continue;
            Io.log(((log+self.verbose)<2)||('[BFIC] Servicing part ' + parts[part]));
            reply = JSON.parse(parts[part]);
            if (self.multipart) Conn.decode(reply);

            if (reply.rid!=undefined && self.requests[reply.rid]) {
              // reply scheduling
              Io.log(((log+self.verbose)<3)||('[BFIC] Servicing rid ' + reply.rid));
              callback=self.requests[reply.rid];
              if (callback) {
                self.requests[reply.rid]=undefined;
                callback(reply);
              }
            }
          }
        });  
      }
      listen();
    }
    try {receiver(this.channel)} catch (e) {Io.out('[BFIC] Error: '+e)};
    this.waiting=true;
    Sch.AddTimer(1000,'Broker Connection Monitor',function () {
      self.alive();
    });
};

netConnection.prototype.debug = function (v) {log=v};

/** Notify the broker server about a value (e.g., a capability)..
 *
 * @param {string} xname
 * @param {string} xval
 * @param {function(string)} callback
 */
netConnection.prototype.notify = function (xname,xval,callback) {
    var self=this;
    var msg= {
      type:'notify',
      hostport:this.hostport,
       xname:xname,
      xvalue:xval
    };
    Io.log(((log+this.verbose)<2)||('[BFIC] NOTIFY: ' + path));
    this.rpccon.stats.op_ask++;

    this.request(msg,
      function(reply) {
        Io.log(((log+self.verbose)<2)||('[BFIC] NOTIFY status: '+path+' STATUS: ' + reply.status));
        if (callback && reply.status=='EOK') Sch.ScheduleCallback([callback]);
        else {
          self._status=Net.Status.STD_IOERR;
          Io.log(((log+self.verbose)<1)||('[BFIC] NOTIFY problem with request ['+
                  self.path+':'+self.srv_channel+']: '+reply.status));    
        }
    });
};


/*
** CLIENT
 */
/** Send a request to the broker
*/
netConnection.prototype.request = function(msg,callback) {
  var data, socket,
      reuse=true,
      self=this;
  
  if (msg.rid==undefined) msg.rid=this.request_id();
  if (this.multipart) Conn.encode(msg);
  
  Io.log(((log+self.verbose)<1)||('[BFIC] Request: '+util.inspect(msg)));
  // Register callback
  self.requests[msg.rid]=callback;
//console.log('REG '+  msg.rid); console.log(msg);
  
  data = JSON.stringify(msg);
  if (this.multipart) this.socket.write(data+Conn.EOM);
  else this.socket.write(data);

};


/** Main entry for broker requests with JSON interface. Called by router. 
 *  msg: JSON 
 *  callback : function (reply:JSON)
 */
netConnection.prototype.send = function (msg,callback) {
  var self=this;
  // console.log(msg)  
  this.request(msg,
    function(reply) {
//console.log('EXEC '+reply.rid); //console.log(reply)
      if (reply.status!='EOK') callback({status:reply.status})
      else if (reply.data) {
        // console.log(reply)
        if (self.mode==Mode.ONECHANN)
          callback(reply);
        else {
          self.router.parse(reply);       
          if (callback) callback({status:reply.status})
        }
      }
      else if (callback) callback(reply);        
    });
}
netConnection.prototype.start = function () {

}

netConnection.prototype.status = function () {
  return this._status;
}
netConnection.prototype.stop = function () {
  Sch.RemoveTimer('Broker Connection Monitor');  
  if (this.socket) this.socket.end();  
  if (this.receiver) this.receiver.close();  
}


module.exports = {
    Mode:Mode,
    /**
     *
     * @param hostport
     * @returns {netConnection}
     * @constructor
     */
    /**
     ** tyoeof options = {hostport,path,channel,router,verbose?}
     */
    Connection: function(options) {
        var obj = new netConnection(options);
        Object.preventExtensions(obj);
        return obj;
    }
};
