// ***** BEGIN LICENSE BLOCK *****
// Version: MPL 1.1/GPL 2.0/LGPL 2.1
// 
// The contents of this file are subject to the Mozilla Public License Version
// 1.1 (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
// http://www.mozilla.org/MPL/
// 
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
// for the specific language governing rights and limitations under the
// License.
// 
// The Original Code is Mozilla Corporation Code.
// 
// The Initial Developer of the Original Code is
// based on the MozRepl project.
// Portions created by the Initial Developer are Copyright (C) 2008
// the Initial Developer. All Rights Reserved.
// 
// Contributor(s):
//  Mikeal Rogers <mikeal.rogers@gmail.com>
//  Massimiliano Mirra <bard@hyperstruct.net>
//
// Alternatively, the contents of this file may be used under the terms of
// either the GNU General Public License Version 2 or later (the "GPL"), or
// the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
// in which case the provisions of the GPL or the LGPL are applicable instead
// of those above. If you wish to allow use of your version of this file only
// under the terms of either the GPL or the LGPL, and not to allow others to
// use your version of this file under the terms of the MPL, indicate your
// decision by deleting the provisions above and replace them with the notice
// and other provisions required by the GPL or the LGPL. If you do not delete
// the provisions above, a recipient may use your version of this file under
// the terms of any one of the MPL, the GPL or the LGPL.
// 
// ***** END LICENSE BLOCK *****

var EXPORTED_SYMBOLS = ["Server", "server", "AsyncRead", "Session", "sessions", "globalRegistry", "startServer"];

var events = {}; Components.utils.import("resource://jsbridge/modules/events.js", events);

const Cc = Components.classes;
const Ci = Components.interfaces;
const loader = Cc['@mozilla.org/moz/jssubscript-loader;1']
    .getService(Ci.mozIJSSubScriptLoader);

var hwindow = Components.classes["@mozilla.org/appshell/appShellService;1"]
    .getService(Components.interfaces.nsIAppShellService)
    .hiddenDOMWindow;
    
var nativeJSON = Components.classes["@mozilla.org/dom/json;1"]
    .createInstance(Components.interfaces.nsIJSON);

var json2 = Components.utils.import("resource://jsbridge/modules/json2.js");

var jsonEncode = json2.JSON.stringify;    

var uuidgen = Components.classes["@mozilla.org/uuid-generator;1"]
    .getService(Components.interfaces.nsIUUIDGenerator);

function AsyncRead (session) {
  this.session = session;
}
AsyncRead.prototype.onStartRequest = function (request, context) {};
AsyncRead.prototype.onStopRequest = function (request, context, status) {
  this.session.onQuit();
}
AsyncRead.prototype.onDataAvailable = function (request, context, inputStream, offset, count) {
  var str = {};
  this.session.instream.readString(count, str);
  this.session.receive(str.value);
}



globalRegistry = {};

function Bridge (session) {
  this.session = session;
  this.registry = globalRegistry;
}
Bridge.prototype._register = function (_type) {
  this.bridgeType = _type;
  if (_type == "backchannel") {
    events.addBackChannel(this);
  }
}
Bridge.prototype.register = function (uuid, _type) {
  try {
    this._register(_type);
    var passed = true;
  } catch(e) {
    if (typeof(e) == "string") {
      var exception = e;
    } else {
      var exception = {'name':e.name, 'message':e.message};
    }
    this.session.encodeOut({'result':false, 'exception':exception, 'uuid':uuid});
  }
  if (passed != undefined) {
    this.session.encodeOut({"result":true, 'eventType':'register', 'uuid':uuid});
  }
  
}
Bridge.prototype._describe = function (obj) {
  var response = {};
  if (obj == null) {
    var type = "null";
  } else {
    var type = typeof(obj);
  }
  if (type == "object") {
    if (obj.length != undefined) {
      var type = "array";
    }
    response.attributes = [];
    for (i in obj) {
      response.attributes = response.attributes.concat(i);
    }
  }
  else if (type != "function"){
    response.data = obj;
  }
  response.type = type;
  return response;
}
Bridge.prototype.describe = function (uuid, obj) {
  var response = this._describe(obj);
  response.uuid = uuid;
  response.result = true;
  this.session.encodeOut(response);
}
Bridge.prototype._set = function (obj) {
  var uuid = uuidgen.generateUUID().toString();
  this.registry[uuid] = obj;
  return uuid;
}
Bridge.prototype.set = function (uuid, obj) {
  var ruuid = this._set(obj);
  this.session.encodeOut({'result':true, 'data':'bridge.registry["'+ruuid+'"]', 'uuid':uuid});
}
Bridge.prototype._setAttribute = function (obj, name, value) {
  obj[name] = value;
  return value;
}
Bridge.prototype.setAttribute = function (uuid, obj, name, value) {
  // log(uuid, String(obj), name, String(value))
  try {
    var result = this._setAttribute(obj, name, value);
  } catch(e) {
    if (typeof(e) == "string") {
      var exception = e;
    } else {
      var exception = {'name':e.name, 'message':e.message};
    }
    this.session.encodeOut({'result':false, 'exception':exception, 'uuid':uuid});
  }
  if (result != undefined) {
    this.set(uuid, obj[name]);
  }
}
Bridge.prototype._execFunction = function (func, args) {
  return func.apply(this.session.sandbox, args);
}
Bridge.prototype.execFunction = function (uuid, func, args) {
  try {
    var data = this._execFunction(func, args);
    var result = true;
  } catch(e) {
    if (typeof(e) == "string") {
      var exception = e;
    } else {
      var exception = {'name':e.name, 'message':e.message};
    }
    this.session.encodeOut({'result':false, 'exception':exception, 'uuid':uuid});
    var result = true;
  }  
  if (data != undefined) {
    this.set(uuid, data);
  } else if ( result == true) {
    this.session.encodeOut({'result':true, 'data':null, 'uuid':uuid});
  } else {
    throw 'Something very bad happened.'
  }
}

