// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as Common from '../../core/common/common.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';

let autofillManagerInstance: AutofillManager;

export class AutofillManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
  #address = '';
  #filledFields: Protocol.Autofill.FilledField[] = [];
  #matches: Match[] = [];
  #autofillModel: SDK.AutofillModel.AutofillModel|null = null;

  private constructor() {
    super();
    SDK.TargetManager.TargetManager.instance().addModelListener(
        SDK.AutofillModel.AutofillModel, SDK.AutofillModel.Events.ADDRESS_FORM_FILLED, this.#addressFormFilled, this,
        {scoped: true});
  }

  static instance(opts: {forceNew: boolean|null} = {forceNew: null}): AutofillManager {
    const {forceNew} = opts;
    if (!autofillManagerInstance || forceNew) {
      autofillManagerInstance = new AutofillManager();
    }
    return autofillManagerInstance;
  }

  async #addressFormFilled({data}: Common.EventTarget.EventTargetEvent<
                           SDK.AutofillModel.EventTypes[SDK.AutofillModel.Events.ADDRESS_FORM_FILLED]>): Promise<void> {
    this.#autofillModel = data.autofillModel;
    this.#processAddressFormFilledData(data.event);
    if (this.#address) {
      this.dispatchEventToListeners(Events.ADDRESS_FORM_FILLED, {
        address: this.#address,
        filledFields: this.#filledFields,
        matches: this.#matches,
      });
    }
  }

  getLastFilledAddressForm(): AddressFormFilledEvent|null {
    if (!this.#address || !this.#autofillModel) {
      return null;
    }
    return {
      address: this.#address,
      filledFields: this.#filledFields,
      matches: this.#matches,
    };
  }

  highlightFilledField(filledField: Protocol.Autofill.FilledField): void {
    const backendNodeId = filledField.fieldId;
    const target = SDK.FrameManager.FrameManager.instance().getFrame(filledField.frameId)?.resourceTreeModel().target();
    if (target) {
      const deferredNode = new SDK.DOMModel.DeferredDOMNode(target, backendNodeId);
      const domModel = target.model(SDK.DOMModel.DOMModel);
      if (deferredNode && domModel) {
        domModel.overlayModel().highlightInOverlay({deferredNode}, 'all');
      }
    }
  }

  clearHighlightedFilledFields(): void {
    SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
  }

  #processAddressFormFilledData({addressUi, filledFields}: Protocol.Autofill.AddressFormFilledEvent): void {
    // Transform addressUi into a single (multi-line) string.
    const concatAddressFields = (addressFields: Protocol.Autofill.AddressFields): string =>
        addressFields.fields.filter(field => field.value.length).map(field => field.value).join(' ');
    this.#address = addressUi.addressFields.map(addressFields => concatAddressFields(addressFields))
                        .filter(str => str.length)
                        .join('\n');

    this.#filledFields = filledFields;
    this.#matches = [];

    // Populate a list of matches by searching in the address string for
    // occurences of filled field values.
    for (let i = 0; i < this.#filledFields.length; i++) {
      if (this.#filledFields[i].value === '') {
        continue;
      }
      // 1) Replace multiple whitespaces with a single space.
      // 2) Escape special characters.
      // 3) For ',' or '.' before whitespace, insert the '?' quantifier.
      const needle = Platform.StringUtilities.escapeForRegExp(this.#filledFields[i].value.replaceAll(/\s/g, ' '))
                         .replaceAll(/([.,]+)\s/g, '$1? ');
      const matches = this.#address.replaceAll(/\s/g, ' ').matchAll(new RegExp(needle, 'g'));
      for (const match of matches) {
        if (typeof match.index !== 'undefined') {
          this.#matches.push({startIndex: match.index, endIndex: match.index + match[0].length, filledFieldIndex: i});
        }
      }
    }
  }
}

/**
 * A Match describes how the value of a filled field corresponds to a substring
 * of address from startIndex to endIndex.
 **/
export interface Match {
  startIndex: number;
  endIndex: number;
  filledFieldIndex: number;
}

export const enum Events {
  ADDRESS_FORM_FILLED = 'AddressFormFilled',
}

export interface AddressFormFilledEvent {
  address: string;
  filledFields: Protocol.Autofill.FilledField[];
  matches: Match[];
}

export interface EventTypes {
  [Events.ADDRESS_FORM_FILLED]: AddressFormFilledEvent;
}
