import ConsoleLogger from '../../../../../ppcp-wc-gateway/resources/js/helper/ConsoleLogger';
import { apmButtonsInit } from '../Helper/ApmButtons';
import {
	getCurrentPaymentMethod,
	PaymentContext,
	PaymentMethods,
} from '../Helper/CheckoutMethodState';
import {
	ButtonEvents,
	dispatchButtonEvent,
	observeButtonEvent,
} from '../Helper/PaymentButtonHelpers';
import { isVisible } from '../Helper/Hiding';
import { isDisabled, setEnabled } from '../Helper/ButtonDisabler';

/**
 * Collection of all available styling options for this button.
 *
 * @typedef {Object} StylesCollection
 * @property {string} Default  - Default button styling.
 * @property {string} MiniCart - Styles for mini-cart button.
 */

/**
 * Collection of all available wrapper IDs that are possible for the button.
 *
 * @typedef {Object} WrapperCollection
 * @property {string} Default     - Default button wrapper.
 * @property {string} Gateway     - Wrapper for separate gateway.
 * @property {string} Block       - Wrapper for block checkout button.
 * @property {string} MiniCart    - Wrapper for mini-cart button.
 * @property {string} SmartButton - Wrapper for smart button container.
 */

/**
 * Adds the provided PaymentButton instance to a global payment-button collection.
 *
 * This is debugging logic that should not be used on a production site.
 *
 * @param {string}        methodName - Used to group the buttons.
 * @param {PaymentButton} button     - Appended to the button collection.
 */
const addToDebuggingCollection = ( methodName, button ) => {
	window.ppcpPaymentButtonList = window.ppcpPaymentButtonList || {};

	const collection = window.ppcpPaymentButtonList;

	collection[ methodName ] = collection[ methodName ] || [];
	collection[ methodName ].push( button );
};

/**
 * Provides a context-independent instance Map for `PaymentButton` components.
 *
 * This function addresses a potential issue in multi-context environments, such as pages using
 * Block-components. In these scenarios, multiple React execution contexts can lead to duplicate
 * `PaymentButton` instances. To prevent this, we store instances in a `Map` that is bound to the
 * document's `body` (the rendering context) rather than to individual React components
 * (execution contexts).
 *
 * The `Map` is created as a non-enumerable, non-writable, and non-configurable property of
 * `document.body` to ensure its integrity and prevent accidental modifications.
 *
 * @return {Map<any, any>} A Map containing all `PaymentButton` instances for the current page.
 */
const getInstances = () => {
	const collectionKey = '__ppcpPBInstances';

	if ( ! document.body[ collectionKey ] ) {
		Object.defineProperty( document.body, collectionKey, {
			value: new Map(),
			enumerable: false,
			writable: false,
			configurable: false,
		} );
	}

	return document.body[ collectionKey ];
};

/**
 * Base class for APM payment buttons, like GooglePay and ApplePay.
 *
 * This class is not intended for the PayPal button.
 */
export default class PaymentButton {
	/**
	 * Defines the implemented payment method.
	 *
	 * Used to identify and address the button internally.
	 * Overwrite this in the derived class.
	 *
	 * @type {string}
	 */
	static methodId = 'generic';

	/**
	 * CSS class that is added to the payment button wrapper.
	 *
	 * Overwrite this in the derived class.
	 *
	 * @type {string}
	 */
	static cssClass = '';

	/**
	 * @type {ConsoleLogger}
	 */
	#logger;

	/**
	 * Whether the payment button is initialized.
	 *
	 * @type {boolean}
	 */
	#isInitialized = false;

	/**
	 * Whether the one-time initialization of the payment gateway is complete.
	 *
	 * @type {boolean}
	 */
	#gatewayInitialized = false;

	/**
	 * The button's context.
	 *
	 * @type {string}
	 */
	#context;

	/**
	 * Object containing the IDs of all possible wrapper elements that might contain this
	 * button; only one wrapper is relevant, depending on the value of the context.
	 *
	 * @type {Object}
	 */
	#wrappers;

	/**
	 * @type {StylesCollection}
	 */
	#styles;

