import { Tracker } from "meteor/tracker";

export class ClientVersions {
  constructor() {
    this._versions = new Map();
    this._watchCallbacks = new Set();
  }

  // Creates a Livedata store for use with `Meteor.connection.registerStore`.
  // After the store is registered, document updates reported by Livedata are
  // merged with the documents in this `ClientVersions` instance.
  createStore() {
    return {
      update: ({ id, msg, fields }) => {
        if (msg === "added" || msg === "changed") {
          this.set(id, fields);
        }
      }
    };
  }

  hasVersions() {
    return this._versions.size > 0;
  }

  get(id) {
    return this._versions.get(id);
  }

  // Adds or updates a version document and invokes registered callbacks for the
  // added/updated document. If a document with the given ID already exists, its
  // fields are merged with `fields`.
  set(id, fields) {
    let version = this._versions.get(id);
    let isNew = false;

    if (version) {
      Object.assign(version, fields);
    } else {
      version = {
        _id: id,
        ...fields
      };

      isNew = true;
      this._versions.set(id, version);
    }

    this._watchCallbacks.forEach(({ fn, filter }) => {
      if (! filter || filter === version._id) {
        fn(version, isNew);
      }
    });
  }

  // Registers a callback that will be invoked when a version document is added
  // or changed. Calling the function returned by `watch` removes the callback.
  // If `skipInitial` is true, the callback isn't be invoked for existing
  // documents. If `filter` is set, the callback is only invoked for documents
  // with ID `filter`.
  watch(fn, { skipInitial, filter } = {}) {
    if (! skipInitial) {
      const resolved = Promise.resolve();

      this._versions.forEach((version) => {
        if (! filter || filter === version._id) {
          resolved.then(() => fn(version, true));
        }
      });
    }

    const callback = { fn, filter };
    this._watchCallbacks.add(callback);

    return () => this._watchCallbacks.delete(callback);
  }

  // A reactive data source for `Autoupdate.newClientAvailable`.
  newClientAvailable(id, fields, currentVersion) {
    function isNewVersion(version) {
      return (
        version._id === id &&
        fields.some((field) => version[field] !== currentVersion[field])
      );
    }

    const dependency = new Tracker.Dependency();
    const version = this.get(id);

    dependency.depend();

    const stop = this.watch(
      (version) => {
        if (isNewVersion(version)) {
          dependency.changed();
          stop();
        }
      },
      { skipInitial: true }
    );

    return !! version && isNewVersion(version);
  }
}