backstage = this;

function Session (transport) {
  this.transpart = transport;
  this.sandbox = Components.utils.Sandbox(backstage);
  this.sandbox.bridge = new Bridge(this);
  this.sandbox.openPreferences = hwindow.openPreferences;
  try {
      this.outstream = transport.openOutputStream(Ci.nsITransport.OPEN_BLOCKING , 0, 0);
      this.stream = transport.openInputStream(0, 0, 0);
      this.instream = Cc['@mozilla.org/intl/converter-input-stream;1']
          .createInstance(Ci.nsIConverterInputStream);
      this.instream.init(this.stream, 'UTF-8', 1024,
                    Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
  } catch(e) {
      log('jsbridge: Error: ' + e);
  }
  // log('jsbridge: Accepted connection.');
  
  this.pump = Cc['@mozilla.org/network/input-stream-pump;1']
      .createInstance(Ci.nsIInputStreamPump);
  this.pump.init(this.stream, -1, -1, 0, 0, false);
  this.pump.asyncRead(new AsyncRead(this), null);
}
Session.prototype.onOutput = function(string) {
  // log('jsbridge write: '+string)
  if (typeof(string) != "string") {
    throw "This is not a string"
  } 
  try {
    this.outstream.write(string, string.length);
  } catch (e) {
    throw "Why is this failing "+string
  }
  // this.outstream.write(string, string.length);
};
Session.prototype.onQuit = function() {
  this.instream.close();
  this.outstream.close();
  sessions.remove(session);
};
Session.prototype.encodeOut = function (obj) {
  try {
    this.onOutput(jsonEncode(obj));
  } catch(e) {
    if (typeof(e) == "string") {
      var exception = e;
    } else {
      var exception = {'name':e.name, 'message':e.message};
    }
    this.onOutput(jsonEncode({'result':false, 'exception':exception}));
  }
  
}
Session.prototype.receive = function(data) {
  // log('jsbrige receive: '+data);
  Components.utils.evalInSandbox(data, this.sandbox);
}

var sessions = {
    _list: [],
    add: function(session) {
        this._list.push(session);
    },
    remove: function(session) {
        var index = this._list.indexOf(session);
        if(index != -1)
            this._list.splice(index, 1);
    },
    get: function(index) {
        return this._list[index];
    },
    quit: function() {
        this._list.forEach(
            function(session) { session.quit; });
        this._list.splice(0, this._list.length);
    }
};

function Server (port) {
  this.port = port;
}
Server.prototype.start = function () {
  try {
    this.serv = Cc['@mozilla.org/network/server-socket;1']
        .createInstance(Ci.nsIServerSocket);
    this.serv.init(this.port, true, -1);
    this.serv.asyncListen(this);
    // log('jsbridge: Listening...');
  } catch(e) {
    log('jsbridge: Exception: ' + e);
  }    
}
Server.prototype.stop = function () {
    log('jsbridge: Closing...');
    this.serv.close();
    this.sessions.quit();
    this.serv = undefined;
}
Server.prototype.onStopListening = function (serv, status) {
// Stub function
}
Server.prototype.onSocketAccepted = function (serv, transport) {
  session = new Session(transport)
  sessions.add(session);
}

function log(msg) {
    dump(msg + '\n');
}

function startServer(port) {
  var server = new Server(port)
  server.start()
}


