/**
 * The `Store` class defines a local data store, and provides methods to
 * retrieve data from the API.
 */
export default class Store {
  constructor(models) {
    /**
     * The local data store. A tree of resource types to IDs, such that
     * accessing data[type][id] will return the model for that type/ID.
     *
     * @type {Object}
     * @protected
     */
    this.data = {};

    /**
     * The model registry. A map of resource types to the model class that
     * should be used to represent resources of that type.
     *
     * @type {Object}
     * @public
     */
    this.models = models;
  }

  /**
   * Push resources contained within an API payload into the store.
   *
   * @param {Object} payload
   * @return {Model|Model[]} The model(s) representing the resource(s) contained
   *     within the 'data' key of the payload.
   * @public
   */
  pushPayload(payload) {
    if (payload.included) payload.included.map(this.pushObject.bind(this));

    const result = payload.data instanceof Array ? payload.data.map(this.pushObject.bind(this)) : this.pushObject(payload.data);

    // Attach the original payload to the model that we give back. This is
    // useful to consumers as it allows them to access meta information
    // associated with their request.
    result.payload = payload;

    return result;
  }

  /**
   * Create a model to represent a resource object (or update an existing one),
   * and push it into the store.
   *
   * @param {Object} data The resource object
   * @return {Model|null} The model, or null if no model class has been
   *     registered for this resource type.
   * @public
   */
  pushObject(data) {
    if (!this.models[data.type]) return null;

    const type = (this.data[data.type] = this.data[data.type] || {});

    if (type[data.id]) {
      type[data.id].pushData(data);
    } else {
      type[data.id] = this.createRecord(data.type, data);
    }

    type[data.id].exists = true;

    return type[data.id];
  }

  /**
   * Make a request to the API to find record(s) of a specific type.
   *
   * @param {String} type The resource type.
   * @param {Integer|Integer[]|Object} [id] The ID(s) of the model(s) to retrieve.
   *     Alternatively, if an object is passed, it will be handled as the
   *     `query` parameter.
   * @param {Object} [query]
   * @param {Object} [options]
   * @return {Promise}
   * @public
   */
  find(type, id, query = {}, options = {}) {
    let params = query;
    let url = app.forum.attribute('apiUrl') + '/' + type;

    if (id instanceof Array) {
      url += '?filter[id]=' + id.join(',');
    } else if (typeof id === 'object') {
      params = id;
    } else if (id) {
      url += '/' + id;
    }

    return app
      .request(
        Object.assign(
          {
            method: 'GET',
            url,
            params,
          },
          options
        )
      )
      .then(this.pushPayload.bind(this));
  }

  /**
   * Get a record from the store by ID.
   *
   * @param {String} type The resource type.
   * @param {Integer} id The resource ID.
   * @return {Model}
   * @public
   */
  getById(type, id) {
    return this.data[type] && this.data[type][id];
  }

  /**
   * Get a record from the store by the value of a model attribute.
   *
   * @param {String} type The resource type.
   * @param {String} key The name of the method on the model.
   * @param {*} value The value of the model attribute.
   * @return {Model}
   * @public
   */
  getBy(type, key, value) {
    return this.all(type).filter((model) => model[key]() === value)[0];
  }

  /**
   * Get all loaded records of a specific type.
   *
   * @param {String} type
   * @return {Model[]}
   * @public
   */
  all(type) {
    const records = this.data[type];

    return records ? Object.keys(records).map((id) => records[id]) : [];
  }

  /**
   * Remove the given model from the store.
   *
   * @param {Model} model
   */
  remove(model) {
    delete this.data[model.data.type][model.id()];
  }

  /**
   * Create a new record of the given type.
   *
   * @param {String} type The resource type
   * @param {Object} [data] Any data to initialize the model with
   * @return {Model}
   * @public
   */
  createRecord(type, data = {}) {
    data.type = data.type || type;

    return new this.models[type](data, this);
  }
}
