baja/obj/Status.js

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

/**
 * Defines {@link baja.Status}.
 * @module baja/obj/Status
 */
define([ "bajaScript/sys",
  "bajaScript/baja/obj/Facets",
  "bajaScript/baja/obj/Simple",
  "bajaScript/baja/obj/objUtil",
  "lex!" ], function (
    baja,
    Facets,
    Simple,
    objUtil,
    lexjs) {
  
  "use strict";
  
  var subclass = baja.subclass,
      callSuper = baja.callSuper,
      objectify = baja.objectify,
      bajaDef = baja.def,
      bajaHasType = baja.hasType,
      strictArg = baja.strictArg,
      strictAllArgs = baja.strictAllArgs,
      
      cacheDecode = objUtil.cacheDecode,
      cacheEncode = objUtil.cacheEncode,
      
      facetsDefault = Facets.DEFAULT,
      statusNullLookup;

  // Revisit after NCCB-24257 with a type extension on control:PriorityLevel
  /**
   * Converts the given control:PriorityLevel to a string representation.
   *
   * @param {baja.FrozenEnum} priorityLevel a `control:PriorityLevel`
   * @param {String} fallbackStr used if the PriorityLevel is the fallback slot
   * @inner
   * @returns {String|Promise<String>}
   */
  function toPriorityLevelString(priorityLevel, fallbackStr) {
    if (!baja.hasType(priorityLevel, 'baja:DynamicEnum')) { return null; }

    var frozen = priorityLevel.getRange().getFrozenType();
    if (frozen && frozen.is('control:PriorityLevel')) {
      var frozenEnum = baja.$('control:PriorityLevel').get(priorityLevel.getOrdinal());
      if (frozenEnum.getTag() !== 'fallback') {
        return frozenEnum.getDisplayTag();
      }
      return fallbackStr || "def";
    }
  }

  function toStringSync(str, facets, fallbackStr) {
    var keys = facets.getKeys(),
      i;
    for (i = 0; i < keys.length; i++) {
      var key = keys[i],
        val = facets.get(key);
      if (key === 'activeLevel') {
        str += ' @ ' + (toPriorityLevelString(val, fallbackStr) || val);
      } else if (val === true) {
        str += ' ' + key;
      } else {
        str += ' ' + key + '=' + val;
      }
    }
    return str;
  }
  
  /**
   * Represents a `baja:Status` in BajaScript.
   * 
   * `Status` provides a bit mask for various standardized 
   * status flags in the Baja control architecture. Plus
   * it provides for arbitrary extensions using `BFacets`.
   * 
   * When creating a `Simple`, always use the `make()` method instead of 
   * creating a new Object.
   *   
   * @class
   * @alias baja.Status
   * @extends baja.Simple
   */
  var Status = function Status(bits, facets) {
    // Constructor should be considered private
    callSuper(Status, this, arguments);  
    this.$bits = bits;
    this.$facets = facets;
  };
  
  subclass(Status, Simple);
  
  /**
   * Make a `Status`.
   * 
   * @param {Object|Number} obj the Object Literal that specifies the method's arguments or Status bits.
   * @param {Number} obj.bits the Status bits.
   * @param {baja.Facets} [obj.facets] the facets for the Status.
   * @param {baja.Status} [obj.orig] if defined, obj.state must also be defined. This is used to create
   *                                 a new Status with one of it's bit states changed (see example above).
   * @param {Boolean} [obj.state] the state of the bit to change (used in conjunction with obj.orig).
   * @returns {baja.Status} the status.
   * 
   * @example
   *   //The bits (Number) or (for more arguments) an Object Literal can be 
   *   //used to specify the method's arguments.
   *   var st1 = baja.Status.make(baja.Status.DOWN | baja.Status.FAULT);
   *   
   *   // ... or for more arguments...
   *   
   *   var st2 = baja.Status.make({
   *     bits: baja.Status.DOWN,
   *     facets: facets
   *   });
   *   
   *   //The make() method can also be used to create a new status with its 
   *   //state changed...
   * 
   *   var newStatus = baja.Status.make({
   *     orig: oldStatus, 
   *     bits: baja.Status.OVERRIDDEN, 
   *     state: true
   *   }};
   */   
  Status.make = function (obj) {
    obj = objectify(obj, "bits");
  
    var orig = obj.orig,
        bits = obj.bits,
        state = obj.state,
        facets = obj.facets,
        nullLookup = Status.$nullLookup;
    
    // If the bits are a baja.Status then get the bits
    if (bajaHasType(bits) && bits.getType().equals(Status.DEFAULT.getType())) {
      bits = bits.getBits();
    }
    
    strictArg(bits, Number);
    
    // Make with original bits...
    if (orig !== undefined) {
      strictAllArgs([ orig, state ], [ Status, Boolean ]);
    
      bits = state ? (orig.$bits | bits) : (orig.$bits & ~bits);
      if (bits === 0 && orig.$facets === facetsDefault) {
        return Status.ok;
      }   
      if (orig.$bits === bits) {
        return orig;
      }   
      facets = orig.$facets;
    }
    
    // Standard make...
    facets = bajaDef(facets, facetsDefault);
    strictArg(facets, Facets);
    
    if (facets === facetsDefault) {
      if (bits === 0) {
        return Status.DEFAULT;
      }
      
      if (bits <= 256) {
        if (!(nullLookup.hasOwnProperty(bits))) {
          nullLookup[bits] = new Status(bits, facets);
        }
        return nullLookup[bits];
      }
    }
        
    return new Status(bits, facets);
  };
   
  /**
   * Make a `Status`.
   *
   * @param {Object|Number} obj the Object Literal that specifies the method's arguments or Status bits.
   * @param {Number} obj.bits the Status bits.
   * @param {baja.Facets} [obj.facets] the facets for the Status.
   * @param {baja.Status} [obj.orig] if defined, obj.state must also be defined. This is used to create
   *                                 a new Status with one of it's bit states changed (see example above).
   * @param {Boolean} [obj.state] the state of the bit to change (used in conjunction with obj.orig).
   * @returns {baja.Status} the status.
   * 
   * @example
   *   //The bits (Number) or (for more arguments) an Object Literal can be 
   *   //used to specify the method's arguments.
   * 
   *   var st1 = baja.Status.make(baja.Status.DOWN | baja.Status.FAULT);
   *   
   *   // ... or for more arguments...
   *   
   *   var st2 = baja.$("baja:Status").make({
   *     bits: baja.Status.DOWN,
   *     facets: facets
   *   });
   *   
   *   //The make method can also be used to create a new status with its 
   *   //state changed...
   *   
   *   var newStatus = baja.$("baja:Status").make({
   *     orig: oldStatus, 
   *     bits: baja.Status.OVERRIDDEN, 
   *     state: true
   *   }};
   */     
  Status.prototype.make = function (obj) {
    return Status.make.apply(Status, arguments);
  };
  
  /**
   * Decode a `Status` from a `String`.
   *
   * @method
   * @param {String} str
   * @returns {baja.Status}
   */   
  Status.prototype.decodeFromString = cacheDecode(function (str) {
    var x,
        semi = str.indexOf(';'),
        bits;
    
    if (semi < 0) {
      x = Status.make(parseInt(str, 16));
    } else {
      bits = parseInt(str.substring(0, semi), 16);
      x = Status.make({ "bits": bits, "facets": facetsDefault.decodeFromString(str.substring(semi + 1), baja.Simple.$unsafeDecode) });
    }
    return x;
  });
      
  /**
   * Encode the `Status` to a `String`.
   *
   * @method
   * @returns {String}
   */  
  Status.prototype.encodeToString = cacheEncode(function () {
    var s = this.$bits.toString(16);
    if (this.$facets !== facetsDefault) {
      s += ";" + this.$facets.encodeToString();
    }
    return s;
  });

  /**
   * Returns a string of just the flags which are set or
   * returns ok if none are set.
   *
   * @returns {String}
   */
  Status.prototype.flagsToString = function (obj) {
    var lex = obj && obj.lex,
        flags = [],
        bits = this.$bits;

    if (!lex || lex.getModuleName() !== 'baja') {
      lex = lexjs.getLexiconFromCache('baja');
    }

    var getDisplayTag = function (tag) {
      return lex ? lex.get('Status.' + tag) : tag;
    };

    // flags to String
    if (!bits) {                           return getDisplayTag('ok'); }
    if (bits & Status.DISABLED) {      flags.push(getDisplayTag('disabled')); }
    if (bits & Status.FAULT) {         flags.push(getDisplayTag('fault')); }
    if (bits & Status.DOWN) {          flags.push(getDisplayTag('down')); }
    if (bits & Status.ALARM) {         flags.push(getDisplayTag('alarm')); }
    if (bits & Status.STALE) {         flags.push(getDisplayTag('stale')); }
    if (bits & Status.OVERRIDDEN) {    flags.push(getDisplayTag('overridden')); }
    if (bits & Status.NULL) {          flags.push(getDisplayTag('null')); }
    if (bits & Status.UNACKED_ALARM) { flags.push(getDisplayTag('unackedAlarm')); }

    return flags.join(',');
  };

  /**
   * Returns the string representation of the 'Status'.
   *
   * This method is invoked synchronously. The string result will be returned
   * directly from this function.
   *
   * **Notes on lexicons:**
   *
   * * A lexicon will be used if it is passed in.
   * * If no lexicon is passed in, the baja lexicon will be used if it has been
   * cached locally.
   * * If the baja lexicon has not been cached, strings units will be
   * represented by their internal tag names (which are in English).
   *
   * @param {Object} obj the Object Literal for the method's arguments.
   * @param [obj.lex] the 'baja' lexicon
   *
   * @returns {String|Promise.<String>}
   */
  Status.prototype.toString = function (obj) {
    var str = '{' + this.flagsToString(obj) + '}',
        facets = this.$facets;

    // facets to String
    if (facets !== facetsDefault) {
      if (!obj) {
        return toStringSync(str, facets, "def");
      }
      return baja.lex({ module: 'control' })
        .then(function (controlLex) {
          return toStringSync(str, facets, controlLex.get('def'));
        });
    }
    return str;
  };

  /**
   * Equality test.
   *
   * @param obj
   * @returns {Boolean}
   */
  Status.prototype.equals = function (obj) {
    if (bajaHasType(obj) && obj.getType().equals(this.getType())) {
      if (this.$facets === facetsDefault) {
        return obj.$bits === this.$bits;
      }
      
      return obj.$bits === this.$bits && this.$facets.equals(obj.$facets);
    }

    return false;
  };
  
  /**
   * Default `Status` instance.
   * @type {baja.Status}
   */
  Status.DEFAULT = new Status(0, facetsDefault);
        
  // If the facets are null then the Status is interned into this Object map
  statusNullLookup = Status.$nullLookup = {};
  
  /**
   * Bit for disabled.
   * @type {Number}
   */
  Status.DISABLED = 0x0001;
  
  /**
   * Bit for fault.
   * @type {Number}
   */
  Status.FAULT = 0x0002;
  
  /**
   * Bit for down.
   * @type {Number}
   */
  Status.DOWN = 0x0004;
  
  /**
   * Bit for alarm.
   * @type {Number}
   */
  Status.ALARM = 0x0008;
  
  /**
   * Bit for stale.
   * @type {Number}
   */
  Status.STALE = 0x0010;
  
  /**
   * Bit for overridden.
   * @type {Number}
   */
  Status.OVERRIDDEN = 0x0020;
  
  /**
   * Bit for null.
   * @type {Number}
   */
  Status.NULL = 0x0040;
  
  /**
   * Bit for unacked alarm.
   * @type {Number}
   */
  Status.UNACKED_ALARM = 0x0080;
  
  /**
   * String used in a `Facets` for identifying the active priority level of a 
   * writable point.
   * @type {String}
   */
  Status.ACTIVE_LEVEL = "activeLevel";
  
  /**
   * `Status` for ok (null facets).
   * @type {baja.Status}
   */   
  Status.ok = Status.DEFAULT;
  
  /**
   * `Status` for disabled (null facets).
   * @type {baja.Status}
   */ 
  Status.disabled = statusNullLookup[Status.DISABLED] = new Status(Status.DISABLED, facetsDefault);
  
  /**
   * `Status` for fault (null facets).
   * @type {baja.Status}
   */ 
  Status.fault = statusNullLookup[Status.FAULT] = new Status(Status.FAULT, facetsDefault);
  
  /**
   * `Status` for down (null facets).
   * @type {baja.Status}
   */ 
  Status.down = statusNullLookup[Status.DOWN] = new Status(Status.DOWN, facetsDefault);
  
  /**
   * `Status` for alarm (null facets).
   * @type {baja.Status}
   */ 
  Status.alarm = statusNullLookup[Status.ALARM] = new Status(Status.ALARM, facetsDefault);
  
  /**
   * `Status` for stale (null facets).
   * @type {baja.Status}
   */ 
  Status.stale = statusNullLookup[Status.STALE] = new Status(Status.STALE, facetsDefault);
  
  /**
   * `Status` for overridden (null facets).
   * @type {baja.Status}
   */ 
  Status.overridden = statusNullLookup[Status.OVERRIDDEN] = new Status(Status.OVERRIDDEN, facetsDefault);
  
  /**
   * `Status` for null status (null facets).
   * @type {baja.Status}
   */ 
  Status.nullStatus = statusNullLookup[Status.NULL] = new Status(Status.NULL, facetsDefault);
  
  /**
   * `Status` for unacked alarm (null facets).
   * @type {baja.Status}
   */ 
  Status.unackedAlarm = statusNullLookup[Status.UNACKED_ALARM] = new Status(Status.UNACKED_ALARM, facetsDefault);
  
  /**
   * Return the facets for the `Status`.
   *
   * @returns {baja.Facets} status facets
   */   
  Status.prototype.getFacets = function () {    
    return this.$facets;
  };
  
  /**
   * Return a value from the status facets.
   *
   * @param {String} name the name of the value to get from the status facets.
   * @param [def] if defined, this value is returned if the name can't be found in 
   *              the status facets.
   * @returns the value from the status facets (null if def is undefined and name can't be found).
   */
  Status.prototype.get = function (name, def) {    
    return this.$facets.get(name, def);
  };
  
  /**
   * Return the `Status` bits.
   *
   * @returns {Number} status bits.
   */
  Status.prototype.getBits = function () {    
    return this.$bits;
  };
  
  /**
   * Return true if the `Status` is not disabled, fault, down
   * stale or null.
   *
   * @returns {Boolean} true if valid.
   */
  Status.prototype.isValid = function () {    
    return (((this.$bits & Status.DISABLED) === 0) &&
            ((this.$bits & Status.FAULT) === 0) && 
            ((this.$bits & Status.DOWN)  === 0) && 
            ((this.$bits & Status.STALE) === 0) && 
            ((this.$bits & Status.NULL)  === 0));  
  };
  
  /**
   * Return true if the `Status` is ok.
   *
   * @returns {Boolean}
   */
  Status.prototype.isOk = function () {
    return this.$bits === 0;  
  };
  
  /**
   * Return true if the `Status` is disabled.
   *
   * @returns {Boolean}
   */
  Status.prototype.isDisabled = function () {
    return (this.$bits & Status.DISABLED) !== 0;  
  };
  
  /**
   * Return true if the `Status` is in fault.
   *
   * @returns {Boolean}
   */
  Status.prototype.isFault = function () {
    return (this.$bits & Status.FAULT) !== 0;  
  };
  
  /**
   * Return true if the `Status` is down.
   *
   * @returns {Boolean}
   */
  Status.prototype.isDown = function () {
    return (this.$bits & Status.DOWN) !== 0;  
  };
  
  /**
   * Return true if the `Status` is in alarm.
   *
   * @returns {Boolean}
   */
  Status.prototype.isAlarm = function () {
    return (this.$bits & Status.ALARM) !== 0;  
  };

  /**
   * Return true if the `Status` is stale.
   *
   * @returns {Boolean}
   */
  Status.prototype.isStale = function () {
    return (this.$bits & Status.STALE) !== 0;  
  };

  /**
   * Return true if the `Status` is overridden.
   *
   * @returns {Boolean}
   */
  Status.prototype.isOverridden = function () {
    return (this.$bits & Status.OVERRIDDEN) !== 0;  
  };
  
  /**
   * Return true if the `Status` is null.
   *
   * @returns {Boolean}
   */
  Status.prototype.isNull = function () {
    return (this.$bits & Status.NULL) !== 0;  
  };
  
  /**
   * Return true if the `Status` is unacked alarm.
   *
   * @returns {Boolean}
   */
  Status.prototype.isUnackedAlarm = function () {
    return (this.$bits & Status.UNACKED_ALARM) !== 0;  
  };  
  
  /**
   * Return the status (itself).
   *
   * @returns {baja.Status} the status (itself).
   */
  Status.prototype.getStatus = function () {
    return this;
  };

  /**
   * @param {number} ordinal
   * @returns {boolean} true if this ordinal is set
   * @since Niagara 4.8
   */
  Status.prototype.getBit = function (ordinal) {
    return !!(this.$bits & ordinal);
  };

  /**
   * @returns {Array.<number>} all known Status ordinals
   * @since Niagara 4.8
   */
  Status.prototype.getOrdinals = function () {
    return [
      Status.DISABLED,
      Status.FAULT,
      Status.DOWN,
      Status.ALARM,
      Status.STALE,
      Status.OVERRIDDEN,
      Status.NULL,
      Status.UNACKED_ALARM
    ];
  };

  /**
   * @returns {baja.EnumSet} an `EnumSet` representing which bits are selected
   * out of the available bits for a `Status`
   * @since Niagara 4.8
   */
  Status.prototype.toEnumSet = function () {
    var that = this,
      ordinals = that.getOrdinals(),
      range = baja.EnumRange.make({
        ordinals: ordinals,
        tags: [
          'disabled', 'fault', 'down', 'alarm',
          'stale', 'overridden', 'null', 'unackedAlarm'
        ]
      });

    return baja.EnumSet.make({
      ordinals: ordinals.filter(function (ordinal) { return that.getBit(ordinal); }),
      range: range
    });
  };
  
  /**
   * Return the status from a `BIStatus`.
   *
   * @returns {baja.Status} resolved status value
   */
  Status.getStatusFromIStatus = function (statusVal) {
    var status = statusVal,
        type = statusVal.getType(),
        out,
        hasOut = false;
    
    if (type.isComplex()) {
      out = statusVal.get("out");
      if (out && out.getType().is("baja:StatusValue")) {
        status = out.getStatus();
        hasOut = true;
      }
    }
   
    if (!hasOut && (type.is("baja:StatusValue") || typeof statusVal.getStatus === "function")) {
      status = statusVal.getStatus();
    }
        
    return status instanceof Status ? status : Status.DEFAULT;
  };

  return Status;
});