	/**
	 * Keeps track of CSS classes that were added to the wrapper element.
	 * We use this list to remove CSS classes that we've added, e.g. to change shape from
	 * pill to rect in the preview.
	 *
	 * @type {string[]}
	 */
	#appliedClasses = [];

	/**
	 * APM relevant configuration; e.g., configuration of the GooglePay button.
	 */
	#buttonConfig;

	/**
	 * Plugin-wide configuration; i.e., PayPal client ID, shop currency, etc.
	 */
	#ppcpConfig;

	/**
	 * A variation of a context bootstrap handler.
	 */
	#externalHandler;

	/**
	 * A variation of a context handler object, like CheckoutHandler.
	 * This handler provides a standardized interface for certain standardized checks and actions.
	 */
	#contextHandler;

	/**
	 * Button attributes.
	 */
	#buttonAttributes;

	/**
	 * Whether the current browser/website support the payment method.
	 *
	 * @type {?boolean}
	 */
	#isEligible = null;

	/**
	 * Whether this button is visible. Modified by `show()` and `hide()`
	 *
	 * @type {boolean}
	 */
	#isVisible = true;

	/**
	 * Whether this button is enabled (can be clicked).
	 *
	 * @type {boolean}
	 */
	#isEnabled = true;

	/**
	 * The currently visible payment button.
	 *
	 * @see {PaymentButton.insertButton}
	 * @type {HTMLElement|null}
	 */
	#button = null;

	/**
	 * List of checks to perform to verify the PaymentButton has is configured correctly.
	 *
	 * @type {{check, errorMessage, shouldPass}[]}
	 */
	#validationChecks = [];

	/**
	 * Factory method to create a new PaymentButton while limiting a single instance per context.
	 *
	 * @param {string}  context          - Button context name.
	 * @param {unknown} externalHandler  - Handler object.
	 * @param {Object}  buttonConfig     - Payment button specific configuration.
	 * @param {Object}  ppcpConfig       - Plugin wide configuration object.
	 * @param {unknown} contextHandler   - Handler object.
	 * @param {Object}  buttonAttributes - Button attributes.
	 * @return {PaymentButton} The button instance.
	 */
	static createButton(
		context,
		externalHandler,
		buttonConfig,
		ppcpConfig,
		contextHandler,
		buttonAttributes
	) {
		const buttonInstances = getInstances();
		const instanceKey = `${ this.methodId }.${ context }`;

		if ( ! buttonInstances.has( instanceKey ) ) {
			const button = new this(
				context,
				externalHandler,
				buttonConfig,
				ppcpConfig,
				contextHandler,
				buttonAttributes
			);

			buttonInstances.set( instanceKey, button );
		}

		return buttonInstances.get( instanceKey );
	}

	/**
	 * Returns a list with all wrapper IDs for the implemented payment method, categorized by
	 * context.
	 *
	 * @abstract
	 * @param {Object} buttonConfig - Payment method specific configuration.
	 * @param {Object} ppcpConfig   - Global plugin configuration.
	 * @return {{MiniCart, Gateway, Block, SmartButton, Default}} The wrapper ID collection.
	 */
	// eslint-disable-next-line no-unused-vars
	static getWrappers( buttonConfig, ppcpConfig ) {
		throw new Error( 'Must be implemented in the child class' );
	}

	/**
	 * Returns a list of all button styles for the implemented payment method, categorized by
	 * context.
	 *
	 * @abstract
	 * @param {Object} buttonConfig - Payment method specific configuration.
	 * @param {Object} ppcpConfig   - Global plugin configuration.
	 * @return {{MiniCart: (*), Default: (*)}} Combined styles, separated by context.
	 */
	// eslint-disable-next-line no-unused-vars
	static getStyles( buttonConfig, ppcpConfig ) {
		throw new Error( 'Must be implemented in the child class' );
	}

