/**
 * Constants
 */

const STRING = 'string';
const NUMBER = 'number';
const FUNCTION = 'function';
const OBJECT = 'object';

/**
 * Class Definition
 */

export class Metadata {
  /**
   * Constructor and Instance Methods
   */

  constructor(data, uid) {
    // Define the main "_data" private property as an immutable property.
    // IMPORTANT: This property can only be set during instance construction.
    Object.defineProperty(this, '_data', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: data,
    });

    // Define the main "_uid" private property as an immutable property.
    // IMPORTANT: This property can only be set during instance construction.
    Object.defineProperty(this, '_uid', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: uid,
    });

    // Define "_custom" properties as an immutable property.
    // IMPORTANT: This property can only be set during instance construction.
    Object.defineProperty(this, '_custom', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: Object.create(null),
    });
  }

  getData() {
    return this._data;
  }

  getDataProperty(propertyName) {
    let propertyValue;
    const _data = this._data;
    if (
      _data instanceof Object ||
      (typeof _data === OBJECT && _data !== null)
    ) {
      propertyValue = _data[propertyName];
    }
    return propertyValue;
  }

  /**
   * Get unique object ID
   */
  getObjectID() {
    return this._uid;
  }

  /**
   * Set custom attribute value
   * @param {String} attribute Custom attribute name
   * @param {Any} value     Custom attribute value
   */
  setCustomAttribute(attribute, value) {
    this._custom[attribute] = value;
  }

  /**
   * Get custom attribute value
   * @param  {String} attribute Custom attribute name
   * @return {Any}              Custom attribute value
   */
  getCustomAttribute(attribute) {
    return this._custom[attribute];
  }

  /**
   * Check if a custom attribute exists
   * @param  {String} attribute Custom attribute name
   * @return {Boolean}          True if custom attribute exists or false if not
   */
  customAttributeExists(attribute) {
    return attribute in this._custom;
  }

  /**
   * Set custom attributes in batch mode.
   * @param {Object} attributeMap An object whose own properties will be used as custom attributes.
   */
  setCustomAttributes(attributeMap) {
    const _hasOwn = Object.prototype.hasOwnProperty;
    const _custom = this._custom;
    for (let attribute in attributeMap) {
      if (_hasOwn.call(attributeMap, attribute)) {
        _custom[attribute] = attributeMap[attribute];
      }
    }
  }

  /**
   * Static Methods
   */

  static isValidUID(uid) {
    return typeof uid === STRING && uid.length > 0;
  }

  static isValidIndex(index) {
    return typeof index === NUMBER && index >= 0 && (index | 0) === index;
  }

  static isValidCallback(callback) {
    return typeof callback === FUNCTION;
  }
}
