baja/comp/Component.js

/**
 * @copyright 2015 Tridium, Inc. All Rights Reserved.
 * @author Gareth Johnson
 */

/**
 * Defines {@link baja.Component}.
 * @module baja/comp/Component
 */
define([ "bajaPromises",
  "bajaScript/nav",
  "bajaScript/baja/comp/Complex",
  "bajaScript/baja/comp/LinkCheck",
  "bajaScript/baja/comp/DynamicProperty",
  "bajaScript/baja/comp/Flags",
  "bajaScript/baja/comp/PropertyAction",
  "bajaScript/baja/comp/PropertyTopic",
  "bajaScript/baja/comp/compUtil",
  "bajaScript/baja/comm/Callback",
  "bajaScript/baja/tag/ComponentTags",
  "bajaScript/baja/tag/SmartTags",
  "bajaScript/baja/tag/ComponentRelations",
  "bajaScript/baja/tag/SmartRelations",
  "lex!",
  "nmodule/js/rc/asyncUtils/promiseMux" ], function (
  Promise,
  baja,
  Complex,
  LinkCheck,
  DynamicProperty,
  Flags,
  PropertyAction,
  PropertyTopic,
  compUtil,
  Callback,
  ComponentTags,
  SmartTags,
  ComponentRelations,
  SmartRelations,
  lexjs,
  promiseMux) {

  "use strict";

  const { callSuper, def: bajaDef, error: bajaError, objectify, strictArg, subclass } = baja;
  const { setContextInFailCallback, setContextInOkCallback, unlease } = compUtil;

    // Internal Component Event flags
  const CHANGED = 0,
    ADDED = 1,
    REMOVED = 2,
    RENAMED = 3,
    REORDERED = 4,
    //PARENTED       = 5, //never used
    //UNPARENTED     = 6, //never used
    //ACTION_INVOKED = 7, //never used
    TOPIC_FIRED = 8,
    FLAGS_CHANGED = 9,
    FACETS_CHANGED = 10,
    //RECATEGORIZED  = 11, //never used
    KNOB_ADDED = 12,
    KNOB_REMOVED = 13,
    SUBSCRIBED = 14,
    UNSUBSCRIBED = 15,
    RELATION_KNOB_ADDED = 16,
    RELATION_KNOB_REMOVED = 17;

  const invalidActionArgErrMsg = "Invalid Action Argument: ";

  const { HIDDEN, READONLY, OPERATOR } = Flags;

  const DISPLAY_NAMES_FLAGS = HIDDEN | READONLY | OPERATOR;

  /**
   * Represents a `baja:Component` in BajaScript.
   *
   * `Component` is the required base class for all
   * Baja component classes.
   *
   * Just like Niagara, `baja:Component` contains a lot of the core
   * functionality of the framework. Unlike `baja:Struct`, a `Component` can
   * contain both frozen and dynamic `Slot`s. Frozen `Slot`s are defined at
   * compile time (typically hard coded in Java) and Dynamic `Slot`s can be
   * added at runtime (i.e. when the Station is running). There are three
   * different types of `Slot`s that a `Component` can contain (`Property`,
   * `Action` and `Topic`).
   *
   * @see baja.Struct
   * @see baja.Property
   * @see baja.Action
   * @see baja.Topic
   *
   * @class
   * @alias baja.Component
   * @extends baja.Complex
   */
  var Component = function Component() {
    callSuper(Component, this, arguments);
    this.$space = null;
    this.$handle = null;
    this.$bPropsLoaded = false;
    this.$subs = [];
    this.$lease = false;
    this.$leaseTicket = baja.clock.expiredTicket;
    this.$knobs = null;
    this.$permissionsStr = null;
    this.$permissions = null;
    this.$nc = true;
    this.$muxed = null;
  };

  subclass(Component, Complex);

  // This is a generic component event handling function
  // that can route events to Component or Subscriber event handlers
  function handleComponentEvent(component, handlers, id, slot, obj, str, cx) {

    if (id === CHANGED) {
      handlers.fireHandlers("changed", bajaError, component, slot, cx);
    } else if (id === ADDED) {
      handlers.fireHandlers("added", bajaError, component, slot, cx);
    } else if (id === REMOVED) {
      handlers.fireHandlers("removed", bajaError, component, slot, obj, cx);
    } else if (id === RENAMED) {
      handlers.fireHandlers("renamed", bajaError, component, slot, str, cx);
    } else if (id === REORDERED) {
      handlers.fireHandlers("reordered", bajaError, component, cx);
    } else if (id === TOPIC_FIRED) {
      handlers.fireHandlers("topicFired", bajaError, component, slot, obj, cx);
    } else if (id === FLAGS_CHANGED) {
      handlers.fireHandlers("flagsChanged", bajaError, component, slot, cx);
    } else if (id === FACETS_CHANGED) {
      handlers.fireHandlers("facetsChanged", bajaError, component, slot, cx);
    } else if (id === SUBSCRIBED) {
      handlers.fireHandlers("subscribed", bajaError, component, cx);
    } else if (id === UNSUBSCRIBED) {
      handlers.fireHandlers("unsubscribed", bajaError, component, cx);
    } else if (id === KNOB_ADDED) {
      handlers.fireHandlers("addKnob", bajaError, component, slot, obj, cx);
    } else if (id === KNOB_REMOVED) {
      handlers.fireHandlers("removeKnob", bajaError, component, slot, obj, cx);
    } else if (id === RELATION_KNOB_ADDED) {
      handlers.fireHandlers("addRelationKnob", bajaError, component, obj, cx);
    } else if (id === RELATION_KNOB_REMOVED) {
      handlers.fireHandlers("removeRelationKnob", bajaError, component, obj, cx);
    }
  }

  // Handle Component child events
  function handleComponentChildEvent(component, handlers, id, str, cx) {
    if (id === RENAMED) {
      handlers.fireHandlers("componentRenamed", bajaError, component, str, cx);
    } else if (id === FLAGS_CHANGED) {
      handlers.fireHandlers("componentFlagsChanged", bajaError, component, cx);
    } else if (id === FACETS_CHANGED) {
      handlers.fireHandlers("componentFacetsChanged", bajaError, component, cx);
    }
  }

  // Handler Reorder Component Child Events  
  function handleReorderComponentChildEvent(component, handlers, cx) {
    handlers.fireHandlers("componentReordered", bajaError, component, cx);
  }

  function fwCompEvent(comp, id, slot, obj, str, cx) {
    var targetVal = null,
      i,
      x,
      component;

    if (comp.isSubscribed() || id === UNSUBSCRIBED) {
      // First support framework callback
      // TODO: Commented out for now for improved performance. Are these really needed?
      /*
      try {
        if (id === CHANGED) {
          comp.$fw("changed", slot, cx);   
        }
        else if (id === ADDED) {
          comp.$fw("added", slot, cx);   
        }
        else if (id === REMOVED) {   
          comp.$fw("removed", slot, obj, cx);   
        }
        else if (id === RENAMED) {
          comp.$fw("renamed", slot, str, cx);
        }
        else if (id === REORDERED) {
          comp.$fw("reordered", cx);
        }
        else if (id === TOPIC_FIRED) {
          comp.$fw("fired", slot, obj, cx);   
        }
        else if (id === SUBSCRIBED) {
          comp.$fw("subscribed", cx);
        }
        else if (id === UNSUBSCRIBED) {
          comp.$fw("unsubscribed", cx);
        }
        else if (id === KNOB_ADDED) {
          comp.$fw("knobAdded", obj, cx);
        }
        else if (id === KNOB_REMOVED) {
          comp.$fw("knobRemoved", obj, cx);
        }
      }
      catch (e) {
        error(e);
      }
      */

      // Route to event handlers on the Component
      if (comp.hasHandlers()) {
        handleComponentEvent(comp, comp, id, slot, obj, str, cx);
      }

      // Route to Subscribers if there are any registered
      if (comp.$subs.length > 0) {
        // Route to all registered Subscribers
        for (i = 0; i < comp.$subs.length; ++i) {
          // Route to event handlers on the Subscriber  
          if (comp.$subs[i].hasHandlers()) {
            handleComponentEvent(comp, comp.$subs[i], id, slot, obj, str, cx);
          }
        }
      }
    }

    if (id === RENAMED) {
      if (slot && slot.isProperty()) {
        targetVal = comp.get(slot);
      }
    } else if (id === FLAGS_CHANGED ||
      id === FACETS_CHANGED) {
      if (slot && slot.isProperty()) {
        targetVal = comp.get(slot);
      }
    }

    // Route to child Component
    if (targetVal !== null && targetVal.getType().isComponent() && targetVal.isSubscribed()) {
      // Route to event handlers on the Component
      handleComponentChildEvent(targetVal, targetVal, id, str, cx);

      // Route to Subscribers if there are any registered
      if (targetVal.$subs.length > 0) {
        // Route to all registered Subscribers
        for (x = 0; x < targetVal.$subs.length; ++x) {
          // Route to event handlers on the Subscriber      
          handleComponentChildEvent(targetVal, targetVal.$subs[x], id, str, cx);
        }
      }
    }

    // Special case for child reordered Component events. We need to route to all child Components on the target Component
    if (id === REORDERED) {
      comp.getSlots(function (slot) {
        return slot.isProperty() && slot.getType().isComponent() && this.get(slot).isSubscribed();
      }).each(function (slot) {
        // Route reordered event
        component = this.get(slot);
        handleReorderComponentChildEvent(component, component, cx);
        if (component.$subs.length > 0) {
          for (i = 0; i < component.$subs.length; ++i) {
            // Route to event handlers on the Subscriber      
            handleReorderComponentChildEvent(component, component.$subs[i], cx);
          }
        }
      });
    }

    // NavEvents
    if (baja.nav.hasHandlers() &&
      comp.isMounted() &&
      comp.isNavChild() &&
      ((id === ADDED && slot.getType().isComponent()) ||
        (id === REMOVED && obj.getType().isComponent()) ||
        (id === RENAMED && slot.getType().isComponent()) ||
        id === REORDERED)) {

      if (id === ADDED) {
        baja.nav.fireHandlers("added", bajaError, baja.nav, comp.getNavOrd(), comp.get(slot), cx);
      } else if (id === REMOVED) {
        baja.nav.fireHandlers("removed", bajaError, baja.nav, comp.getNavOrd(), slot.getName(), obj, cx);
      } else if (id === RENAMED) {
        baja.nav.fireHandlers("renamed", bajaError, baja.nav, comp.getNavOrd(), comp.get(slot), str, cx);
      } else if (id === REORDERED) {
        baja.nav.fireHandlers("reordered", bajaError, baja.nav, comp.getNavOrd(), cx);
      }
    }
  }

  const hasOperatorFlag = (slot) => !!(slot.getFlags() & baja.Flags.OPERATOR);
  const hasReadonlyFlag = (slot) => !!(slot.getFlags() & baja.Flags.READONLY);

  /**
   * Set a Slot's flags.
   *
   * If the `Component` is mounted, this will **asynchronously** set the Slot
   * Flags on the server.
   *
   * For callbacks, the `this` keyword is set to the `Component` instance.
   *
   * @param {Object} obj the object literal for the method's arguments.
   * @param {baja.Slot|String} obj.slot the Slot or Slot name.
   * @param {Number} obj.flags the new flags for the Slot.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok function
   * callback. Called once the method has succeeded.
   * @param {Function} [obj.fail] the fail function callback. Called if this
   * method has an error.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   * @param [obj.cx] the Context (used internally by BajaScript).
   * @returns {Promise} a promise that will be resolved once the flags have been
   * set.
   *
   * @example
   *   <caption>
   *     An object literal is used to specify the method's arguments.
   *   </caption>
   *
   *   myObj.setFlags({
   *     slot: 'outsideAirTemp',
   *     flags: baja.Flags.SUMMARY,
   *     batch // if defined, any network calls will be batched into this object (optional)
   *   })
   *     .then(function () {
   *       baja.outln('flags were set');
   *     })
   *     .catch(function (err) {
   *       baja.error('error setting flags: ' + err);
   *     });
   */
  Component.prototype.setFlags = function (obj) {
    obj = objectify(obj);

    var slot = obj.slot,
      flags = obj.flags,
      cx = obj.cx,
      serverDecode = cx && cx.serverDecode,
      commit = cx && cx.commit,
      cb = new Callback(obj.ok, obj.fail, obj.batch);

    // Ensure 'this' is Component in callbacks...
    setContextInOkCallback(this, cb);
    setContextInFailCallback(this, cb);

    try {
      slot = this.getSlot(slot);

      // Short circuit some of this if this is called from a Server decode
      if (!serverDecode) {
        // Validate arguments
        strictArg(slot, baja.Slot);
        strictArg(flags, Number);

        if (slot === null) {
          throw new Error("Could not find Slot: " + obj.slot);
        }

        // Subclass check
        if (typeof this.checkSetFlags === "function") {
          this.checkSetFlags(slot, flags, cx);
        }
      }

      // Check if this is a proxy. If so then trap it...
      if (!commit && this.isMounted() && this.$space.hasCallbacks()) {
        this.$space.getCallbacks().setFlags(this, slot, flags, cb);
        return cb.promise();
      }

      // Set the flags for the Slot
      slot.$setFlags(flags);

      if (cx) {
        if (typeof cx.displayName === "string") {
          slot.$setDisplayName(cx.displayName);
        }
        if (typeof cx.display === "string") {
          slot.$setDisplay(cx.display);
        }
      }

      // Fire Component Event
      fwCompEvent(this, FLAGS_CHANGED, slot, null, null, cx);

      cb.ok();
    } catch (err) {
      cb.fail(err);
    }

    return cb.promise();
  };

  /**
   * Set a dynamic Slot's facets.
   *
   * If the `Component` is mounted, this will **asynchronously* change the
   * facets on the server.
   *
   * For callbacks, the `this` keyword is set to the Component instance.
   *
   * @param {Object} obj the object literal for the method's arguments.
   * @param {baja.Slot|String} obj.slot the Slot of Slot name.
   * @param {baja.Facets} obj.facets the new facets for the dynamic Slot.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok function
   * callback. Called once the method has succeeded.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail function
   * callback. Called if this method has an error.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   * @param [obj.cx] the Context (used internally by BajaScript).
   * @returns {Promise} a promise that will be resolved once the Facets have
   * been set.
   *
   * @example
   *   <caption>
   *     An object literal is used to specify the method's arguments.
   *   </caption>
   *
   *   myObj.setFacets({
   *     slot: 'outsideAirTemp',
   *     facets: baja.Facets.make({ foo: 'boo' }),
   *     batch // if defined, any network calls will be batched into this object (optional)
   *   })
   *     .then(function () {
   *       baja.outln('facets have been set');
   *     })
   *     .catch(function (err) {
   *       baja.error('error setting facets: ' + err);
   *     });
   */
  Component.prototype.setFacets = function (obj) {
    obj = objectify(obj);
    var cb = new Callback(obj.ok, obj.fail, obj.batch),
      slot = obj.slot,
      facets = obj.facets,
      cx = obj.cx,
      serverDecode = cx && cx.serverDecode,
      commit = cx && cx.commit;

    // Ensure 'this' is Component in callbacks...
    setContextInOkCallback(this, cb);
    setContextInFailCallback(this, cb);

    try {
      slot = this.getSlot(slot);

      if (facets === null) {
        facets = baja.Facets.DEFAULT;
      }

      // Short circuit some of this if this is the result of a Server Decode
      if (!serverDecode) {
        // Validate arguments
        strictArg(slot, baja.Slot);
        strictArg(facets, baja.Facets);

        if (slot === null) {
          throw new Error("Could not find Slot: " + obj.slot);
        }

        if (slot.isFrozen()) {
          throw new Error("Cannot set facets of frozen Slot: " + slot.getName());
        }

        // Subclass check
        if (typeof this.checkSetFacets === "function") {
          this.checkSetFacets(slot, facets, cx);
        }
      }

      // Check if this is a proxy. If so then trap it...
      if (!commit && this.isMounted() && this.$space.hasCallbacks()) {
        this.$space.getCallbacks().setFacets(this, slot, facets, cb);
        return cb.promise();
      }

      // Set the flags for the Slot
      slot.$setFacets(facets);

      if (cx) {
        if (typeof cx.displayName === "string") {
          slot.$setDisplayName(cx.displayName);
        }
        if (typeof cx.display === "string") {
          slot.$setDisplay(cx.display);
        }
      }

      // Fire Component Event
      fwCompEvent(this, FACETS_CHANGED, slot, facets, null, cx);

      cb.ok();
    } catch (err) {
      cb.fail(err);
    }

    return cb.promise();
  };

  /**
   * Add a dynamic `Property` to a Component.
   *
   * If the value extends `baja:Action`, the new slot is also an Action.
   * If the value extends `baja:Topic`, the new slot is also a Topic.
   *
   * If the `Complex` is mounted, this will **asynchronously** add
   * the `Property` to the `Component` on the server.
   *
   * For callbacks, the 'this' keyword is set to the Component instance.
   *
   * @see baja.Facets
   * @see baja.Flags
   * @see baja.Component#getUniqueName
   *
   * @param {Object} obj the object literal for the method's arguments.
   * @param {String} obj.slot the Slot name the unique name to use as the String
   * key for the slot.  If null is passed, then a unique name will automatically
   * be generated. If the name ends with the '?' character a unique name will
   * automatically be generated by appending numbers to the specified name. The
   * name must meet the "name" production in the SlotPath BNF grammar.
   * Informally this means that the name must start with an ASCII letter and
   * contain only ASCII letters, ASCII digits, or '_'.  Escape sequences can be
   * specified using the '$' char.  Use `baja.SlotPath.escape()` to escape
   * illegal characters.
   * @param {baja.Value} obj.value the value to be added
   * @param {Number} [obj.flags] optional Slot flags.
   * @param {baja.Facets} [obj.facets] optional Slot Facets.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. This
   * function is called once the Property has been added to the Server. The
   * function is passed the new `Property` that has just been added.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * This function is called if the Property fails to add. Any error information
   * is passed into this function.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   * @param [obj.cx] the Context (used internally by BajaScript).
   * @returns {Promise.<baja.Property>} a promise that will be resolved with the
   * newly added Property.
   *
   * @example
   *   <caption>
   *     An object literal is used to specify the method's arguments.
   *   </caption>
   *
   *   myObj.add({
   *     slot: 'foo',
   *     value: 'slot value',
   *     facets: baja.Facets.make({ doo: 'boo'), // Optional
   *     flags: baja.Flags.SUMMARY, // Optional  
   *     batch // if defined, any network calls will be batched into this object (optional)
   *   })
   *     .then(function (prop) {
   *       baja.outln('added a new Property named "' + prop + '"');
   *     })
   *     .catch(function (err) {
   *       baja.error('error adding Property: ' + err);
   *     });
   */
  Component.prototype.add = function (obj) {
    obj = objectify(obj, "value");

    var cb = new Callback(obj.ok, obj.fail, obj.batch),
      slotName = obj.slot,
      val = obj.value,
      flags = obj.flags,
      facets = obj.facets,
      cx = obj.cx,
      serverDecode,
      commit,
      displayName,
      display,
      p;

    // Ensure 'this' is Component in callbacks...
    setContextInOkCallback(this, cb);
    setContextInFailCallback(this, cb);


    try {
      slotName = bajaDef(slotName, null);
      flags = bajaDef(flags, 0);
      facets = bajaDef(facets, baja.Facets.DEFAULT);
      cx = bajaDef(cx, null);
      serverDecode = cx && cx.serverDecode;
      commit = cx && cx.commit;

      // Short-circuit some of this if this is the result of a Server Decode
      if (!serverDecode) {
        // Validate arguments
        strictArg(slotName, String);
        strictArg(val);
        strictArg(flags, Number);
        strictArg(facets, baja.Facets);
        strictArg(cx);

        if (!baja.hasType(val)) {
          throw new Error("Can only add BValue Types as Component Properties");
        }
        if (val.getType().isAbstract()) {
          throw new Error("Cannot add Abstract Type to Component: " + val.getType());
        }
        if (!val.getType().isValue()) {
          throw new Error("Cannot add non Value Types as Properties to a Component");
        }
        if (val === this) {
          throw new Error("Illegal argument value === this");
        }
        // Custom check add
        if (typeof this.checkAdd === "function") {
          this.checkAdd(slotName, val, flags, facets, cx);
        }
        if (val.getType().isComponent()) {
          if (typeof val.isParentLegal === "function") {
            if (!this.getType().is("baja:UnrestrictedFolder")) {
              if (!val.isParentLegal(this)) {
                throw new Error("Illegal parent: " + this.getType() + " for child " + val.getType());
              }
            }
          }
          if (typeof this.isChildLegal === "function") {
            if (!this.isChildLegal(val)) {
              throw new Error("Illegal child: " + val.getType() + " for parent " + this.getType());
            }
          }
        }
      }

      // Check if this is a proxy. If so then trap it...
      if (!commit && this.isMounted() && this.$space.hasCallbacks()) {
        this.$space.getCallbacks().add(this,
          slotName,
          val,
          flags,
          facets,
          cb);
        return cb.promise();
      }

      if (!serverDecode) {
        if (slotName === null) {
          slotName = this.getUniqueName(val.getType().getTypeName()); // TODO: Need extra argument checking before this is reached
        } else if (slotName.substring(slotName.length - 1, slotName.length) === "?") {
          slotName = this.getUniqueName(slotName.substring(0, slotName.length - 1));
        }

        baja.SlotPath.verifyValidName(slotName);
      }

      // Check for duplicate Slot
      if (this.$map.get(slotName) !== null) {
        throw new Error("Duplicate Slot: " + slotName);
      }

      if (val.getType().isComplex() && val.getParent() !== null) {
        throw new Error("Complex already parented: " + val.getType());
      }

      displayName = baja.SlotPath.unescape(slotName);
      display = "";

      if (cx) {
        if (typeof cx.displayName === "string") {
          displayName = cx.displayName;
        }
        if (typeof cx.display === "string") {
          display = cx.display;
        }
      }

      if (val.getType().isAction()) {
        p = new PropertyAction(slotName, displayName, display, flags, facets, val);
      } else if (val.getType().isTopic()) {
        p = new PropertyTopic(slotName, displayName, display, flags, facets, val);
      } else {
        p = new DynamicProperty(slotName, displayName, display, flags, facets, val);
      }

      // Add the Slot to the map
      this.$map.put(slotName, p);

      // Set up any parenting if needed      
      if (val.getType().isComplex()) {
        val.$parent = this;
        val.$propInParent = p;

        // If we have a Component then attempt to mount it
        if (val.getType().isComponent() && this.isMounted()) {
          this.$space.$fw("mount", val);
        }
      }

      // Fire Component Event
      fwCompEvent(this, ADDED, p, null, null, cx);

      cb.ok(p);
    } catch (err) {
      cb.fail(err);
    }
    return cb.promise();
  };

  /**
   * Return a unique name for a potential new Slot in this `Component`.
   *
   * Please note, this method inspects the current Slots this Component has loaded
   * to find a unique name. Therefore, if this Component is a Proxy, it must be
   * fully loaded and subscribed. Also please refrain from using this method in a
   * batch operation since it's likely the other operations in the batch will influence
   * a Slot name's uniqueness.
   *
   * @param {String} slotName the initial Slot name used to ensure uniqueness. This must be
   *                          a valid Slot name.
   * @returns {String} a unique name.
   */
  Component.prototype.getUniqueName = function (slotName) {
    baja.SlotPath.verifyValidName(slotName);

    let slotNum = slotName.match(/\d*$/)[0] || 0;
    let baseName = slotName.replace(/\d*$/, '');
    let uniqueName = slotName;

    for (let i = 0; i < slotNum.length - 1; i++) {
      const char = slotNum.substring(i, i + 1);
      if (char !== '0') {
        break;
      }
      baseName = baseName + char;
    }

    while (this.getSlot(uniqueName) !== null) {
      slotNum++;
      uniqueName = baseName + slotNum;
    }
    return uniqueName;
  };

  function removeUnmountEvent(component, handlers, cx) {
    handlers.fireHandlers("unmount", bajaError, component, cx);
  }

  function removePropagateUnmountEvent(component, cx, hasNavEventHandlers) {
    // If the Component is subscribed then trigger the unmount events
    if (component.isSubscribed()) {
      removeUnmountEvent(component, component, cx);

      if (component.$subs.length > 0) {
        // Route to all registered Subscribers
        var i;
        for (i = 0; i < component.$subs.length; ++i) {
          // Route to event handlers on the Subscriber      
          removeUnmountEvent(component, component.$subs[i], cx);
        }
      }
    }

    // Fire Nav events
    if (hasNavEventHandlers && component.isNavChild()) {
      baja.nav.fireHandlers("unmount", bajaError, component, cx);
    }

    // Search all child Components and trigger the unmount event
    component.getSlots(function (slot) {
      return slot.isProperty() && slot.getType().isComponent();
    }).each(function (slot) {
      removePropagateUnmountEvent(this.get(slot), cx, hasNavEventHandlers);
    });
  }

  /**
   * Remove the dynamic `Slot` by the specified name.
   *
   * If the `Complex` is mounted, this will **asynchronously** remove
   * the Property from the `Component` on the server.
   *
   * For callbacks, the 'this' keyword is set to the Component instance.
   *
   * @param {baja.Slot|String|Object} obj the Slot, Slot name, `Complex`
   * instance or an object literal.
   * @param {String} obj.slot the Slot, Slot name or `Complex` instance to
   * remove.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. This
   * function is called once the Property has been removed from the Server.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * This function is called if the Property fails to remove. Any error
   * information is passed into this function.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   * @param [obj.cx] the Context (used internally by BajaScript).
   * @returns {Promise} a promise that will be resolved once the Property has
   * been removed.
   *
   * @example
   *   <caption>
   *     The Slot, Slot name, a Complex or an object literal can be used for
   *     the method's arguments.
   *   </caption>
   *
   *   myObj.remove("foo");
   *
   *   //...or via the Slot itself...
   *
   *   myObj.remove(theFooSlot);
   *
   *   //...or remove the Complex instance from the parent...
   *
   *   myObj.remove(aComplexInstance);
   *
   *   //... of if more arguments are needed then via object literal notation...
   *
   *   myObj.remove({
   *     slot: 'foo',
   *     batch // if defined, any network calls will be batched into this object (optional)
   *   })
   *     .then(function () {
   *       baja.outln('Property has been removed');
   *     })
   *     .catch(function (err) {
   *       baja.error('error removing Property: ' + err);
   *     });
   */
  Component.prototype.remove = function (obj) {
    obj = objectify(obj, "slot");

    var slot = obj.slot,
      cx = obj.cx,
      serverDecode = cx && cx.serverDecode,
      commit = cx && cx.commit,
      cb = new Callback(obj.ok, obj.fail, obj.batch),
      val = null;

    // Ensure 'this' is Component in callbacks...
    setContextInOkCallback(this, cb);
    setContextInFailCallback(this, cb);

    try {
      strictArg(slot);

      if (baja.hasType(slot) && slot.getType().isComplex()) {
        val = slot;
        slot = slot.getPropertyInParent();
      } else {
        slot = this.getSlot(slot);
      }

      // Short circuit some of this on a Server decode
      if (!serverDecode) {
        if (slot === null) {
          throw new Error("Invalid slot for Component remove");
        }
        if (!slot.isProperty() || slot.isFrozen()) {
          throw new Error("Cannot remove Slot that isn't a dynamic Property: " + slot.getName());
        }

        // Subclass check
        if (typeof this.checkRemove === "function") {
          this.checkRemove(slot, cx);
        }
      }

      // Check if this is a proxy. If so then trap it...
      if (!commit && this.isMounted() && this.$space.hasCallbacks()) {
        this.$space.getCallbacks().remove(this, slot, cb);
        return cb.promise();
      }

      // TODO: Remove links?

      if (val === null) {
        val = this.get(slot);
      }

      // Unparent
      if (val.getType().isComplex()) {

        // Unmount from Component Space
        if (val.getType().isComponent() && val.isNavChild()) {
          // Trigger unmount event to this component and all child Components just before it's properly removed
          removePropagateUnmountEvent(val, cx, baja.nav.hasHandlers("unmount"));

          // If we have a Component then attempt to unmount it
          // (includes unregistering from subscription)
          if (this.isMounted()) {
            this.$space.$fw("unmount", val);
          }
        }

        val.$parent = null;
        val.$propInParent = null;
      }

      // Remove the Component from the Slot Map
      this.$map.remove(slot.getName());

      // Fire Component Event
      fwCompEvent(this, REMOVED, slot, val, null, cx);

      cb.ok();
    } catch (err) {
      cb.fail(err);
    }
    return cb.promise();
  };

  /**
   * Rename the specified dynamic Slot.
   *
   * If the `Component` is mounted, this will **asynchronously** rename
   * the Slot in the Component on the server.
   *
   * For callbacks, the `this` keyword is set to the Component instance.
   *
   * @param {Object} obj the object literal used for the method's arguments.
   * @param {baja.Slot|String} obj.slot the dynamic Slot or dynamic Slot name
   * that will be renamed
   * @param {String} obj.newName the new name of the Slot. The name must meet
   * the "name" production in the `SlotPath` BNF grammar.  Informally this means
   * that the name must start with an ASCII letter, and contain only ASCII
   * letters, ASCII digits, or '_'. Escape sequences can be specified using the
   * '$' char. Use `baja.SlotPath.escape()` to escape illegal characters.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. This
   * function is called once the Slot has been renamed.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * This function is called if the Slot fails to rename. Any error information
   * is passed into this function.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   * @param [obj.cx] the Context (used internally by BajaScript).
   * @returns {Promise} a promise that will be resolved once the slot has been
   * renamed.
   *
   * @example
   *   <caption>
   *     An object literal is used for the method's arguments.
   *   </caption>
   *
   *   myObj.rename({
   *     slot: 'foo',
   *     newName: 'boo',
   *     batch // if defined, any network calls will be batched into this object (optional)
   *   })
   *     .then(function () {
   *       baja.outln('slot has been renamed');
   *     })
   *     .catch(function (err) {
   *       baja.error('error renaming slot: ' + err);
   *     });
   */
  Component.prototype.rename = function (obj) {
    obj = objectify(obj);

    var slot = obj.slot,
      newName = obj.newName,
      cx = obj.cx,
      serverDecode = cx && cx.serverDecode,
      commit = cx && cx.commit,
      cb = new Callback(obj.ok, obj.fail, obj.batch),
      s,
      oldName;

    // Ensure 'this' is Component in callbacks...
    setContextInOkCallback(this, cb);
    setContextInFailCallback(this, cb);

    try {
      s = this.getSlot(slot);

      if (s === null) {
        throw new Error("Cannot rename. Slot doesn't exist: " + slot + " -> " + newName);
      }

      // Short circuit some of these checks on a Server decode 
      if (!serverDecode) {
        strictArg(s, baja.Slot);
        strictArg(newName, String);

        baja.SlotPath.verifyValidName(newName);

        if (s.isFrozen()) {
          throw new Error("Cannot rename frozen Slot: " + slot + " -> " + newName);
        }
        if (this.getSlot(newName) !== null) {
          throw new Error("Cannot rename. Slot name already used: " + slot + " -> " + newName);
        }

        // Subclass check
        if (typeof this.checkRename === "function") {
          this.checkRename(s, newName, cx);
        }
      }

      // Record the old name
      oldName = s.getName();

      // Check if this is a proxy. If so then trap it...
      if (!commit && this.isMounted() && this.$space.hasCallbacks()) {
        this.$space.getCallbacks().rename(this, oldName, newName, cb);
        return cb.promise();
      }

      // Rename the Component from the Slot Map
      if (!this.$map.rename(oldName, newName)) {
        throw new Error("Cannot rename: " + oldName + " -> " + newName);
      }

      s.$slotName = newName;

      if (cx) {
        if (typeof cx.displayName === "string") {
          s.$setDisplayName(cx.displayName);
        }
        if (typeof cx.display === "string") {
          s.$setDisplay(cx.display);
        }
      }

      // Fire Component Event    
      fwCompEvent(this, RENAMED, s, null, oldName, cx);

      cb.ok();
    } catch (err) {
      cb.fail(err);
    }

    return cb.promise();
  };

  /**
   * Sets display names of one or more slots of this component.
   * If an empty string is given for obj.newDisplayName, the display name will
   * be removed.
   * In addition to firing "added" or "changed" component events on this
   * component, a synthetic "rename" event is also fired on each of the slot 
   * whose display name is changed. 
   * 
   * @param {Object} obj 
   * @param {baja.Slot|Array.<baja.Slot>|string} obj.slot
   * @param {Array.<string>|string} obj.newDisplayName
   * @returns {Promise}
   * @since Niagara 4.13
   */
  Component.prototype.setDisplayName = function (obj) {
    const slot = obj.slot,
      newDisplayName = obj.newDisplayName,
      cx = obj.cx;

    const slots = Array.isArray(slot) ? slot : [ slot ],
      newDisplayNames = Array.isArray(newDisplayName) ? newDisplayName : [ newDisplayName ];

    let arg = {};

    slots.forEach((slot, i) => {
      if (newDisplayNames[i]) {
        arg[slot] = newDisplayNames[i];
      }
    });

    return this.$setDisplayNameMuxed({ slots, arg, cx });
  };

  /**
   * Uses promise muxing to work with multiple add/set calls to the 
   * "displayNames" slot of this component.
   * @private
   * @param {Object} obj 
   * @returns {Promise}
   */
  Component.prototype.$setDisplayNameMuxed = function (obj) {
    const that = this;

    if (!this.$muxed) {
      this.$muxed = promiseMux({
        exec: function (paramsArray) {
          const proms = Array(paramsArray.length).fill(Promise.resolve());
          const displayNames = that.get('displayNames');
          const slots = paramsArray.map((params) => params.slots).flat();
          const cxs = paramsArray.map((params) => params.cx).flat();

          let newArg = (displayNames && displayNames.toObject()) || {};
          paramsArray.forEach(({ arg, slots }) => {
            slots.forEach((slot) => {
              if (arg[slot]) {
                newArg[slot] = arg[slot];
              } else {
                delete newArg[slot];
              }
            });
          });

          const nameMap = baja.NameMap.make(newArg);
          const hasDisplayNames = that.has('displayNames');
          let setSlotPromise;
          if (nameMap.isNull()) {
            setSlotPromise = hasDisplayNames && that.remove('displayNames');
          } else {
            setSlotPromise = that[hasDisplayNames ? 'set' : 'add']({
              slot: 'displayNames',
              value: nameMap,
              flags: DISPLAY_NAMES_FLAGS
            });
          }
          proms[paramsArray.length - 1] = setSlotPromise;

          return Promise.all(proms)
            .then((result) => {
              /*
              if two setDisplayNames for the same slot coalesce into each other, this will fire two
              renamed events, even though officially, the display name only changed once. i don't
              have an opinion right now whether this is good or bad. revisit if required.
               */
              slots.forEach((slot, i) => {
                // Fire synthetic "renamed" component and nav event
                slot && that.$syntheticSetDisplayName(slot, cxs[i]);
              });
              return result;
            });
        },
        coalesce: false
      });
    }

    return this.$muxed(obj);
  };

  /**
   * Fire a "renamed" component event after its display name has been set.
   * Note that this operation is "synthetic" in the sense that an actual 
   * component rename itself has not occurred.
   * All the component subscribers and nav event listeners will get this event
   * and handle any UI updates similar to a rename operation.
   * 
   * @private 
   * @param {baja.Slot|string} slot 
   * @param {object} [cx] 
   * @since Niagara 4.13
   */
  Component.prototype.$syntheticSetDisplayName = function (slot, cx) {
    slot = this.getSlot(slot); //get the actual slot ("slot" can be a string) 
    const oldName = slot.getName();
    fwCompEvent(this, RENAMED, slot, null, oldName, cx);
  };

  function getKeyIndex(k, keys) {
    var i;
    for (i = 0; i < keys.length; ++i) {
      if (k === keys[i]) {
        return i;
      }
    }
    throw new Error("Could not find Property in reorder: " + k);
  }

  function getNewPropsIndex(k, dynamicProperties) {
    var i;
    for (i = 0; i < dynamicProperties.length; ++i) {
      if (k === dynamicProperties[i].getName()) {
        return i;
      }
    }
    throw new Error("Could not find dynamic Property in reorder: " + k);
  }

  /**
   * Reorder the `Component`'s dynamic Properties.
   *
   * If the `Component` is mounted, this will **asynchronously** reorder
   * the dynamic Properties in the Component on the Server.
   *
   * For callbacks, the `this` keyword is set to the `Component` instance.
   *
   * @param {Array.<baja.Property|String>|Object} obj the array of Properties
   * or Property names, or an object literal used for the method's arguments.
   * @param {Array.<baja.Property|String>} obj.dynamicProperties an array of
   * Properties or Property names for the slot order.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. This
   * function is called once the dynamic Properties have been reordered.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * This function is called if the dynamic Properties fail to reorder. Any
   * error information is passed into this function.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be batched into this object.
   * @param [obj.cx] the Context (used internally by BajaScript).
   * @returns {Promise} a promise that will be resolved once the dynamic
   * properties have been reordered.
   *
   * @example
   *   <caption>
   *     A Property array or an object literal can used for the method's arguments.
   *   </caption>
   *
   *   // Order via an array of Properties...
   *   myObj.reorder([booProp, fooProp, dooProp]);
   *
   *   // ...or order via an array of Property names...
   *   myObj.reorder(["boo", "foo", "doo"]);
   *
   *   // ...or for more arguments, use an object literal...
   *   myObj.reorder({
   *     dynamicProperties: [booProp, fooProp, dooProp], // Can also be a Property name array!
   *     batch // if defined, any network calls will be batched into this object (optional)
   *   })
   *     .then(function () {
   *       baja.outln('slots have been reordered');
   *     })
   *     .catch(function (err) {
   *       baja.outln('error reordering slots: ' + err);
   *     });
   */
  Component.prototype.reorder = function (obj) {
    obj = objectify(obj, "dynamicProperties");

    var that = this,
      dynamicProperties = obj.dynamicProperties,
      cx = obj.cx,
      serverDecode = cx && cx.serverDecode,
      commit = cx && cx.commit,
      cb = new Callback(obj.ok, obj.fail, obj.batch),
      i,
      preGetSlot,
      currentDynProps,
      dynPropCount,
      keys;

    // Ensure 'this' is Component in callbacks...
    setContextInOkCallback(that, cb);
    setContextInFailCallback(that, cb);

    // If this is a commit and the component is mounted then only process if the component is subscribed and fully loaded.
    // Otherwise it probably won't work
    if (commit && that.isMounted() && !(that.isSubscribed() && that.$bPropsLoaded)) {
      cb.ok();
      return cb.promise();
    }

    try {
      // Verify the array contents
      if (!serverDecode) {
        strictArg(dynamicProperties, Array);
      }

      for (i = 0; i < dynamicProperties.length; ++i) {
        preGetSlot = dynamicProperties[i];
        dynamicProperties[i] = that.getSlot(dynamicProperties[i]);

        if (!dynamicProperties[i]) {
          throw new Error("Could not find dynamic Property to reorder: " + preGetSlot + " in " + this.toPathString());
        }
        if (dynamicProperties[i].isFrozen()) {
          throw new Error("Cannot reorder frozen Properties");
        }
      }

      currentDynProps = that.getSlots().dynamic().properties();
      dynPropCount = 0;

      while (currentDynProps.next()) {
        ++dynPropCount;
      }

      if (dynPropCount === 0) {
        throw new Error("Cannot reorder. No dynamic Props!");
      }
      if (dynPropCount !== dynamicProperties.length) {
        throw new Error("Cannot reorder. Actual count: " + dynPropCount + " != " + dynamicProperties.length);
      }

      // Subclass check
      if (!serverDecode && typeof that.checkReorder === "function") {
        that.checkReorder(dynamicProperties, cx);
      }

      // Check if this is a proxy. If so then trap it...
      if (!commit && that.isMounted() && that.$space.hasCallbacks()) {
        that.$space.getCallbacks().reorder(that, dynamicProperties, cb);
        return cb.promise();
      }

      // Get a copy of the keys used in the SlotMap
      keys = that.$map.getKeys();

      // Sort the map accordingly
      that.$map.sort(function (a, b) {
        var as = that.$map.get(a),
          bs = that.$map.get(b);

        // If both Properties are frozen then just compare against the current indexes
        if (as.isFrozen() && bs.isFrozen()) {
          return getKeyIndex(a, keys) < getKeyIndex(b, keys) ? -1 : 1;
        }
        if (as.isFrozen()) {
          return -1;
        }
        if (bs.isFrozen()) {
          return 1;
        }
        // If both Properties are dynamic then we can order as per the new array
        return getNewPropsIndex(a, dynamicProperties) < getNewPropsIndex(b, dynamicProperties) ? -1 : 1;
      });

      // Fire Component Event
      fwCompEvent(that, REORDERED, null, null, null, cx);

      cb.ok();
    } catch (err) {
      cb.fail(err);
    }

    return cb.promise();
  };

  /**
   * Invoke an Action.
   *
   * If the `Component` is mounted, this will **asynchronously** invoke
   * the Action on the Component in the Server.
   *
   * For callbacks, the `this` keyword is set to the `Component` instance.
   *
   * @param {baja.Action|String|Object} obj the Action, Action name or object
   * literal for the method's arguments.
   * @param {baja.Action|String} obj.slot the Action or Action name.
   * @param [obj.value] the Action's argument.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. This
   * function is called once Action has been invoked. If the Action has a
   * returned argument, this will be passed to this function.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * This function is called if the Action fails to invoke. Any error
   * information is passed into this function.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   * @param [obj.cx] the Context (used internally by BajaScript).
   * @returns {Promise.<baja.Value|null>} a promise that will be resolved once
   * the Action has been invoked.
   *
   * @example
   *   <caption>
   *     A Slot, Slot name or an object literal can used for the method's arguments.
   *   </caption>
   *
   *   // Invoke the Action via its Action Slot...
   *   myObj.invoke(fooAction);
   *
   *   // ...or via the Action's Slot name...
   *   myObj.invoke("foo");
   *
   *   // ...or for more arguments, use an object literal...
   *   myObj.invoke({
   *     slot: actionSlot, // Can also be an Action Slot name
   *     value: 'the Action argument',
   *     batch // if defined, any network calls will be batched into this object (optional)
   *   })
   *     .then(function (returnValue) {
   *       baja.outln('action successfully invoked and returned: ' + returnValue);
   *     })
   *     .catch(function (err) {
   *       baja.error('error invoking action: ' + err);
   *     });
   *
   * @example
   *   <caption>
   *     Please note that auto-generated convenience methods are created and
   *     added to a Component for invoking frozen Actions. If the name of the
   *     auto-generated Action method is already used, BajaScript will attach
   *     a number to the end of the method name so it becomes unique. For
   *     example, the 'set' Action on a NumericWritable would be called 'set1'
   *     because Component already has a 'set' method.
   *   </caption>
   *
   *   // Invoke an Action called 'override'. Pass in an argument
   *   myPoint.override(overrideVal);
   *
   *   // ...or via an object literal for more arguments...
   *   myPoint.override({
   *     value: overrideVal
   *     batch // if defined, any network calls will be batched into this object (optional)
   *   })
   *     .then(function (returnValue) {
   *       baja.outln('action successfully invoked and returned: ' + returnValue);
   *     })
   *     .catch(function (err) {
   *       baja.error('error invoking action: ' + err);
   *     });
   */
  Component.prototype.invoke = function (obj) {
    obj = objectify(obj, "slot");

    var that = this,
      action = obj.slot,
      arg = bajaDef(obj.value, null),
      cx = obj.cx,
      retVal = null,
      cb = new Callback(obj.ok, obj.fail, obj.batch),
      flags = 0,
      paramType;

    // Ensure 'this' is Component in callbacks...
    setContextInOkCallback(that, cb);
    setContextInFailCallback(that, cb);

    try {
      action = that.getSlot(action);

      if (action === null || !action.isAction()) {
        throw new Error("Action does not exist: " + obj.slot);
      }

      // Get Slot flags so we can test for 'async'
      flags = that.getFlags(action);
      paramType = action.getParamType();

      // Check we have a valid argument for this Action
      if (paramType) {
        if (baja.hasType(arg)) {
          if (arg.getType().isNumber() && paramType.isNumber() && !arg.getType().equals(paramType)) {
            // Recreate the number with the correct boxed type if the type spec differs
            arg = paramType.getInstance().constructor.make(arg.valueOf());
          } else if (!arg.getType().is(paramType)) {
            throw new Error(invalidActionArgErrMsg + arg);
          }
        } else {
          throw new Error(invalidActionArgErrMsg + arg);
        }
      }
    } catch (err) {
      // Notify fail
      cb.fail(err);

      // We should ALWAYS bail after calling a callback fail!
      return cb.promise();
    }

    function inv() {
      try {
        if (that.isMounted() && that.$space.hasCallbacks()) {
          // If mounted then make a network call for the Action invocation
          that.$space.getCallbacks().invokeAction(that, action, arg, cb);
          return;
        }

        if (!action.isProperty()) {
          // Invoke do method of Action
          var s = "do" + action.getName().capitalizeFirstLetter();
          if (typeof that[s] === "function") {
            // Invoke but ensure null is returned if the function returns undefined
            retVal = bajaDef(that[s](arg, cx), null);
          } else {
            throw new Error("Could not find do method for Action: " + action.getName());
          }
        } else {
          // If the Action is also a Property then forward its invocation on to the value
          retVal = bajaDef(that.get(action).invoke(that, arg, cx), null);
        }

        cb.ok(retVal);
      } catch (err) {
        cb.fail(err);
      }
    }

    if ((flags & Flags.ASYNC) !== Flags.ASYNC) {
      inv();
    } else {
      baja.runAsync(inv);
    }

    return cb.promise();
  };

  /**
   * Fire a Topic.
   *
   * If the Component is mounted, this will **asynchronously** fire
   * the Topic on the Component in the Server.
   *
   * For callbacks, the `this` keyword is set to the Component instance.
   *
   * @param {baja.Action|String|Object} obj the Topic, Topic name or object
   * literal for the method's arguments.
   * @param {baja.Action|String} obj.slot the Topic or Topic name.
   * @param [obj.value] the Topic's event.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. This
   * function is called once the Topic has been fired.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * This function is called if the Topic fails to fire. Any error information
   * is passed into this function.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   * @param [obj.cx] the Context (used internally by BajaScript).
   * @returns {Promise} a promise that will be resolved once the Topic has been
   * fired.
   *
   * @example
   *   <caption>
   *     A Slot, Slot name or an object literal can used for the method's arguments.
   *   </caption>
   *
   *   // Fire the Topic via its Topic Slot...
   *   myObj.fire(fooTopic);
   *
   *   // ...or via the Topic's Slot name...
   *   myObj.fire("foo");
   *
   *   // ...or for more arguments, use an object literal...
   *   myObj.fire({
   *     slot: topicSlot, // Can also be a Topic Slot name
   *     value: "the Topic event argument",
   *     batch // if defined, any network calls will be batched into this object (optional)
   *   })
   *     .then(function () {
   *       baja.outln('topic has been fired');
   *     })
   *     .catch(function (err) {
   *       baja.error('error firing topic: ' + err);
   *     });
   *
   * @example
   *   <caption>
   *     Please note that auto-generated convenience methods are created and
   *     added to a Component for firing frozen Topics. If the name of the
   *     auto-generated Topic method is already used, BajaScript will attach a
   *     number to the end of the method name so it becomes unique.
   *   </caption>
   *
   *   // Fire a Topic called 'foo'
   *   myObj.fireFoo();
   *
   *   // Fire a Topic called foo with an event argument...
   *   myObj.fireFoo("the Topic event argument");
   *
   *   // ...or via an object literal for more arguments...
   *   myObj.fireFoo({
   *     value: "the Topic event argument",
   *     batch // if defined, any network calls will be batched into this object (optional)
   *   })
   *     .then(function () {
   *       baja.outln('topic has been fired');
   *     })
   *     .catch(function (err) {
   *       baja.error('error firing topic: ' + err);
   *     });
   */
  Component.prototype.fire = function (obj) {
    obj = objectify(obj, "slot");

    var topic = obj.slot,
      event = obj.value,
      cx = obj.cx,
      serverDecode = cx && cx.serverDecode,
      commit = cx && cx.commit,
      cb = new Callback(obj.ok, obj.fail, obj.batch);

    // Ensure 'this' is Component in callbacks...
    setContextInOkCallback(this, cb);
    setContextInFailCallback(this, cb);

    try {
      // Ensure we have a Topic Slot
      topic = this.getSlot(topic);
      if (!topic.isTopic()) {
        throw new Error("Slot is not a Topic: " + topic.getName());
      }

      // Ensure event is not undefined
      event = bajaDef(event, null);

      // Short circuit some of this on a Server decode
      if (!serverDecode) {
        // Validate the event
        if (event !== null) {
          if (!baja.hasType(event)) {
            throw new Error("Topic event is not a BValue");
          }
          if (event.getType().isAbstract()) {
            throw new Error("Topic event is has abstract Type: " + event.getType());
          }
          if (!event.getType().isValue()) {
            throw new Error("Topic event is not a BValue: " + event.getType());
          }
        }
      }

      // Check if this is a proxy. If so then trap it...
      if (!commit && this.isMounted() && this.$space.hasCallbacks()) {
        this.$space.getCallbacks().fire(this, topic, event, cb);
        return cb.promise();
      }

      // If the Topic is a Property then fire on the Property
      if (topic.isProperty()) {
        this.get(topic).fire(this, event, cx);
      }

      // Fire component event for Topic
      fwCompEvent(this, TOPIC_FIRED, topic, event, null, cx);
    } catch (err) {
      cb.fail(err);
    }

    return cb.promise();
  };

  /**
   * Internal Framework Method.
   *
   * @private
   *
   * @see baja.Complex#$fw
   */
  Component.prototype.$fw = function (x, a, b, c, d) {
    var k, that = this;

    if (x === "modified") {
      // Fire a Component modified event for Property changed    
      fwCompEvent(that, CHANGED, a, null, null, b);
      return;
    }

    if (x === "fwSubscribed") {
      fwCompEvent(that, SUBSCRIBED, null, null, null, b);
      return;
    }

    if (x === "fwUnsubscribed") {
      fwCompEvent(that, UNSUBSCRIBED, null, null, null, b);
      return;
    }

    if (x === "modifyTrap") {
      // Check if this is a proxy. If so then trap any modifications
      if (that.isMounted() && that.$space.hasCallbacks() && !(d && d.commit)) {
        that.$space.getCallbacks().set(that,
          a,  // propertyPath
          b,  // value
          c); // callback
        return true;
      }
      return false;
    }

    if (x === "installKnob") {
      // Add a knob to the Component
      that.$knobs = that.$knobs || {};
      that.$knobCount = that.$knobCount || 0;

      that.$knobs[a.getId()] = a;
      a.getSourceComponent = function () { return that; };
      ++that.$knobCount;

      fwCompEvent(that, KNOB_ADDED, that.getSlot(a.getSourceSlotName()), /*a=Knob*/a, null, /*b=Context*/b);
      return;
    }

    if (x === "uninstallKnob") {
      // Remove the knob from the Component
      if (this.$knobs && this.$knobs.hasOwnProperty(a)) {
        k = this.$knobs[a];
        delete this.$knobs[a];
        --this.$knobCount;

        fwCompEvent(this, KNOB_REMOVED, /*b=Slot name*/this.getSlot(b), k, null, /*c=Context*/c);
      }
      return;
    }

    if (x === "installRelationKnob") {
      // Add a relation knob to the Component
      this.$rknobs = this.$rknobs || {};
      this.$rknobCount = this.$rknobCount || 0;

      this.$rknobs[a.getId()] = a;
      a.getEndpointComponent = function () { return that; };
      ++this.$rknobCount;

      fwCompEvent(this, RELATION_KNOB_ADDED, null, /*a=RelationKnob*/a, null, /*b=Context*/b);
      return;
    }

    if (x === "uninstallRelationKnob") {
      // Remove the relation knob from the Component
      if (this.$rknobs && this.$rknobs.hasOwnProperty(a)) {
        k = this.$rknobs[a];
        delete this.$rknobs[a];
        --this.$rknobCount;

        fwCompEvent(this, RELATION_KNOB_REMOVED, null, k, null, /*b=Context*/b);
      }
      return;
    }

    if (x === "setPermissions") {
      // Set the permissions on the Component
      this.$permissionsStr = a;

      // Nullify any decoded permissions
      this.$permissions = null;

      //no return - fall through
    }

    return callSuper("$fw", Component, this, arguments);
  };

  /**
   * Return true if the `Component` is mounted inside a Space.
   *
   * @returns {Boolean}
   */
  Component.prototype.isMounted = function () {
    return this.$space !== null;
  };

  /**
   * Return the Component Space.
   *
   * @returns the Component Space for this `Component` (if mounted) otherwise return null.
   */
  Component.prototype.getComponentSpace = function () {
    return this.$space;
  };

  /**
   * Return the `Component`'s handle.
   *
   * @returns {String|null} handle for this Component (if mounted), otherwise
   * null.
   */
  Component.prototype.getHandle = function () {
    return this.$handle;
  };

  /**
   * Return the ORD in session for this `Component`.
   *
   * @returns {baja.Ord} ORD in Session for this `Component` (or null if not mounted).
   */
  Component.prototype.getOrdInSession = function () {
    return this.getHandle() === null ? null : baja.Ord.make("station:|h:" + this.getHandle());
  };

  /**
   * Subscribe a number of `Component`s for a period of time.
   *
   * The default period of time is 10 seconds.
   *
   * Please note that a {@link baja.Subscriber} can also be used to put a
   * `Component` into and out of subscription.
   *
   * If the `Component` is mounted and it can be subscribed, this will result in
   * an **asynchronous** network call.
   *
   * If any of the the `Component`s are already leased, the lease timer will
   * just be renewed.
   *
   * For callbacks, the `this` keyword is set to whatever the `comps` argument
   * was originally set to.
   *
   * @see baja.Subscriber
   * @see baja.Component#lease
   *
   * @param {Object} obj an object literal for the method's arguments.
   * @param {Array.<baja.Component>|baja.Component} obj.comps the Components
   * to be subscribed.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
   * Called once the Component has been subscribed.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * Called if the Component fails to subscribe. Any errors will be passed to
   * this function.
   * @param {Number|baja.RelTime} [obj.time] the number of milliseconds or RelTime
   * for the lease. Defaults to 10,000 milliseconds.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will
   * be batched into this object. The timer will only be started once the
   * batch has fully committed.
   * @returns {Promise} a promise that will be resolved once the component(s)
   * have been subscribed.
   *
   * @example
   *   <caption>
   *     A time (Number or baja.RelTime) or an object literal can be used to
   *     specify the method's arguments.
   *   </caption>
   *
   *   // Lease an array of Components for the default time period
   *   myComp.lease([comp1, comp2, comp3]);
   *
   *   // ...or lease for 2 and half minutes...
   *   myComp.lease({
   *     time: baja.RelTime.make({minutes: 2, seconds: 30}),
   *     comps: [comp1, comp2, comp3]
   *   })
   *     .then(function () {
   *       baja.outln('components have been subscribed');
   *     })
   *     .catch(function (err) {
   *       baja.outln('components failed to subscribe: ' + err);
   *     });
   */
  Component.lease = function (obj) {
    obj = objectify(obj, "comps");
    var cb = new Callback(obj.ok, obj.fail, obj.batch),
      comps = obj.comps,
      time = bajaDef(obj.time, /*10 seconds*/10000),
      space = null,
      i,
      handles = [],
      compsToSub = [],
      promises = [],
      j;

    function scheduleUnlease(comp, time) {
      // Cancel the current lease ticket
      comp.$leaseTicket.cancel();

      // Schedule to expire the Subscription in 60 seconds
      comp.$leaseTicket = baja.clock.schedule(function () {
        unlease(comp);
      }, time);
    }

    try {
      if (!comps) {
        throw new Error("Must specify Components for lease");
      }

      // Ensure 'comps' is used as the context in the callback...
      setContextInOkCallback(comps, cb);
      setContextInFailCallback(comps, cb);

      // If a rel time then get the number of milliseconds from it
      if (baja.hasType(time) && time.getType().is("baja:RelTime")) {
        time = time.getMillis();
      }

      strictArg(time, Number);
      if (time < 1) {
        throw new Error("Invalid lease time (time must be > 0 ms): " + time);
      }

      if (!(comps instanceof Array)) {
        comps = [ comps ];
      }

      if (comps.length === 0) {
        cb.ok();
        return cb.promise();
      }

      for (i = 0; i < comps.length; ++i) {
        // Check all Components are valid
        strictArg(comps[i], Component);
        if (!comps[i].isMounted()) {
          throw new Error("Cannot subscribe unmounted Component!");
        }
        if (!space) {
          space = comps[i].getComponentSpace();
        }
        if (space !== comps[i].getComponentSpace()) {
          throw new Error("All Components must belong to the same Component Space!");
        }
      }

      cb.addOk(function (ok, fail) {
        var x;
        for (x = 0; x < comps.length; ++x) {
          scheduleUnlease(comps[x], time);
        }

        ok();
      });

      // Build handles we want to subscribe
      for (j = 0; j < comps.length; ++j) {
        // See if we need to make any network calls.
        if (!comps[j].$subDf || Promise.isRejected(comps[j].$subDf.promise())) {
          handles.push("h:" + comps[j].getHandle());
          compsToSub.push({
            comp: comps[j],
            df: (comps[j].$subDf = Promise.deferred())
          });
        }

        comps[j].$lease = true;

        promises.push(comps[j].$subDf.promise());
      }

      // When all of these promises are resolved then we're done.
      cb.addOk(function (ok, fail) {
        Promise.all(promises).then(ok, fail);
      });

      // If there's currently a lease active then renew it by calling ok on the callback
      if (handles.length === 0) {
        cb.ok();
        return cb.promise();
      }

      // Signal that each Component has been subscribed
      cb.addOk(function (ok, fail) {
        var i;
        for (i = 0; i < compsToSub.length; ++i) {
          try {
            compsToSub[i].comp.$fw("fwSubscribed");
          } catch (err0) {
            bajaError(err0);
          } finally {
            try {
              compsToSub[i].df.resolve();
            } catch (err1) {
              bajaError(err1);
            }
          }
        }

        ok();
      });

      cb.addFail(function (ok, fail, err) {
        var i;
        for (i = 0; i < compsToSub.length; ++i) {
          try {
            compsToSub[i].df.reject(err);
          } catch (err0) {
            bajaError(err0);
          }
        }
        fail(err);
      });

      // Make network call for subscription      
      space.getCallbacks().subscribe(handles, cb, obj.importAsync);
    } catch (err) {
      cb.fail(err);
    }

    return cb.promise();
  };

  /**
   * Subscribe a `Component` for a period of time.
   *
   * The default lease time is 10 seconds.
   *
   * Please note that a {@link baja.Subscriber} can also be used to put a
   * `Component` into and out of subscription.
   *
   * If the `Component` is mounted and it can be subscribed, this will result in
   * an **asynchronous** network call.
   *
   * If `lease` is called while the `Component` is already leased, the timer
   * will just be renewed.
   *
   * For callbacks, the `this` keyword is set to the `Component` instance.
   *
   * @see baja.Subscriber
   * @see baja.Component.lease
   *
   * @param {Number|baja.RelTime|Object} [obj] the number of milliseconds,
   * RelTime or an object literal for the method's arguments.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
   * Called once the Component has been subscribed.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * Called if the Component fails to subscribe. Any errors will be passed to
   * this function.
   * @param {Number|baja.RelTime} [obj.time] the number of milliseconds or
   * RelTime for the lease.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object. The timer will only be started once the batch has
   * fully committed.
   * @returns {Promise} a promise that will be resolved once the component has
   * been subscribed.
   *
   * @example
   *   <caption>
   *     A time (Number or baja.RelTime) or an object literal can be used to
   *     specify the method's arguments.
   *   </caption>
   *
   *   // Lease for 15 seconds
   *   myComp.lease(15000);
   *
   *   // ...or lease for 2 and half minutes...
   *   myComp.lease(baja.RelTime.make({minutes: 2, seconds: 30}));
   *
   *   // ...or lease using an object literal for more arguments...
   *   myComp.lease({
   *     time: 1000, // in milliseconds. Can also be a RelTime.
   *     batch // if defined, any network calls will be batched into this object (optional)
   *   })
   *     .then(function () {
   *       baja.outln('component has been leased');
   *     })
   *     .catch(function (err) {
   *       baja.error('failed to lease component: ' + err);
   *     });
   */
  Component.prototype.lease = function (obj) {
    obj = objectify(obj, "time");
    obj.comps = this;
    return Component.lease(obj);
  };

  /**
   * Is the component subscribed?
   *
   * @returns {Boolean}
   */
  Component.prototype.isSubscribed = function () {
    // Component is subscribed if is leased or a Subscriber is registered on it
    return this.$lease || this.$subs.length > 0;
  };

  /**
   * Load all of the Slots on the `Component`.
   *
   * If the `Component` is mounted and it can be loaded, this will result in
   * an **asynchronous** network call.
   *
   * For callbacks, the `this` keyword is set to the `Component` instance.
   *
   * @param {object} [obj]
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
   * Called once the Component has been loaded.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * Called if the Component fails to load. Any errors will be passed to this
   * function.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   * @returns {Promise} a promise that will be resolved once the slots have
   * been loaded.
   *
   * @example
   *   <caption>
   *     An optional object literal can be used to specify the method's arguments.
   *   </caption>
   *
   *   myComp.loadSlots({
   *     batch // if defined, any network calls will be batched into this object (optional)
   *   })
   *     .then(function () {
   *       baja.outln('slots have been loaded');
   *     })
   *     .catch(function (err) {
   *       baja.error('failed to load slots: ' + err);
   *     });
   */
  Component.prototype.loadSlots = function (obj) {
    obj = objectify(obj);

    let loadSlotsPromise = this.$loadSlotsPromise;
    if (!loadSlotsPromise) {
      const cb = new Callback(baja.ok, baja.fail, obj.batch);
      if (!this.$bPropsLoaded && this.isMounted() && this.$space.hasCallbacks()) {
        this.$space.getCallbacks().loadSlots("h:" + this.getHandle(), 0, cb);
      } else {
        cb.ok();
      }
      loadSlotsPromise = this.$loadSlotsPromise = cb.promise();
    }

    const cb = obj.cb || new Callback(obj.ok, obj.fail);

    // Ensure 'this' is Component in callbacks...
    setContextInOkCallback(this, cb);
    setContextInFailCallback(this, cb);

    loadSlotsPromise.then((result) => cb.ok(result), (err) => cb.fail(err));

    return cb.promise();
  };

  /**
   * Make a Server Side Call.
   *
   * Sometimes it's useful to invoke a method on the server from BajaScript.
   * A Server Side Call is how this is achieved.
   *
   * This will result in an **asynchronous** network call.
   *
   * In order to make a Server Side Call, the developer needs to first create a
   * Niagara (Server Side) class that implements the
   * `box:javax.baja.box.BIServerSideCallHandler` interface.
   * The implementation should also declare itself as an Agent on the target
   * Component Type (more information in Java interface docs).
   * For callbacks, the 'this' keyword is set to the Component instance.
   *
   * @param {Object} obj the object literal for the method's arguments.
   * @param {String} obj.typeSpec the type specification of the Server Side Call
   * Handler (`moduleName:typeName`).
   * @param {String} obj.methodName the name of the method to invoke in the
   * Server Side Call Handler
   * @param obj.value the value for the server side method argument (must be a
   * BajaScript Type)
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
   * Called once the Server Side Call has been invoked. Any return value is
   * passed to this function.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * Called if the Component fails to load. Any errors will be passed to this
   * function.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   * @returns {Promise.<baja.Value|null>} a promise that will be resolved once
   * the server side call has completed.
   *
   * @example
   *   <caption>
   *     Here's an example of how a method implemented by this handler can be invoked.
   *   </caption>
   *
   *   // A resolved and mounted Component...
   *   myComp.serverSideCall({
   *     typeSpec: "foo:MyServerSideCallHandler", // The TypeSpec (moduleName:typeName) of the Server Side Call Handler
   *     methodName: "bar", // The name of the public method we wish to invoke in the handler
   *     value: "the argument for the method", // The argument to pass into the method (this can be any Baja Object/Component structure).
   *                                              It will be deserialized automatically by Niagara.
   *     batch // if defined, any network calls will be batched into this object (optional)
   *   })
   *    .then(function (returnValue) {
   *      baja.outln('server side call has completed with: ' + returnValue);
   *    })
   *    .catch(function (err) {
   *      baja.error('server side call failed: ' + err);
   *    });
   */
  Component.prototype.serverSideCall = function (obj) {
    obj = objectify(obj);

    var typeSpec = obj.typeSpec,
      methodName = obj.methodName,
      val = bajaDef(obj.value, null),
      cb = new Callback(obj.ok, obj.fail, obj.batch);

    // Ensure 'this' is Component in callbacks...
    setContextInOkCallback(this, cb);
    setContextInFailCallback(this, cb);

    try {
      // Check arguments
      strictArg(typeSpec, String);
      strictArg(methodName, String);
      strictArg(val);

      // Can only make this call on proper mounted Components that have Space Callbacks
      if (this.isMounted() && this.$space.hasCallbacks()) {
        this.$space.getCallbacks().serverSideCall(this, typeSpec, methodName, val, cb);
      } else {
        throw new Error("Unable to make serverSideCall on non-proxy Component Space");
      }
    } catch (err) {
      cb.fail(err);
    }

    return cb.promise();
  };

  /**
   * Return the Slot Path of the `Component`.
   *
   * @returns {baja.SlotPath} the Slot Path or null if not mounted.
   */
  Component.prototype.getSlotPath = function () {
    if (!this.isMounted()) {
      return null;
    }

    var slotNames = [],
      b;

    function getParNames(comp) {
      slotNames.push(comp.getName());

      var p = comp.getParent();
      if (p !== null) {
        getParNames(p);
      }
    }

    getParNames(this);
    b = slotNames.reverse().join("/");
    if (b.length === 0) {
      b = "/";
    }
    return new baja.SlotPath(b);
  };

  /**
   * Return the path string of the `Component`.
   *
   * @returns {String} the Path String or null if not mounted.
   */
  Component.prototype.toPathString = function () {
    if (!this.isMounted()) {
      return null;
    }
    return this.getSlotPath().getBody();
  };

  /**
   * Return the Nav ORD for the `Component`.
   *
   * @param {object} [params]
   * @param {boolean} [params.sessionAware]
   * @returns {baja.Ord} the Nav ORD or null if it's not mounted.
   * @see baja.NavContainer#getNavOrd
   */
  Component.prototype.getNavOrd = function (params) {
    if (!this.isMounted()) {
      return null;
    }
    var spaceOrd = this.$space.getAbsoluteOrd(params);
    if (spaceOrd === null) {
      return null;
    }
    return baja.Ord.make(spaceOrd + "|" + this.getSlotPath());
  };

  /**
   * Return the Nav Name for the `Component`.
   *
   * @returns {String}
   */
  Component.prototype.getNavName = function () {
    var name = this.getName(),
      space;

    if (name !== null) {
      return name;
    }

    space = this.getComponentSpace();
    if (space && space.getRootComponent() !== null) {
      return space.getNavName();
    }

    return null;
  };

  /**
   * Return the type if the object the nav node navigates too.
   *
   * For a Component, this is just a string version of its own type spec.
   *
   * @returns {String} The nav node type spec.
   */
  Component.prototype.getNavTypeSpec = function () {
    return this.getType().toString();
  };

  /**
   * Return the Nav Display Name for the `Component`.
   *
   * @returns {String}
   */
  Component.prototype.getNavDisplayName = function () {
    return this.getDisplayName();
  };

  /**
   * Return the Nav Parent for the `Component`.
   *
   * @returns parent Nav Node
   */
  Component.prototype.getNavParent = function () {
    var parent = this.getParent();
    return parent || this.getComponentSpace();
  };

  /**
   * Access the Nav Children for the `Component`.
   *
   * @see baja.NavContainer#getNavChildren
   */
  Component.prototype.getNavChildren = function (obj) {
    obj = objectify(obj, "ok");

    var cb = new Callback(obj.ok, obj.fail, obj.batch),
      that = this,
      kids;

    if (that.isMounted() && that.$space.hasCallbacks()) {
      // If we're mounted then make a network call to get the NavChildren since this
      // is always implemented Server Side
      that.$space.getCallbacks().getNavChildren(that.getHandle(), cb);
    } else {
      kids = [];
      that.getSlots().properties().isComponent().each(function (slot) {
        if ((that.getFlags(slot) & baja.Flags.HIDDEN) === 0) {
          kids.push(that.get(slot));
        }
      });
      cb.ok(kids);
    }

    return cb.promise();
  };

  /**
   * Return the Icon for the `Component`
   *
   * @returns {baja.Icon}
   */
  Component.prototype.getIcon = function () {
    var icon = this.get('icon');
    if (baja.hasType(icon, 'baja:Icon')) {
      return icon;
    }
    return this.getType().getIcon();
  };

  /**
   * Return the Nav Icon for the `Component`
   *
   * @returns {baja.Icon}
   */
  Component.prototype.getNavIcon = function () {
    return this.getIcon();
  };

  /**
   * Return the Nav Description for the `Component`.
   *
   * @returns {String}
   */
  Component.prototype.getNavDescription = function () {
    return this.getType().toString();
  };

  // Mix-in the event handlers for baja.Component  
  baja.event.mixin(Component.prototype);

  // These comments are added for the benefit of JsDoc Toolkit...  

  /**
   * Attach an Event Handler to this `Component` instance.
   *
   * When an instance of `Component` is subscribed to a `Component` running
   * in the Station, BajaScript can be used to listen for Component Events.
   *
   * An event handler consists of a name and a function. When the
   * function is called, `this` will map to the target `Component` the handler
   * is attached to.
   *
   * For a list of all the event handlers and some of this method's more advanced
   * features, please see {@link baja.Subscriber#attach}.
   *
   * @function baja.Component#attach
   *
   * @see baja.Subscriber
   * @see baja.Component#detach
   * @see baja.Component#getHandlers
   * @see baja.Component#hasHandlers
   *
   * @param {String} event handler name
   * @param {Function} func the event handler function
   *
   * @example
   *   <caption>
   *     A common event to attach to would be a Property changed event.
   *   </caption>
   *
   *   // myPoint is a mounted and subscribed Component...
   *   myPoint.attach("changed", function (prop, cx) {
   *     if (prop.getName() === "out") {
   *       baja.outln("The output of the point is: " + this.getOutDisplay());
   *     }
   *   });
   */

  /**
   * Detach an Event Handler from the `Component`.
   *
   * If no arguments are used with this method then all events are removed.
   *
   * For some of this method's more advanced features, please see
   * {@link baja.Subscriber#detach}.
   *
   * For a list of all the event handlers, please see {@link baja.Subscriber#attach}.
   *
   * @function baja.Component#detach
   *
   * @see baja.Subscriber
   * @see baja.Component#attach
   * @see baja.Component#getHandlers
   * @see baja.Component#hasHandlers
   *
   * @param {String} [hName] the name of the handler to detach from the Component.
   * @param {Function} [func] the function to remove from the Subscriber. It's recommended to supply this just in case
   *                          other scripts have added event handlers.
   */

  /**
   * Return an array of event handlers.
   *
   * For a list of all the event handlers, please see
   * {@link baja.Subscriber#attach}.
   *
   * To access multiple handlers, insert a space between the handler names.
   *
   * @function baja.Component#getHandlers
   *
   * @see baja.Subscriber
   * @see baja.Component#detach
   * @see baja.Component#attach
   * @see baja.Component#hasHandlers
   *
   * @param {String} hName the name of the handler
   * @returns {Array.<Function>}
   */

  /**
   * Return true if there any handlers registered for the given handler name.
   *
   * If no handler name is specified then test to see if there are any
   * handlers registered at all.
   *
   * Multiple handlers can be tested for by using a space character between the names.
   *
   * For a list of all the event handlers, please see
   * {@link baja.Subscriber#attach}.
   *
   * @function baja.Component#hasHandlers
   *
   * @see baja.Subscriber
   * @see baja.Component#detach
   * @see baja.Component#attach
   * @see baja.Component#getHandlers
   *
   * @param {String} [hName] the name of the handler. If undefined, then see if there are any
   *                         handlers registered at all.
   * @returns {Boolean}
   */

  /**
   * Returns the default parameter for an Action.
   *
   * For unmounted Components, an error will be thrown. If mounted in a Proxy
   * Component Space, this will result in an asynchronous network call.
   *
   * For callbacks, the `this` keyword is set to the `Component` instance.
   *
   * @param {Object} obj the object literal for the method's arguments.
   * @param {baja.Action|String} obj.slot the Action or Action name.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
   * Called once the Action Parameter Default has been received. Any return
   * value is passed to this function (could be null if no Action parameter is
   * defined).
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   * @returns {Promise.<baja.Value|null>} a promise that will be resolved once
   * the callbacks have been invoked.
   *
   * @example
   *   // A resolved and mounted Component...
   *   myComp.getActionParameterDefault({
   *     slot: 'myAction',
   *     batch // if defined, any network calls will be batched into this object (optional)
   *   })
   *     .then(function (param) {
   *       if (param === null) {
   *         baja.outln('action takes no parameters');
   *       } else {
   *         baja.outln('action parameter is: ' + param);
   *       }
   *     })
   *     .catch(function (err) {
   *       baja.error('could not retrieve action parameter: ' + err);
   *     });
   */
  Component.prototype.getActionParameterDefault = function (obj) {
    obj = objectify(obj, "slot");

    var that = this,
      cb = new Callback(obj.ok, obj.fail, obj.batch),
      slot = that.getSlot(obj.slot),
      def;

    // Ensure 'this' is Component in callbacks...
    setContextInOkCallback(that, cb);
    setContextInFailCallback(that, cb);

    try {
      // Check arguments  
      if (slot === null) {
        throw new Error("Unable to find Action: " + obj.slot);
      }
      if (!slot.isAction()) {
        throw new Error("Slot is not an Action: " + obj.slot);
      }

      if (that.isMounted() && that.$space.hasCallbacks()) {
        // If mounted, then make a network call for the Action invocation
        that.$space.getCallbacks().getActionParameterDefault(that, slot, cb);
      } else {
        // If not mounted, call the deprecated method to obtain the error message for this unsupported operation
        def = slot.getParamDefault();
        cb.ok(def);
      }
    } catch (err) {
      cb.fail(err);
    }

    return cb.promise();
  };

  /**
   * Return the knob count for the `Component`.
   *
   * @returns {Number} the number of knobs installed on the `Component`
   */
  Component.prototype.getKnobCount = function () {
    return this.$knobCount || 0;
  };

  /**
   * Return the Knobs for a particular Slot or the whole `Component`.
   *
   * If no slot is passed in all the knobs for the `Component` will be returned.
   *
   * @param {baja.Slot|String} [slot] the Slot or Slot name.
   *
   * @returns {Array.<Object>} array of knobs
   */
  Component.prototype.getKnobs = function (slot) {
    if (!this.$knobCount) {
      return [];
    }

    var p,
      knob,
      k = [],
      mySlot;

    if (arguments.length > 0) {
      // Find Knobs for a particular Slot
      mySlot = this.getSlot(slot);

      if (!mySlot) {
        throw new Error("Invalid Slot: " + slot);
      }
    }


    // Build up knobs array
    for (p in this.$knobs) {
      if (this.$knobs.hasOwnProperty(p)) {
        knob = this.$knobs[p];
        if (mySlot) {
          if (knob.getSourceSlotName() === mySlot.getName()) {
            k.push(knob);
          }
        } else {
          k.push(knob);
        }
      }
    }

    return k;
  };

  /**
   * Return the relation knob count for the component.
   *
   * @returns {Number} The number of relation knobs installed on the component.
   */
  Component.prototype.getRelationKnobCount = function () {
    return this.$rknobCount || 0;
  };

  /**
   * Return all of the relation knobs for the component.
   *
   * @returns {Array.<Object>} An array of relation knobs.
   */
  Component.prototype.getRelationKnobs = function () {
    var that = this,
      rknobs = [],
      p;

    if (that.$rknobCount && that.$rknobs) {
      for (p in that.$rknobs) {
        if (that.$rknobs.hasOwnProperty(p)) {
          rknobs.push(that.$rknobs[p]);
        }
      }
    }

    return rknobs;
  };

  /**
   * Return the relation knob for the specified id.
   *
   * @param {String} id The id.
   *
   * @returns {Object|null} The relation knob or null if nothing can be found.
   */
  Component.prototype.getRelationKnob = function (id) {
    var rknobs = this.getRelationKnobs(),
      i;

    for (i = 0; i < rknobs.length; ++i) {
      if (rknobs[i].getRelationId() === id) {
        return rknobs[i];
      }
    }

    return null;
  };

  /**
   * Return the Links for a particular Slot or the whole `Component`.
   *
   * If no slot is passed in all the links for the `Component` will be returned.
   *
   * @param {baja.Slot|String} [slot] the `Slot` or Slot name.
   *
   * @returns {Array.<baja.Struct>} array of links.
   */
  Component.prototype.getLinks = function (slot) {
    var links = [],
      mySlot;

    if (arguments.length === 0) {
      // If no arguments then return all the knobs for this component
      this.getSlots(function (s) {
        return s.isProperty() && s.getType().isLink();
      }).each(function (s) {
        links.push(this.get(s));
      });
    } else {
      // Find Links for a particular Slot
      mySlot = this.getSlot(slot);
      if (mySlot === null) {
        throw new Error("Invalid Slot: " + slot);
      }
      this.getSlots(function (s) {
        return s.isProperty() && s.getType().isLink() && this.get(s).getTargetSlotName() === mySlot.getName();
      }).each(function (s) {
        links.push(this.get(s));
      });
    }

    return links;
  };

  /**
   * Create an instance of a Link to link from the specified source component
   * to this one.
   *
   * For unmounted `Component`s, by default this method resolves a plain
   * `baja:Link` instance. If mounted in a Proxy Component Space, this will
   * result in an asynchronous network call.
   *
   * For callbacks, the `this` keyword is set to the `Component` instance.
   *
   * @param {baja.LinkCheck~LinkCreateInfo} obj the object literal for the
   * method's arguments. The `target` property will be set to this instance on
   * which you're calling `makeLink()`.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. A
   * Link will be passed as an argument to this function.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   * @returns {Promise.<baja.Struct>} a promise that will be resolved with the
   * newly added Link.
   *
   * @example
   * component.makeLink({
   *   source: sourceComponent,
   *   sourceSlot: 'sourceSlot',
   *   targetSlot: 'myTargetSlot',
   *   batch // if defined, any network calls will be batched into this object (optional)
   * })
   *   .then(function (link) {
   *     baja.outln('link created: ' + link);
   *   })
   *   .catch(function (err) {
   *     baja.error('failed to create link: ' + err);
   *   });
   */
  Component.prototype.makeLink = function (obj) {
    obj = objectify(obj);

    var that = this,
      cb = new Callback(obj.ok, obj.fail, obj.batch),
      source = obj.source,
      sourceSlot = obj.sourceSlot,
      targetSlot = obj.targetSlot,
      link;

    // Ensure 'this' is Component in callbacks...
    setContextInOkCallback(that, cb);
    setContextInFailCallback(that, cb);

    try {
      strictArg(source, Component);

      sourceSlot = source.getSlot(sourceSlot);
      targetSlot = that.getSlot(targetSlot);

      if (!targetSlot) {
        throw new Error(fromBajaLex('makeLink.invalidTarget', 'Invalid Target Slot.'));
      }

      if (!sourceSlot) {
        throw new Error(fromBajaLex('makeLink.invalidSource', 'Invalid Source Slot.'));
      }

      if (that.isMounted() && that.$space.hasCallbacks()) {
        // If mounted then make a network call to get the link
        that.$space.getCallbacks().makeLink(source, sourceSlot, that, targetSlot, cb);
      } else {
        // If not mounted then just return it from the Slot
        link = baja.$("baja:Link");
        cb.ok(link);
      }
    } catch (err) {
      cb.fail(err);
    }
    return cb.promise();
  };

  /**
   * Check the validity of a link from the specified source `Component` to this
   * one.
   *
   * The target and source components must be mounted, leased / subscribed,
   * otherwise this function will fail.
   *
   * For callbacks, the `this` keyword is set to the `Component` instance.
   *
   * @param {baja.LinkCheck~LinkCreateInfo} obj the object literal for the
   * method's arguments. The `target` property will be set to this instance on
   * which you're calling `checkLink()`.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. A
   * Link will be passed as an argument to this function.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   * @returns {Promise.<baja.LinkCheck>} a promise that will be resolved with the
   * LinkCheck object.
   *
   * @example
   * component.checkLink({
   *   source: sourceComponent,
   *   sourceSlot: 'sourceSlot',
   *   targetSlot: 'myTargetSlot',
   *   batch // if defined, any network calls will be batched into this object (optional)
   * })
   *   .then(function (linkCheck) {
   *     baja.outln('link checked: ' + linkCheck.isValid());
   *   })
   *   .catch(function (err) {
   *     baja.error('failed to check link: ' + err);
   *   });
   */
  Component.prototype.checkLink = function (obj) {
    obj = objectify(obj);

    var that = this,
      cb = new Callback(obj.ok, obj.fail, obj.batch),
      source = obj.source,
      sourceSlot = obj.sourceSlot,
      targetSlot = obj.targetSlot;

    // Ensure 'this' is Component in callbacks...
    setContextInOkCallback(that, cb);
    setContextInFailCallback(that, cb);

    strictArg(source, Component);

    try {
      // Verify target and source are subscribed.
      if (!that.isSubscribed() || !source.isSubscribed()) {
        throw new Error(fromBajaLex('linkcheck.notSubscribed', 'Component.checkLink requires target source to be subscribed.'));
      }
      sourceSlot = source.getSlot(sourceSlot);
      targetSlot = that.getSlot(targetSlot);
      if (!targetSlot) {
        cb.ok(LinkCheck.makeInvalid(fromBajaLex('linkcheck.invalidTarget', 'Invalid link target.')));
      } else if (!sourceSlot) {
        cb.ok(LinkCheck.makeInvalid(fromBajaLex('linkcheck.invalidSource', 'Invalid link source.')));
      } else if (that.isMounted() && that.$space.hasCallbacks()) {
        var linkCheck = that.doCheckLink(obj);
        if (linkCheck instanceof LinkCheck) {
          cb.ok(linkCheck);
          return cb.promise();
        } else {
          cb.addOk(function (ok, fail, serverLinkCheck) {
            if (serverLinkCheck.isValid()) {
              Promise.resolve(linkCheck)
                .then(function (linkCheck) {
                  ok(linkCheck || serverLinkCheck);
                })
                .catch(fail);
            } else {
              ok(serverLinkCheck);
            }
          });
          // If mounted then make a network call to get the link check.
          // This ensures that BComponent.doCheckLink is called.
          that.$space.getCallbacks().checkLink(source, sourceSlot, that, targetSlot, cb);
        }
      } else {
        throw new Error(fromBajaLex('linkcheck.onlyMounted', 'Component.checkLink only supports mounted online components'));
      }
    } catch (err) {
      cb.fail(err);
    }

    return cb.promise();
  };

  /**
   * This is an override point to specify additional link checking
   * between the specified source and the target slot. The default
   * implementation is to return undefined, delegating the link checking to the
   * station. If you know the link is valid without checking the station,
   * return or resolve LinkCheck.makeValid(). If you know the link would result
   * in an error condition then return or resolve to an invalid LinkCheck
   * with the appropriate reason.
   *
   * @param {baja.LinkCheck~LinkCreateInfo} obj the object literal for the
   * method's arguments. The `target` property will be set to this instance on
   * which you're calling `doCheckLink()`.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. A
   * LinkCheck will be passed as an argument to this function.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   *
   * @returns {null|LinkCheck|Promise.<LinkCheck>} a falsy value to delegate
   * the decision to the server. Otherwise, return a LinkCheck object, or a
   * promise resolving to one.
   */
  Component.prototype.doCheckLink = function (obj) {
  };

  /**
   * Return the permissions for this `Component`.
   *
   * @returns {baja.Permissions}
   */
  Component.prototype.getPermissions = function () {
    var p = this.$permissions;
    if (!p) {
      if (typeof this.$permissionsStr === "string") {
        p = this.$permissions = baja.Permissions.make(this.$permissionsStr);
      } else {
        p = baja.Permissions.all;
      }
    }
    return p;
  };

  /**
   * Return true if this Component is a Nav Child.
   *
   * @returns {Boolean} true if this Component is a Nav Child.
   */
  Component.prototype.isNavChild = function () {
    return this.$nc;
  };

  /**
   * Returns a promise that resolves to the Component's tags. If the Component
   * is mounted under a Proxy Component Space, a network call will be made
   * for the Component's implied tags to include into the result.
   *
   * @param {Object} obj the object literal for the method's arguments.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. A
   * collection of tags will be passed as an argument to this function.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   *
   * @returns {Promise.<module:baja/tag/ComponentTags>} a promise that resolves
   * to the Component's tags.
   *
   * @example
   * component.tags({
   *   batch // if defined, any network calls will be batched into this object (optional)
   * })
   *   .then(function (tags) {
   *     baja.outln(tags.getAll().map(function (tag) {
   *       return tag.getId() + ' = ' + tag.getValue();
   *     }).join());
   *   })
   *   .catch(function (err) {
   *     baja.error('failed to retrieve tags: ' + err);
   *   });
   */
  Component.prototype.tags = function (obj) {
    obj = obj || {};

    var that = this,
      cb = new Callback(obj.ok, obj.fail, obj.batch);

    // Ensure 'this' is Component in callbacks...
    setContextInOkCallback(that, cb);
    setContextInFailCallback(that, cb);

    if (that.isMounted() && that.$space.hasCallbacks()) {

      cb.addOk(function (ok, fail, tags) {
        ok(new SmartTags(new ComponentTags(that), tags));
      });

      that.$space.getCallbacks().getImpliedTags(that.getHandle(), cb);
    } else {
      cb.ok(new ComponentTags(that));
    }

    return cb.promise();
  };

  /**
   * Returns a promise that resolves to the Component's relations. If the
   * Component is mounted under a Proxy Component Space, a network call will be
   * made for the Component's implied relations to include into the result.
   *
   * @param {Object} [obj] the object literal for the method's arguments.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. A
   * link will be passed as an argument to this function.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   *
   * @returns {Promise.<baja/tag/ComponentRelations>} a promise that resolves
   * to the Component's relations.
   *
   * @example
   * component.relations({
   *   batch // if defined, any network calls will be batched into this object (optional)
   * })
   *   .then(function (relations) {
   *     baja.outln(relations.getAll().map(function (relation) {
   *       return relation.getId() + ' = ' + relation.getEndpointOrd();
   *     }).join());
   *   })
   *   .catch(function (err) {
   *     baja.error('failed to retrieve relations: ' + err);
   *   });
   */
  Component.prototype.relations = function (obj) {
    obj = obj || {};

    var that = this,
      cb = new Callback(obj.ok, obj.fail, obj.batch);

    // Ensure 'this' is Component in callbacks...
    setContextInOkCallback(that, cb);
    setContextInFailCallback(that, cb);

    if (that.isMounted() && that.$space.hasCallbacks()) {

      cb.addOk(function (ok, fail, relations) {
        ok(new SmartRelations(new ComponentRelations(that), relations));
      });

      that.$space.getCallbacks().getImpliedRelations(that.getHandle(), cb);
    } else {
      cb.ok(new ComponentRelations(that));
    }

    return cb.promise();
  };

  /**
   * Invokes a Niagara RPC call on the Component running in the Station.
   * The method must implement the 'NiagaraRpc' annotation.
   *
   * Any extra arguments passed in will be encoded in raw JSON and passed up
   * to the Server. If one of those arguments is a 'baja.comm.Batch', the
   * call will be batched accordingly.
   *
   * @param {String} methodName The method name of the RPC call to invoke on
   * the Server.
   * @returns {Promise.<baja.Value|null>} A promise that is resolved once the
   * RPC call has completed. If the Station RPC call returns a value then it
   * will be encoded and returned as a value in the promise.
   */
  Component.prototype.rpc = function (methodName) {
    var args = Array.prototype.slice.call(arguments);
    return baja.rpc.apply(baja.comm, [ this.getOrdInSession() ].concat(args));
  };

  /**
   * Returns a promise that resolves to the agent list for this component.
   *
   * @see baja.registry.getAgents
   *
   * @param  {Array<String>} [is] An optional array of filters to add to the
   * agent query.
   * @param  {baja.comm.Batch} batch An optional object used to batch network
   * calls together.
   * @returns {Promise.<Array.<Object>>} A promise that will resolve with the
   * Agent Info.
   */
  Component.prototype.getAgents = function (is, batch) {
    var that = this;
    return baja.registry.getAgents(that.isMounted() ?
      that.getOrdInSession() : "type:" + that.getType(), is, batch);
  };

  /**
   * If the target is to a slot within this component
   * then use the read permission based on the slot's
   * operator flag.  If the target is the component itself
   * return if operator read is enabled.
   *
   * @since Niagara 4.12
   * @param {module:baja/ord/OrdTarget} ordTarget
   * @returns {boolean} true if the component has required permissions to be read
   */
  Component.prototype.canRead = function (ordTarget) {
    ordTargetMustBeToThisComponent(this, ordTarget);
    const slot = ordTarget.getSlotInComponent();
    return this.$canRead(slot && this.getSlot(slot));
  };

  /**
   * @private
   * @param {baja.Slot|string} [slot] the slot to check if I can read. If no slot specified, check
   * to see if this component itself is readable - only operator read required. If I do not have
   * the given slot, I can't read it.
   * @returns {boolean}
   * @see BComponent#canRead
   */
  Component.prototype.$canRead = function (slot) {
    const permissions = this.getPermissions();

    if (!slot) {
      return permissions.hasOperatorRead();
    }

    slot = this.getSlot(slot);

    if (!slot) {
      return false;
    }

    return permissions.hasAdminRead() || (hasOperatorFlag(slot) && permissions.hasOperatorRead());
  };

  /**
   * If the target is to a slot within this component
   * then use the write permission based on the slot's
   * readonly and operator flags.  If the target is the
   * component itself return if operator write is enabled.
   * 
   * @since Niagara 4.10
   * @param {module:baja/ord/OrdTarget} ordTarget
   * @returns {boolean} true if the component has required permissions to be written into
   */
  Component.prototype.canWrite = function (ordTarget) {
    ordTargetMustBeToThisComponent(this, ordTarget);
    const slot = ordTarget.getSlotInComponent();
    return this.$canWrite(slot && this.getSlot(slot));
  };

  /**
   * @private
   * @param {baja.Slot|string} [slot] the slot to check if I can write. If no slot specified, check
   * to see if this component itself is writable - only operator writable required. If I do not have
   * the given slot, I can't write to it.
   * @returns {boolean}
   * @see BComponent#canWrite
   */
  Component.prototype.$canWrite = function (slot) {
    const permissions = this.getPermissions();

    if (!slot) {
      return permissions.hasOperatorWrite();
    }

    slot = this.getSlot(slot);

    if (!slot || hasReadonlyFlag(slot)) {
      return false;
    }

    return permissions.hasAdminWrite() || (hasOperatorFlag(slot) && permissions.hasOperatorWrite());
  };

  /**
   * If the target is to a slot within this component
   * then use the invoke permission based on the slot's
   * operator flag.  If the target is the component itself
   * return if operator invoke is enabled.
   *
   * @since Niagara 4.12
   * @param {module:baja/ord/OrdTarget} ordTarget
   * @returns {boolean} true if the component has required permissions to be invoked
   */
  Component.prototype.canInvoke = function (ordTarget) {
    ordTargetMustBeToThisComponent(this, ordTarget);
    const slot = ordTarget.getSlotInComponent();
    return this.$canInvoke(slot && this.getSlot(slot));
  };

  /**
   * @private
   * @param {baja.Slot|string} [slot] the slot to check if I can invoke. If no slot specified, check
   * to see if this component itself is invokable - only operator invoke required. If I do not have
   * the given slot, I can't invoke it.
   * @returns {boolean}
   * @see BComponent#canInvoke
   */
  Component.prototype.$canInvoke = function (slot) {
    const permissions = this.getPermissions();

    if (!slot) {
      return permissions.hasOperatorInvoke();
    }

    slot = this.getSlot(slot);

    if (!slot) {
      return false;
    }

    return permissions.hasAdminInvoke() || (hasOperatorFlag(slot) && permissions.hasOperatorInvoke());
  };

  /**
   * @param {baja.Component} comp
   * @param {module:baja/ord/OrdTarget} ordTarget
   */
  function ordTargetMustBeToThisComponent(comp, ordTarget) {
    if (ordTarget.getComponent() !== comp) {
      throw new Error('OrdTarget is not to this component');
    }
  }

  function fromBajaLex(key, def) {
    return lexjs.$getSync({ module: 'baja', key, def });
  }

  return Component;
});