text: import _ from 'lodash';
  import { urlError, wrapError } from './utils';
  import extend from './extend';
  import { sync } from './sync';
  import { mixinMethods } from './mixin-methods';
  import { Events } from './events';
  
  // Model
  // -----
  
  // **Models** are the basic data object in the framework -- frequently
  // representing a row in a table in a database on your server. A discrete chunk
  // of data and a bunch of useful, related methods for performing computations
  // and transformations on that data.
  
  // Create a new model with the specified attributes. A client id (`cid`)
  // is automatically generated and assigned for you.
  export const Model = /*#__PURE__*/ (() => {
    // eslint-disable-next-line no-shadow
    const Model = class Model {
      static extend = extend;
  
      constructor(attributes, options) {
        let attrs = attributes || {};
        options || (options = {});
        this.preinitialize.apply(this, arguments);
        this.cid = _.uniqueId(this.cidPrefix);
        this.attributes = {};
        if (options.collection) this.collection = options.collection;
        if (options.parse) attrs = this.parse(attrs, options) || {};
        const defaults = _.result(this, 'defaults');
        attrs = _.defaults({ ...defaults, ...attrs }, defaults);
        this.set(attrs, options);
        this.changed = {};
        this.initialize.apply(this, arguments);
      }
  
      // Return a copy of the model's `attributes` object.
      toJSON(options) {
        return { ...this.attributes };
      }
  
      // Proxy `sync` by default -- but override this if you need custom syncing
      // semantics for *this* particular model.
      sync(...args) {
        return sync.apply(this, args);
      }
  
      // Get the value of an attribute.
      get(attr) {
        return _.get(this.attributes, [attr]);
      }
  
      // Get the HTML-escaped value of an attribute.
      escape(attr) {
        return _.escape(this.get(attr));
      }
  
      // Returns `true` if the attribute contains a value that is not null
      // or undefined.
      has(attr) {
        return this.get(attr) != null;
      }
  
      // Special-cased proxy to underscore's `_.matches` method.
      matches(attrs) {
        return !!_.iteratee(attrs, this)(this.attributes);
      }
  
      // Set a hash of model attributes on the object, firing `"change"`. This is
      // the core primitive operation of a model, updating the data and notifying
      // anyone who needs to know about the change in state. The heart of the beast.
      set(key, val, options) {
        if (key == null) return this;
  
        // Handle both `"key", value` and `{key: value}` -style arguments.
        let attrs;
        if (typeof key === 'object') {
          attrs = key;
          options = val;
        } else {
          (attrs = {})[key] = val;
        }
  
        options || (options = {});
  
        // Run validation.
        if (!this._validate(attrs, options)) return false;
  
        // Extract attributes and options.
        const unset = options.unset;
        const silent = options.silent;
        const changes = [];
        const changing = this._changing;
        this._changing = true;
  
        if (!changing) {
          this._previousAttributes = { ...this.attributes };
          this.changed = {};
        }
  
        const current = this.attributes;
        const changed = this.changed;
        const prev = this._previousAttributes;
  
        // For each `set` attribute, update or delete the current value.
        for (const attr in attrs) {
          val = attrs[attr];
          if (!_.isEqual(current[attr], val)) changes.push(attr);
          if (!_.isEqual(prev[attr], val)) {
            changed[attr] = val;
          } else {
            delete changed[attr];
          }
          unset ? delete current[attr] : (current[attr] = val);
        }
  
        // Update the `id`.
        if (this.idAttribute in attrs) this.id = this.get(this.idAttribute);
  
        // Trigger all relevant attribute changes.
        if (!silent) {
          if (changes.length) this._pending = options;
          for (let i = 0; i < changes.length; i++) {
            this.trigger('change:' + changes[i], this, current[changes[i]], options);
          }
        }
  
        // You might be wondering why there's a `while` loop here. Changes can
        // be recursively nested within `"change"` events.
        if (changing) return this;
        if (!silent) {
          while (this._pending) {
            options = this._pending;
            this._pending = false;
            this.trigger('change', this, options);
          }
        }
        this._pending = false;
        this._changing = false;
        return this;
      }
  
      // Remove an attribute from the model, firing `"change"`. `unset` is a noop
      // if the attribute doesn't exist.
      unset(attr, options) {
        return this.set(attr, void 0, { ...options, unset: true });
      }
  
      // Clear all attributes on the model, firing `"change"`.
      clear(options) {
        const attrs = {};
        for (const key in this.attributes) attrs[key] = void 0;
        return this.set(attrs, { ...options, unset: true });
      }
  
      // Determine if the model has changed since the last `"change"` event.
      // If you specify an attribute name, determine if that attribute has changed.
      hasChanged(attr) {
        if (attr == null) return !_.isEmpty(this.changed);
        return _.has(this.changed, [attr]);
      }
  
      // Return an object containing all the attributes that have changed, or
      // false if there are no changed attributes. Useful for determining what
      // parts of a view need to be updated and/or what attributes need to be
      // persisted to the server. Unset attributes will be set to undefined.
      // You can also pass an attributes object to diff against the model,
      // determining if there *would be* a change.
      changedAttributes(diff) {
        if (!diff) return this.hasChanged() ? { ...this.changed } : false;
        const old = this._changing ? this._previousAttributes : this.attributes;
        const changed = {};
        let hasChanged;
        for (const attr in diff) {
          const val = diff[attr];
          if (_.isEqual(old[attr], val)) continue;
          changed[attr] = val;
          hasChanged = true;
        }
        return hasChanged ? changed : false;
      }
  
      // Get the previous value of an attribute, recorded at the time the last
      // `"change"` event was fired.
      previous(attr) {
        if (attr == null || !this._previousAttributes) return null;
        return this._previousAttributes[attr];
      }
  
      // Get all of the attributes of the model at the time of the previous
      // `"change"` event.
      previousAttributes() {
        return { ...this._previousAttributes };
      }
  
      // Fetch the model from the server, merging the response with the model's
      // local attributes. Any changed attributes will trigger a "change" event.
      fetch(options) {
        options = { parse: true, ...options };
        const success = options.success;
        options.success = (resp) => {
          const serverAttrs = options.parse ? this.parse(resp, options) : resp;
          if (!this.set(serverAttrs, options)) return false;
          if (success) success.call(options.context, this, resp, options);
          this.trigger('sync', this, resp, options);
        };
        wrapError(this, options);
        return this.sync('read', this, options);
      }
  
      // Set a hash of model attributes, and sync the model to the server.
      // If the server returns an attributes hash that differs, the model's
      // state will be `set` again.
      save(key, val, options) {
        // Handle both `"key", value` and `{key: value}` -style arguments.
        let attrs;
        if (key == null || typeof key === 'object') {
          attrs = key;
          options = val;
        } else {
          (attrs = {})[key] = val;
        }
  
        options = { validate: true, parse: true, ...options };
        const wait = options.wait;
  
        // If we're not waiting and attributes exist, save acts as
        // `set(attr).save(null, opts)` with validation. Otherwise, check if
        // the model will be valid when the attributes, if any, are set.
        if (attrs && !wait) {
          if (!this.set(attrs, options)) return false;
        } else if (!this._validate(attrs, options)) {
          return false;
        }
  
        // After a successful server-side save, the client is (optionally)
        // updated with the server-side state.
        const success = options.success;
        const attributes = this.attributes;
        options.success = (resp) => {
          // Ensure attributes are restored during synchronous saves.
          this.attributes = attributes;
          let serverAttrs = options.parse ? this.parse(resp, options) : resp;
          if (wait) serverAttrs = { ...attrs, ...serverAttrs };
          if (serverAttrs && !this.set(serverAttrs, options)) return false;
          if (success) success.call(options.context, this, resp, options);
          this.trigger('sync', this, resp, options);
        };
        wrapError(this, options);
  
        // Set temporary attributes if `{wait: true}` to properly find new ids.
        if (attrs && wait) this.attributes = { ...attributes, ...attrs };
  
        const method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update';
        if (method === 'patch' && !options.attrs) options.attrs = attrs;
        const xhr = this.sync(method, this, options);
  
        // Restore attributes.
        this.attributes = attributes;
  
        return xhr;
      }
  
      // Destroy this model on the server if it was already persisted.
      // Optimistically removes the model from its collection, if it has one.
      // If `wait: true` is passed, waits for the server to respond before removal.
      destroy(options) {
        options = { ...options };
        const success = options.success;
        const wait = options.wait;
  
        const destroy = () => {
          this.stopListening();
          this.trigger('destroy', this, this.collection, options);
        };
  
        options.success = (resp) => {
          if (wait) destroy();
          if (success) success.call(options.context, this, resp, options);
          if (!this.isNew()) this.trigger('sync', this, resp, options);
        };
  
        let xhr = false;
        if (this.isNew()) {
          _.defer(options.success);
        } else {
          wrapError(this, options);
          xhr = this.sync('delete', this, options);
        }
        if (!wait) destroy();
        return xhr;
      }
  
      // Default URL for the model's representation on the server -- if you're using
      // the restful methods, override this to change the endpoint that will be
      // called.
      url() {
        const base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();
        if (this.isNew()) return base;
        const id = this.get(this.idAttribute);
        return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id);
      }
  
      // **parse** converts a response into the hash of attributes to be `set` on
      // the model. The default implementation is just to pass the response along.
      parse(resp, options) {
        return resp;
      }
  
      // Create a new model with identical attributes to this one.
      clone() {
        return new this.constructor(this.attributes);
      }
  
      // A model is new if it has never been saved to the server, and lacks an id.
      isNew() {
        return !this.has(this.idAttribute);
      }
  
      // Check if the model is currently in a valid state.
      isValid(options) {
        return this._validate({}, { ...options, validate: true });
      }
  
      // Run validation against the next complete set of model attributes,
      // returning `true` if all is well. Otherwise, fire an `"invalid"` event.
      _validate(attrs, options) {
        if (!options.validate || !this.validate) return true;
        attrs = { ...this.attributes, ...attrs };
        const error = (this.validationError = this.validate(attrs, options) || null);
        if (!error) return true;
        this.trigger('invalid', this, error, { ...options, validationError: error });
        return false;
      }
    };
  
    const proto = Model.prototype;
    Object.assign(proto, Events);
  
    // A hash of attributes whose current and previous value differ.
    proto.changed = null;
  
    // The value returned during the last failed validation.
    proto.validationError = null;
  
    // The default name for the JSON `id` attribute is `"id"`. MongoDB and CouchDB
    // users may want to set this to `"_id"`.
    proto.idAttribute = 'id';
  
    // The prefix is used to create the client id which is used to identify models
    // locally. You may want to override this if you're experiencing name clashes
    // with model ids.
    proto.cidPrefix = 'c';
  
    // preinitialize is an empty function by default. You can override it with a
    // function or object.  preinitialize will run before any instantiation logic
    // is run in the Model.
    proto.preinitialize = _.noop;
  
    // Initialize is an empty function by default. Override it with your own
    // initialization logic.
    proto.initialize = _.noop;
  
    // Lodash methods that we want to implement on the Model, mapped to the number
    // of arguments they take.
  
    mixinMethods(
      Model,
      [
        ['chain', _.chain, 1],
        ['entries', _.entries, 1],
        ['invert', _.invert, 1],
        ['isEmpty', _.isEmpty, 1],
        ['keys', _.keys, 1],
        ['omit', _.omit, 0],
        ['omitBy', _.omitBy, 1],
        ['pick', _.pick, 0],
        ['pickBy', _.pickBy, 1],
        ['values', _.values, 1],
      ],
      'attributes'
    );
  
    // Translated, roughly, from underscore:
    proto.pairs = proto.entries;
  
    return Model;
  })();
  