function SasoEventticketsValidator_WC_frontend($, phpObject) {
	const { __, _x, _n, sprintf } = wp.i18n;
	let _self = this;
	let inputTypes = [phpObject.inputType, "text", "value"];

	function init() {
		_addHandlerToTheCodeFields();
		_addHandlerToAddToCartButtons(); // for shop and product pages
	}

	function addStyleCode(content) {
		let c = document.createElement('style');
		c.innerHTML = content;
		document.getElementsByTagName("head")[0].appendChild(c);
	}

	function getPidFromAddToCartButton(btn){
		return btn.data('product_id') || btn.attr('data-product_id') || btn.val() || null;
	}

	function _addHandlerToAddToCartButtons() {
		if (!phpObject.fieldKey) return;
		if (!phpObject.has_daychooser) return; // only if at least one product has a date picker

		function findDateForPid(pid, $btn){
			let input_id = phpObject.fieldKey+'_'+pid;

			return $(document.body)
				.find('input[data-input-type="daychooser"][data-plugin="event"][data-is-shop-page="1"][data-cart-item-id="'+input_id+'"]')
				.first();
		}

		function checkDate(btn, e) {
			var pid = getPidFromAddToCartButton(btn);
			if (!pid) return true; // manche Themes weichen ab; dann greift serverseitige Prüfung

			var input = findDateForPid(pid, btn);
			if (!input || !input.length) return true; // kein Feld gefunden, dann greift serverseitige Prüfung

			var val = (input && input.val ? (input.val()||'').trim() : '');

			if (!val) {
				if (e) {
					e.preventDefault();
					e.stopPropagation();
					e.stopImmediatePropagation();
				}
				// Kurzes Feedback (ersetze gern durch eigenes Notice-System)
				alert(phpObject.daychooser_warning ? phpObject.daychooser_warning : __('Please choose a valid date.', 'event-tickets-with-ticket-scanner'));
				return false;
			}
			return true;
		}

		// product page
		$(document).on('click', '.single_add_to_cart_button', function(e){
			var btn = $(this);

			if (!checkDate(btn, e)) {
				return false;
			}
		});

		// product page with AJAX add to cart
		var form = document.querySelector('form.cart');
		if (form) {
			form.addEventListener('submit', function(e){
				var btn = $(form).find('.single_add_to_cart_button').first();
				if (!btn || btn.length == 0) return; // no button found, then server side check
				if (!checkDate(btn, e)) {
					return false;
				}
				var pid = getPidFromAddToCartButton(btn);
				if (!pid) return; // manche Themes weichen ab; dann greift serverseitige Prüfung
				var $input = findDateForPid(pid, btn);
				if ($input && $input.length > 0) {
					var val    = ($input && $input.val ? ($input.val()||'').trim() : '');
					if (val == "") return false;
					// add hidden input field to the form if not already present
					var hiddenInput = form.querySelector('input[name="'+phpObject.fieldKey+'"]');
					if (!hiddenInput) {
						hiddenInput = document.createElement('input');
						hiddenInput.type = 'hidden';
						hiddenInput.name = phpObject.fieldKey;
						form.appendChild(hiddenInput);
					}
					hiddenInput.value = val ? val : '';
					// indicate that a day chooser was used
					var hiddenInputIndicator = form.querySelector('input[name="'+phpObject.fieldDayChooserIndicator+'"]');
					if (!hiddenInputIndicator) {
						hiddenInputIndicator = document.createElement('input');
						hiddenInputIndicator.type = 'hidden';
						hiddenInputIndicator.name = phpObject.fieldDayChooserIndicator;
						form.appendChild(hiddenInputIndicator);
					}
					hiddenInputIndicator.value = 1;
				}
			});
		}

		// shop/archive page: intercept ANY add-to-cart link with data-product_id
		// Works with both WC Classic (ajax_add_to_cart) and WC Blocks (plain <a> link)
		$(document.body).on('click', 'a[data-product_id]', function(e){
			var btn = $(this);
			var pid = getPidFromAddToCartButton(btn);
			if (!pid) return;

			var $input = findDateForPid(pid, btn);
			if (!$input || !$input.length) return; // not a daychooser product — let WC handle normally

			var val = ($input.val() || '').trim();
			if (!val) {
				e.preventDefault();
				e.stopImmediatePropagation();
				alert(phpObject.daychooser_warning ? phpObject.daychooser_warning : __('Please choose a valid date.', 'event-tickets-with-ticket-scanner'));
				return false;
			}

			// For AJAX buttons (classic WC): let the adding_to_cart handler below add data
			if (btn.hasClass('ajax_add_to_cart')) return;

			// For non-AJAX buttons (WC Blocks / plain links): append date to URL
			e.preventDefault();
			e.stopImmediatePropagation();
			var url = btn.attr('href');
			if (!url) return;
			url += (url.indexOf('?') > -1 ? '&' : '?');
			url += encodeURIComponent(phpObject.fieldKey) + '=' + encodeURIComponent(val);
			url += '&' + encodeURIComponent(phpObject.fieldDayChooserIndicator) + '=1';
			var nonce = document.querySelector('input[name="'+phpObject.nonceKey+'"]');
			if (nonce) url += '&' + encodeURIComponent(phpObject.nonceKey) + '=' + encodeURIComponent(nonce.value);
			window.location.href = url;
		});

		// shop page with AJAX add to cart (classic WC)
		$(document.body).on('adding_to_cart', function(e, $button, data){
			var pid = getPidFromAddToCartButton($button);
			if (!pid) return;

			var $input = findDateForPid(pid, $button);
			if ($input && $input.length > 0) {
				var val    = ($input && $input.val ? ($input.val()||'').trim() : '');
				if (val == "") return false;
				data[phpObject.fieldKey] = val ? val : '';
				data[phpObject.fieldDayChooserIndicator] = 1;
			}

			var nonce = document.querySelector('input[name="'+phpObject.nonceKey+'"]');
			data[phpObject.nonceKey] = nonce ? nonce.value : '';
		});

	}

	function _addHandlerToTheCodeFields() {
		let isStoring = false;
		let waitingTimeout = null;
		let isChanged = false;

		function sendCode(elem, code, type) {
			//clearWaitingTimeout();
			if (!isStoring) {
				$('div[class="woocommerce"]').block({
	 				//message: '...loading...',
	 				message: null,
	 				overlayCSS: {
	 					background: '#fff',
	 					opacity: 0.6
	 				}
	 			});
				isStoring = true;
				let cart_item_id = elem.attr('data-cart-item-id');
				let cart_item_count = elem.attr('data-cart-item-count');
				let nonce = phpObject.nonce;
		 		$.ajax(
		 			{
		 				type: 'POST',
		 				url: phpObject.ajaxurl,
		 				data: {
		 					action: phpObject.action,
		 					a: 'updateSerialCodeToCartItem',
		 					security: nonce,
		 					cart_item_id: cart_item_id,
							cart_item_count: cart_item_count,
							type: type,
		 					code: code
		 				},
		 				success: function( response ) {
		 					$('div[class="woocommerce"]').unblock();
		 					$('.cart_totals').unblock();
		 					if (response.success) {
			 					elem.val(response.code);
		 					} else {
		 						if (response.msg) alert(response.msg);
		 					}
							isStoring = false;
							//window.location.reload();
		 				}
		 			}
		 		)
	 		}
		}

		function clearWaitingTimeout() {
			clearTimeout(waitingTimeout);
		}
		function setWaitingTimeout(elem, code) {
			clearWaitingTimeout();
			waitingTimeout = setTimeout(()=>{
				if (isChanged) {
					isChanged = false;
					sendCode(elem, code);
				}
			}, 2500);
		}

		// finde die code text inputs
		// eventcoderrestriction is no longer used, but still in the code
		$('body').find('input[data-input-type="eventcoderestriction"][data-plugin="event"]')
			.on('keydown',function(e){
				if (e.which === 13) {
					e.preventDefault();
					let elem = $(this);
					isChanged = true;
					sendCode(elem, elem.val().trim(), "saso_eventtickets_request_name_per_ticket");
				}
			})
			.on('paste', event=>{
				isStoring = false;
				let elem = $(event.srcElement);
				let code = (event.clipboardData || window.clipboardData).getData('text');
				if (typeof code == "string") {
					code = code.trim();
					isChanged = true;
					sendCode(elem, code, "saso_eventtickets_request_name_per_ticket");
				} else { alert("no text"); }
			})
			.on('change',function(){
				let elem = $(this);
				let code = elem.val().trim();
				//let cart_item_id = elem.data('cart-item-id');
				//let d = document.querySelector('input[data-cart-item-id="'+cart_item_id+'"]').value
				isChanged = true;
				sendCode(elem, code, "saso_eventtickets_request_name_per_ticket");
			})
			/*
			.on('blur',function(){
				let elem = $(this);
				let code = elem.val().trim();
				if (code != "" && isChanged) {
					let cart_item_id = elem.data('cart-item-id');
					//let d = document.querySelector('input[data-cart-item-id="'+cart_item_id+'"]').value
					sendCode(elem, code, "saso_eventtickets_request_name_per_ticket");
				}
			})
			*/
			.removeAttr('disabled');

		$('body').find('input[data-input-type="text"][data-plugin="event"]')
			.on('keydown',function(e){
				if (e.which === 13) {
					e.preventDefault();
					let elem = $(this);
					isChanged = true;
					sendCode(elem, elem.val().trim(), "saso_eventtickets_request_name_per_ticket");
				}
			})
			.on('paste', event=>{
				isStoring = false;
				let elem = $(event.srcElement);
				let code = (event.clipboardData || window.clipboardData).getData('text');
				if (typeof code == "string") {
					code = code.trim();
					isChanged = true;
					sendCode(elem, code, "saso_eventtickets_request_name_per_ticket");
				} else { alert("no text"); }
			})
			.on('change',function(){
				let elem = $(this);
				let code = elem.val().trim();
				isChanged = true;
				sendCode(elem, code, "saso_eventtickets_request_name_per_ticket");
			})
			.removeAttr('disabled');

		$('body').find('select[data-input-type="value"][data-plugin="event"]')
			.on('change',function(){
				let elem = $(this);
				let code = elem.val().trim();
				isChanged = true;
				sendCode(elem, code, "saso_eventtickets_request_value_per_ticket");
			})
			.removeAttr('disabled');

		// Resolve a min/max value (number-of-days, YYYY-MM-DD, or empty) → JS Date or null.
		function _sasoResolveLimitDate(opt) {
			if (opt == null || opt === '') return null;
			if (typeof opt === 'number' && !isNaN(opt)) {
				let d = new Date(); d.setHours(0, 0, 0, 0); d.setDate(d.getDate() + opt);
				return d;
			}
			let s = String(opt).trim();
			let m = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
			if (m) return new Date(parseInt(m[1], 10), parseInt(m[2], 10) - 1, parseInt(m[3], 10));
			let n = parseInt(s, 10);
			if (!isNaN(n)) {
				let d = new Date(); d.setHours(0, 0, 0, 0); d.setDate(d.getDate() + n);
				return d;
			}
			return null;
		}

		// Selectability rules — kept in sync with beforeShowDay below intentionally.
		function _sasoIsDateSelectable(date, attrs) {
			if (attrs.excludeWdays) {
				let excludedDays = attrs.excludeWdays.split(',');
				if (excludedDays.indexOf(date.getDay().toString()) !== -1) return false;
			}
			let today = new Date(); today.setHours(0, 0, 0, 0);
			let chk = new Date(date.getFullYear(), date.getMonth(), date.getDate());
			if (chk < today) return false;
			if (attrs.excludeDates) {
				let excludedDates = attrs.excludeDates.split(',');
				let m = date.getMonth() + 1;
				let d = date.getDate();
				let dateStr = date.getFullYear() + '-' + (m < 10 ? '0' : '') + m + '-' + (d < 10 ? '0' : '') + d;
				if (excludedDates.indexOf(dateStr) !== -1) return false;
			}
			return true;
		}

		// Scan up to 24 months forward from `from` for the first selectable day.
		// Used as datepicker.defaultDate so the picker opens on the first month
		// that actually has free slots — instead of an all-grey current month.
		function _sasoFindFirstSelectableDate(attrs, from, until) {
			let cap = 24;
			let start = from ? new Date(from.getFullYear(), from.getMonth(), from.getDate())
				: (function () { let t = new Date(); t.setHours(0, 0, 0, 0); return t; })();
			let scanFrom = new Date(start);
			for (let i = 0; i < cap; i++) {
				let monthEnd = new Date(scanFrom.getFullYear(), scanFrom.getMonth() + 1, 0);
				let lastDay = (until && monthEnd > until) ? until : monthEnd;
				for (let d = new Date(scanFrom); d <= lastDay; d.setDate(d.getDate() + 1)) {
					if (_sasoIsDateSelectable(d, attrs)) return new Date(d);
				}
				scanFrom = new Date(scanFrom.getFullYear(), scanFrom.getMonth() + 1, 1);
				if (until && scanFrom > until) return null;
			}
			return null;
		}

		$('body').find('input[data-input-type="daychooser"][data-plugin="event"]')
			.on('keydown', function(e) { e.preventDefault(); })
			.on('paste', function(e) { e.preventDefault(); })
			.each((idx, input) => {
				let elem_intern = $(input);
				let dateFormat = elem_intern.attr('placeholder');
				dateFormat = dateFormat != null ? dateFormat.trim() : '';
				dateFormat = dateFormat ? dateFormat : 'YYYY-MM-DD';
				dateFormat = 'YYYY-MM-DD';
				elem_intern.attr('placeholder', __(dateFormat));
				let data_offset_start = 0;
				let data_offset_end = 0;
				try {
					data_offset_start =	parseInt(elem_intern.attr('data-offset-start'));
				} catch (error) {
					//console.log(error);
				}
				if (elem_intern.attr('min') && elem_intern.attr('min').length > 0) {
					data_offset_start = elem_intern.attr('min');
				}
				// Same-day cutoff: runs after min-attribute override so absolute start dates
				// (ticket_start_date) cannot silently undo the cutoff effect.
				// If current time >= cutoff and the resolved minDate still includes today, shift to tomorrow.
				let _saso_cutoff = elem_intern.attr('data-cutoff-time');
				if (_saso_cutoff) {
					let _cp = _saso_cutoff.split(':');
					let _cutoffDate = new Date();
					_cutoffDate.setHours(parseInt(_cp[0], 10), parseInt(_cp[1], 10), 0, 0);
					if (new Date() >= _cutoffDate) {
						let _todayMidnight = new Date(); _todayMidnight.setHours(0, 0, 0, 0);
						let _resolvedMin = _sasoResolveLimitDate(data_offset_start);
						if (_resolvedMin === null || _resolvedMin <= _todayMidnight) {
							data_offset_start = 1;
						}
					}
				}
				try {
					data_offset_end = parseInt(elem_intern.attr('data-offset-end'));
				} catch (error) {
					//console.log(error);
				}
				if (elem_intern.attr('max') && elem_intern.attr('max').length > 0) {
					data_offset_end = elem_intern.attr('max');
				}
				//let today = new Date();
				//let start = new Date(today.getFullYear(), today.getMonth(), today.getDate() + data_offset_start);
				//let end = new Date(today.getFullYear(), today.getMonth(), today.getDate() + data_offset_end);

				let _saso_attrs = {
					excludeWdays: elem_intern.attr('data-exclude-wdays') || '',
					excludeDates: elem_intern.attr('data-exclude-dates') || ''
				};
				let _saso_minAbs = _sasoResolveLimitDate(data_offset_start);
				let _saso_maxAbs = _sasoResolveLimitDate(data_offset_end);
				let _saso_default = _sasoFindFirstSelectableDate(_saso_attrs, _saso_minAbs, _saso_maxAbs);

				elem_intern.datepicker({
					dateFormat: 'yy-mm-dd',
					//dateFormat: dateFormat,
					showWeek: true,
					firstDay: 1,
					hideIfNoPrevNext : true,
					minDate: data_offset_start,
					maxDate: data_offset_end,
					defaultDate: _saso_default,
					beforeShow: function(input, options) {
						this._sasoevent_input_field = $(input);
					},
					beforeShowDay: function(date) { // https://api.jqueryui.com/datepicker/#option-beforeShow
						let day = date.getDay();
						let data_exclude_wdays = this._sasoevent_input_field.attr('data-exclude-wdays');
						let selectable = true;
						let cssClass = '';
						let toolTipp = '';
						if (data_exclude_wdays && data_exclude_wdays.length > 0) {
							let excludedDays = data_exclude_wdays.split(',');
							selectable = excludedDays.indexOf(day.toString()) == -1;

							cssClass = selectable ? '' : 'ui-datepicker-unselectable';
							toolTipp = selectable ? '' : __('This day is not selectable');
						}
						if (selectable) {
							// check if the date is in the past
							let today = new Date();
							let y = date.getFullYear();
							let m = date.getMonth() + 1;
							let d = date.getDate();
							let dateStr = y + '-' + (m < 10 ? '0' : '') + m + '-' + (d < 10 ? '0' : '') + d;
							let todayStr = today.getFullYear() + '-' + (today.getMonth() + 1 < 10 ? '0' : '') + (today.getMonth() + 1) + '-' + (today.getDate() < 10 ? '0' : '') + today.getDate();
							if (dateStr < todayStr) {
								selectable = false;
								cssClass = 'ui-datepicker-unselectable';
								toolTipp = __('This day is not selectable');
							}
						}
						if (selectable) {
							let data_exclude_dates = this._sasoevent_input_field.attr('data-exclude-dates');
							if (data_exclude_dates && data_exclude_dates.length > 0) {
								let excludedDates = data_exclude_dates.split(',');
								let y = date.getFullYear();
								let m = date.getMonth() + 1;
								let d = date.getDate();
								let dateStr = y + '-' + (m < 10 ? '0' : '') + m + '-' + (d < 10 ? '0' : '') + d;
								selectable = excludedDates.indexOf(dateStr) == -1;

								cssClass = selectable ? '' : 'ui-datepicker-unselectable';
								toolTipp = selectable ? '' : __('This day is not selectable');
							}
						}
						return [selectable, cssClass, toolTipp];
						//return [true, ''];
					}
				});
			})
			.on('change',event=>{
				let elem_intern = $(event.target);

				if (elem_intern.attr('data-is-shop-page') == "1") return; // only on cart and checkout page

				//console.log('change datepicker', elem_intern.attr('id'));
				let date_value = elem_intern.val().trim();
				if (elem_intern.attr('data-previous-value') == date_value) return; // no change
				elem_intern.attr('data-previous-value', date_value);
				if (date_value) {
					sendCode(elem_intern, date_value, "saso_eventtickets_request_daychooser");
					isChanged = true;
					let to_be_changed = [];
					// update the other date pickers if no value is set to use this date
					let data_cart_item_id = elem_intern.attr('data-cart-item-id');
					$('body').find('input[data-input-type="daychooser"][data-plugin="event"][id^="saso_eventtickets_request_daychooser['+data_cart_item_id+']"]').each((idx, input_to_update) => {
						//console.log('update datepicker', input_to_update);
						let input_elem = $(input_to_update);
						let v = input_elem.val().trim();
						if (!v) {
							//console.log(input_elem.attr("id")+' set value to: '+date_value);
							input_elem.val(date_value); // is not working somehow, so skip this step for now. The value is shown and send to the server, but on the checkout the other fields are empty
							to_be_changed.push(input_elem);
						}
					});

					// remove the related error message on the cart
					//let data_cart_item_count = elem.attr('data-cart-item-count');
					//$('li[data-cart-item-id="'+data_cart_item_id+'"][data-cart-item-count="'+data_cart_item_count+'"]').remove();
					// send data to the server

					let counter = 0;
					let wait = 0;
					to_be_changed.forEach(input => {
						//console.log(input.attr("id")+'send code: '+date_value);
						//console.log(input);
						window.setTimeout(()=>{
							sendCode(input, date_value, "saso_eventtickets_request_daychooser");
						}, wait);
						counter++;
						if (wait == 0) wait = 250;
						if (counter == to_be_changed.length) {
							$(document.body).trigger('update_checkout');
						}
					});
				} // end date_value
			})
			.each(function() {
				// Only remove disabled if not in a locked container (seats selected)
				if (!$(this).closest('.saso-datepicker-locked').length) {
					$(this).removeAttr('disabled');
				}
			});

		//addStyleCode('#ui-datepicker-div > table {background-color: white;}');
	}

	init();

	return {
		_addHandlerToTheCodeFields: _addHandlerToTheCodeFields,
	};
}

(function($){
 	$(document).ready(function(){
 		window.SasoEventticketsValidator_WC_frontend = SasoEventticketsValidator_WC_frontend($, SasoEventticketsValidator_phpObject);
 	});
})(jQuery);