VanillaObject() {
    const json = { ...this };
    delete json.elementWeakRef;
    return json;
  }

  static fromVanillaObject(obj) {
    const element = lazy.FormAutofillUtils.getElementByIdentifier(
      obj.elementId
    );
    return element ? Object.assign(new FieldDetail(element), obj) : null;
  }

  static create(
    element,
    form,
    fieldName = null,
    {
      autocompleteInfo = null,
      fathomConfidence = null,
      isVisible = true,
      isLookup = false,
    } = {}
  ) {
    const fieldDetail = new FieldDetail(element);

    fieldDetail.elementId =
      lazy.FormAutofillUtils.getElementIdentifier(element);
    fieldDetail.rootElementId = lazy.FormAutofillUtils.getElementIdentifier(
      form.rootElement
    );
    fieldDetail.identifier = `${element.id}/${element.name}`;
    fieldDetail.localName = element.localName;

    if (Array.isArray(fieldName)) {
      fieldDetail.fieldName = fieldName[0] ?? "";
      fieldDetail.alternativeFieldName = fieldName[1] ?? "";
    } else {
      fieldDetail.fieldName = fieldName;
    }

    if (!fieldDetail.fieldName) {
      fieldDetail.reason = "unknown";
    } else if (autocompleteInfo) {
      fieldDetail.reason = "autocomplete";
      fieldDetail.section = autocompleteInfo.section;
      fieldDetail.addressType = autocompleteInfo.addressType;
      fieldDetail.contactType = autocompleteInfo.contactType;
      fieldDetail.credentialType = autocompleteInfo.credentialType;
      fieldDetail.sectionName =
        autocompleteInfo.section || autocompleteInfo.addressType;
    } else if (fathomConfidence) {
      fieldDetail.reason = "fathom";
      fieldDetail.confidence = fathomConfidence;
    } else {
      fieldDetail.reason = "regex-heuristic";
    }

    try {
      fieldDetail.browsingContextId =
        element.localName == "iframe"
          ? element.browsingContext.id
          : BrowsingContext.getFromWindow(element.ownerGlobal).id;
    } catch {
      /* unit test doesn't have ownerGlobal */
    }

    fieldDetail.isVisible = isVisible;

    // Info required by heuristics
    fieldDetail.maxLength = element.maxLength;

    fieldDetail.isLookup = isLookup;

    return fieldDetail;
  }
}

/**
 * A scanner for traversing all elements in a form. It also provides a
 * cursor (parsingIndex) to indicate which element is waiting for parsing.
 *
 * The scanner retrives the field detail by calling heuristics handlers
 * `inferFieldInfo` function.
 */
export class FieldScanner {
  #parsingIndex = 0;

  #fieldDetails = [];

  /**
   * Create a FieldScanner based on form elements with the existing
   * fieldDetails.
   *
   * @param {Array<FieldDetails>} fieldDetails
   *        An array of fieldDetail object to be scanned.
   */
  constructor(fieldDetails) {
    this.#fieldDetails = fieldDetails;
  }

  /**
   * This cursor means the index of the element which is waiting for parsing.
   *
   * @returns {number}
   *          The index of the element which is waiting for parsing.
   */
  get parsingIndex() {
    return this.#parsingIndex;
  }

  get parsingFinished() {
    return this.parsingIndex >= this.#fieldDetails.length;
  }

  /**
   * Move the parsingIndex to the next elements. Any elements behind this index
   * means the parsing tasks are finished.
   *
   * @param {number} index
   *        The latest index of elements waiting for parsing.
   */
  set parsingIndex(index) {
    if (index > this.#fieldDetails.length) {
      throw new Error("The parsing index is out of range.");
    }
    this.#parsingIndex = index;
  }

  /**
   * Retrieve the field detail by the index. If the field detail is not ready,
   * the elements will be traversed until matching the index.
   *
   * @param {number} index
   *        The index of the element that you want to retrieve.
   * @returns {object}
   *          The field detail at the specific index.
   */
  getFieldDetailByIndex(index) {
    if (index >= this.#fieldDetails.length) {
      return null;
    }

    return this.#fieldDetails[index];
  }

  getFieldsMatching(matchFn, includeInvisible = false) {
    let fields = [];

    for (let idx = 0; this.elementExisting(idx); idx++) {
      let field = this.#fieldDetails[idx];
      if ((includeInvisible || field.isVisible) && matchFn(field)) {
        fields.push(field);
      }
    }

    return fields;
  }

  /**
   * Return the index of the first visible field found with the given name.
   *
   * @param {string} fieldName
   *        The field name to find.
   * @param {string} includeInvisible
   *        Whether to find non-visible fields.
   * @returns {number}
   *          The index of the element or -1 if not found.
   */
  getFieldIndexByName(fieldName, includeInvisible = false) {
    for (let idx = 0; this.elementExisting(idx); idx++) {
      let field = this.#fieldDetails[idx];
      if (
        field.fieldName == fieldName &&
        (includeInvisible || field.isVisible)
      ) {
        return idx;
      }
    }

    return -1;
  }

  /**
   * When a field detail should be changed its fieldName after parsing, use
   * this function to update the fieldName which is at a specific index.
   *
   * @param {number} index
   *        The index indicates a field detail to be updated.
   * @param {string} fieldName
   *        The new name of the field
   * @param {boolean} [ignoreAutocomplete=false]
   *        Whether to change the field name when the field name is determined by
   *        autocomplete attribute
   */
  updateFieldName(index, fieldName, ignoreAutocomplete = false) {
    if (index >= this.#fieldDetails.length) {
      throw new Error("Try to update the non-existing field detail.");
    }

    const fieldDetail = this.#fieldDetails[index];
    if (fieldDetail.fieldName == fieldName) {
      return;
    }

    if (!ignoreAutocomplete && fieldDetail.reason == "autocomplete") {
      return;
    }

    fieldDetail.fieldName = fieldName;
    fieldDetail.reason = "update-heuristic";
  }

  elementExisting(index) {
    return index < this.#fieldDetails.length;
  }
}

export default FieldScanner;
PK