function makeSpreadsheet() {
  const cells = {};
  return {
    observers: [],
    _getNode(sheetName, name) {
      const resolvedName = `${sheetName}!${name}`;

      const existing = cells[resolvedName];
      if (existing) {
        return existing;
      }

      cells[resolvedName] = {
        name: resolvedName,
        sheet: sheetName,
        value: null,
      };
      return cells[resolvedName];
    },

    prewarmCache(sheetName = '__global', name, value) {
      this._getNode(sheetName, name).value = value;
    },

    bind(sheetName, binding, fields, cb) {
      const { name } = binding;
      const resolvedName = `${sheetName}!${name}`;
      if (!this.observers[resolvedName]) {
        this.observers[resolvedName] = [];
      }
      this.observers[resolvedName].push(cb);

      const node = this._getNode(sheetName, name);
      cb(node);

      // bind returns a function which unsubscribes itself. In this mock
      // it's a noop.
      return () => {
        this.observers[resolvedName] = this.observers[resolvedName].filter(
          x => x !== cb,
        );
      };
    },

    create(sheetName, name, expr) {
      this.set(sheetName, name, expr);
    },

    get(sheetName, name) {
      return this._getNode(sheetName, name);
    },

    getValue(sheetName, name) {
      return this._getNode(sheetName, name).value;
    },

    set(sheetName, name, expr) {
      const node = this._getNode(sheetName, name);
      node.value = expr;

      const resolvedName = `${sheetName}!${name}`;
      if (this.observers[resolvedName]) {
        this.observers[resolvedName].forEach(cb => cb(node));
      }
    },

    getCellNames(sheetName) {
      const names = Object.keys(cells);
      if (sheetName) {
        return names.filter(name => name.startsWith(sheetName + '!'));
      }
      return names;
    },

    getCells() {
      return cells;
    },

    setCells(cells) {
      Object.keys(cells).forEach(sheet => {
        Object.keys(cells[sheet]).forEach(name => {
          this.set(sheet, name, cells[sheet][name]);
        });
      });
    },

    deleteCells(cells) {
      Object.keys(cells).forEach(sheet => {
        cells[sheet].forEach(name => {
          this.set(sheet, name, '');
        });
      });
    },

    batchChange(batch) {
      this.setCells(batch.updateCells);
      this.deleteCells(batch.deleteCells);
    },
  };
}

export default makeSpreadsheet;