/* 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/. */

/**
 * Loaded in the unprivileged frame of each payment dialog.
 *
 * Communicates with privileged code via DOM Events.
 */

/* import-globals-from unprivileged-fallbacks.js */

var paymentRequest = {
  _nextMessageID: 1,
  domReadyPromise: null,

  init() {
    // listen to content
    window.addEventListener("paymentChromeToContent", this);

    window.addEventListener("keydown", this);

    this.domReadyPromise = new Promise(function dcl(resolve) {
      window.addEventListener("DOMContentLoaded", resolve, { once: true });
    }).then(this.handleEvent.bind(this));

    // This scope is now ready to listen to the initialization data
    this.sendMessageToChrome("initializeRequest");
  },

  handleEvent(event) {
    switch (event.type) {
      case "DOMContentLoaded": {
        this.onPaymentRequestLoad();
        break;
      }
      case "keydown": {
        if (event.code != "KeyD" || !event.altKey || !event.ctrlKey) {
          break;
        }
        this.toggleDebuggingConsole();
        break;
      }
      case "unload": {
        this.onPaymentRequestUnload();
        break;
      }
      case "paymentChromeToContent": {
        this.onChromeToContent(event);
        break;
      }
      default: {
        throw new Error("Unexpected event type");
      }
    }
  },

  /**
   * @param {string} messageType
   * @param {[object]} detail
   * @returns {number} message ID to be able to identify a reply (where applicable).
   */
  sendMessageToChrome(messageType, detail = {}) {
    let messageID = this._nextMessageID++;
    log.debug("sendMessageToChrome:", messageType, messageID, detail);
    let event = new CustomEvent("paymentContentToChrome", {
      bubbles: true,
      detail: Object.assign(
        {
          messageType,
          messageID,
        },
        detail
      ),
    });
    document.dispatchEvent(event);
    return messageID;
  },

  toggleDebuggingConsole() {
    let debuggingConsole = document.getElementById("debugging-console");
    if (debuggingConsole.hidden && !debuggingConsole.src) {
      debuggingConsole.src = "debugging.html";
    }
    debuggingConsole.hidden = !debuggingConsole.hidden;
  },

  onChromeToContent({ detail }) {
    let { messageType } = detail;
    log.debug("onChromeToContent:", messageType);

    switch (messageType) {
      case "responseSent": {
        let { request } = document
          .querySelector("payment-dialog")
          .requestStore.getState();
        document.querySelector("payment-dialog").requestStore.setState({
          changesPrevented: true,
          request: Object.assign({}, request, { completeStatus: "processing" }),
        });
        break;
      }
      case "showPaymentRequest": {
        this.onShowPaymentRequest(detail);
        break;
      }
      case "updateState": {
        document.querySelector("payment-dialog").setStateFromParent(detail);
        break;
      }
    }
  },

  onPaymentRequestLoad() {
    log.debug("onPaymentRequestLoad");
    window.addEventListener("unload", this, { once: true });

    // Automatically show the debugging console if loaded with a truthy `debug` query parameter.
    if (new URLSearchParams(location.search).get("debug")) {
      this.toggleDebuggingConsole();
    }
  },

  async onShowPaymentRequest(detail) {
    // Handle getting called before the DOM is ready.
    log.debug("onShowPaymentRequest:", detail);
    await this.domReadyPromise;

    log.debug("onShowPaymentRequest: domReadyPromise resolved");
    log.debug("onShowPaymentRequest, isPrivate?", detail.isPrivate);

    let paymentDialog = document.querySelector("payment-dialog");
    let state = {
      request: detail.request,
      savedAddresses: detail.savedAddresses,
      savedBasicCards: detail.savedBasicCards,
      // Temp records can exist upon a reload during development.
      tempAddresses: detail.tempAddresses,
      tempBasicCards: detail.tempBasicCards,
      isPrivate: detail.isPrivate,
      page: {
        id: "payment-summary",
      },
    };

    let hasSavedAddresses = !!Object.keys(this.getAddresses(state)).length;
    let hasSavedCards = !!Object.keys(this.getBasicCards(state)).length;
    let shippingRequested = state.request.paymentOptions.requestShipping;

    // Onboarding wizard flow.
    if (!hasSavedAddresses && shippingRequested) {
      state.page = {
        id: "shipping-address-page",
        onboardingWizard: true,
      };

      state["shipping-address-page"] = {
        guid: null,
      };
    } else if (!hasSavedAddresses && !hasSavedCards) {
      state.page = {
        id: "billing-address-page",
        onboardingWizard: true,
      };

      state["billing-address-page"] = {
        guid: null,
      };
    } else if (!hasSavedCards) {
      state.page = {
        id: "basic-card-page",
        onboardingWizard: true,
      };
      state["basic-card-page"] = {
        selectedStateKey: "selectedPaymentCard",
      };
    }

    await paymentDialog.setStateFromParent(state);

    this.sendMessageToChrome("paymentDialogReady");
  },

  openPreferences() {
    this.sendMessageToChrome("openPreferences");
  },

  cancel() {
    this.sendMessageToChrome("paymentCancel");
  },

  pay(data) {
    this.sendMessageToChrome("pay", data);
  },

  closeDialog() {
    this.sendMessageToChrome("closeDialog");
  },

  changePaymentMethod(data) {
    this.sendMessageToChrome("changePaymentMethod", data);
  },

  changeShippingAddress(data) {
    this.sendMessageToChrome("changeShippingAddress", data);
  },

  changeShippingOption(data) {
    this.sendMessageToChrome("changeShippingOption", data);
  },

  changePayerAddress(data) {
    this.sendMessageToChrome("changePayerAddress", data);
  },

  /**
   * Add/update an autofill storage record.
   *
   * If the the `guid` argument is provided update the record; otherwise, add it.
   * @param {string} collectionName The autofill collection that record belongs to.
   * @param {object} record The autofill record to add/update
   * @param {string} [guid] The guid of the autofill record to update
   * @returns {Promise} when the update response is received
   */
  updateAutofillRecord(collectionName, record, guid) {
    return new Promise((resolve, reject) => {
      let messageID = this.sendMessageToChrome("updateAutofillRecord", {
        collectionName,
        guid,
        record,
      });

      window.addEventListener("paymentChromeToContent", function onMsg({
        detail,
      }) {
        if (
          detail.messageType != "updateAutofillRecord:Response" ||
          detail.messageID != messageID
        ) {
          return;
        }
        log.debug("updateAutofillRecord: response:", detail);
        window.removeEventListener("paymentChromeToContent", onMsg);
        document
          .querySelector("payment-dialog")
          .setStateFromParent(detail.stateChange);
        if (detail.error) {
          reject(detail);
        } else {
          resolve(detail);
        }
      });
    });
  },

  /**
   * @param {object} state object representing the UI state
   * @param {string} selectedMethodID (GUID) uniquely identifying the selected payment method
   * @returns {object?} the applicable modifier for the payment method
   */
  getModifierForPaymentMethod(state, selectedMethodID) {
    let basicCards = this.getBasicCards(state);
    let selectedMethod = basicCards[selectedMethodID] || null;
    if (selectedMethod && selectedMethod.methodName !== "basic-card") {
      throw new Error(
        `${selectedMethod.methodName} (${selectedMethodID}) ` +
          `is not a supported payment method`
      );
    }
    let modifiers = state.request.paymentDetails.modifiers;
    if (!selectedMethod || !modifiers || !modifiers.length) {
      return null;
    }
    let appliedModifier = modifiers.find(modifier => {
      // take the first matching modifier
      if (
        modifier.supportedMethods &&
        modifier.supportedMethods != selectedMethod.methodName
      ) {
        return false;
      }
      let supportedNetworks =
        (modifier.data && modifier.data.supportedNetworks) || [];
      return (
        !supportedNetworks.length ||
        supportedNetworks.includes(selectedMethod["cc-type"])
      );
    });
    return appliedModifier || null;
  },

  /**
   * @param {object} state object representing the UI state
   * @returns {object} in the shape of `nsIPaymentItem` representing the total
   *                   that applies to the selected payment method.
   */
  getTotalItem(state) {
    let methodID = state.selectedPaymentCard;
    if (methodID) {
      let modifier = paymentRequest.getModifierForPaymentMethod(
        state,
        methodID
      );
      if (modifier && modifier.hasOwnProperty("total")) {
        return modifier.total;
      }
    }
    return state.request.paymentDetails.totalItem;
  },

  onPaymentRequestUnload() {
    // remove listeners that may be used multiple times here
    window.removeEventListener("paymentChromeToContent", this);
  },

  _sortObjectsByTimeLastUsed(objects) {
    let sortedValues = Object.values(objects).sort((a, b) => {
      let aLastUsed = a.timeLastUsed || a.timeLastModified;
      let bLastUsed = b.timeLastUsed || b.timeLastModified;
      return bLastUsed - aLastUsed;
    });
    let sortedObjects = {};
    for (let obj of sortedValues) {
      sortedObjects[obj.guid] = obj;
    }
    return sortedObjects;
  },

  getAddresses(state) {
    let addresses = Object.assign(
      {},
      state.savedAddresses,
      state.tempAddresses
    );
    return this._sortObjectsByTimeLastUsed(addresses);
  },

  getBasicCards(state) {
    let cards = Object.assign({}, state.savedBasicCards, state.tempBasicCards);
    return this._sortObjectsByTimeLastUsed(cards);
  },

  maybeCreateFieldErrorElement(container) {
    let span = container.querySelector(".error-text");
    if (!span) {
      span = document.createElement("span");
      span.className = "error-text";
      container.appendChild(span);
    }
    return span;
  },
};

paymentRequest.init();

export default paymentRequest;
