/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* import-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/
import LabelledCheckbox from "../components/labelled-checkbox.js";
import PaymentRequestPage from "../components/payment-request-page.js";
import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
import paymentRequest from "../paymentRequest.js";
import HandleEventMixin from "../mixins/HandleEventMixin.js";
/* import-globals-from ../unprivileged-fallbacks.js */

/**
 * <address-form></address-form>
 *
 * Don't use document.getElementById or document.querySelector* to access form
 * elements, use querySelector on `this` or `this.form` instead so that elements
 * can be found before the element is connected.
 *
 * XXX: Bug 1446164 - This form isn't localized when used via this custom element
 * as it will be much easier to share the logic once we switch to Fluent.
 */

export default class AddressForm extends HandleEventMixin(
  PaymentStateSubscriberMixin(PaymentRequestPage)
) {
  constructor() {
    super();

    this.genericErrorText = document.createElement("div");
    this.genericErrorText.setAttribute("aria-live", "polite");
    this.genericErrorText.classList.add("page-error");

    this.cancelButton = document.createElement("button");
    this.cancelButton.className = "cancel-button";
    this.cancelButton.addEventListener("click", this);

    this.backButton = document.createElement("button");
    this.backButton.className = "back-button";
    this.backButton.addEventListener("click", this);

    this.saveButton = document.createElement("button");
    this.saveButton.className = "save-button primary";
    this.saveButton.addEventListener("click", this);

    this.persistCheckbox = new LabelledCheckbox();
    this.persistCheckbox.className = "persist-checkbox";

    // Combination of AddressErrors and PayerErrors as keys
    this._errorFieldMap = {
      addressLine: "#street-address",
      city: "#address-level2",
      country: "#country",
      dependentLocality: "#address-level3",
      email: "#email",
      // Bug 1472283 is on file to support
      // additional-name and family-name.
      // XXX: For now payer name errors go on the family-name and address-errors
      //      go on the given-name so they don't overwrite each other.
      name: "#family-name",
      organization: "#organization",
      phone: "#tel",
      postalCode: "#postal-code",
      // Bug 1472283 is on file to support
      // additional-name and family-name.
      recipient: "#given-name",
      region: "#address-level1",
      // Bug 1474905 is on file to properly support regionCode. See
      // full note in paymentDialogWrapper.js
      regionCode: "#address-level1",
    };

    // The markup is shared with form autofill preferences.
    let url = "formautofill/editAddress.xhtml";
    this.promiseReady = this._fetchMarkup(url).then(doc => {
      this.form = doc.getElementById("form");
      return this.form;
    });
  }

  _fetchMarkup(url) {
    return new Promise((resolve, reject) => {
      let xhr = new XMLHttpRequest();
      xhr.responseType = "document";
      xhr.addEventListener("error", reject);
      xhr.addEventListener("load", evt => {
        resolve(xhr.response);
      });
      xhr.open("GET", url);
      xhr.send();
    });
  }

  connectedCallback() {
    this.promiseReady.then(form => {
      this.body.appendChild(form);

      let record = undefined;
      this.formHandler = new EditAddress(
        {
          form,
        },
        record,
        {
          DEFAULT_REGION: PaymentDialogUtils.DEFAULT_REGION,
          getFormFormat: PaymentDialogUtils.getFormFormat,
          findAddressSelectOption: PaymentDialogUtils.findAddressSelectOption,
          countries: PaymentDialogUtils.countries,
        }
      );

      // The EditAddress constructor adds `input` event listeners on the same element,
      // which update field validity. By adding our event listeners after this constructor,
      // validity will be updated before our handlers get the event
      this.form.addEventListener("input", this);
      this.form.addEventListener("invalid", this);
      this.form.addEventListener("change", this);

      // The "invalid" event does not bubble and needs to be listened for on each
      // form element.
      for (let field of this.form.elements) {
        field.addEventListener("invalid", this);
      }

      this.body.appendChild(this.persistCheckbox);
      this.body.appendChild(this.genericErrorText);

      this.footer.appendChild(this.cancelButton);
      this.footer.appendChild(this.backButton);
      this.footer.appendChild(this.saveButton);
      // Only call the connected super callback(s) once our markup is fully
      // connected, including the shared form fetched asynchronously.
      super.connectedCallback();
    });
  }

  render(state) {
    if (!this.id) {
      throw new Error("AddressForm without an id");
    }
    let record;
    let { page, [this.id]: addressPage } = state;

    if (this.id && page && page.id !== this.id) {
      log.debug(`${this.id}: no need to further render inactive page`);
      return;
    }

    let editing = !!addressPage.guid;
    this.cancelButton.textContent = this.dataset.cancelButtonLabel;
    this.backButton.textContent = this.dataset.backButtonLabel;
    if (editing) {
      this.saveButton.textContent = this.dataset.updateButtonLabel;
    } else {
      this.saveButton.textContent = this.dataset.nextButtonLabel;
    }

    this.persistCheckbox.label = this.dataset.persistCheckboxLabel;
    this.persistCheckbox.infoTooltip = this.dataset.persistCheckboxInfoTooltip;

    this.backButton.hidden = page.onboardingWizard;
    this.cancelButton.hidden = !page.onboardingWizard;

    this.pageTitleHeading.textContent = editing
      ? this.dataset.titleEdit
      : this.dataset.titleAdd;
    this.genericErrorText.textContent = page.error;

    let addresses = paymentRequest.getAddresses(state);

    // If an address is selected we want to edit it.
    if (editing) {
      record = addresses[addressPage.guid];
      if (!record) {
        throw new Error(
          "Trying to edit a non-existing address: " + addressPage.guid
        );
      }
      // When editing an existing record, prevent changes to persistence
      this.persistCheckbox.hidden = true;
    } else {
      let {
        saveAddressDefaultChecked,
      } = PaymentDialogUtils.getDefaultPreferences();
      if (typeof saveAddressDefaultChecked != "boolean") {
        throw new Error(`Unexpected non-boolean value for saveAddressDefaultChecked from
          PaymentDialogUtils.getDefaultPreferences(): ${typeof saveAddressDefaultChecked}`);
      }
      // Adding a new record: default persistence to the pref value when in a not-private session
      this.persistCheckbox.hidden = false;
      this.persistCheckbox.checked = state.isPrivate
        ? false
        : saveAddressDefaultChecked;
    }

    let selectedStateKey = this.getAttribute("selected-state-key").split("|");
    log.debug(`${this.id}#render got selectedStateKey: ${selectedStateKey}`);

    if (addressPage.addressFields) {
      this.form.dataset.addressFields = addressPage.addressFields;
    } else {
      this.form.dataset.addressFields = "mailing-address tel";
    }
    this.formHandler.loadRecord(record);

    // Add validation to some address fields
    this.updateRequiredState();

    // Show merchant errors for the appropriate address form.
    let merchantFieldErrors = AddressForm.merchantFieldErrorsForForm(
      state,
      selectedStateKey
    );
    for (let [errorName, errorSelector] of Object.entries(
      this._errorFieldMap
    )) {
      let errorText = "";
      // Never show errors on an 'add' screen as they would be for a different address.
      if (editing && merchantFieldErrors) {
        if (errorName == "region" || errorName == "regionCode") {
          errorText =
            merchantFieldErrors.regionCode || merchantFieldErrors.region || "";
        } else {
          errorText = merchantFieldErrors[errorName] || "";
        }
      }
      let container = this.form.querySelector(errorSelector + "-container");
      let field = this.form.querySelector(errorSelector);
      field.setCustomValidity(errorText);
      let span = paymentRequest.maybeCreateFieldErrorElement(container);
      span.textContent = errorText;
    }

    this.updateSaveButtonState();
  }

  onChange(event) {
    if (event.target.id == "country") {
      this.updateRequiredState();
    }
    this.updateSaveButtonState();
  }

  onInvalid(event) {
    if (event.target instanceof HTMLFormElement) {
      this.onInvalidForm(event);
    } else {
      this.onInvalidField(event);
    }
  }

  onClick(evt) {
    switch (evt.target) {
      case this.cancelButton: {
        paymentRequest.cancel();
        break;
      }
      case this.backButton: {
        let currentState = this.requestStore.getState();
        const previousId = currentState.page.previousId;
        let state = {
          page: {
            id: previousId || "payment-summary",
          },
        };
        if (previousId) {
          state[previousId] = Object.assign({}, currentState[previousId], {
            preserveFieldValues: true,
          });
        }
        this.requestStore.setState(state);
        break;
      }
      case this.saveButton: {
        if (this.form.checkValidity()) {
          this.saveRecord();
        }
        break;
      }
      default: {
        throw new Error("Unexpected click target");
      }
    }
  }

  onInput(event) {
    event.target.setCustomValidity("");
    this.updateSaveButtonState();
  }

  /**
   * @param {Event} event - "invalid" event
   * Note: Keep this in-sync with the equivalent version in basic-card-form.js
   */
  onInvalidField(event) {
    let field = event.target;
    let container = field.closest(`#${field.id}-container`);
    let errorTextSpan = paymentRequest.maybeCreateFieldErrorElement(container);
    errorTextSpan.textContent = field.validationMessage;
  }

  onInvalidForm() {
    this.saveButton.disabled = true;
  }

  updateRequiredState() {
    for (let field of this.form.elements) {
      let container = field.closest(`#${field.id}-container`);
      if (field.localName == "button" || !container) {
        continue;
      }
      let span = container.querySelector(".label-text");
      span.setAttribute(
        "fieldRequiredSymbol",
        this.dataset.fieldRequiredSymbol
      );
      container.toggleAttribute("required", field.required && !field.disabled);
    }
  }

  updateSaveButtonState() {
    this.saveButton.disabled = !this.form.checkValidity();
  }

  async saveRecord() {
    let record = this.formHandler.buildFormObject();
    let currentState = this.requestStore.getState();
    let {
      page,
      tempAddresses,
      savedBasicCards,
      [this.id]: addressPage,
    } = currentState;
    let editing = !!addressPage.guid;

    if (
      editing
        ? addressPage.guid in tempAddresses
        : !this.persistCheckbox.checked
    ) {
      record.isTemporary = true;
    }

    let successStateChange;
    const previousId = page.previousId;
    if (page.onboardingWizard && !Object.keys(savedBasicCards).length) {
      successStateChange = {
        "basic-card-page": {
          selectedStateKey: "selectedPaymentCard",
          // Preserve field values as the user may have already edited the card
          // page and went back to the address page to make a correction.
          preserveFieldValues: true,
        },
        page: {
          id: "basic-card-page",
          previousId: this.id,
          onboardingWizard: page.onboardingWizard,
        },
      };
    } else {
      successStateChange = {
        page: {
          id: previousId || "payment-summary",
          onboardingWizard: page.onboardingWizard,
        },
      };
    }

    if (previousId) {
      successStateChange[previousId] = Object.assign(
        {},
        currentState[previousId]
      );
      successStateChange[previousId].preserveFieldValues = true;
    }

    try {
      let { guid } = await paymentRequest.updateAutofillRecord(
        "addresses",
        record,
        addressPage.guid
      );
      let selectedStateKey = this.getAttribute("selected-state-key").split("|");

      if (selectedStateKey.length == 1) {
        Object.assign(successStateChange, {
          [selectedStateKey[0]]: guid,
        });
      } else if (selectedStateKey.length == 2) {
        // Need to keep properties like preserveFieldValues from getting removed.
        let subObj = Object.assign({}, successStateChange[selectedStateKey[0]]);
        subObj[selectedStateKey[1]] = guid;
        Object.assign(successStateChange, {
          [selectedStateKey[0]]: subObj,
        });
      } else {
        throw new Error(
          `selectedStateKey not supported: '${selectedStateKey}'`
        );
      }

      this.requestStore.setState(successStateChange);
    } catch (ex) {
      log.warn("saveRecord: error:", ex);
      this.requestStore.setState({
        page: {
          id: this.id,
          onboardingWizard: page.onboardingWizard,
          error: this.dataset.errorGenericSave,
        },
      });
    }
  }

  /**
   * Get the dictionary of field-specific merchant errors relevant to the
   * specific form identified by the state key.
   * @param {object} state The application state
   * @param {string[]} stateKey The key in state to return address errors for.
   * @returns {object} with keys as PaymentRequest field names and values of
   *                   merchant-provided error strings.
   */
  static merchantFieldErrorsForForm(state, stateKey) {
    let { paymentDetails } = state.request;
    switch (stateKey.join("|")) {
      case "selectedShippingAddress": {
        return paymentDetails.shippingAddressErrors;
      }
      case "selectedPayerAddress": {
        return paymentDetails.payerErrors;
      }
      case "basic-card-page|billingAddressGUID": {
        // `paymentMethod` can be null.
        return (
          (paymentDetails.paymentMethodErrors &&
            paymentDetails.paymentMethodErrors.billingAddress) ||
          {}
        );
      }
      default: {
        throw new Error("Unknown selectedStateKey");
      }
    }
  }
}

customElements.define("address-form", AddressForm);
