Source: common/models/machine.js

define(["lib/jquery",        "lib/underscore",           "common/network/dispatcher",
        "common/util/log",   "common/util/id_generator", "common/models/capability",
        "common/models/rpc", "ARCore/machines",          "require",
        "lib/meteor",        "ARCore/db",                "common/constants",
        "common/util/check"],
function($,                   _,                          Dispatcher,
         Log,                 IDGenerator,                Capability,
         RPC,                 Machines,                   require,
         meteor,              DB,                         Constants,
         check) {

    "use strict";

    var Deps = meteor.deps.Deps;

    var hasPrefix = function(prefix, string) {
        var stringPrefix = string.substring(0, prefix.length);
        return stringPrefix === prefix;
    };

    var stripPrefix = function(prefix, string) {
        if(hasPrefix(prefix, string)) {
            string = string.substring(prefix.length);
        }

        return string;
    };

    // descriptive shortcuts for hasPrefix and stripPrefix, made via partial
    // application
    var capIsRPC = _.bind(hasPrefix, window, Constants.db.RPC_CAP_PREFIX);
    var stripRPCPrefix = _.bind(stripPrefix, window, Constants.db.RPC_CAP_PREFIX);

    var capIsDB = _.bind(hasPrefix, window, Constants.db.DB_CAP_PREFIX);
    var stripDBPrefix = _.bind(stripPrefix, window, Constants.db.DB_CAP_PREFIX);

    var dbIsPublished = _.bind(hasPrefix, window, Constants.db.PUBLISHED_PREFIX+Constants.db.NAME_SEPARATOR);
    var stripPublishedPrefix = _.bind(stripPrefix, window, Constants.db.PUBLISHED_PREFIX+Constants.db.NAME_SEPARATOR);

    var dbIsShared = _.bind(hasPrefix, window, Constants.db.SHARED_PREFIX+Constants.db.NAME_SEPARATOR);
    var stripSharedPrefix = _.bind(stripPrefix, window, Constants.db.SHARED_PREFIX+Constants.db.NAME_SEPARATOR);

    /**
     * @privconstructor
     * @class Represents a LuXoR device. To find devices, see {@link Lens.Machines}
     *
     * @property {String} id       UUID of this machine, or "localhost".
     * @property {String} hostname The hostname of the machine. Only guarenteed
     *                             to be available on localhost; may be null for
     *                             other machines.
     * @name Machine
     */
    var Machine = function(id, hostname) {
        check(id, String);

        /** @alias Machine.prototype */
        var self = {};

        // capability name -> Capability
        var capMap = {};

        // rpc name -> RPC
        var rpcMap = {};

        // published db name -> DB
        var dbMap = {};

        // shared db name -> DB
        var sharedDBMap = {};

        var nameDep = new Deps.Dependency();
        var capsDep = new Deps.Dependency();

        hostname = hostname || null;

        // property so we can check if an object is a Machine
        self._isLensMachine = true;

        /**
         * Returns the name of this machine: Either the hostname, or, if the
         * hostname is unavailable, the UUID. For localhost, always returns
         * "localhost".
         *
         * @return {String}
         */
        self.name = function() {
            nameDep.depend();

            if(self.isLocalhost()) {
                return "localhost";
            }

            return this.hostname || this.id;
        };

        /**
         * Returns the capabilities advertised by this machine.
         *
         * @return {Array<Capability>} An array of {@link Capability}
         */
        self.capabilities = function() {
            capsDep.depend();
            return _.values(capMap);
        };

        /**
         * Finds a capability by name.
         *
         * @param {String} name The name of the capability.
         *
         * @return {Capability} A {@link Capability}, or null if there is no
         *                      such capability.
         */
        self.capability = function(name) {
            check(name, String);

            capsDep.depend();
            return capMap[name] || null;
        };

        /**
         * Returns the RPCs advertised by this machine.
         *
         * @return {Array<RPC>} An array of {@link RPC}
         */
        self.rpcs = function() {
            capsDep.depend();
            return _.values(rpcMap);
        };

        /**
         * Finds an RPC by name.
         *
         * @param {String} name The name of the RPC.
         *
         * @return {RPC} An {@link RPC}, or null if there is no such RPC.
         */
        self.rpc = function(name) {
            check(name, String);

            capsDep.depend();
            return rpcMap[name] || null;
        };


        /**
         * Returns the databases published by this machine.
         *
         * @return {Array<Lens.DB>} An array of {@link Lens.DB}
         */
        self.dbs = function() {
            capsDep.depend();
            return _.values(dbMap);
        };

        /**
         * Finds a database published by this machine.
         *
         * @param {String} name The name of the database.
         *
         * @return {Lens.DB} A {@link Lens.DB}, or null if there is no such
         *                   database.
         */
        self.db = function(name) {
            check(name, String);

            capsDep.depend();
            return dbMap[name] || null;
        };

        /**
         * Finds a shared database that this machine is the master for.
         *
         * @param {String} name The name of the database.
         *
         * @return {Lens.DB} A {@link Lens.DB}, or null if there is no such
         *                   database.
         *
         * @private
         */
        self._sharedDB = function(name) {
            return sharedDBMap[name] || null;
        };

        /**
         * Identical to {@link Lens.Capabilities.subscribe}, but adds
         * this machine as filter.machine.
         */
        self.subscribe = function() {
            var filter, fn;
            if(arguments.length === 1) {
                filter = {machine: self};
                fn = arguments[0];
            } else {
                fn = arguments[0];
                filter = arguments[1];
                check(filter, Object);

                filter.machine = self;
            }

            return require("ARCore/machines").subscribe(fn, filter);
        };

        /**
         * Returns true if this is the local LuXoR device.
         *
         * @return {Boolean}
         */
        self.isLocalhost = function() {
            return (id === "localhost");
        };

        /**
         * Ensures that this Machine has the given capability. Succeeds if the
         * capability is already enabled or if a request to enable it succeeds;
         * fails if a request to enable it fails.
         * @param  {String}   capName    Name of the capability.
         * @param  {Function} [callback] This callback is invoked if the
         *                               request is successful.
         *
         * @return {Promise} A jQuery promise. You can attach callbacks using
         *                   this promise or use jQuery's tools to do
         *                   parallel requests. If you passed a callback,
         *                   that callback will automatically be registered as
         *                   a success handler for this promise.
         */
        self.ensure = function(capName, callback) {
            check(capName, String);
            check(callback, check.Match.Optional(Function));

            var deferred = $.Deferred();

            var ensureCap = function() {
                self.capability(capName).ensure(callback).done(function() {
                    deferred.resolve();
                }).fail(function() {
                    deferred.reject();
                });
            };


            if(self.capability(capName)) {
                // it exists, we can ensure it immediately
                ensureCap();
            }
            else {
                // no such capability, wait for it to exist.
                self.subscribe(ensureCap, {name: capName, state: "added"});
            }

            return deferred.promise();
        };

        /**
         * Registers a new RPC that doesn't return a value. Nearby machines will
         * be able to call this RPC.
         *
         * Throws an error if called on a machine other than localhost.
         *
         * See {@tutorial rpcs} for more information on defining RPCs.
         *
         * @param  {String}   name Name of the RPC.
         * @param  {Function} fn   A function that implements the RPC.
         *
         * @return {Binding} A binding. Clearing this binding will deregister
         *                   the RPC.
         */
        self.registerVoidRPC = function(name, fn) {
            check(name, String);
            check(fn, Function);

            if(!self.isLocalhost()) {
                throw "Cannot define RPC on non-local machine";
            }
            return RPC._registerRPC(name, fn, false);
        };

        /**
         * Registers a new RPC that returns a value. Nearby machines will
         * be able to call this RPC.
         *
         * Throws an error if called on a machine other than localhost.
         *
         * See {@tutorial rpcs} for information on defining RPCs.
         *
         * @param  {String}   name Name of the RPC.
         * @param  {Function} fn   A function that implements the RPC.
         *
         * @return {Binding} A binding. Clearing this binding will deregister
         *                   the RPC.
         */
        self.registerValuedRPC = function(name, fn) {
            check(name, String);
            check(fn, Function);

            if(!this.isLocalhost()) {
                throw "Cannot define RPC on non-local machine";
            }

            return RPC._registerRPC(name, fn, true);
        };

        /**
         * Adds a capability to this machine.
         *
         * @param {String}                 name  Name for the Capability
         * @param {Object<String, String>} props Arbitrary properties
         *
         * @return {Capability} capability The new capability
         * @private
         */
        self._addCap = function(name, props) {
            capsDep.changed();

            var cap, type;
            if(capIsRPC(name)) {
                name = stripRPCPrefix(name);
                cap = RPC(self, name, props);
                type = "rpc";
                rpcMap[name] = cap;
            }
            else if(capIsDB(name)) {
                name = stripDBPrefix(name);
                cap = DB._connect(self, name);
                type = "db";

                if(dbIsPublished(name)) {
                    dbMap[stripPublishedPrefix(name)] = cap;
                }
                else if(dbIsShared(name)) {
                    sharedDBMap[stripSharedPrefix(name)] = cap;
                }
                else {
                    throw "Invalid database name: " + name;
                }
            }
            else {
                cap = Capability(self, name, props);
                type = "capability";
                capMap[name] = cap;

                if(name === "Nearby" && props.name) {
                    hostname = props.name;
                    nameDep.changed();
                }
            }

            return [cap, type];
        };

        /**
         * Removes a capability from this machine.
         *
         * @param  {String}  name  Name of the capability.
         *
         * @return {Capability} capability The removed capability
         *
         * @private
         */
        self._removeCap = function(name) {
            capsDep.changed();

            var removedCapability;
            var type;
            if(capIsRPC(name)) {
                type = "rpc";
                name = stripRPCPrefix(name);
                removedCapability = rpcMap[name];
                delete rpcMap[name];
            }
            if(capIsDB(name)) {
                type = "db";
                name = stripDBPrefix(name);
                if(dbMap[name]) {
                    removedCapability = dbMap[name];
                    delete dbMap[name];
                }
                else if(sharedDBMap[name]) {
                    removedCapability = sharedDBMap[name];
                    delete sharedDBMap[name];
                }
            }
            else {
                type = "capability";
                var oldName = self.name();
                removedCapability = capMap[name];
                delete capMap[name];

                if(self.name() !== oldName) {
                    nameDep.changed();
                }
            }

            return [removedCapability, type];
        };

        self.id = id;
        Object.defineProperty(self, "hostname", {get: function() { return hostname; }});

        Object.freeze(self);
        return self;
    };

    return Machine;
});