ate(changes).catch(console.error);
  },

  async getEntry(guid) {
    let results = await this._search(this._getEntryCols, { guid });
    if (!results.length) {
      return null;
    }
    return { name: results[0].fieldname, value: results[0].value };
  },

  async getGUID(name, value) {
    // Query for the provided entry.
    let query = { fieldname: name, value };
    let results = await this._search(this._guidCols, query);
    return results.length ? results[0].guid : null;
  },

  async hasGUID(guid) {
    // We could probably use a count function here, but search exists...
    let results = await this._search(this._guidCols, { guid });
    return !!results.length;
  },

  async replaceGUID(oldGUID, newGUID) {
    let changes = {
      op: "update",
      guid: oldGUID,
      newGuid: newGUID,
    };
    await this._update(changes);
  },
};

export function FormEngine(service) {
  SyncEngine.call(this, "Forms", service);
}

FormEngine.prototype = {
  _storeObj: FormStore,
  _trackerObj: FormTracker,
  _recordObj: FormRec,

  syncPriority: 6,

  get prefName() {
    return "history";
  },

  async _findDupe(item) {
    return FormWrapper.getGUID(item.name, item.value);
  },
};
Object.setPrototypeOf(FormEngine.prototype, SyncEngine.prototype);

function FormStore(name, engine) {
  Store.call(this, name, engine);
}
FormStore.prototype = {
  async _processChange(change) {
    // If this._changes is defined, then we are applying a batch, so we
    // can defer it.
    if (this._changes) {
      this._changes.push(change);
      return;
    }

    // Otherwise we must handle the change right now.
    await FormWrapper._update(change);
  },

  async applyIncomingBatch(records, countTelemetry) {
    Async.checkAppReady();
    // We collect all the changes to be made then apply them all at once.
    this._changes = [];
    let failures = await Store.prototype.applyIncomingBatch.call(
      this,
      records,
      countTelemetry
    );
    if (this._changes.length) {
      await FormWrapper._update(this._changes);
    }
    delete this._changes;
    return failures;
  },

  async getAllIDs() {
    let results = await FormWrapper._search(["guid"], []);
    let guids = {};
    for (let result of results) {
      guids[result.guid] = true;
    }
    return guids;
  },

  async changeItemID(oldID, newID) {
    await FormWrapper.replaceGUID(oldID, newID);
  },

  async itemExists(id) {
    return FormWrapper.hasGUID(id);
  },

  async createRecord(id, collection) {
    let record = new FormRec(collection, id);
    let entry = await FormWrapper.getEntry(id);
    if (entry != null) {
      record.name = entry.name;
      record.value = entry.value;
    } else {
      record.deleted = true;
    }
    return record;
  },

  async create(record) {
    this._log.trace("Adding form record for " + record.name);
    let change = {
      op: "add",
      guid: record.id,
      fieldname: record.name,
      value: record.value,
    };
    await this._processChange(change);
  },

  async remove(record) {
    this._log.trace("Removing form record: " + record.id);
    let change = {
      op: "remove",
      guid: record.id,
    };
    await this._processChange(change);
  },

  async update() {
    this._log.trace("Ignoring form record update request!");
  },

  async wipe() {
    let change = {
      op: "remove",
    };
    await FormWrapper._update(change);
  },
};
Object.setPrototypeOf(FormStore.prototype, Store.prototype);

function FormTracker(name, engine) {
  LegacyTracker.call(this, name, engine);
}
FormTracker.prototype = {
  QueryInterface: ChromeUtils.generateQI([
    "nsIObserver",
    "nsISupportsWeakReference",
  ]),

  onStart() {
    Svc.Obs.add("satchel-storage-changed", this.asyncObserver);
  },

  onStop() {
    Svc.Obs.remove("satchel-storage-changed", this.asyncObserver);
  },

  async observe(subject, topic, data) {
    if (this.ignoreAll) {
      return;
    }
    switch (topic) {
      case "satchel-storage-changed":
        if (data == "formhistory-add" || data == "formhistory-remove") {
          let guid = subject.QueryInterface(Ci.nsISupportsString).toString();
          await this.trackEntry(guid);
        }
        break;
    }
  },

  async trackEntry(guid) {
    const added = await this.addChangedID(guid);
    if (added) {
      this.score += SCORE_INCREMENT_MEDIUM;
    }
  },
};
Object.setPrototypeOf(FormTracker.prototype, LegacyTracker.prototype);

class FormsProblemData extends CollectionProblemData {
  getSummary() {
    // We don't support syncing deleted form data, so "clientMissing" isn't a problem
    return super.getSummary().filter(entry => entry.name !== "clientMissing");
  }
}

export class FormValidator extends CollectionValidator {
  constructor() {
    super("forms", "id", ["name", "value"]);
    this.ignoresMissingClients = true;
  }

  emptyProblemData() {
    return new FormsProblemData();
  }

  async getClientItems() {
    return FormWrapper._search(["guid", "fieldname", "value"], {});
  }

  normalizeClientItem(item) {
    return {
      id: item.guid,
      guid: item.guid,
      name: item.fieldname,
      fieldname: item.fieldname,
      value: item.value,
      original: item,
    };
  }

  async normalizeServerItem(item) {
    let res = Object.assign(
      {
        guid: item.id,
        fieldname: item.name,
        original: item,
      },
      item
    );
    // Missing `name` or `value` causes the getGUID call to throw
    if (item.name !== undefined && item.value !== undefined) {
      let guid = await FormWrapper.getGUID(item.name, item.value);
      if (guid) {
        res.guid = guid;
        res.id = guid;
        res.duped = true;
      }
    }

    return res;
  }
}
PK