// ------------------------------------------------------------------------------
// Model
// ------------------------------------------------------------------------------
function Model(id, data) {
  this.id = id;
  this.data = data;
}

const models = (Model._models = {});
const proxies = {};

// Get and set proxy
const _modelProxyHandler = {
  get: function (target, prop, receiver) {
    if (typeof target[prop] === 'function') return target[prop].bind(target);
    else if (prop in target) return Reflect.get(target, prop, receiver);
    else return target.get(prop);
  },
  set: function (obj, prop, value) {
    if (prop === '_class') {
      obj._class = value;
    } else if (prop === 'id') {
      console.log(`Noodl.Object warning: id is readonly (Id is ${obj.id}, trying to set to ${value})`);
      return true; //if a proxy doesn't return true an exception will be thrown
    } else {
      obj.set(prop, value);
    }
    return true;
  },
  ownKeys(target) {
    return Reflect.ownKeys(target.data);
  },
  getOwnPropertyDescriptor(target, prop) {
    return Object.getOwnPropertyDescriptor(target.data, prop);
  }
};

Model.get = function (id) {
  if (id === undefined) id = Model.guid();
  if (!models[id]) {
    models[id] = new Model(id, {});
    proxies[id] = new Proxy(models[id], _modelProxyHandler);
  }
  return proxies[id];
};

Model.create = function (data) {
  var modelData = data ? data : {};
  var m = Model.get(modelData.id);
  for (var key in modelData) {
    if (key === 'id') continue;
    m.set(key, modelData[key]);
  }

  return m;
};

Model.exists = function (id) {
  return models[id] !== undefined;
};

Model.instanceOf = function (collection) {
  return collection instanceof Model || collection.target instanceof Model;
};

function _randomString(size) {
  if (size === 0) {
    throw new Error('Zero-length randomString is useless.');
  }
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 'abcdefghijklmnopqrstuvwxyz' + '0123456789';
  let objectId = '';
  for (let i = 0; i < size; ++i) {
    objectId += chars[Math.floor((1 + Math.random()) * 0x10000) % chars.length];
  }
  return objectId;
}

Model.guid = function guid() {
  return _randomString(10);
};

Model.prototype.on = function (event, listener) {
  if (!this.listeners) this.listeners = {};
  if (!this.listeners[event]) this.listeners[event] = [];
  this.listeners[event].push(listener);
};

Model.prototype.off = function (event, listener) {
  if (!this.listeners) return;
  if (!this.listeners[event]) return;
  var idx = this.listeners[event].indexOf(listener);
  if (idx !== -1) this.listeners[event].splice(idx, 1);
};

Model.prototype.notify = function (event, args) {
  if (!this.listeners) return;
  if (!this.listeners[event]) return;

  var l = this.listeners[event].slice(); //clone in case listeners array is modified in the callbacks
  for (var i = 0; i < l.length; i++) {
    l[i](args);
  }
};

Model.prototype.setAll = function (obj) {
  for (var i in obj) {
    if (i === 'id') continue; // Skip id
    if (this.data[i] !== obj[i]) {
      var old = this.data[i];
      this.data[i] = obj[i];
      this.notify('change', { name: i, value: obj[i], old: old });
    }
  }
};

Model.prototype.fill = function (value = null) {
  for (const key in this.data) {
    if (key === 'id') continue; // Skip id
    const temp = this.data[key];
    this.data[key] = value;
    this.notify('change', { name: key, value: this.data[key], old: temp });
  }
};

Model.prototype.set = function (name, value, args) {
  if (args && args.resolve && name.indexOf('.') !== -1) {
    // We should resolve path references
    var path = name.split('.');
    var model = this;
    for (var i = 0; i < path.length - 1; i++) {
      var v = model.get(path[i]);
      if (Model.instanceOf(v)) model = v;
      else return; // Path resolve failed
    }
    model.set(path[path.length - 1], value);
    return;
  }

  const forceChange = args && args.forceChange;

  var oldValue = this.data[name];
  this.data[name] = value;
  (forceChange || oldValue !== value) &&
    (!args || !args.silent) &&
    this.notify('change', { name: name, value: value, old: oldValue });
};

Model.prototype.getId = function () {
  return this.id;
};

Model.prototype.get = function (name, args) {
  if (args && args.resolve && name.indexOf('.') !== -1) {
    // We should resolve path references
    var path = name.split('.');
    var model = this;
    for (var i = 0; i < path.length - 1; i++) {
      var v = model.get(path[i]);
      if (Model.instanceOf(v)) model = v;
      else return; // Path resolve failed
    }
    return model.get(path[path.length - 1]);
  }

  return this.data[name];
};

Model.prototype.toJSON = function () {
  return Object.assign({}, this.data, { id: this.id });
};

Model.Scope = function () {
  this.models = {};
  this.proxies = {};
};

Model.Scope.prototype.get = function (id) {
  if (id === undefined) id = Model.guid();
  if (!this.models[id]) {
    this.models[id] = new Model(id, {});
    this.proxies[id] = new Proxy(this.models[id], _modelProxyHandler);
  }
  return this.proxies[id];
};

Model.Scope.prototype.create = function (data) {
  var modelData = data ? data : {};
  var m = this.get(modelData.id);
  for (var key in modelData) {
    if (key === 'id') continue;
    m.set(key, modelData[key]);
  }

  return m;
};

Model.Scope.prototype.exists = function (id) {
  return this.models[id] !== undefined;
};

Model.Scope.prototype.instanceOf = function (collection) {
  return collection instanceof Model || collection.target instanceof Model;
};

Model.Scope.prototype.guid = function guid() {
  return _randomString(10);
};

Model.Scope.prototype.reset = function () {
  this.models = {};
  this.proxies = {};
  delete this._cloudStore;
};

module.exports = Model;
