/**
 * ObservableObject.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

/**
 * This class is a object that is observable when properties changes a change event gets emitted.
 *
 * @private
 * @class tinymce.data.ObservableObject
 */
define(
  'tinymce.core.data.ObservableObject',
  [
    'tinymce.core.data.Binding',
    'tinymce.core.util.Class',
    'tinymce.core.util.Observable',
    'tinymce.core.util.Tools'
  ], function (Binding, Class, Observable, Tools) {
    function isNode(node) {
      return node.nodeType > 0;
    }

    // Todo: Maybe this should be shallow compare since it might be huge object references
    function isEqual(a, b) {
      var k, checked;

      // Strict equals
      if (a === b) {
        return true;
      }

      // Compare null
      if (a === null || b === null) {
        return a === b;
      }

      // Compare number, boolean, string, undefined
      if (typeof a !== "object" || typeof b !== "object") {
        return a === b;
      }

      // Compare arrays
      if (Tools.isArray(b)) {
        if (a.length !== b.length) {
          return false;
        }

        k = a.length;
        while (k--) {
          if (!isEqual(a[k], b[k])) {
            return false;
          }
        }
      }

      // Shallow compare nodes
      if (isNode(a) || isNode(b)) {
        return a === b;
      }

      // Compare objects
      checked = {};
      for (k in b) {
        if (!isEqual(a[k], b[k])) {
          return false;
        }

        checked[k] = true;
      }

      for (k in a) {
        if (!checked[k] && !isEqual(a[k], b[k])) {
          return false;
        }
      }

      return true;
    }

    return Class.extend({
      Mixins: [Observable],

      /**
       * Constructs a new observable object instance.
       *
       * @constructor
       * @param {Object} data Initial data for the object.
       */
      init: function (data) {
        var name, value;

        data = data || {};

        for (name in data) {
          value = data[name];

          if (value instanceof Binding) {
            data[name] = value.create(this, name);
          }
        }

        this.data = data;
      },

      /**
       * Sets a property on the value this will call
       * observers if the value is a change from the current value.
       *
       * @method set
       * @param {String/object} name Name of the property to set or a object of items to set.
       * @param {Object} value Value to set for the property.
       * @return {tinymce.data.ObservableObject} Observable object instance.
       */
      set: function (name, value) {
        var key, args, oldValue = this.data[name];

        if (value instanceof Binding) {
          value = value.create(this, name);
        }

        if (typeof name === "object") {
          for (key in name) {
            this.set(key, name[key]);
          }

          return this;
        }

        if (!isEqual(oldValue, value)) {
          this.data[name] = value;

          args = {
            target: this,
            name: name,
            value: value,
            oldValue: oldValue
          };

          this.fire('change:' + name, args);
          this.fire('change', args);
        }

        return this;
      },

      /**
       * Gets a property by name.
       *
       * @method get
       * @param {String} name Name of the property to get.
       * @return {Object} Object value of propery.
       */
      get: function (name) {
        return this.data[name];
      },

      /**
       * Returns true/false if the specified property exists.
       *
       * @method has
       * @param {String} name Name of the property to check for.
       * @return {Boolean} true/false if the item exists.
       */
      has: function (name) {
        return name in this.data;
      },

      /**
       * Returns a dynamic property binding for the specified property name. This makes
       * it possible to sync the state of two properties in two ObservableObject instances.
       *
       * @method bind
       * @param {String} name Name of the property to sync with the property it's inserted to.
       * @return {tinymce.data.Binding} Data binding instance.
       */
      bind: function (name) {
        return Binding.create(this, name);
      },

      /**
       * Destroys the observable object and fires the "destroy"
       * event and clean up any internal resources.
       *
       * @method destroy
       */
      destroy: function () {
        this.fire('destroy');
      }
    });
  }
);