/**
 * GoValid QR Label Layout.
 *
 * @package GoValid_QR
 */
( function( $ ) {
	'use strict';

	// === STATE ===
	var state = {
		qrCodes: [],        // Array of { uuid, name, formatted_id, image_url }
		currentPage: 1,
		totalPages: 1,
		zoom: 1,
		settings: {},
		labelStyles: { bold: false, italic: false, underline: false }
	};

	var PAPER_SIZES = {
		a4:     { w: 210, h: 297 },
		letter: { w: 216, h: 279 },
		a3:     { w: 297, h: 420 },
		legal:  { w: 216, h: 356 }
	};

	// Default settings — used for "Reset to Defaults" and as a reference
	var DEFAULT_SETTINGS = {
		paperSize: 'a4', orientation: 'portrait', customW: 210, customH: 297,
		cols: 3, rows: 4,
		marginTop: 10, marginRight: 10, marginBottom: 10, marginLeft: 10,
		spacingH: 2, spacingV: 2,
		qrSize: 85, qrHPos: 50, qrVPos: 50,
		showLabels: true, labelContent: 'name', labelPos: 'bottom',
		labelHOffset: 0, labelVOffset: 0,
		labelFont: 'Arial, sans-serif', labelSize: 10, labelColor: '#333333',
		labelBold: false, labelItalic: false, labelUnder: false,
		labelBg: false, labelBgColor: '#ffffff',
		frameStyle: 'none', frameColor: '#000000', frameFill: '#ffffff', frameStroke: 2
	};

	// Built-in presets — ship with the plugin so new users have ready templates
	var BUILTIN_PRESETS = [
		{
			name: 'A4 Ticket Blue (5×6)',
			settings: $.extend( {}, DEFAULT_SETTINGS, {
				cols: 5, rows: 6,
				showLabels: true, labelContent: 'id', labelPos: 'bottom',
				labelVOffset: -8,
				labelFont: "'Trebuchet MS', sans-serif", labelSize: 10, labelColor: '#ffffff',
				labelBold: true,
				labelBg: true, labelBgColor: '#1461db',
				frameStyle: 'ticket', frameColor: '#2c33f2', frameFill: '#c2f3ff', frameStroke: 2
			})
		}
	];

	// MM to pixel ratio for preview (3.78 px per mm at 96 DPI)
	var MM_PX = 3.78;

	// === UTILITIES ===
	function escapeHtml( str ) {
		var div = document.createElement( 'div' );
		div.appendChild( document.createTextNode( str ) );
		return div.innerHTML;
	}

	function escapeAttr( str ) {
		return String( str )
			.replace( /&/g, '&amp;' )
			.replace( /</g, '&lt;' )
			.replace( />/g, '&gt;' )
			.replace( /"/g, '&quot;' )
			.replace( /'/g, '&#039;' );
	}

	function debounce( fn, delay ) {
		var timer = null;
		return function() {
			var context = this;
			var args = arguments;
			clearTimeout( timer );
			timer = setTimeout( function() {
				fn.apply( context, args );
			}, delay );
		};
	}

	// === PUBLIC API ===
	// Called by govalid-admin.js when user clicks Label Layout button
	window.govalidOpenLabelLayout = function( selectedQrs ) {
		state.qrCodes = selectedQrs;
		state.currentPage = 1;
		state.zoom = 1;
		openModal();
		renderQrTags();

		// Auto-load last used settings (or defaults on first open)
		var lastUsed = getLastUsed();
		if ( lastUsed ) {
			applySettings( lastUsed );
		}
		renderPresetDropdown();

		readSettings();
		renderPreview();
		toggleExportButtons( state.qrCodes.length > 0 );
	};

	// === MODAL ===
	function openModal() {
		$( '#govalid-label-layout-modal' ).fadeIn( 200 );
		$( 'body' ).addClass( 'govalid-ll-modal-open' ); // overflow hidden via CSS
	}

	function closeModal() {
		saveLastUsed();
		$( '#govalid-label-layout-modal' ).fadeOut( 200 );
		$( 'body' ).removeClass( 'govalid-ll-modal-open' );
	}

	function toggleExportButtons( enabled ) {
		$( '#govalid-ll-export-pdf, #govalid-ll-export-png, #govalid-ll-export-jpg, #govalid-ll-print' )
			.prop( 'disabled', !enabled );
	}

	// === QR TAGS ===
	function renderQrTags() {
		var $container = $( '#govalid-ll-qr-tags' ).empty();
		state.qrCodes.forEach( function( qr, i ) {
			var displayName = escapeHtml( qr.name || qr.formatted_id || qr.uuid );
			$container.append(
				'<div class="govalid-ll-qr-tag" data-index="' + i + '">' +
					'<span class="govalid-ll-qr-tag-text" title="' + escapeAttr( qr.name || '' ) + '">' + displayName + '</span>' +
					'<button type="button" class="govalid-ll-qr-tag-remove" data-index="' + i + '">&times;</button>' +
				'</div>'
			);
		});
		$( '#govalid-ll-qr-count' ).text( state.qrCodes.length );
		initTagSortable();
		toggleExportButtons( state.qrCodes.length > 0 );
	}

	var tagSortable = null;
	function initTagSortable() {
		if ( tagSortable ) {
			tagSortable.destroy();
		}
		var el = document.getElementById( 'govalid-ll-qr-tags' );
		if ( !el ) {
			return;
		}
		tagSortable = new Sortable( el, {
			animation: 150,
			ghostClass: 'sortable-ghost',
			dragClass: 'sortable-drag',
			onEnd: function( evt ) {
				// Reorder state.qrCodes
				var item = state.qrCodes.splice( evt.oldIndex, 1 )[0];
				state.qrCodes.splice( evt.newIndex, 0, item );
				renderPreview();
			}
		});
	}

	function removeQrCode( index ) {
		state.qrCodes.splice( index, 1 );
		renderQrTags();
		readSettings();
		renderPreview();
	}

	// === ADD MORE PICKER ===
	function showAddQrPicker() {
		// Create picker overlay inside the modal container
		var $modal = $( '#govalid-label-layout-modal' );
		if ( $modal.find( '.govalid-ll-picker-overlay' ).length ) {
			return;
		}

		var $overlay = $( '<div class="govalid-ll-picker-overlay">' +
			'<div class="govalid-ll-picker">' +
				'<div class="govalid-ll-picker-header">' +
					'<h3>' + escapeHtml( govalidQR.i18n.add_qr_codes || 'Add QR Codes' ) + '</h3>' +
					'<button type="button" class="govalid-ll-picker-close">&times;</button>' +
				'</div>' +
				'<div class="govalid-ll-picker-search" style="padding:8px 16px;">' +
					'<input type="text" class="govalid-ll-input" placeholder="Search..." id="govalid-ll-picker-search" />' +
				'</div>' +
				'<div class="govalid-ll-picker-body"><p style="text-align:center;color:#999;">Loading...</p></div>' +
				'<div class="govalid-ll-picker-footer">' +
					'<button type="button" class="govalid-btn govalid-btn-xs govalid-ll-picker-cancel">Cancel</button>' +
					'<button type="button" class="govalid-btn govalid-btn-xs govalid-btn-primary govalid-ll-picker-add">Add Selected</button>' +
				'</div>' +
			'</div>' +
		'</div>' );

		$modal.find( '.govalid-label-layout-container' ).append( $overlay );

		// Fetch QR codes
		$.ajax({
			url: govalidQR.restUrl + 'qr-codes',
			method: 'GET',
			data: { per_page: 100 },
			beforeSend: function( xhr ) {
				xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
			},
			success: function( resp ) {
				var items = ( resp.data && resp.data.qr_codes ) || [];
				var existingUuids = {};
				state.qrCodes.forEach( function( qr ) {
					existingUuids[qr.uuid] = true;
				});

				var html = '';
				items.forEach( function( qr ) {
					var uuid = qr.uuid || qr.qr_id;
					var name = qr.qr_name || qr.name || uuid;
					var imgUrl = qr.qr_image_url || qr.image_url || '';
					if ( imgUrl && imgUrl.indexOf( 'http' ) !== 0 ) {
						imgUrl = govalidQR.baseUrl + imgUrl;
					}
					var formattedId = qr.formatted_uuid || uuid;
					var checked = existingUuids[uuid] ? ' checked disabled' : '';

					html += '<label class="govalid-ll-picker-item">' +
						'<input type="checkbox" value="' + escapeAttr( uuid ) + '" ' +
							'data-name="' + escapeAttr( name ) + '" ' +
							'data-fid="' + escapeAttr( formattedId ) + '" ' +
							'data-img="' + escapeAttr( imgUrl ) + '"' +
							checked + ' />' +
						( imgUrl
							? '<img src="' + escapeAttr( imgUrl ) + '" alt="" />'
							: '<span class="dashicons dashicons-screenoptions" style="font-size:36px;color:#ccc;"></span>'
						) +
						'<span>' + escapeHtml( name ) + ' <small style="color:#999;">' + escapeHtml( formattedId ) + '</small></span>' +
					'</label>';
				});
				$overlay.find( '.govalid-ll-picker-body' ).html(
					html || '<p style="text-align:center;color:#999;">No QR codes found.</p>'
				);
			},
			error: function() {
				$overlay.find( '.govalid-ll-picker-body' ).html(
					'<p style="text-align:center;color:#c00;">Failed to load QR codes.</p>'
				);
			}
		});

		// Search filter
		$overlay.on( 'input', '#govalid-ll-picker-search', function() {
			var q = $( this ).val().toLowerCase();
			$overlay.find( '.govalid-ll-picker-item' ).each( function() {
				var text = $( this ).text().toLowerCase();
				$( this ).toggle( text.indexOf( q ) !== -1 );
			});
		});

		// Close picker
		$overlay.on( 'click', '.govalid-ll-picker-close, .govalid-ll-picker-cancel', function() {
			$overlay.remove();
		});

		// Add selected
		$overlay.on( 'click', '.govalid-ll-picker-add', function() {
			var existingUuids = {};
			state.qrCodes.forEach( function( qr ) {
				existingUuids[qr.uuid] = true;
			});

			$overlay.find( '.govalid-ll-picker-body input[type="checkbox"]:checked:not(:disabled)' ).each( function() {
				var $cb = $( this );
				state.qrCodes.push({
					uuid: $cb.val(),
					name: $cb.data( 'name' ),
					formatted_id: $cb.data( 'fid' ),
					image_url: $cb.data( 'img' )
				});
			});
			$overlay.remove();
			renderQrTags();
			readSettings();
			renderPreview();
		});
	}

	// === SETTINGS READER ===
	function readSettings() {
		state.settings = {
			paperSize:    $( '#govalid-ll-paper-size' ).val() || 'a4',
			orientation:  $( 'input[name="govalid-ll-orientation"]:checked' ).val() || 'portrait',
			customW:      parseInt( $( '#govalid-ll-custom-w' ).val() ) || 210,
			customH:      parseInt( $( '#govalid-ll-custom-h' ).val() ) || 297,
			cols:         parseInt( $( '#govalid-ll-cols' ).val() ) || 3,
			rows:         parseInt( $( '#govalid-ll-rows' ).val() ) || 4,
			marginTop:    parseInt( $( '#govalid-ll-margin-top' ).val() ) || 10,
			marginRight:  parseInt( $( '#govalid-ll-margin-right' ).val() ) || 10,
			marginBottom: parseInt( $( '#govalid-ll-margin-bottom' ).val() ) || 10,
			marginLeft:   parseInt( $( '#govalid-ll-margin-left' ).val() ) || 10,
			spacingH:     parseInt( $( '#govalid-ll-spacing-h' ).val() ) || 2,
			spacingV:     parseInt( $( '#govalid-ll-spacing-v' ).val() ) || 2,
			qrSize:       parseInt( $( '#govalid-ll-qr-size' ).val() ) || 85,
			qrHPos:       parseInt( $( '#govalid-ll-qr-hpos' ).val() ) || 50,
			qrVPos:       parseInt( $( '#govalid-ll-qr-vpos' ).val() ) || 50,
			showLabels:   $( '#govalid-ll-show-labels' ).is( ':checked' ),
			labelContent: $( '#govalid-ll-label-content' ).val() || 'name',
			labelPos:     $( '#govalid-ll-label-position' ).val() || 'bottom',
			labelHOffset: parseInt( $( '#govalid-ll-label-hoffset' ).val() ) || 0,
			labelVOffset: parseInt( $( '#govalid-ll-label-voffset' ).val() ) || 0,
			labelFont:    $( '#govalid-ll-label-font' ).val() || 'Arial, sans-serif',
			labelSize:    parseInt( $( '#govalid-ll-label-size' ).val() ) || 10,
			labelColor:   $( '#govalid-ll-label-color' ).val() || '#333333',
			labelBold:    state.labelStyles.bold,
			labelItalic:  state.labelStyles.italic,
			labelUnder:   state.labelStyles.underline,
			labelBg:      $( '#govalid-ll-label-bg' ).is( ':checked' ),
			labelBgColor: $( '#govalid-ll-label-bg-color' ).val() || '#ffffff',
			frameStyle:   ( $( '.govalid-ll-frame-option.active' ).data( 'frame' ) ) || 'none',
			frameColor:   $( '#govalid-ll-frame-color' ).val() || '#000000',
			frameFill:    $( '#govalid-ll-frame-fill' ).val() || '#ffffff',
			frameStroke:  parseInt( $( '#govalid-ll-frame-stroke' ).val() ) || 2
		};
	}

	// === APPLY SETTINGS (reverse of readSettings) ===
	function applySettings( s ) {
		if ( !s ) {
			return;
		}

		// Paper
		$( '#govalid-ll-paper-size' ).val( s.paperSize || 'a4' );
		$( '#govalid-ll-custom-size' ).toggle( s.paperSize === 'custom' );
		$( '#govalid-ll-custom-w' ).val( s.customW || 210 );
		$( '#govalid-ll-custom-h' ).val( s.customH || 297 );
		$( 'input[name="govalid-ll-orientation"][value="' + ( s.orientation || 'portrait' ) + '"]' ).prop( 'checked', true );

		// Grid
		$( '#govalid-ll-cols' ).val( s.cols || 3 );
		$( '#govalid-ll-cols-val' ).text( s.cols || 3 );
		$( '#govalid-ll-rows' ).val( s.rows || 4 );
		$( '#govalid-ll-rows-val' ).text( s.rows || 4 );

		// Margins
		$( '#govalid-ll-margin-top' ).val( s.marginTop != null ? s.marginTop : 10 );
		$( '#govalid-ll-margin-right' ).val( s.marginRight != null ? s.marginRight : 10 );
		$( '#govalid-ll-margin-bottom' ).val( s.marginBottom != null ? s.marginBottom : 10 );
		$( '#govalid-ll-margin-left' ).val( s.marginLeft != null ? s.marginLeft : 10 );
		$( '#govalid-ll-spacing-h' ).val( s.spacingH != null ? s.spacingH : 2 );
		$( '#govalid-ll-spacing-v' ).val( s.spacingV != null ? s.spacingV : 2 );

		// QR size & position
		$( '#govalid-ll-qr-size' ).val( s.qrSize || 85 );
		$( '#govalid-ll-qr-size-val' ).text( s.qrSize || 85 );
		$( '#govalid-ll-qr-hpos' ).val( s.qrHPos != null ? s.qrHPos : 50 );
		$( '#govalid-ll-qr-hpos-val' ).text( s.qrHPos != null ? s.qrHPos : 50 );
		$( '#govalid-ll-qr-vpos' ).val( s.qrVPos != null ? s.qrVPos : 50 );
		$( '#govalid-ll-qr-vpos-val' ).text( s.qrVPos != null ? s.qrVPos : 50 );

		// Labels
		$( '#govalid-ll-show-labels' ).prop( 'checked', s.showLabels !== false );
		$( '#govalid-ll-label-options' ).toggle( s.showLabels !== false );
		$( '#govalid-ll-label-content' ).val( s.labelContent || 'name' );
		$( '#govalid-ll-label-position' ).val( s.labelPos || 'bottom' );
		$( '#govalid-ll-label-hoffset' ).val( s.labelHOffset || 0 );
		$( '#govalid-ll-label-hoffset-val' ).text( s.labelHOffset || 0 );
		$( '#govalid-ll-label-voffset' ).val( s.labelVOffset || 0 );
		$( '#govalid-ll-label-voffset-val' ).text( s.labelVOffset || 0 );
		$( '#govalid-ll-label-font' ).val( s.labelFont || 'Arial, sans-serif' );
		$( '#govalid-ll-label-size' ).val( s.labelSize || 10 );
		$( '#govalid-ll-label-size-val' ).text( s.labelSize || 10 );
		$( '#govalid-ll-label-color' ).val( s.labelColor || '#333333' );

		// Style buttons
		state.labelStyles.bold = !!s.labelBold;
		state.labelStyles.italic = !!s.labelItalic;
		state.labelStyles.underline = !!s.labelUnder;
		$( '.govalid-ll-style-btn[data-style="bold"]' ).toggleClass( 'active', !!s.labelBold );
		$( '.govalid-ll-style-btn[data-style="italic"]' ).toggleClass( 'active', !!s.labelItalic );
		$( '.govalid-ll-style-btn[data-style="underline"]' ).toggleClass( 'active', !!s.labelUnder );

		// Label background
		$( '#govalid-ll-label-bg' ).prop( 'checked', !!s.labelBg );
		$( '#govalid-ll-label-bg-color' ).val( s.labelBgColor || '#ffffff' ).toggle( !!s.labelBg );

		// Frame
		$( '.govalid-ll-frame-option' ).removeClass( 'active' );
		$( '.govalid-ll-frame-option[data-frame="' + ( s.frameStyle || 'none' ) + '"]' ).addClass( 'active' );
		$( '#govalid-ll-frame-options' ).toggle( s.frameStyle !== 'none' && !!s.frameStyle );
		$( '#govalid-ll-frame-color' ).val( s.frameColor || '#000000' );
		$( '#govalid-ll-frame-fill' ).val( s.frameFill || '#ffffff' );
		$( '#govalid-ll-frame-stroke' ).val( s.frameStroke || 2 );
		$( '#govalid-ll-frame-stroke-val' ).text( s.frameStroke || 2 );
	}

	// === PRESET STORAGE (localStorage) ===
	var LS_PRESETS  = 'govalid_ll_presets';
	var LS_LAST     = 'govalid_ll_last_used';

	function getPresets() {
		try {
			return JSON.parse( localStorage.getItem( LS_PRESETS ) ) || [];
		} catch ( e ) {
			return [];
		}
	}

	function savePresets( presets ) {
		localStorage.setItem( LS_PRESETS, JSON.stringify( presets ) );
	}

	function getLastUsed() {
		try {
			return JSON.parse( localStorage.getItem( LS_LAST ) );
		} catch ( e ) {
			return null;
		}
	}

	function saveLastUsed() {
		readSettings();
		localStorage.setItem( LS_LAST, JSON.stringify( state.settings ) );
	}

	function renderPresetDropdown() {
		var $sel = $( '#govalid-ll-preset-select' );
		var i18n = ( govalidQR && govalidQR.i18nLabel ) || {};

		// Remember current value
		var curVal = $sel.val();

		$sel.empty();
		$sel.append( '<option value="" disabled selected>' + escapeHtml( i18n.select_preset || '— Select Preset —' ) + '</option>' );

		// Last Used
		if ( getLastUsed() ) {
			$sel.append( '<option value="__last__">' + escapeHtml( i18n.last_used || 'Last Used' ) + '</option>' );
		}

		// Default
		$sel.append( '<option value="__default__">' + escapeHtml( i18n.default_settings || 'Default Settings' ) + '</option>' );

		// Built-in presets
		var $builtIn = $( '<optgroup label="' + escapeAttr( i18n.templates || 'Templates' ) + '"></optgroup>' );
		BUILTIN_PRESETS.forEach( function( p, idx ) {
			$builtIn.append( '<option value="__builtin_' + idx + '">' + escapeHtml( p.name ) + '</option>' );
		});
		$sel.append( $builtIn );

		// User-saved presets
		var presets = getPresets();
		if ( presets.length ) {
			var $saved = $( '<optgroup label="' + escapeAttr( i18n.saved_presets || 'My Presets' ) + '"></optgroup>' );
			presets.forEach( function( p, idx ) {
				$saved.append( '<option value="' + idx + '">' + escapeHtml( p.name ) + '</option>' );
			});
			$sel.append( $saved );
		}

		// Restore selection if still valid
		if ( curVal && $sel.find( 'option[value="' + curVal + '"]' ).length ) {
			$sel.val( curVal );
		}

		// Toggle delete button
		updateDeleteButton();
	}

	function updateDeleteButton() {
		var val = $( '#govalid-ll-preset-select' ).val();
		// Only user-saved presets can be deleted (not __last__, __default__, __builtin_*)
		var isUserPreset = val !== null && val !== '' &&
			val !== '__last__' && val !== '__default__' &&
			String( val ).indexOf( '__builtin_' ) !== 0;
		$( '#govalid-ll-preset-delete' ).prop( 'disabled', !isUserPreset );
	}

	function getPaperDims() {
		var s = state.settings;
		var size = PAPER_SIZES[s.paperSize] || { w: s.customW, h: s.customH };
		var w = size.w, h = size.h;
		if ( s.orientation === 'landscape' ) {
			var tmp = w;
			w = h;
			h = tmp;
		}
		return { w: w, h: h };
	}

	// === PREVIEW RENDERER ===
	function renderPreview() {
		var s = state.settings;
		var paper = getPaperDims();
		var $paper = $( '#govalid-ll-paper' );

		// Paper dimensions in px for preview
		var paperWpx = paper.w * MM_PX;
		var paperHpx = paper.h * MM_PX;

		$paper.css({
			width: paperWpx + 'px',
			height: paperHpx + 'px',
			transform: 'scale(' + state.zoom + ')',
			transformOrigin: 'top center'
		});

		// Usable area
		var marginT = s.marginTop * MM_PX;
		var marginR = s.marginRight * MM_PX;
		var marginB = s.marginBottom * MM_PX;
		var marginL = s.marginLeft * MM_PX;
		var spacingH = s.spacingH * MM_PX;
		var spacingV = s.spacingV * MM_PX;

		var availW = paperWpx - marginL - marginR;
		var availH = paperHpx - marginT - marginB;

		var cellW = ( availW - spacingH * ( s.cols - 1 ) ) / s.cols;
		var cellH = ( availH - spacingV * ( s.rows - 1 ) ) / s.rows;

		if ( cellW < 10 ) {
			cellW = 10;
		}
		if ( cellH < 10 ) {
			cellH = 10;
		}

		// Pagination
		var cellsPerPage = s.cols * s.rows;
		state.totalPages = Math.max( 1, Math.ceil( state.qrCodes.length / cellsPerPage ) );
		if ( state.currentPage > state.totalPages ) {
			state.currentPage = state.totalPages;
		}

		var startIdx = ( state.currentPage - 1 ) * cellsPerPage;

		// Build grid
		$paper.empty();
		var $grid = $( '<div class="govalid-ll-grid"></div>' ).css({
			position: 'absolute',
			top: marginT + 'px',
			left: marginL + 'px',
			display: 'grid',
			gridTemplateColumns: 'repeat(' + s.cols + ', ' + cellW + 'px)',
			gridTemplateRows: 'repeat(' + s.rows + ', ' + cellH + 'px)',
			columnGap: spacingH + 'px',
			rowGap: spacingV + 'px'
		});

		for ( var i = 0; i < cellsPerPage; i++ ) {
			var qrIdx = startIdx + i;
			var qr = state.qrCodes[qrIdx] || null;
			var $cell = createCell( qr, s, cellW, cellH );
			$grid.append( $cell );
		}

		$paper.append( $grid );
		updatePagination();
	}

	function createCell( qr, s, cellW, cellH ) {
		var $cell = $( '<div class="govalid-ll-cell"></div>' );

		if ( !qr ) {
			$cell.addClass( 'govalid-ll-cell-empty' );
			$cell.html( '<span>' + escapeHtml( govalidQR.i18n.empty_cell || 'Empty' ) + '</span>' );
			return $cell;
		}

		// Determine label area height
		var labelH = 0;
		if ( s.showLabels && ( s.labelPos === 'top' || s.labelPos === 'bottom' ) ) {
			labelH = ( s.labelSize * 1.4 ) + 4; // font size + padding
		}

		// Container for image positioning
		var imgAreaH = cellH - labelH;
		var imgAreaW = cellW;
		if ( s.showLabels && ( s.labelPos === 'left' || s.labelPos === 'right' ) ) {
			imgAreaW = cellW * 0.7; // 70% for image, 30% for label
		}

		// QR image
		var qrDim = Math.min( imgAreaW, imgAreaH ) * ( s.qrSize / 100 );
		var $img = $( '<img />' )
			.attr( 'src', qr.image_url )
			.attr( 'alt', qr.name || '' )
			.attr( 'crossorigin', 'anonymous' )
			.css({
				width: qrDim + 'px',
				height: qrDim + 'px',
				objectFit: 'contain'
			});

		// Build cell layout based on label position
		if ( s.showLabels ) {
			var labelText = getLabelText( qr, s );
			var $label = createLabel( labelText, s );

			if ( s.labelPos === 'top' ) {
				$cell.css({ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-start' });
				$cell.append( $label ).append(
					$( '<div class="govalid-ll-cell-img-wrap"></div>' ).css({
						flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%'
					}).append( $img )
				);
			} else if ( s.labelPos === 'bottom' ) {
				$cell.css({ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-start' });
				$cell.append(
					$( '<div class="govalid-ll-cell-img-wrap"></div>' ).css({
						flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%'
					}).append( $img )
				).append( $label );
			} else if ( s.labelPos === 'left' ) {
				$cell.css({ display: 'flex', flexDirection: 'row', alignItems: 'center' });
				$label.css({ writingMode: 'vertical-lr', transform: 'rotate(180deg)', maxHeight: cellH + 'px', maxWidth: '30%' });
				$cell.append( $label ).append(
					$( '<div class="govalid-ll-cell-img-wrap"></div>' ).css({
						flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center'
					}).append( $img )
				);
			} else if ( s.labelPos === 'right' ) {
				$cell.css({ display: 'flex', flexDirection: 'row', alignItems: 'center' });
				$label.css({ writingMode: 'vertical-lr', maxHeight: cellH + 'px', maxWidth: '30%' });
				$cell.append(
					$( '<div class="govalid-ll-cell-img-wrap"></div>' ).css({
						flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center'
					}).append( $img )
				).append( $label );
			}
			// Apply H/V offset to label using transform so it doesn't affect flex layout
			var hOff = s.labelHOffset || 0;
			var vOff = s.labelVOffset || 0;
			if ( hOff || vOff ) {
				var existingTransform = $label.css( 'transform' );
				var translate = 'translate(' + hOff + 'px, ' + vOff + 'px)';
				if ( existingTransform && existingTransform !== 'none' ) {
					$label.css( 'transform', existingTransform + ' ' + translate );
				} else {
					$label.css( 'transform', translate );
				}
			}
		} else {
			$cell.css({ display: 'flex', alignItems: 'center', justifyContent: 'center' });
			$cell.append( $img );
		}

		// Frame
		if ( s.frameStyle !== 'none' ) {
			var $frame = $( '<div class="govalid-ll-cell-frame"></div>' );
			var frameCss = {
				borderColor: s.frameColor,
				borderWidth: s.frameStroke + 'px',
				backgroundColor: s.frameFill
			};

			switch ( s.frameStyle ) {
				case 'basic':
					frameCss.borderStyle = 'solid';
					break;
				case 'rounded':
					frameCss.borderStyle = 'solid';
					frameCss.borderRadius = '8px';
					break;
				case 'double':
					frameCss.borderStyle = 'double';
					frameCss.borderWidth = Math.max( 3, s.frameStroke ) + 'px';
					break;
				case 'dashed':
					frameCss.borderStyle = 'dashed';
					break;
				case 'dotted':
					frameCss.borderStyle = 'dotted';
					break;
				case 'shadow':
					frameCss.border = 'none';
					frameCss.boxShadow = '2px 2px 6px rgba(0,0,0,0.25)';
					frameCss.backgroundColor = s.frameFill;
					break;
				case 'pill':
					frameCss.borderStyle = 'solid';
					frameCss.borderRadius = '50%';
					break;
				case 'groove':
					frameCss.borderStyle = 'groove';
					frameCss.borderWidth = Math.max( 3, s.frameStroke ) + 'px';
					break;
				case 'ridge':
					frameCss.borderStyle = 'ridge';
					frameCss.borderWidth = Math.max( 3, s.frameStroke ) + 'px';
					break;
				case 'inset':
					frameCss.borderStyle = 'inset';
					frameCss.borderWidth = Math.max( 3, s.frameStroke ) + 'px';
					break;
				case 'outset':
					frameCss.borderStyle = 'outset';
					frameCss.borderWidth = Math.max( 3, s.frameStroke ) + 'px';
					break;
				case 'ticket':
					frameCss.borderStyle = 'dashed';
					frameCss.borderRadius = '12px';
					break;
				case 'elegant':
					frameCss.borderStyle = 'solid';
					frameCss.borderWidth = '1px';
					frameCss.boxShadow = 'inset 0 0 0 3px ' + s.frameFill + ', inset 0 0 0 4px ' + s.frameColor;
					break;
				case 'glow':
					frameCss.borderStyle = 'solid';
					frameCss.boxShadow = '0 0 8px ' + s.frameColor + '80';
					break;
				case 'stamp':
					frameCss.borderStyle = 'solid';
					frameCss.outline = s.frameStroke + 'px dashed ' + s.frameColor;
					frameCss.outlineOffset = '3px';
					break;
			}
			$frame.css( frameCss );
			$cell.css( 'position', 'relative' ).prepend( $frame );
			// Ensure content is above frame
			$cell.children().not( '.govalid-ll-cell-frame' ).css( 'position', 'relative' ).css( 'zIndex', 1 );
		}

		return $cell;
	}

	function getLabelText( qr, s ) {
		if ( s.labelContent === 'id' ) {
			return qr.formatted_id || qr.uuid;
		}
		if ( s.labelContent === 'both' ) {
			return ( qr.name || '' ) + '\n' + ( qr.formatted_id || qr.uuid );
		}
		return qr.name || qr.uuid;
	}

	function createLabel( text, s ) {
		var $label = $( '<div class="govalid-ll-cell-label"></div>' );
		$label.text( text ); // text() for escaping
		$label.css({
			fontFamily: s.labelFont,
			fontSize: s.labelSize + 'px',
			color: s.labelColor,
			fontWeight: s.labelBold ? 'bold' : 'normal',
			fontStyle: s.labelItalic ? 'italic' : 'normal',
			textDecoration: s.labelUnder ? 'underline' : 'none'
		});
		if ( s.labelBg ) {
			$label.css({ backgroundColor: s.labelBgColor, padding: '1px 4px', borderRadius: '2px' });
		}
		return $label;
	}

	function updatePagination() {
		$( '#govalid-ll-current-page' ).text( state.currentPage );
		$( '#govalid-ll-total-pages' ).text( state.totalPages );
		$( '#govalid-ll-prev-page' ).prop( 'disabled', state.currentPage <= 1 );
		$( '#govalid-ll-next-page' ).prop( 'disabled', state.currentPage >= state.totalPages );
	}

	// === FRAME SELECTOR ===
	function initFrameSelector() {
		var frames = [
			{ id: 'none',    label: 'None',    css: 'border:1px dashed #ccc;color:#999;font-size:9px;' },
			{ id: 'basic',   label: 'Basic',   css: 'border:2px solid #333;' },
			{ id: 'rounded', label: 'Rounded', css: 'border:2px solid #333;border-radius:6px;' },
			{ id: 'double',  label: 'Double',  css: 'border:3px double #333;' },
			{ id: 'dashed',  label: 'Dashed',  css: 'border:2px dashed #333;' },
			{ id: 'dotted',  label: 'Dotted',  css: 'border:2px dotted #333;' },
			{ id: 'shadow',  label: 'Shadow',  css: 'box-shadow:2px 2px 5px rgba(0,0,0,0.3);' },
			{ id: 'pill',    label: 'Pill',     css: 'border:2px solid #333;border-radius:50%;' },
			{ id: 'groove',  label: 'Groove',   css: 'border:3px groove #888;' },
			{ id: 'ridge',   label: 'Ridge',    css: 'border:3px ridge #888;' },
			{ id: 'inset',   label: 'Inset',    css: 'border:3px inset #888;' },
			{ id: 'outset',  label: 'Outset',   css: 'border:3px outset #888;' },
			{ id: 'ticket',  label: 'Ticket',   css: 'border:2px dashed #333;border-radius:12px;background:repeating-linear-gradient(90deg,transparent,transparent 4px,#eee 4px,#eee 5px);' },
			{ id: 'elegant', label: 'Elegant',  css: 'border:1px solid #333;box-shadow:inset 0 0 0 3px #fff,inset 0 0 0 4px #333;' },
			{ id: 'glow',    label: 'Glow',     css: 'border:2px solid #2563eb;box-shadow:0 0 8px rgba(37,99,235,0.5);' },
			{ id: 'stamp',   label: 'Stamp',    css: 'border:2px solid #333;outline:2px dashed #333;outline-offset:3px;' }
		];

		var $container = $( '#govalid-ll-frame-selector' ).empty();
		frames.forEach( function( f ) {
			var activeClass = f.id === 'none' ? ' active' : '';
			$container.append(
				'<div class="govalid-ll-frame-option' + activeClass + '" data-frame="' + f.id + '" title="' + f.label + '">' +
					'<div style="width:32px;height:32px;' + f.css + 'display:flex;align-items:center;justify-content:center;">' +
					( f.id === 'none' ? '<span style="font-size:9px;">None</span>' : '' ) +
					'</div>' +
				'</div>'
			);
		});
	}

	// === ZOOM ===
	function adjustZoom( delta ) {
		state.zoom = Math.min( 2, Math.max( 0.25, state.zoom + delta ) );
		$( '#govalid-ll-zoom-level' ).text( Math.round( state.zoom * 100 ) + '%' );
		$( '#govalid-ll-paper' ).css( 'transform', 'scale(' + state.zoom + ')' );
	}

	function resetZoom() {
		state.zoom = 1;
		$( '#govalid-ll-zoom-level' ).text( '100%' );
		$( '#govalid-ll-paper' ).css( 'transform', 'scale(1)' );
	}

	// === EXPORT FUNCTIONS ===
	function showLoading( text ) {
		var $modal = $( '.govalid-label-layout-container' );
		$modal.find( '.govalid-ll-loading' ).remove();
		$modal.append(
			'<div class="govalid-ll-loading">' +
				'<div class="govalid-ll-spinner"></div>' +
				'<span>' + escapeHtml( text ) + '</span>' +
			'</div>'
		);
	}

	function hideLoading() {
		$( '.govalid-ll-loading' ).remove();
	}

	function getDateStr() {
		var d = new Date();
		return d.getFullYear() + '-' +
			String( d.getMonth() + 1 ).padStart( 2, '0' ) + '-' +
			String( d.getDate() ).padStart( 2, '0' );
	}

	function exportAsPDF() {
		if ( !state.qrCodes.length ) {
			return;
		}
		showLoading( govalidQR.i18n.generating_pdf || 'Generating PDF...' );

		var savedPage = state.currentPage;
		var savedZoom = state.zoom;

		// Reset zoom for capture
		state.zoom = 1;
		$( '#govalid-ll-paper' ).css( 'transform', 'scale(1)' );

		var paper = getPaperDims();
		var orientation = state.settings.orientation === 'landscape' ? 'l' : 'p';
		var pdf = new jspdf.jsPDF( orientation, 'mm', [paper.w, paper.h] );

		var pageNum = 0;
		var totalPgs = state.totalPages;

		function captureNextPage() {
			pageNum++;
			if ( pageNum > totalPgs ) {
				// Done - save
				pdf.save( 'qr_labels_' + getDateStr() + '.pdf' );
				// Restore
				state.currentPage = savedPage;
				state.zoom = savedZoom;
				renderPreview();
				$( '#govalid-ll-paper' ).css( 'transform', 'scale(' + savedZoom + ')' );
				hideLoading();
				return;
			}

			if ( pageNum > 1 ) {
				pdf.addPage( [paper.w, paper.h], orientation );
			}

			state.currentPage = pageNum;
			renderPreview();

			// Wait for images to load
			setTimeout( function() {
				var paperEl = document.getElementById( 'govalid-ll-paper' );
				html2canvas( paperEl, {
					scale: 3,
					useCORS: true,
					allowTaint: true,
					backgroundColor: '#ffffff',
					logging: false
				}).then( function( canvas ) {
					var imgData = canvas.toDataURL( 'image/jpeg', 0.95 );
					pdf.addImage( imgData, 'JPEG', 0, 0, paper.w, paper.h );
					captureNextPage();
				}).catch( function() {
					hideLoading();
					alert( govalidQR.i18n.export_error || 'Export failed.' );
				});
			}, 300 );
		}

		// Start capture chain
		setTimeout( captureNextPage, 200 );
	}

	function exportAsImage( format ) {
		if ( !state.qrCodes.length ) {
			return;
		}
		showLoading( govalidQR.i18n.generating_image || 'Generating image...' );

		var savedZoom = state.zoom;
		state.zoom = 1;
		$( '#govalid-ll-paper' ).css( 'transform', 'scale(1)' );

		setTimeout( function() {
			var paperEl = document.getElementById( 'govalid-ll-paper' );
			html2canvas( paperEl, {
				scale: 3,
				useCORS: true,
				allowTaint: true,
				backgroundColor: '#ffffff',
				logging: false
			}).then( function( canvas ) {
				var mime = format === 'jpg' ? 'image/jpeg' : 'image/png';
				var ext = format === 'jpg' ? 'jpg' : 'png';
				var quality = format === 'jpg' ? 0.95 : undefined;
				var dataUrl = canvas.toDataURL( mime, quality );

				var link = document.createElement( 'a' );
				link.download = 'qr_labels_p' + state.currentPage + '_' + getDateStr() + '.' + ext;
				link.href = dataUrl;
				document.body.appendChild( link );
				link.click();
				document.body.removeChild( link );

				// Restore
				state.zoom = savedZoom;
				$( '#govalid-ll-paper' ).css( 'transform', 'scale(' + savedZoom + ')' );
				hideLoading();
			}).catch( function() {
				state.zoom = savedZoom;
				$( '#govalid-ll-paper' ).css( 'transform', 'scale(' + savedZoom + ')' );
				hideLoading();
				alert( govalidQR.i18n.export_error || 'Export failed.' );
			});
		}, 200 );
	}

	function printLayout() {
		if ( !state.qrCodes.length ) {
			return;
		}
		showLoading( 'Preparing print...' );

		var savedPage = state.currentPage;
		var savedZoom = state.zoom;
		state.zoom = 1;
		$( '#govalid-ll-paper' ).css( 'transform', 'scale(1)' );

		var paper = getPaperDims();
		var pages = [];
		var pageNum = 0;

		function captureForPrint() {
			pageNum++;
			if ( pageNum > state.totalPages ) {
				// Build print window
				var printWin = window.open( '', '_blank' );
				var htmlContent = '<!DOCTYPE html><html><head><title>QR Labels</title>' +
					'<style>' +
					'body{margin:0;padding:0;}' +
					'img{display:block;width:' + paper.w + 'mm;height:' + paper.h + 'mm;}' +
					'.page-break{page-break-before:always;}' +
					'@media print{body{margin:0;}@page{size:' + paper.w + 'mm ' + paper.h + 'mm;margin:0;}}' +
					'</style></head><body>';

				pages.forEach( function( dataUrl, i ) {
					htmlContent += ( i > 0 ? '<div class="page-break"></div>' : '' ) +
						'<img src="' + dataUrl + '" />';
				});
				htmlContent += '<script>window.onload=function(){setTimeout(function(){window.print();window.close();},500);}<\/script>';
				htmlContent += '</body></html>';

				printWin.document.write( htmlContent );
				printWin.document.close();

				// Restore
				state.currentPage = savedPage;
				state.zoom = savedZoom;
				renderPreview();
				$( '#govalid-ll-paper' ).css( 'transform', 'scale(' + savedZoom + ')' );
				hideLoading();
				return;
			}

			state.currentPage = pageNum;
			renderPreview();

			setTimeout( function() {
				html2canvas( document.getElementById( 'govalid-ll-paper' ), {
					scale: 3,
					useCORS: true,
					allowTaint: true,
					backgroundColor: '#ffffff',
					logging: false
				}).then( function( canvas ) {
					pages.push( canvas.toDataURL( 'image/png' ) );
					captureForPrint();
				}).catch( function() {
					hideLoading();
				});
			}, 300 );
		}

		setTimeout( captureForPrint, 200 );
	}

	// === COLLAPSIBLE SECTIONS ===
	function initCollapsibles() {
		// Paper Settings starts expanded, others collapsed
		$( '.govalid-ll-collapsible' ).each( function() {
			var $s = $( this );
			var section = $s.data( 'section' );
			if ( section !== 'paper' ) {
				$s.find( '.govalid-ll-section-body' ).hide();
				$s.find( '.dashicons' ).removeClass( 'dashicons-arrow-down-alt2' ).addClass( 'dashicons-arrow-up-alt2' );
			}
		});
	}

	// === EVENT BINDINGS ===
	function bindEvents() {
		// Close modal
		$( document ).on( 'click', '#govalid-ll-close', closeModal );
		$( document ).on( 'keydown', function( e ) {
			if ( e.key === 'Escape' && $( '#govalid-label-layout-modal' ).is( ':visible' ) ) {
				closeModal();
			}
		});

		// Collapsible toggle
		$( document ).on( 'click', '.govalid-ll-section-toggle', function() {
			var $section = $( this ).closest( '.govalid-ll-collapsible' );
			var $body = $section.find( '.govalid-ll-section-body' );
			var $icon = $( this ).find( '.dashicons' );
			$body.slideToggle( 200 );
			$icon.toggleClass( 'dashicons-arrow-down-alt2 dashicons-arrow-up-alt2' );
		});

		// Settings changes trigger preview update (debounced)
		var debouncedUpdate = debounce( function() {
			readSettings();
			renderPreview();
		}, 150 );

		$( document ).on( 'change input', '.govalid-ll-controls select, .govalid-ll-controls input', debouncedUpdate );

		// Range value displays
		var rangeDisplays = {
			'govalid-ll-cols': 'govalid-ll-cols-val',
			'govalid-ll-rows': 'govalid-ll-rows-val',
			'govalid-ll-qr-size': 'govalid-ll-qr-size-val',
			'govalid-ll-qr-hpos': 'govalid-ll-qr-hpos-val',
			'govalid-ll-qr-vpos': 'govalid-ll-qr-vpos-val',
			'govalid-ll-label-size': 'govalid-ll-label-size-val',
			'govalid-ll-label-hoffset': 'govalid-ll-label-hoffset-val',
			'govalid-ll-label-voffset': 'govalid-ll-label-voffset-val',
			'govalid-ll-frame-stroke': 'govalid-ll-frame-stroke-val'
		};
		Object.keys( rangeDisplays ).forEach( function( inputId ) {
			$( document ).on( 'input', '#' + inputId, function() {
				$( '#' + rangeDisplays[inputId] ).text( this.value );
			});
		});

		// Paper size: show/hide custom size
		$( document ).on( 'change', '#govalid-ll-paper-size', function() {
			$( '#govalid-ll-custom-size' ).toggle( $( this ).val() === 'custom' );
		});

		// Show/hide label options
		$( document ).on( 'change', '#govalid-ll-show-labels', function() {
			$( '#govalid-ll-label-options' ).toggle( $( this ).is( ':checked' ) );
		});

		// Label background toggle
		$( document ).on( 'change', '#govalid-ll-label-bg', function() {
			$( '#govalid-ll-label-bg-color' ).toggle( $( this ).is( ':checked' ) );
		});

		// Style buttons (bold/italic/underline)
		$( document ).on( 'click', '.govalid-ll-style-btn', function() {
			var style = $( this ).data( 'style' );
			$( this ).toggleClass( 'active' );
			state.labelStyles[style] = $( this ).hasClass( 'active' );
			readSettings();
			renderPreview();
		});

		// Frame selector
		$( document ).on( 'click', '.govalid-ll-frame-option', function() {
			$( '.govalid-ll-frame-option' ).removeClass( 'active' );
			$( this ).addClass( 'active' );
			var isNone = $( this ).data( 'frame' ) === 'none';
			$( '#govalid-ll-frame-options' ).toggle( !isNone );
			readSettings();
			renderPreview();
		});

		// Zoom
		$( document ).on( 'click', '#govalid-ll-zoom-in', function() {
			adjustZoom( 0.15 );
		});
		$( document ).on( 'click', '#govalid-ll-zoom-out', function() {
			adjustZoom( -0.15 );
		});
		$( document ).on( 'click', '#govalid-ll-zoom-reset', resetZoom );

		// Pagination
		$( document ).on( 'click', '#govalid-ll-prev-page', function() {
			if ( state.currentPage > 1 ) {
				state.currentPage--;
				renderPreview();
			}
		});
		$( document ).on( 'click', '#govalid-ll-next-page', function() {
			if ( state.currentPage < state.totalPages ) {
				state.currentPage++;
				renderPreview();
			}
		});

		// Export buttons
		$( document ).on( 'click', '#govalid-ll-export-pdf', exportAsPDF );
		$( document ).on( 'click', '#govalid-ll-export-png', function() {
			exportAsImage( 'png' );
		});
		$( document ).on( 'click', '#govalid-ll-export-jpg', function() {
			exportAsImage( 'jpg' );
		});
		$( document ).on( 'click', '#govalid-ll-print', printLayout );

		// Tag removal
		$( document ).on( 'click', '.govalid-ll-qr-tag-remove', function() {
			var idx = parseInt( $( this ).data( 'index' ) );
			removeQrCode( idx );
		});

		// Add more
		$( document ).on( 'click', '#govalid-ll-add-more', showAddQrPicker );

		// === PRESET EVENTS ===
		// Load preset from dropdown
		$( document ).on( 'change', '#govalid-ll-preset-select', function() {
			var val = $( this ).val();
			var settings = null;

			if ( val === '__last__' ) {
				settings = getLastUsed();
			} else if ( val === '__default__' ) {
				settings = $.extend( {}, DEFAULT_SETTINGS );
			} else if ( String( val ).indexOf( '__builtin_' ) === 0 ) {
				var bIdx = parseInt( val.replace( '__builtin_', '' ) );
				if ( BUILTIN_PRESETS[bIdx] ) {
					settings = $.extend( {}, BUILTIN_PRESETS[bIdx].settings );
				}
			} else {
				var presets = getPresets();
				var idx = parseInt( val );
				if ( presets[idx] ) {
					settings = presets[idx].settings;
				}
			}

			if ( settings ) {
				applySettings( settings );
				readSettings();
				renderPreview();
			}
			updateDeleteButton();
		});

		// Save current as preset
		$( document ).on( 'click', '#govalid-ll-preset-save', function() {
			var i18n = ( govalidQR && govalidQR.i18nLabel ) || {};
			var name = prompt( i18n.preset_name_prompt || 'Enter a name for this preset:' );
			if ( !name || !name.trim() ) {
				return;
			}
			readSettings();
			var presets = getPresets();
			presets.push({
				name: name.trim(),
				settings: $.extend( {}, state.settings ),
				created: Date.now()
			});
			savePresets( presets );
			renderPresetDropdown();
			// Select the newly saved preset
			$( '#govalid-ll-preset-select' ).val( String( presets.length - 1 ) );
			updateDeleteButton();
		});

		// Delete selected preset
		$( document ).on( 'click', '#govalid-ll-preset-delete', function() {
			var val = $( '#govalid-ll-preset-select' ).val();
			if ( val === null || val === '' || val === '__last__' || val === '__default__' || String( val ).indexOf( '__builtin_' ) === 0 ) {
				return;
			}
			var i18n = ( govalidQR && govalidQR.i18nLabel ) || {};
			if ( !confirm( i18n.confirm_delete || 'Delete this preset?' ) ) {
				return;
			}
			var presets = getPresets();
			var idx = parseInt( val );
			presets.splice( idx, 1 );
			savePresets( presets );
			renderPresetDropdown();
		});
	}

	// === INIT ===
	$( document ).ready( function() {
		if ( !$( '#govalid-label-layout-modal' ).length ) {
			return;
		}
		initFrameSelector();
		initCollapsibles();
		bindEvents();
	});

})( jQuery );
