baja/comp/Complex.js

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

/**
 * Defines {@link baja.Complex}.
 * @module baja/comp/Complex
 */
define([
  "bajaScript/comm",
  "bajaScript/baja/comp/Flags",
  "bajaScript/baja/comp/Property",
  "bajaScript/baja/comp/SlotCursor",
  "bajaScript/baja/obj/Value",
  "bajaScript/baja/comm/Callback",
  "bajaPromises" ], function (
  baja,
  Flags,
  Property,
  SlotCursor,
  Value,
  Callback,
  Promise) {
  
  "use strict";

  const { callSuper, def: bajaDef, objectify, strictArg, subclass } = baja;
  const COPYING_CONTEXT = { type: "copying" };
  const DEFAULT_ON_CLONE = Flags.DEFAULT_ON_CLONE;
  const REMOVE_ON_CLONE = Flags.REMOVE_ON_CLONE;
  
  function applyObjToComplex(clx, obj) {
    var slotName,
        value,
        oldValue,
        complexIsComponent = isComponent(clx);
    
    for (slotName in obj) {
      if (obj.hasOwnProperty(slotName)) {
        value = obj[slotName];
        if (clx.has(slotName)) {
          
          // If value is a Object Literal then recursively apply it
          if (value && value.constructor === Object) {
            applyObjToComplex(clx.get(slotName), value);
          } else {
            clx.set({
              slot: slotName,
              value: value
            });
          }
        } else if (complexIsComponent) {
          
          // If value is an Object Literal then recursively apply it 
          if (value && value.constructor === Object) {
            oldValue = value;
            value = new baja.Component();
            applyObjToComplex(value, oldValue);
          }
          
          clx.add({
            slot: slotName,
            value: value
          });
        }
      }
    }
  }

  function getOwnProperty(obj, key) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      return obj[key];
    }
  }
  
  /**
   * `Complex` is the `Value` which is defined by one or more
   * property slots. `Complex` is never used directly, rather
   * it is the base class for `Struct` and `Component`.
   * 
   * Since `Complex` relates to a abstract `Type`, this Constructor should
   * never be directly used to create a new object.
   *
   * Even when subclassing `Struct` or `Component`, as in a Type Extension, it
   * may not be safe to call the constructor directly because frozen slots may
   * not be applied. The best way to obtain an instance of a `Complex` is by
   * using `baja.$()`.
   *
   * @see baja.Struct
   * @see baja.Component
   * @see baja.$
   * 
   * @class
   * @alias baja.Complex
   * @extends baja.Value
   */
  var Complex = function Complex() {
    callSuper(Complex, this, arguments);
    this.$map = new baja.OrderedMap();
    this.$parent = null;
    this.$propInParent = null; 
  };
  
  subclass(Complex, Value);
  
  /**
   * Called once the frozen Slots have been loaded onto the Complex.
   *
   * @param {Object} [arg]     
   *
   * @private
   */
  Complex.prototype.contractCommitted = function (arg) {
    // If this is a complex and there was an argument then attempt
    // to set (of add if a Component) Properties...
    if (arg && arg.constructor === Object) {
      applyObjToComplex(this, arg);
    }
  };
  
  /**
   * Return the name of the `Component`.
   * 
   * The name is taken from the parent `Component`'s Property for this 
   * `Component` instance.
   * 
   * @returns {String} name (null if not mounted).
   */
  Complex.prototype.getName = function () { 
    return this.$propInParent === null ? null : this.$propInParent.getName();
  };
  
  /**
   * Return a display name.
   * 
   * If a Slot is defined as an argument, the display name for the slot will 
   * be returned. If no Slot is defined, the display name of the `Complex` 
   * will be returned.
   *
   * @param {baja.Slot|String} [slot] the Slot or Slot name.
   *
   * @returns {String} the display name (or null if none available).
   */
  Complex.prototype.getDisplayName = function (slot) {
    var s,
        nameMap,
        nameMapDisp,
        entry;

    // If no Slot defined then get the display name of the Complex
    if (slot === undefined) {
      return this.getPropertyInParent() === null ? null : this.getParent().getDisplayName(this.getPropertyInParent());
    }
  
    slot = this.getSlot(slot);
    
    // Bail if this slot doesn't exist
    if (slot === null) {
      return null;
    }
    
    // See if a BNameMap is being used.
    nameMap = this.get("displayNames");
    
    if (nameMap && nameMap.getType().getTypeSpec().equals("baja:NameMap")) {
      
      // BOX does a special encoding for name maps by predecoding their format values. This saves us 
      // having to do anything for decoding since we want to access the display names synchronously.
      if (!nameMap.$decodedMap) {
        nameMapDisp = this.getDisplay("displayNames");
        if (nameMapDisp) {
          nameMap.$decodedMap = JSON.parse(nameMapDisp);
        }
      }
               
      if (nameMap.$decodedMap) {
        entry = getOwnProperty(nameMap.$decodedMap, slot.getName());
        if (entry) {
          return entry;
        }
      } else {
        entry = nameMap.get(slot.getName());
        if (entry) {
          // If we've found an entry then used this for the Slot's display name
          return entry.toString();
        }
      }
    }
    
    // This should be ok but just double check to ensure we have a display string
    s = slot.$getDisplayName();
    if (typeof s !== "string") {
      s = "";
    }
    
    // If there is no display name then default to unescaping the slot name
    if (s === "") {
      s = baja.SlotPath.unescape(slot.getName());
    }
    
    return s;
  };
  
  /**
   * Return a display string.
   * 
   * If a Slot argument is defined, the display value for the Slot will be
   * returned. If a Slot argument is not defined, the display value for the
   * Complex will be returned.
   * 
   * @param {baja.Property|String} [slot]  the Slot or Slot name.
   * @param {object} [cx] as of Niagara 4.10, you can pass a context as the
   * second argument to use for string formatting. When a context is passed,
   * a Promise will be returned to be resolved with the formatted string.
   *
   * @returns {String|null|Promise.<String|null>} display (or null if none available).
   *
   * @example
   *   <caption>Printing display values of slots</caption>
   *   
   *   // myPoint has a Property named out...
   *   baja.outln('The display string of the out Property: ' + myPoint.getDisplay('out'));
   *
   *   // or use string formatting:
   *   return myPoint.getDisplay('out', { precision: 5 })
   *     .then(function (str) { baja.outln('formatted display string: ' + str); });
   */
  Complex.prototype.getDisplay = function (slot, cx) {
    var display = null;

    if (slot === undefined) {
      slot = this.getPropertyInParent();

      if (cx && hasCustomToString(this)) {
        display = this.toString(cx);
      } else {
        display = slot && slot.$getDisplay();
      }
      return cx ? Promise.resolve(display) : display;
    }

    slot = this.getSlot(slot);

    if (cx) {
      if (slot) {
        if (slot.isProperty()) {
          var value = this.get(slot);
          if ((value.getType().isSimple() && !(value instanceof baja.DefaultSimple)) ||
              (isComplex(value) && hasCustomToString(value))) {
            display = value.toString(cx);
          } else {
            display = slot.$getDisplay();
          }
        } else {
          return Promise.reject(new Error('"' + slot + '" is not a Property'));
        }
      }
      return Promise.resolve(display);
    } else {
      if (!slot) {
        return null;
      }
      if (!slot.isProperty()) {
        throw new Error('"' + slot + '" is not a Property');
      }
      return slot.$getDisplay();
    }
  };
      
  /**
   * Return the `String` representation.
   *
   * @returns {String}
   */
  Complex.prototype.toString = function () {
    var str = this.getDisplay();
    return typeof str === "string" ? str : this.getType().toString();
  };

  /**
   * Utilize the slot facets, context, and slot to resolve the proper toString.
   * @param {baja.Property|String} slot
   * @param {object} [cx] the context
   * @return {Promise.<String>}
   * @throws {Error} if the slot name isn't a Property.
   * @since Niagara 4.11
   */
  Complex.prototype.propertyValueToString = function (slot, cx) {
    var slotResult = this.getSlot(slot);
    if (!slotResult || !slotResult.isProperty()) {
      throw new Error('"' + slot + '" is not a Property');
    }
    cx = Object.assign(this.getFacets(slotResult).toObject(), cx);
    return this.get(slot).toString(cx);
  };
  
  /**
   * Return the parent.
   *
   * @returns {baja.Complex} parent
   */
  Complex.prototype.getParent = function () {
    return this.$parent;
  };

  /**
   * Get the nearest ancestor of this object which is
   * an instance of `Component`.  If this object is itself
   * a `Component`, then return `this`.  Return
   * null if this object doesn't exist under a `Component`.
   *
   * @since Niagara 4.10
   * @returns {baja.Component | null}
   */
  Complex.prototype.getParentComponent = function () {

    var complex = this;
    while (complex && !isComponent(complex)) {
      complex = complex.getParent();
    }

    return complex;
  };

  /**
   * @param {baja.Complex|*} comp the descendent complex to test
   * @returns {boolean} if this Complex is an ancestor of the given Complex (but not the same
   * instance). Returns false if the given parameter is not actually a Complex.
   * @since Niagara 4.12
   */
  Complex.prototype.isAncestorOf = function (comp) {
    if (!isComplex(comp)) {
      return false;
    }

    let ancestor = comp;
    while ((ancestor = ancestor.getParent())) {
      if (ancestor === this) { return true; }
    }

    return false;
  };
  
  /**
   * Return the `Property` in the parent.
   *
   * @returns {baja.Property} the `Property` in the parent (null if not mounted).
   */
  Complex.prototype.getPropertyInParent = function () {
    return this.$propInParent;
  };
  
  /**
   * Return the `Slot`.
   * 
   * This is useful method to ensure you have the `Slot` instance instead of the 
   * Slot name String. If a `Slot` is passed in, it will simply be checked and 
   * returned.
   *
   * @param {baja.Slot|String} slot the `Slot` or Slot name.
   * @returns {baja.Slot} the `Slot` for the `Component` (or null if the
   * `Slot` doesn't exist).
   */
  Complex.prototype.getSlot = function (slot) {
    if (typeof slot === "string") {
      return this.$map.get(slot);
    }

    strictArg(slot, baja.Slot);
    return this.$map.get(slot.getName());
  };
      
  /**
   * Return a `Cursor` for accessing a `Complex`'s Slots.
   * 
   * Please see {@link module:baja/comp/SlotCursor} for useful builder methods.
   *
   * @param {Function} [filter]  function to filter out the Slots we're not interested in.
   *                             The filter function will be passed each `Slot` to see if it should be
   *                             be included. The function must return false to filter out a value and true
   *                             to keep it.
   *
   * @returns {module:baja/comp/SlotCursor} a `Cursor` for iterating through the
   * `Complex`'s Slots.
   * @see baja.Complex#get
   * 
   * @example
   *   // A Cursor for Dynamic Properties
   *   var frozenPropCursor = myComp.getSlots().dynamic().properties();
   *
   *   // A Cursor for Frozen Actions
   *   var frozenPropCursor = myComp.getSlots().frozen().actions();
   *
   *   // An Array of Control Points
   *   var valArray = myComp.getSlots().properties().is("control:ControlPoint").toValueArray();
   *
   *   // An Array of Action Slots
   *   var actionArray = myComp.getSlots().actions().toArray();
   *
   *   // An Object Map of slot name/value pairs
   *   var map = myComp.getSlots().properties().toMap();
   * 
   *   // The very first dynamic Property
   *   var firstProp = myComp.getSlots().dynamic().properties().first();
   *
   *   // The very last dynamic Property
   *   var lastProp = myComp.getSlots().dynamic().properties().last();
   *
   *   // The very first dynamic Property value
   *   var firstVal = myComp.getSlots().dynamic().properties().firstValue();
   *
   *   // The very first dynamic Property value
   *   var lastVal = myComp.getSlots().dynamic().properties().lastValue();
   *
   *   // All the Slots that start with the name 'foo'
   *   var slotNameCursor = myComp.getSlots().slotName(/^foo/);
   *
   *   // Use a custom Cursor to find all of the Slots that have a particular facets key/value
   *   var custom = myComp.getSlots(function (slot) {
   *      return slot.isProperty() && (this.getFacets(slot).get("myKey", "def") === "foo");
   *   });
   * 
   *   // Same as above
   *   var custom2 = myComp.getSlots().filter(function (slot) {
   *      return slot.isProperty() && (this.getFacets(slot).get("myKey", "def") === "foo");
   *   });
   *   
   *   // All Slots marked summary on the Component
   *   var summarySlotCursor = myComp.getSlots().flags(baja.Flags.SUMMARY);
   *
   *   // Call function for each Property that's a ControlPoint
   *   myComp.getSlots().is("control:ControlPoint").each(function (slot) {
   *     baja.outln("The Nav ORD for the ControlPoint: " + this.get(slot).getNavOrd();
   *   });
   *
   * @example
   * <caption>You may see better performance if you can filter on slots
   * instead of on values.<caption>
   *
   * // slower
   * var links = component.getSlots().toValueArray()
   *   .filter(function (value) { return value.getType().is('baja:Link'); });
   *
   * // faster - this avoids unnecessarily decoding every non-Link slot
   * var links = component.getSlots().is('baja:Link').toValueArray();
   */
  Complex.prototype.getSlots = function (filter) {
    var cursor = this.$map.getCursor(this, SlotCursor);
    if (filter) {
      cursor.filter(filter);
    }
    return cursor;
  };
  
  /**
   * Return `Flags` for a slot or for the `Complex`'s parent Property.
   * 
   * If no arguments are provided and the `Complex` has a parent, the 
   * flags for the parent's `Property` will be returned. 
   *
   * @see baja.Flags
   *
   * @param {baja.Slot|String} [slot] `Slot` or Slot name.
   * @returns {Number} the flags for the `Slot` or the parent's `Property` flags.
   */
  Complex.prototype.getFlags = function (slot) { 
    // If no arguments are specified then attempt to get parent properly slot Flags
    if (arguments.length === 0) {
      if (this.$parent === null || this.$propInParent === null) {
        throw new Error("Complex has no parent");
      }
      
      return this.$parent.getFlags(this.$propInParent);
    }

    var mySlot = this.getSlot(slot);
    if (mySlot === null) {
      throw new Error("Slot doesn't exist: " + slot);
    }
    return mySlot.getFlags();
  };
    
  /**
   * Return a `Property`'s value.
   * 
   * Note that when an instance of a `Complex` is created, auto-generated 
   * accessors are created to make accessing a frozen `Property`'s value 
   * convenient (see example).
   *
   * Performance notes:
   *
   * BajaScript performs "lazy decoding" on Complexes - this means that after a
   * Complex is retrieved from the station, its slot values are kept in their
   * raw JSON encoding until they are actually needed (by calling `get()`). This
   * helps performance by not spending CPU time decoding values that won't be
   * used. It follows that calling `getSlot(slot)` will be much faster than
   * calling `get(slot)` for the first time, so consider filtering on slots
   * rather than on values, if possible, in performance-critical situations. See
   * example under `getSlots()`.
   * 
   * @param {baja.Property|String} prop the Property or Property name.
   * @returns {baja.Value} the value for the Property (null if the Property doesn't exist).
   * @see baja.Complex#getSlots
   * 
   * @example
   *   <caption>
   *     Note that when an instance of a Complex is created, 
   *     auto-generated setters are created to make setting a frozen Property's 
   *     value convenient. The auto-generated setter is in the format of 
   *     <code>set(first letter is capitalized)SlotName(...)</code>.
   *   </caption>
   *   
   *   // myPoint has a frozen Property named out...
   *   var val = myPoint.getOut();
   *   val = myPoint.get('out'); //equivalent
   */
  Complex.prototype.get = function (prop) {
    prop = this.getSlot(prop);
    if (prop === null) {
      return null;
    }
    return prop.$getValue();
  };
  
  /**
   * Return true if the `Slot` exists.
   *
   * @param {baja.Property|String} prop the Property or Property name
   * @returns {Boolean}
   */
  Complex.prototype.has = function (prop) {
    return this.getSlot(prop) !== null;
  };
  
  /**
   * **Deprecated.**
   * Return the result of `valueOf` on the specified Property's value.
   * If `valueOf` is not available then the `Property`'s value is returned.
   *
   * @deprecated see UI Changelog in Doc Developer
   * @see baja.Complex#get
   *
   * @param {baja.Property|String} prop the `Property` or Property name.
   * @returns the `valueOf` for the `Property`'s value or the `Property`'s 
   * value (null if the `Property` doesn't exist).
   */
  Complex.prototype.getValueOf = function (prop) {
    var v = this.get(prop);
    if (v !== null && typeof v.valueOf === "function") {
      return v.valueOf();
    }

    return v;
  };
  
  function syncStruct(fromVal, toVal) {
    fromVal.getSlots().properties().each(function (fromProp) {
      var toProp = toVal.getSlot(fromProp.getName());
      
      // Sync value display and slot display name
      fromProp.$setDisplay(toProp.$getDisplay());
      fromProp.$setDisplayName(toProp.$getDisplayName());
      
      if (fromProp.getType().isStruct()) {
        // If another struct then sync appropriately
        syncStruct(fromProp.$getValue(), toProp.$getValue());
      } else {
        // If a simple then directly set the value
        fromProp.$setValue(toProp.$getValue());
      }      
    });
  }
        
  /**
   * Set a `Property`'s value.
   * 
   * If the Complex is mounted, this will **asynchronously** set the Property's
   * value on the Server. 
   *
   * @name baja.Complex#set
   * @function
   *
   * @param {Object} obj  the object literal for the method's arguments.
   * @param {baja.Property|String} obj.slot  the `Property` or Property name 
   * the value will be set on.
   * @param obj.value  the value being set (Type must extend `baja:Value`).
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok function
   * callback. Called once the network call has succeeded on the Server.
   * @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 value has been
   * set on the Complex. If the complex is mounted, will be resolved once the
   * value has been saved to the server.
   * 
   * @example 
   *   <caption>
   *   An object literal is used to specify the method's arguments.
   *   </caption>
   *   myObj.set({
   *     slot: "outsideAirTemp",
   *     value: 23.5,
   *     batch: myBatch // if defined, any network calls will be batched into this object (optional)
   *   })
   *     .then(function () {
   *       baja.outln('value has been set on the server');
   *     })
   *     .catch(function (err) {
   *       baja.error('value failed to set on the server: ' + err);
   *     });
   *
   * @example 
   *   <caption>
   *     Note that when an instance of a Complex is created, 
   *     auto-generated setters are created to make setting a frozen Property's 
   *     value convenient. The auto-generated setter is in the format of 
   *     <code>set(first letter is capitalized)SlotName(...)</code>.
   *   </caption>
   *   
   *   // myPoint has a Property named outsideAirTemp...
   *   myObj.setOutsideAirTemp(23.5);
   *   
   *   // ... or via an Object Literal if more arguments are needed...
   *   
   *   myObj.setOutsideAirTemp({ value: 23.5, batch: myBatch })
   *     .then(function () {
   *       baja.outln('value has been set on the server');
   *     });
   */
  Complex.prototype.set = function (obj) {    
    obj = objectify(obj);

    var cb = new Callback(obj.ok, obj.fail, obj.batch),
        prop = obj.slot,
        val = obj.value,
        propType,
        valType,
        isClx,
        cx = bajaDef(obj.cx, null),
        serverDecode = cx && cx.serverDecode,
        commit = cx && cx.commit,
        syncStructVals = cx && cx.syncStructVals,
        comp = this;     // Ensure 'this' is the Component in the ok and fail callback...
    
    // Find the top level Component
    while (comp !== null && !isComponent(comp)) {
      comp = comp.getParent();
    }
    
    cb.addOk(function (ok, fail, resp) {
      if (comp !== null) {
        ok.call(comp, resp);
      } else {
        ok(resp);
      }
    });

    cb.addFail(function (ok, fail, err) {
      if (comp !== null) {
        fail.call(comp, err);
      } else {
        fail(err);
      }
    });

    try {
      prop = this.getSlot(prop);

      // If decoding from the Server then short circuit some of this
      if (!serverDecode) {        
        // Validate arguments
        strictArg(prop, Property);
        strictArg(val);
        
        if (prop === null) {
          throw new Error("Could not find Property: " + obj.slot);
        }
        if (!baja.hasType(val)) {
          throw new Error("Can only set BValue Types as Component Properties");
        }

        propType = prop.getType();
        valType = val.getType();

        if (valType.isAbstract()) {
          throw new Error("Cannot set value in Complex to Abstract Type: " + valType);
        }

        // NCCB-20264 If val and prop types are both numbers, don't do this type coercion
        // unless the slot is frozen
        // If it's a dynamic slot, change the type of the slot
        if (prop.isFrozen()) {
          if (valType.isNumber() && propType.isNumber() &&
            !valType.equals(propType) &&
            !propType.isAbstract()) {
            // Recreate the number with the correct boxed type if the type spec differs
            val = propType.getInstance().constructor.make(val.valueOf());
          }
        }

        if (!valType.isValue()) {
          throw new Error("Cannot set non Value Types as Properties in a Complex");
        }
        if (val === this) {
          throw new Error("Illegal argument: this === value");
        }
      }

      if (cx) {
        if (typeof cx.displayName === "string") {
          prop.$setDisplayName(cx.displayName);
        }
        if (typeof cx.display === "string") {
          prop.$setDisplay(cx.display);
        }
      }
      
      if (val.equals(prop.$getValue())) {
        // TODO: May need to check for mounted on Components here
        cb.ok();
        return cb.promise();
      }
            
      // Return if this set is trapped. If the set is trapped then the set operation will
      // be proxied off to a remote Space elsewhere...
      if (!commit && this.$fw("modifyTrap", [ prop ], val, cb, cx)) {
        return cb.promise();
      }
                  
      // Unparent    
      isClx = isComplex(val);
      if (isClx) {    
        if (val.getParent()) {
          throw new Error("Complex already parented: " + val.getType());
        } 
      
        val.$parent = null;
        val.$propInParent = null;     
      }
            
      // If this is the same Struct from a Server decode then attempt to sync it
      // rather than completely replace it...
      if (syncStructVals && val.getType().isStruct() && val.getType().equals(prop.getType())) {
        syncStruct(/*from*/prop.$getValue(), /*to*/val);
      } else {  
        // Set new Property value
        prop.$setValue(val);
          
        // Parent
        if (isClx) {     
          val.$parent = this;
          val.$propInParent = prop;
          
          // If we have a Component then attempt to mount it
          if (isComponent(val) && this.isMounted()) {
            this.$space.$fw("mount", val);      
          }
        }
      }      
      
      // Invoke modified event (this will bubble up to a Component for the changed callback etc).
      this.$fw("modified", prop, cx);
      
      // TODO: Modified a link. Need to set up Knobs?
      cb.ok();
    } catch (err) {
      cb.fail(err);
    }
    
    return cb.promise();
  };
  
  /**
   * Load all of the Slots on the `Complex`.
   *
   * @param {Object} [obj] the object literal for the method's arguments.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok function
   * callback. Called once network call has succeeded on the Server.
   * @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.
   * @returns {Promise} a promise to be resolved when the slots have been
   * loaded.
   *
   * @see baja.Component#loadSlots
   */
  Complex.prototype.loadSlots = function (obj) {
    if (obj && obj.ok && typeof obj.ok === "function") {
      obj.ok.call(this);
    }
    return Promise.resolve();
  };
    
  /**
   * Return the `Facets` for a Slot.
   * 
   * If no arguments are provided and the `Complex` has a parent, the 
   * facets for the parent's Property will be returned. 
   *
   * @param {baja.Slot|String} [slot]  the `Slot` or Slot name.
   * @returns {baja.Facets} the `Facets` for the `Slot` (or null if Slot not
   * found) or the parent's Property facets.
   */
  Complex.prototype.getFacets = function (slot) {
    // If no arguments are specified then attempt to get parent properly slot Flags
    if (arguments.length === 0) {
      if (this.$parent !== null && this.$propInParent !== null) {
        return this.$parent.getFacets(this.$propInParent);
      }
      
      return null;
    }

    const mySlot = this.getSlot(slot);

    if (!mySlot) { return null; }

    const slotFacets = mySlot.getFacets();

    if (mySlot.isFrozen()) {
      const [ closestComponent, slotPath ] = findClosestComponent(this);

      if (closestComponent) {
        const facetsMap = closestComponent.get('slotFacets_');
        if (baja.hasType(facetsMap, 'baja:FacetsMap')) {
          return baja.Facets.make(facetsMap.get(slotPath.concat(slot).join('/')), slotFacets);
        }
      }
    }

    return slotFacets;
  };
    
  /**
   * Compare if all of this object's properties are equal to the specified object.
   *
   * @param {baja.Complex} obj
   * @returns {Boolean} true if this object is equivalent to the specified object.
   */
  Complex.prototype.equivalent = function (obj) { 
    if (!baja.hasType(obj)) {
      return false;
    }
  
    if (!obj.getType().equals(this.getType())) {
      return false;
    }
    
    if (this.$map.getSize() !== obj.$map.getSize()) {
      return false;
    }
    
    // Compare flags and names for all slots.
    let slots = obj.getSlots(),
        s,
        val;

    while (slots.next()) {

      s = slots.get();
      // Always check flags
      if (!this.getFlags(s.getName()).equals(obj.getFlags(s))) {
        return false;
      }

      if (s.isProperty()) {

        val = obj.get(s);

        if (val === null) {
          return false;
        }

        // Compare Property values
        if (!val.equivalent(this.get(s.getName()))) {
          return false;
        }
      }
      
      // Ensure they're the same order in the SlotMap
      if (this.$map.getIndex(s.getName()) !== slots.getIndex()) {
        return false;
      }
    }
       
    return true;
  };
  
  /**
   * Create a clone of this `Complex`.
   * 
   * If the `exact` argument is true and this `Complex` is a `Component` then
   * the `defaultOnClone` and `removeOnClone` flags will be ignored.
   *
   * @param {Boolean} [exact=false] flag to indicate whether to create an exact copy
   * @returns {baja.Complex} cloned Complex.
   */
  Complex.prototype.newCopy = function (exact) {
    var newInstance = this.getType().getInstance(),
        props = this.getSlots().properties(),    
        p,
        val;
    
    while (props.next()) {
      p = props.get();
       
      // If default on clone and it's not an exact copy then skip this Property
      if (!exact && (DEFAULT_ON_CLONE & p.getFlags()) === DEFAULT_ON_CLONE) {
        continue;
      }
      
      // Make a copy of the Property value
      val = this.get(p).newCopy(exact);
      
      if (p.isFrozen()) {
        newInstance.set({
          "slot": p.getName(), 
          "value": val,  
          "cx": COPYING_CONTEXT
        });
      } else {
        // If remove on clone and it's not an exact copy then skip copying this Property
        if (!exact && (REMOVE_ON_CLONE & p.getFlags()) === REMOVE_ON_CLONE) {
          continue;
        }
      
        // TODO: Skip BLinks, dynamic slots added in constructor and slots with removeOnClone set
        newInstance.add({
          "slot": p.getName(), 
          "value": val, 
          "flags": p.getFlags(), 
          "facets": p.getFacets(), 
          "cx": COPYING_CONTEXT
        });
      }
      
      // Copy of flags
      newInstance.getSlot(p.getName()).$setFlags(p.getFlags());

      // Copy of displayNames
      newInstance.getSlot(p.getName()).$setDisplayName(p.$getDisplayName());
    }
    
    return newInstance;
  };
  
    
  /**
   * Internal framework method.
   * 
   * This method should only be used by Tridium developers. It follows
   * the same design pattern as Niagara's Component 'fw' method.
   *
   * @private
   */
  Complex.prototype.$fw = function (x, a, b, c, d) {    
    if (x === "modified") {
      if (this.$parent !== null) {
        this.$parent.$fw(x, this.$propInParent, b, c, d);
      }
    } else if (x === "modifyTrap") {
      if (this.$parent !== null) {
        a.push(this.$propInParent);
        return this.$parent.$fw(x, a, b, c, d);
      }
      
      return false;
    }
  };

  /**
   * Return true if there is an overridden toString method.
   *
   * @inner
   * @param {baja.Complex} complex
   * @return {boolean}
   */
  function hasCustomToString(complex) {
    return complex.toString !== Complex.prototype.toString;
  }

  /**
   * @param {baja.Complex} complex
   * @returns {Array} first element is the closest `baja.Component` (which may
   * be the given complex if it itself is a component), null if the complex is
   * not rooted somewhere under a component. Second element is an array of slots
   * leading from the closest component to the given complex (empty array if
   * they are the same instance).
   */
  function findClosestComponent(complex) {
    const slotPath = [];
    let closestComponent = complex;
    while (closestComponent && !isComponent(closestComponent)) {
      slotPath.splice(0, 0, closestComponent.getName());
      closestComponent = closestComponent.getParent();
    }
    return [ closestComponent, slotPath ];
  }

  function isComponent(comp) {
    return baja.hasType(comp, 'baja:Component');
  }

  function isComplex(comp) {
    return baja.hasType(comp, 'baja:Complex');
  }

  return Complex;
});