	/**
	 * Initialize the payment button instance.
	 *
	 * Do not create new button instances directly; use the `createButton` method instead
	 * to avoid multiple button instances handling the same context.
	 *
	 * @private
	 * @param {string} context          - Button context name.
	 * @param {Object} externalHandler  - Handler object.
	 * @param {Object} buttonConfig     - Payment button specific configuration.
	 * @param {Object} ppcpConfig       - Plugin wide configuration object.
	 * @param {Object} contextHandler   - Handler object.
	 * @param {Object} buttonAttributes - Button attributes.
	 */
	constructor(
		context,
		externalHandler = null,
		buttonConfig = {},
		ppcpConfig = {},
		contextHandler = null,
		buttonAttributes = {}
	) {
		if ( this.methodId === PaymentButton.methodId ) {
			throw new Error( 'Cannot initialize the PaymentButton base class' );
		}

		if ( ! buttonConfig ) {
			buttonConfig = {};
		}

		const isDebugging = !! buttonConfig?.is_debug;
		const methodName = this.methodId.replace( /^ppcp?-/, '' );

		this.#context = context;
		this.#buttonConfig = buttonConfig;
		this.#ppcpConfig = ppcpConfig;
		this.#externalHandler = externalHandler;
		this.#contextHandler = contextHandler;
		this.#buttonAttributes = buttonAttributes;

		this.#logger = new ConsoleLogger( methodName, context );

		if ( isDebugging ) {
			this.#logger.enabled = true;
			addToDebuggingCollection( methodName, this );
		}

		this.#wrappers = this.constructor.getWrappers(
			this.#buttonConfig,
			this.#ppcpConfig
		);
		this.applyButtonStyles( this.#buttonConfig );

		this.registerValidationRules(
			this.#assertIsInvalid.bind( this ),
			this.#assertIsValid.bind( this )
		);

		apmButtonsInit( this.#ppcpConfig );
		this.initEventListeners();
	}

	/**
	 * Internal ID of the payment gateway.
	 *
	 * @readonly
	 * @return {string} The internal gateway ID, defined in the derived class.
	 */
	get methodId() {
		return this.constructor.methodId;
	}

	/**
	 * CSS class that is added to the button wrapper.
	 *
	 * @readonly
	 * @return {string} CSS class, defined in the derived class.
	 */
	get cssClass() {
		return this.constructor.cssClass;
	}

	/**
	 * Whether the payment button was fully initialized.
	 *
	 * @readonly
	 * @return {boolean} True indicates, that the button was fully initialized.
	 */
	get isInitialized() {
		return this.#isInitialized;
	}

	/**
	 * The button's context.
	 *
	 * TODO: Convert the string to a context-object (primitive obsession smell)
	 *
	 * @readonly
	 * @return {string} The button context.
	 */
	get context() {
		return this.#context;
	}

	/**
	 * Configuration, specific for the implemented payment button.
	 *
	 * @return {Object} Configuration object.
	 */
	get buttonConfig() {
		return this.#buttonConfig;
	}

	/**
	 * Plugin-wide configuration; i.e., PayPal client ID, shop currency, etc.
	 *
	 * @return {Object} Configuration object.
	 */
	get ppcpConfig() {
		return this.#ppcpConfig;
	}

	/**
	 * @return {Object} The bootstrap handler instance, or an empty object.
	 */
	get externalHandler() {
		return this.#externalHandler || {};
	}

	/**
	 * Access the button's context handler.
	 * When no context handler was provided (like for a preview button), an empty object is
	 * returned.
	 *
	 * @return {Object} The context handler instance, or an empty object.
	 */
	get contextHandler() {
		return this.#contextHandler || {};
	}

	/**
	 * Whether customers need to provide shipping details during payment.
	 *
	 * Can be extended by child classes to take method specific configuration into account.
	 *
	 * @return {boolean} True means, shipping fields are displayed and must be filled.
	 */
	get requiresShipping() {
		// Default check: Is shipping enabled in WooCommerce?
		return (
			'function' === typeof this.contextHandler.shippingAllowed &&
			this.contextHandler.shippingAllowed()
		);
	}

	/**
	 * Button wrapper details.
	 *
	 * @readonly
	 * @return {WrapperCollection} Wrapper IDs.
	 */
	get wrappers() {
		return this.#wrappers;
	}

	/**
	 * Returns the context-relevant button style object.
	 *
	 * @readonly
	 * @return {string} Styling options.
	 */
	get style() {
		if ( PaymentContext.MiniCart === this.context ) {
			return this.#styles.MiniCart;
		}

		return this.#styles.Default;
	}

	/**
	 * Returns the context-relevant wrapper ID.
	 *
	 * @readonly
	 * @return {string} The wrapper-element's ID (without the `#` prefix).
	 */
	get wrapperId() {
		if ( PaymentContext.MiniCart === this.context ) {
			return this.wrappers.MiniCart;
		} else if ( this.isSeparateGateway ) {
			return this.wrappers.Gateway;
		} else if ( PaymentContext.Blocks.includes( this.context ) ) {
			return this.wrappers.Block;
		}

		return this.wrappers.Default;
	}

	/**
	 * Whether the button is placed inside a classic gateway context.
	 *
	 * Classic gateway contexts are: Classic checkout, Pay for Order page.
	 *
	 * @return {boolean} True indicates, the button is located inside a classic gateway.
	 */
	get isInsideClassicGateway() {
		return PaymentContext.Gateways.includes( this.context );
	}

	/**
	 * Determines if the current payment button should be rendered as a stand-alone gateway.
	 * The return value `false` usually means, that the payment button is bundled with all available
	 * payment buttons.
	 *
	 * The decision depends on the button context (placement) and the plugin settings.
	 *
	 * @return {boolean} True, if the current button represents a stand-alone gateway.
	 */
	get isSeparateGateway() {
		return (
			this.#buttonConfig.is_wc_gateway_enabled &&
			this.isInsideClassicGateway
		);
	}

	/**
	 * Whether the currently selected payment gateway is set to the payment method.
	 *
	 * Only relevant on checkout pages where "classic" payment gateways are rendered.
	 *
	 * @return {boolean} True means that this payment method is selected as current gateway.
	 */
	get isCurrentGateway() {
		if ( ! this.isInsideClassicGateway ) {
			// This means, the button's visibility is managed by another script.
			return true;
		}

		/*
		 * We need to rely on `getCurrentPaymentMethod()` here, as the `CheckoutBootstrap.js`
		 * module fires the "ButtonEvents.RENDER" event before any PaymentButton instances are
		 * created. I.e. we cannot observe the initial gateway selection event.
		 */
		const currentMethod = getCurrentPaymentMethod();

		if ( this.isSeparateGateway ) {
			return this.methodId === currentMethod;
		}

		// Button is rendered inside the Smart Buttons block.
		return PaymentMethods.PAYPAL === currentMethod;
	}

	/**
	 * Flags a preview button without actual payment logic.
	 *
	 * @return {boolean} True indicates a preview instance that has no payment logic.
	 */
	get isPreview() {
		return PaymentContext.Preview === this.context;
	}

	/**
	 * Whether the browser can accept this payment method.
	 *
	 * @return {boolean} True, if payments are technically possible.
	 */
	get isEligible() {
		return this.#isEligible;
	}

	/**
	 * Changes the eligibility state of this button component.
	 *
	 * @param {boolean} newState Whether the browser can accept payments.
	 */
	set isEligible( newState ) {
		if ( newState === this.#isEligible ) {
			return;
		}

		this.#isEligible = newState;
		this.triggerRedraw();
	}

	/**
	 * The visibility state of the button.
	 * This flag does not reflect actual visibility on the page, but rather, if the button
	 * is intended/allowed to be displayed, in case all other checks pass.
	 *
	 * @return {boolean} True indicates, that the button can be displayed.
	 */
	get isVisible() {
		return this.#isVisible;
	}

	/**
	 * Change the visibility of the button.
	 *
	 * A visible button does not always force the button to render on the page. It only means, that
	 * the button is allowed or not allowed to render, if certain other conditions are met.
	 *
	 * @param {boolean} newState Whether rendering the button is allowed.
	 */
	set isVisible( newState ) {
		if ( this.#isVisible === newState ) {
			return;
		}

		this.#isVisible = newState;
		this.triggerRedraw();
	}

	/**
	 * The enabled/disabled state of the button (whether it can be clicked).
	 *
	 * @return {boolean} True indicates, that the button is enabled.
	 */
	get isEnabled() {
		return this.#isEnabled;
	}

	/**
	 * Change the enabled/disabled state of the button.
	 *
	 * @param {boolean} newState Whether the button is enabled.
	 */
	set isEnabled( newState ) {
		if ( this.#isEnabled === newState ) {
			return;
		}

		this.#isEnabled = newState;
		this.triggerRedraw();
	}

	/**
	 * Returns the HTML element that wraps the current button
	 *
	 * @readonly
	 * @return {HTMLElement|null} The wrapper element, or null.
	 */
	get wrapperElement() {
		return document.getElementById( this.wrapperId );
	}

	/**
	 * Returns the standard PayPal smart button selector for the current context.
	 *
	 * @return {string | null} The selector, or null if not available.
	 */
	get ppcpButtonWrapperSelector() {
		if ( PaymentContext.Blocks.includes( this.context ) ) {
			return null;
		}

		if ( this.context === PaymentContext.MiniCart ) {
			return this.ppcpConfig?.button?.mini_cart_wrapper;
		}

		return this.ppcpConfig?.button?.wrapper;
	}

	/**
	 * Checks whether the main button-wrapper is present in the current DOM.
	 *
	 * @readonly
	 * @return {boolean} True, if the button context (wrapper element) is found.
	 */
	get isPresent() {
		return this.wrapperElement instanceof HTMLElement;
	}

	/**
	 * Checks, if the payment button is still attached to the DOM.
	 *
	 * WooCommerce performs some partial reloads in many cases, which can lead to our payment
	 * button
	 * to move into the browser's memory. In that case, we need to recreate the button in the
	 * updated DOM.
	 *
	 * @return {boolean} True means, the button is still present (and typically visible) on the
	 *     page.
	 */
	get isButtonAttached() {
		if ( ! this.#button ) {
			return false;
		}

		let parent = this.#button.parentElement;
		while ( parent?.parentElement ) {
			if ( 'BODY' === parent.tagName ) {
				return true;
			}

			parent = parent.parentElement;
		}

		return false;
	}

	/**
	 * Log a debug detail to the browser console.
	 *
	 * @param {any} args
	 */
	log( ...args ) {
		this.#logger.log( ...args );
	}

	/**
	 * Log an error message to the browser console.
	 *
	 * @param {any} args
	 */
	error( ...args ) {
		this.#logger.error( ...args );
	}

	/**
	 * Open or close a log-group
	 *
	 * @param {?string} [label=null] Group label.
	 */
	logGroup( label = null ) {
		this.#logger.group( label );
	}

	/**
	 * Register a validation check that marks the configuration as invalid when passed.
	 *
	 * @param {Function} check        - A function that returns a truthy value if the check passes.
	 * @param {string}   errorMessage - The error message to display if the check fails.
	 */
	#assertIsInvalid( check, errorMessage ) {
		this.#validationChecks.push( {
			check,
			errorMessage,
			shouldPass: false,
		} );
	}

	/**
	 * Register a validation check that instantly marks the configuration as valid when passed.
	 *
	 * @param {Function} check - A function that returns a truthy value if the check passes.
	 */
	#assertIsValid( check ) {
		this.#validationChecks.push( { check, shouldPass: true } );
	}

	/**
	 * Defines a series of validation steps to ensure the payment button is configured correctly.
	 *
	 * Each validation step is executed in the order they are defined within this method.
	 *
	 * If a validation step using `invalidIf` returns true, the configuration is immediately considered
	 * invalid, and an error message is logged. Conversely, if a validation step using `validIf`
	 * returns true, the configuration is immediately considered valid.
	 *
	 * If no validation step returns true, the configuration is assumed to be valid by default.
	 *
	 * @param {(condition: () => boolean, errorMessage: string) => void} invalidIf - Registers a validation step that fails if the condition returns true.
	 * @param {(condition: () => boolean) => void}                       validIf   - Registers a validation step that passes if the condition returns true.
	 */
	// eslint-disable-next-line no-unused-vars
	registerValidationRules( invalidIf, validIf ) {}

	/**
	 * Determines if the current button instance has valid and complete configuration details.
	 * Used during initialization to decide if the button can be initialized or should be skipped.
	 *
	 * All required validation steps must be registered in the constructor of the derived class
	 * using `this.addValidationFailure()` or `this.addValidationSuccess()`.
	 *
	 * @param {boolean} [silent=false] - Set to true to suppress console errors.
	 * @return {boolean} True indicates the config is valid and initialization can continue.
	 */
	validateConfiguration( silent = false ) {
		for ( const step of this.#validationChecks ) {
			const result = step.check();

			if ( step.shouldPass && result ) {
				// If a success check passes, mark as valid immediately.
				return true;
			}

			if ( ! step.shouldPass && result ) {
				// If a failure check passes, mark as invalid.
				if ( ! silent && step.errorMessage ) {
					this.error( step.errorMessage );
				}

				return false;
			}
		}

		return true;
	}

	applyButtonStyles( buttonConfig, ppcpConfig = null ) {
		if ( ! ppcpConfig ) {
			ppcpConfig = this.ppcpConfig;
		}

		this.#styles = this.constructor.getStyles( buttonConfig, ppcpConfig );

		if ( this.isInitialized ) {
			this.triggerRedraw();
		}
	}

	/**
	 * Configures the button instance. Must be called before the initial `init()`.
	 *
	 * Parameters are defined by the derived class.
	 *
	 * @abstract
	 */
	configure() {}

	/**
	 * Must be named `init()` to simulate "protected" visibility:
	 * Since the derived class also implements a method with the same name, this method can only
	 * be called by the derived class, but not from any other code.
	 */
	init() {
		this.#isInitialized = true;
	}

	/**
	 * Must be named `reinit()` to simulate "protected" visibility:
	 * Since the derived class also implements a method with the same name, this method can only
	 * be called by the derived class, but not from any other code.
	 */
	reinit() {
		this.#isInitialized = false;
		this.#isEligible = false;
	}

	triggerRedraw() {
		this.showPaymentGateway();

		dispatchButtonEvent( {
			event: ButtonEvents.REDRAW,
			paymentMethod: this.methodId,
		} );
	}

	/**
	 * Applies the visibility and enabled state from the PayPal button.
	 * Intended for the product page, may not work correctly on the checkout page.
	 */
	syncProductButtonsState() {
		const ppcpButton = document.querySelector(
			this.ppcpButtonWrapperSelector
		);
		if ( ! ppcpButton ) {
			return;
		}

		this.isVisible = isVisible( ppcpButton );
		this.isEnabled = ! isDisabled( ppcpButton );
	}

	/**
	 * Attaches event listeners to show/hide or enable/disable the payment button when needed.
	 */
	initEventListeners() {
		// Refresh the button - this might show, hide or re-create the payment button.
		observeButtonEvent( {
			event: ButtonEvents.REDRAW,
			paymentMethod: this.methodId,
			callback: () => this.refresh(),
		} );

		// Events relevant for buttons inside a payment gateway.
		if ( this.isInsideClassicGateway ) {
			const parentMethod = this.isSeparateGateway
				? this.methodId
				: PaymentMethods.PAYPAL;

			// Hide the button right after the user selected _any_ gateway.
			observeButtonEvent( {
				event: ButtonEvents.INVALIDATE,
				callback: () => ( this.isVisible = false ),
			} );

			// Show the button (again) when the user selected the current gateway.
			observeButtonEvent( {
				event: ButtonEvents.RENDER,
				paymentMethod: parentMethod,
				callback: () => ( this.isVisible = true ),
			} );
		}

		// On the product page, copy the visibility and enabled state from the PayPal button.
		if ( this.context === PaymentContext.Product ) {
			jQuery( document ).on(
				'ppcp-shown ppcp-hidden ppcp-enabled ppcp-disabled',
				( ev, data ) => {
					if (
						! jQuery( data.selector ).is(
							this.ppcpButtonWrapperSelector
						)
					) {
						return;
					}

					this.syncProductButtonsState();
				}
			);
		}
	}

	/**
	 * Refreshes the payment button on the page.
	 */
	refresh() {
		if ( ! this.isPresent ) {
			return;
		}
		if ( ! this.isEligible ) {
			this.wrapperElement.style.display = 'none';
			return;
		}

		this.applyWrapperStyles();

		if ( this.isEligible && this.isCurrentGateway && this.isVisible ) {
			if ( ! this.isButtonAttached ) {
				this.log( 'refresh.addButton' );
				this.addButton();
			}
		}
	}

	/**
	 * Makes the payment gateway visible by removing initial inline styles from the DOM.
	 * Also, removes the button-placeholder container from the smart button block.
	 *
	 * Only relevant on the checkout page, i.e., when `this.isSeparateGateway` is `true`
	 */
	showPaymentGateway() {
		if (
			this.#gatewayInitialized ||
			! this.isSeparateGateway ||
			! this.isEligible
		) {
			return;
		}

		const styleSelector = `style[data-hide-gateway="${ this.methodId }"]`;
		const wrapperSelector = `#${ this.wrappers.Default }`;
		const paymentMethodLi = document.querySelector(
			`.wc_payment_method.payment_method_${ this.methodId }`
		);

		document
			.querySelectorAll( styleSelector )
			.forEach( ( el ) => el.remove() );

		if (
			paymentMethodLi.style.display === 'none' ||
			paymentMethodLi.style.display === ''
		) {
			paymentMethodLi.style.display = 'block';
		}

		document
			.querySelectorAll( wrapperSelector )
			.forEach( ( el ) => el.remove() );

		this.log( 'Show gateway' );
		this.#gatewayInitialized = true;

		// This code runs only once, during button initialization, and fixes the initial visibility.
		this.isVisible = this.isCurrentGateway;
	}

	/**
	 * Applies CSS classes and inline styling to the payment button wrapper.
	 */
	applyWrapperStyles() {
		const wrapper = this.wrapperElement;
		const { shape, height } = this.style;

		for ( const classItem of this.#appliedClasses ) {
			wrapper.classList.remove( classItem );
		}

		this.#appliedClasses = [];

		const newClasses = [
			`ppcp-button-${ shape }`,
			'ppcp-button-apm',
			this.cssClass,
		];

		wrapper.classList.add( ...newClasses );
		this.#appliedClasses.push( ...newClasses );

		if ( height ) {
			wrapper.style.height = `${ height }px`;
		}

		// Apply the wrapper visibility.
		wrapper.style.display = this.isVisible ? 'block' : 'none';

		// Apply the enabled/disabled state.
		// On the product page, use the form to display error messages if clicked while disabled.
		const form =
			this.context === PaymentContext.Product ? 'form.cart' : null;
		setEnabled( wrapper, this.isEnabled, form );
	}

	/**
	 * Creates a new payment button (HTMLElement) and must call `this.insertButton()` to display
	 * that button in the correct wrapper.
	 *
	 * @abstract
	 */
	addButton() {
		throw new Error( 'Must be implemented by the child class' );
	}

	/**
	 * Prepares the button wrapper element and inserts the provided payment button into the DOM.
	 *
	 * If a payment button was previously inserted to the wrapper, calling this method again will
	 * first remove the previous button.
	 *
	 * @param {HTMLElement} button - The button element to inject.
	 */
	insertButton( button ) {
		if ( ! this.isPresent ) {
			return;
		}

		const wrapper = this.wrapperElement;

		if ( this.#button ) {
			this.removeButton();
		}

		this.log( 'insertButton', button );

		this.#button = button;
		wrapper.appendChild( this.#button );
	}

	/**
	 * Removes the payment button from the DOM.
	 */
	removeButton() {
		if ( ! this.isPresent || ! this.#button ) {
			return;
		}

		this.log( 'removeButton' );

		try {
			this.wrapperElement.removeChild( this.#button );
		} catch ( Exception ) {
			// Ignore this.
		}

		this.#button = null;
	}
}
