jQuery( function( $ ) {

	"use strict"

	/**
	 * Google Pay handler.
	 *
	 * @since 5.10.0
	 *
	 * @type {SV_WC_Google_Pay_Handler_v5_13_0} object
	 */
	window.SV_WC_Google_Pay_Handler_v5_13_0 = class SV_WC_Google_Pay_Handler_v5_13_0 {

		/**
		 * Handler constructor.
		 *
		 * @since 5.10.0
		 *
		 * @param {Object} params The plugin ID
		 * @param {string} params.plugin_id The plugin ID
		 * @param {string} params.merchant_id The merchant ID
		 * @param {string} params.merchant_name The site name
		 * @param {string} params.gateway_id The gateway ID
		 * @param {string} params.gateway_id_dasherized The gateway ID dasherized
		 * @param {string} params.environment The gateway environment (PRODUCTION or TEST)
		 * @param {string} params.ajax_url The AJAX URL
		 * @param {string} params.recalculate_totals_nonce Nonce for the recalculate_totals AJAX action
		 * @param {string} params.process_nonce Nonce for the process AJAX action
		 * @param {string} params.button_style The button style
		 * @param {string[]} params.card_types The supported card types
		 * @param {string[]} params.available_countries Array of two-letter country codes the gateway is available for
		 * @param {string[]} params.currency_code WC configured currency
		 * @param {boolean} params.needs_shipping Whether or not the cart or product needs shipping
		 * @param {string} params.generic_error The generic error message
		 * @param {string} params.product_id The product ID if we are on a Product page
		 */
		constructor( params ) {

			let {
				plugin_id,
				merchant_id,
				merchant_name,
				gateway_id,
				gateway_id_dasherized,
				environment,
				ajax_url,
				recalculate_totals_nonce,
				process_nonce,
				button_style,
				card_types,
				available_countries,
				currency_code,
				needs_shipping,
				generic_error
			} = params;

			this.gatewayID              = gateway_id;
			this.merchantID             = merchant_id;
			this.merchantName           = merchant_name;
			this.environment            = environment;
			this.ajaxURL                = ajax_url;
			this.recalculateTotalsNonce = recalculate_totals_nonce;
			this.processNonce           = process_nonce;
			this.buttonStyle            = button_style;
			this.availableCountries     = available_countries;
			this.currencyCode           = currency_code;
			this.needsShipping          = needs_shipping;
			this.genericError           = generic_error;

			if ( params.product_id ) {
				this.productID = params.product_id;
			}

			/**
			 * Card networks supported by your site and your gateway
			 *
			 * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#CardParameters|CardParameters}
			 */
			const allowedCardNetworks = card_types;

			/**
			 * Define the version of the Google Pay API referenced when creating your configuration
			 *
			 * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#PaymentDataRequest|apiVersion in PaymentDataRequest}
			 */
			this.baseRequest = {
				apiVersion: 2,
				apiVersionMinor: 0
			};

			/**
			 * Card authentication methods supported by your site and your gateway
			 *
			 * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#CardParameters|CardParameters}
			 *
			 * @todo confirm your processor supports Android device tokens for your supported card networks
			 */
			const allowedCardAuthMethods = [ 'PAN_ONLY', 'CRYPTOGRAM_3DS' ];

			/**
			 * Identify your gateway and your site's gateway merchant identifier
			 *
			 * The Google Pay API response will return an encrypted payment method capable
			 * of being charged by a supported gateway after payer authorization
			 *
			 * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#gateway|PaymentMethodTokenizationSpecification}
			 */
			const tokenizationSpecification = {
				type: 'PAYMENT_GATEWAY',
				parameters: {
					'gateway': plugin_id,
					'gatewayMerchantId': this.merchantID
				}
			};

			/**
			 * Describe your site's support for the CARD payment method and its required fields
			 *
			 * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#CardParameters|CardParameters}
			 */
			this.baseCardPaymentMethod = {
				type: 'CARD',
				parameters: {
					allowedAuthMethods: allowedCardAuthMethods,
					allowedCardNetworks: allowedCardNetworks,
					billingAddressRequired: true,
					billingAddressParameters: {
						format: 'FULL',
						phoneNumberRequired: true
					}
				}
			};

			/**
			 * Describe your site's support for the CARD payment method including optional fields
			 *
			 * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#CardParameters|CardParameters}
			 */
			this.cardPaymentMethod = Object.assign(
				{},
				this.baseCardPaymentMethod,
				{
					tokenizationSpecification: tokenizationSpecification
				}
			);

			/**
			 * An initialized google.payments.api.PaymentsClient object or null if not yet set
			 *
			 * @see {@link getGooglePaymentsClient}
			 */
			this.paymentsClient = null;
		}

		/**
		 * Configure your site's support for payment methods supported by the Google Pay
		 * API.
		 *
		 * Each member of allowedPaymentMethods should contain only the required fields,
		 * allowing reuse of this base request when determining a viewer's ability
		 * to pay and later requesting a supported payment method
		 *
		 * @returns {object} Google Pay API version, payment methods supported by the site
		 */
		getGoogleIsReadyToPayRequest() {

			return Object.assign(
				{},
				this.baseRequest,
				{
					allowedPaymentMethods: [ this.baseCardPaymentMethod ]
				}
			);
		}

		/**
		 * Configure support for the Google Pay API
		 *
		 * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#PaymentDataRequest|PaymentDataRequest}
		 *
		 * @param {function} resolve callback
		 * @returns {object} PaymentDataRequest fields
		 */
		getGooglePaymentDataRequest( resolve ) {

			return this.getGoogleTransactionInfo( ( transactionInfo ) => {

				const paymentDataRequest = Object.assign( {}, this.baseRequest );
				paymentDataRequest.allowedPaymentMethods = [ this.cardPaymentMethod ];
				paymentDataRequest.transactionInfo = transactionInfo;
				paymentDataRequest.merchantInfo = {
					merchantId: this.merchantID,
					merchantName: this.merchantName
				};

				paymentDataRequest.emailRequired = true;
				paymentDataRequest.callbackIntents = [ 'PAYMENT_AUTHORIZATION' ];

				if ( this.needsShipping ) {
					paymentDataRequest.callbackIntents = [ 'SHIPPING_ADDRESS', 'SHIPPING_OPTION', 'PAYMENT_AUTHORIZATION' ];
					paymentDataRequest.shippingAddressRequired = true;
					paymentDataRequest.shippingAddressParameters = this.getGoogleShippingAddressParameters();
					paymentDataRequest.shippingOptionRequired = true;
				}

				resolve( paymentDataRequest );
			} );
		}

		/**
		 * Return an active PaymentsClient or initialize
		 *
		 * @see {@link https://developers.google.com/pay/api/web/reference/client#PaymentsClient|PaymentsClient constructor}
		 * @returns {google.payments.api.PaymentsClient} Google Pay API client
		 */
		getGooglePaymentsClient() {
			if ( this.paymentsClient === null ) {
				let args = {
					environment: this.environment,
					merchantInfo: {
						merchantName: this.merchantName,
						merchantId: this.merchantID
					},
					paymentDataCallbacks: {
						onPaymentAuthorized: ( paymentData ) => this.onPaymentAuthorized( paymentData ),
					}
				};

				if ( this.needsShipping ) {
					args.paymentDataCallbacks.onPaymentDataChanged = ( paymentData ) => this.onPaymentDataChanged( paymentData );
				}

				this.paymentsClient = new google.payments.api.PaymentsClient( args );
			}
			return this.paymentsClient;
		}

		/**
		 * Handles payment authorization callback intent.
		 *
		 * @param {object} paymentData response from Google Pay API after a payer approves payment.
		 * @see {@link https://developers.google.com/pay/api/web/reference/response-objects#PaymentData|PaymentData object reference}
		 *
		 * @returns Promise<{object}> Promise object to complete or fail the transaction.
		 */
		onPaymentAuthorized( paymentData ) {

			this.blockUI();

			return new Promise( (resolve, reject) => {

				// handle the response
				try {
					this.processPayment( paymentData, resolve );
				}	catch( err ) {
					reject( {
						transactionState: 'ERROR',
						error: {
							intent: 'PAYMENT_AUTHORIZATION',
							message: 'Payment could not be processed',
							reason: 'PAYMENT_DATA_INVALID'
						}
					} );
				}

				this.unblockUI();
			} );
		}

		/**
		 * Handles dynamic buy flow shipping address and shipping options callback intents.
		 *
		 * @param {object} intermediatePaymentData response from Google Pay API a shipping address or shipping option is selected in the payment sheet.
		 * @see {@link https://developers.google.com/pay/api/web/reference/response-objects#IntermediatePaymentData|IntermediatePaymentData object reference}
		 *
		 * @see {@link https://developers.google.com/pay/api/web/reference/response-objects#PaymentDataRequestUpdate|PaymentDataRequestUpdate}
		 * @returns Promise<{object}> Promise of PaymentDataRequestUpdate object to update the payment sheet.
		 */
		onPaymentDataChanged( intermediatePaymentData ) {

			this.blockUI();

			return new Promise(( resolve, reject ) => {

				try {
					let shippingAddress = intermediatePaymentData.shippingAddress;
					let shippingOptionData = intermediatePaymentData.shippingOptionData;
					let chosenShippingMethod = '';

					if ( intermediatePaymentData.callbackTrigger == 'SHIPPING_OPTION' ) {
						chosenShippingMethod = shippingOptionData.id;
					}

					this.getUpdatedTotals( shippingAddress, chosenShippingMethod, ( paymentDataRequestUpdate ) => {

						if ( paymentDataRequestUpdate.newShippingOptionParameters.shippingOptions.length == 0 ) {
							paymentDataRequestUpdate = {
								error: this.getGoogleUnserviceableAddressError()
							};
						}

						resolve( paymentDataRequestUpdate );
					} );

				}	catch( err ) {
					this.failPayment( 'Could not load updated totals or process payment data request update. ' + err );
				}

				this.unblockUI();
			} );
		}

		/**
		 * Provide Google Pay API with a payment amount, currency, and amount status
		 *
		 * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#TransactionInfo|TransactionInfo}
		 *
		 * @param {function} resolve callback
		 * @returns {object} transaction info, suitable for use as transactionInfo property of PaymentDataRequest
		 */
		getGoogleTransactionInfo( resolve ) {

			// get transaction info from cart
			const data = {
				action: `wc_${this.gatewayID}_google_pay_get_transaction_info`,
			}

			if ( this.productID ) {
				data.productID = this.productID;
			}

			$.post( this.ajaxURL, data, ( response ) => {

				if ( response.success ) {
					resolve( JSON.parse( response.data ) )
				} else {
					this.failPayment( 'Could not build transaction info. ' + response.data.message );
				}
			} );
		}

		/**
		 * Get updated totals and shipping options via AJAX for use in the PaymentDataRequest
		 *
		 * @see {@link https://developers.google.com/pay/api/web/reference/response-objects#PaymentDataRequestUpdate|PaymentDataRequestUpdate}
		 *
		 * @param {object} shippingAddress shipping address
		 * @param {object} shippingMethod chosen shipping method
		 * @param {function} resolve callback
		 */
		getUpdatedTotals( shippingAddress, shippingMethod, resolve ) {

			const data = {
				action: `wc_${this.gatewayID}_google_pay_recalculate_totals`,
				'nonce': this.recalculateTotalsNonce,
				shippingAddress,
				shippingMethod
			}

			if ( this.productID ) {
				data.productID = this.productID;
			}

			$.post( this.ajaxURL, data, ( response ) => {

				if ( response.success ) {
					resolve( JSON.parse( response.data ) )
				} else {
					this.failPayment( 'Could not recalculate totals. ' + response.data.message );
				}
			} );
		}

		/**
		 * Provide Google Pay API with shipping address parameters when using dynamic buy flow.
		 *
		 * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#ShippingAddressParameters|ShippingAddressParameters}
		 * @returns {object} shipping address details, suitable for use as shippingAddressParameters property of PaymentDataRequest
		 */
		getGoogleShippingAddressParameters() {

			return {
				allowedCountryCodes: this.availableCountries
			};
		}

		/**
		 * Provide Google Pay API with a payment data error.
		 *
		 * @see {@link https://developers.google.com/pay/api/web/reference/response-objects#PaymentDataError|PaymentDataError}
		 * @returns {object} payment data error, suitable for use as error property of PaymentDataRequestUpdate
		 */
		getGoogleUnserviceableAddressError() {
			return {
				reason: 'SHIPPING_ADDRESS_UNSERVICEABLE',
				message: 'Cannot ship to the selected address',
				intent: 'SHIPPING_ADDRESS'
			};
		}

		/**
		 * Add a Google Pay purchase button alongside an existing checkout button
		 *
		 * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#ButtonOptions|Button options}
		 * @see {@link https://developers.google.com/pay/api/web/guides/brand-guidelines|Google Pay brand guidelines}
		 */
		addGooglePayButton() {

			const paymentsClient = this.getGooglePaymentsClient();
			const button = paymentsClient.createButton( {
				onClick: ( event ) => this.onGooglePaymentButtonClicked( event ),
				buttonColor: this.buttonStyle,
				buttonSizeMode: 'fill'
			} );
			document.getElementById( 'sv-wc-google-pay-button-container' ).appendChild( button );
		}

		/**
		 * Prefetch payment data to improve performance
		 *
		 * @see {@link https://developers.google.com/pay/api/web/reference/client#prefetchPaymentData|prefetchPaymentData()}
		 */
		prefetchGooglePaymentData() {

			this.getGooglePaymentDataRequest( ( paymentDataRequest ) => {

				// transactionInfo must be set but does not affect cache
				paymentDataRequest.transactionInfo = {
					totalPriceStatus: 'NOT_CURRENTLY_KNOWN',
					currencyCode: this.currencyCode
				};
				const paymentsClient = this.getGooglePaymentsClient();
				paymentsClient.prefetchPaymentData( paymentDataRequest );
			} );
		}

		/**
		 * Process payment data returned by the Google Pay API
		 *
		 * @see {@link https://developers.google.com/pay/api/web/reference/response-objects#PaymentData|PaymentData object reference}
		 *
		 * @param {object} paymentData response from Google Pay API after user approves payment
		 * @param {function} resolve callback
		 */
		processPayment( paymentData, resolve ) {

			// pass payment token to your gateway to process payment
			const data = {
				action: `wc_${this.gatewayID}_google_pay_process_payment`,
				nonce: this.processNonce,
				paymentData: JSON.stringify( paymentData ),
			}

			if ( this.productID && ! this.needsShipping ) {
				data.productID = this.productID;
			}

			return $.post( this.ajaxURL, data, ( response ) => {
				if ( response.success ) {
					resolve( {
						transactionState: 'SUCCESS'
					} );
					window.location = response.data.redirect;
				} else {
					resolve( {
						transactionState: 'ERROR',
						error: {
							intent: 'SHIPPING_ADDRESS',
							message: 'Invalid data',
							reason: 'PAYMENT_DATA_INVALID'
						}
					} );
					this.failPayment( 'Payment could not be processed. ' + response.data.message );
				}
			} );
		}

		/**
		 * Show Google Pay payment sheet when Google Pay payment button is clicked
		 */
		onGooglePaymentButtonClicked( event ) {

			event.preventDefault();

			this.blockUI();

			this.getGooglePaymentDataRequest( ( paymentDataRequest ) => {

				const paymentsClient = this.getGooglePaymentsClient();
				try {
					paymentsClient.loadPaymentData( paymentDataRequest );
				} catch ( err ) {
					this.failPayment( 'Could not load payment data. ' + err );
				}

				this.unblockUI();
			} );
		}

		/**
		 * Initialize Google PaymentsClient after Google-hosted JavaScript has loaded
		 *
		 * Display a Google Pay payment button after confirmation of the viewer's
		 * ability to pay.
		 */
		init() {

			// initialize for the various pages
			if ( $( 'form.cart' ).length ) {
				this.initProductPage();
			} else if ( $( 'form.woocommerce-cart-form' ).length ) {
				this.initCartPage();
			} else if ( $( 'form.woocommerce-checkout' ).length) {
				this.initCheckoutPage()
			} else {
				return;
			}

			this.initGooglePay();
		}

		/**
		 * Initializes Google Pay.
		 */
		initGooglePay() {

			const paymentsClient = this.getGooglePaymentsClient();
			paymentsClient.isReadyToPay( this.getGoogleIsReadyToPayRequest() )
				.then( ( response ) => {
					if ( response.result ) {
						this.addGooglePayButton();
						// prefetch payment data to improve performance
						this.prefetchGooglePaymentData();
					}
				} )
				.catch( ( err ) => {
					this.failPayment( 'Google Pay is not ready. ' + err );
				} );
		}

		/**
		 * Initializes the product page.
		 */
		initProductPage() {
			this.uiElement = $( 'form.cart' );
		}

		/**
		 * Initializes the cart page.
		 */
		initCartPage() {
			this.uiElement = $( 'form.woocommerce-cart-form' ).parents( 'div.woocommerce' );

			// re-init if the cart totals are updated
			$( document.body ).on( 'updated_cart_totals', () => {
				this.initGooglePay();
			} );
		}

		/**
		 * Initializes the checkout page.
		 */
		initCheckoutPage() {
			this.uiElement = $( 'form.woocommerce-checkout' );
		}

		/**
		 * Fails the purchase based on the gateway result.
		 */
		failPayment( error ) {

			console.error( '[Google Pay] ' + error );

			this.unblockUI();

			this.renderErrors( [ this.genericError ] );
		}

		/**
		 * Renders any new errors and bring them into the viewport.
 		 */
		renderErrors( errors ) {

			// hide and remove any previous errors
			$( '.woocommerce-error, .woocommerce-message' ).remove();

			// add errors
			this.uiElement.prepend( '<ul class="woocommerce-error"><li>' + errors.join( '</li><li>' ) + '</li></ul>' );

			// unblock UI
			this.uiElement.removeClass( 'processing' ).unblock();

			// scroll to top
			$( 'html, body' ).animate( { scrollTop: this.uiElement.offset().top - 100 }, 1000 );
		}

		/**
		 * Blocks the payment form UI.
		 */
		blockUI() {
			this.uiElement.block( { message: null, overlayCSS: { background: '#fff', opacity: 0.6 } } );
		}

		/**
		 * Unblocks the payment form UI.
		 */
		unblockUI() {
			this.uiElement.unblock();
		}
	}

	$( document.body ).trigger( 'sv_wc_google_pay_handler_v5_13_0_loaded' );

} );
