/**
 * Public/Frontend JavaScript
 *
 * @package Spin_Wheel
 */

(function ($) {
    'use strict';

    const SWP_UTM_STORAGE_KEY = 'swp_utm_attribution_v1';
    const SWP_UTM_PARAM_KEYS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid', 'fbclid'];

    function swpUtmParseSearch(search) {
        const out = {};
        if (typeof URLSearchParams === 'undefined') {
            return out;
        }
        const params = new URLSearchParams(search || (typeof window !== 'undefined' ? window.location.search : '') || '');
        SWP_UTM_PARAM_KEYS.forEach(function (k) {
            const v = params.get(k);
            if (v !== null && String(v).trim() !== '') {
                out[k] = String(v).trim().slice(0, 500);
            }
        });
        return out;
    }

    function swpUtmMergeUrlIntoSessionStorage() {
        if (typeof sessionStorage === 'undefined') {
            return;
        }
        const fromUrl = swpUtmParseSearch();
        if (!Object.keys(fromUrl).length) {
            return;
        }
        let stored = {};
        try {
            stored = JSON.parse(sessionStorage.getItem(SWP_UTM_STORAGE_KEY) || '{}');
        } catch (e) {
            stored = {};
        }
        if (typeof stored !== 'object' || stored === null) {
            stored = {};
        }
        Object.assign(stored, fromUrl);
        try {
            sessionStorage.setItem(SWP_UTM_STORAGE_KEY, JSON.stringify(stored));
        } catch (e) {
            /* storage unavailable */
        }
    }

    function swpUtmReadFromSessionStorage() {
        if (typeof sessionStorage === 'undefined') {
            return {};
        }
        try {
            const raw = sessionStorage.getItem(SWP_UTM_STORAGE_KEY);
            const o = raw ? JSON.parse(raw) : {};
            return o && typeof o === 'object' ? o : {};
        } catch (e) {
            return {};
        }
    }

    $(function () {
        swpUtmMergeUrlIntoSessionStorage();
    });

    /** @type {HTMLCanvasElement|undefined} */
    let swpGridLabelMeasureCanvas;

    /**
     * Canvas measureText helper for LuckyGrid prize labels (word wrap + ellipsis).
     */
    function swpGetGridLabelMeasureContext() {
        if (typeof document === 'undefined') {
            return null;
        }
        if (!swpGridLabelMeasureCanvas) {
            swpGridLabelMeasureCanvas = document.createElement('canvas');
        }
        return swpGridLabelMeasureCanvas.getContext('2d');
    }

    /**
     * LuckyGrid draws line i with baseline at top + (i + 1) * lineHeight — center block in cell height.
     */
    function swpLuckyGridTextBlockTopPx(cellH, lineHeightPx, lineCount) {
        const h = Math.max(1, cellH);
        const lh = Math.max(1, lineHeightPx);
        const n = Math.max(1, lineCount);
        let topPx = h / 2 - (lh * (n + 1)) / 2;
        if (topPx < 0) {
            topPx = (h - n * lh) / 2;
        }
        return Math.max(0, Math.round(topPx));
    }

    /**
     * Nine-squares prize cell backgrounds — aligned with `presetStyles` in admin-modern.js (big_wheel excluded).
     *
     * Ring Grid pool order matches the clockwise ring walk used by the admin `ringGrid`
     * preset (and the rainbow ring on its preset card thumbnail). External Spin pool is
     * row-major rainbow matching the `grid2x3` thumbnail. Keeping these in lock-step with
     * the admin presets ensures the prize pickers, admin preview, and live frontend all
     * render the same look after applying a preset.
     */
    const SWP_NINE_SQUARE_RING_PRESET_COLORS = [
        '#ff6b9d', '#fcc419', '#51cf66', '#cc5de8',
        '#94d82d', '#4dabf7', '#ff922b', '#339af0'
    ];
    const SWP_NINE_SQUARE_GRID2X3_PRESET_COLORS = [
        '#ff6b9d', '#fcc419', '#51cf66',
        '#339af0', '#cc5de8', '#94d82d'
    ];
    const SWP_NINE_SQUARE_RADIAL_PLAYFUL_COLORS = [
        '#7dd3c0', '#fde047', '#7eb8ff', '#fda4af', '#c4b5fd', '#fb923c', '#67e8f9', '#86efac'
    ];
    /** Theme presets (4 prize slots cycled for 8+ cells). */
    const SWP_NINE_SQUARE_THEME_PRESET_PRIZE_COLORS = {
        default: ['#fff', '#ffd7b7', '#fff', '#ffd7b7'],
        classic: ['#fceced', '#fff', '#fceced', '#fff'],
        ocean: ['#fdebc3', '#fff', '#fdebc3', '#fff'],
        festive: ['#e8b1ff', '#fff', '#e8b1ff', '#fff'],
        crimson: ['#fce2bf', '#fff', '#fce2bf', '#fff'],
        megaReward: ['#eff1f1', '#ccefff', '#eff1f1', '#ccefff'],
        candyPop: ['#fff', '#fff6c4', '#fff', '#fff6c4'],
    };

    /**
     * Circle / radial grid: skip cells with no label and no image after preparePrizesData().
     */
    function swpNineSquaresPrizeIsRenderable(prize) {
        if (!prize) {
            return false;
        }
        const hasImg = prize.imgs && prize.imgs.length > 0
            && prize.imgs.some((img) => img && String(img.src || '').trim());
        const rawText = prize.fonts && prize.fonts[0]
            ? String(prize.fonts[0].text || '').replace(/\n/g, ' ').trim()
            : '';
        return hasImg || rawText.length > 0;
    }

    /**
     * Word wrap on spaces only; overflow becomes ellipsis on the last allowed line.
     */
    function swpBuildGridLabelLines(ctx, rawText, maxWidth, maxLines) {
        if (!ctx) {
            const single = (rawText || '').toString().replace(/\s+/g, ' ').trim();
            return [single || ''];
        }
        const text = (rawText || '').toString().replace(/\s+/g, ' ').trim();
        if (!text) {
            return [''];
        }
        maxLines = Math.max(1, maxLines);
        const words = text.split(' ');
        const ell = '\u2026';
        const tw = (s) => ctx.measureText(s).width;

        const clipWord = (word) => {
            if (tw(word) <= maxWidth) {
                return word;
            }
            let w = word;
            while (w.length > 1 && tw(w + ell) > maxWidth) {
                w = w.slice(0, -1);
            }
            return w + ell;
        };

        const lines = [];
        let wi = 0;

        while (wi < words.length && lines.length < maxLines) {
            const onLastLine = (lines.length === maxLines - 1);

            if (onLastLine) {
                const rest = words.slice(wi).join(' ');
                if (tw(rest) <= maxWidth) {
                    lines.push(rest);
                    break;
                }
                let line = '';
                while (wi < words.length) {
                    const w = words[wi];
                    const trial = line ? (line + ' ' + w) : w;
                    if (tw(trial) <= maxWidth) {
                        line = trial;
                        wi++;
                    } else {
                        break;
                    }
                }
                if (!line) {
                    lines.push(clipWord(words[wi]));
                    wi++;
                } else if (wi < words.length) {
                    let t = line;
                    while (t.length && tw(t + ell) > maxWidth) {
                        const sp = t.lastIndexOf(' ');
                        if (sp > 0) {
                            t = t.slice(0, sp);
                        } else {
                            t = t.slice(0, -1);
                        }
                    }
                    lines.push((t || line) + ell);
                } else {
                    lines.push(line);
                }
                break;
            }

            let line2 = '';
            while (wi < words.length) {
                const w = words[wi];
                const trial = line2 ? (line2 + ' ' + w) : w;
                if (tw(trial) <= maxWidth) {
                    line2 = trial;
                    wi++;
                } else {
                    break;
                }
            }
            if (line2) {
                lines.push(line2);
            } else if (wi < words.length) {
                lines.push(clipWord(words[wi]));
                wi++;
            }
        }

        return lines.length ? lines : [''];
    }

    // Spin Wheel Class
    class SpinWheel {
        constructor(container) {
            this.$container = $(container);
            this.wheelId = this.$container.data('wheel-id');
            this.wheelType = this.$container.data('wheel-type');
            this.wheelMode = this.$container.data('wheel-mode');
            this.wheelData = JSON.parse(this.$container.find('.swp-wheel-data').text());
            this.formSettings = this.wheelData.formSettings || {};
            this.wheelInstance = null;
            this.userData = {};
            this.isAutoRotating = false;
            this.autoRotationInterval = null;
            this.isSpinning = false;
            
            // Initialize sound effects
            this.sounds = {
                spin: null,
                win: null
            };
            this.initSounds();

            this.init();
        }
        
        initSounds() {
            // Only initialize if sound effects are enabled
            if (!swpData.enableSoundEffects || swpData.enableSoundEffects == 0 || swpData.enableSoundEffects === '0') {
                this.sounds = {}; // Clear any existing sounds
                return;
            }
            
            try {
                this.sounds.spin = new Audio(swpData.pluginUrl + 'public/assets/sounds/spin-01.ogg');
                this.sounds.win = new Audio(swpData.pluginUrl + 'public/assets/sounds/win-01.ogg');
                
                // Preload sounds
                this.sounds.spin.load();
                this.sounds.win.load();
                
                // Set volume
                this.sounds.spin.volume = 0.5;
                this.sounds.win.volume = 0.6;
            } catch (error) {
                console.warn('Could not initialize sound effects:', error);
            }
        }
        
        playSound(soundName) {
            if (!swpData.enableSoundEffects || !this.sounds[soundName]) {
                return;
            }
            
            try {
                // Reset sound to beginning
                this.sounds[soundName].currentTime = 0;
                // Play sound
                this.sounds[soundName].play().catch(err => {
                    console.warn('Could not play sound:', err);
                });
            } catch (error) {
                console.warn('Error playing sound:', error);
            }
        }
        
        stopSound(soundName) {
            if (!swpData.enableSoundEffects || !this.sounds[soundName]) {
                return;
            }
            
            try {
                // Pause the sound
                this.sounds[soundName].pause();
                // Reset to beginning
                this.sounds[soundName].currentTime = 0;
            } catch (error) {
                console.warn('Error stopping sound:', error);
            }
        }

        init() {
            this.bindEvents();
            // Initialize wheel immediately on page load
            this.initWheel();
        }

        bindEvents() {
            const self = this;

            // Form submission
            this.$container.find('.swp-entry-form').on('submit', function (e) {
                e.preventDefault();
                self.handleFormSubmit($(this));
            });

            // Clear error state on input
            this.$container.find('.swp-form-control').on('input focus', function () {
                $(this).removeClass('error');
                $(this).parent().find('.swp-error-message').remove();
            });

            // Close result modal
            this.$container.find('.swp-close-btn').on('click', function () {
                self.closeResult();
            });

            // OTP: Send button
            this.$container.find('.swp-btn-send-otp').on('click', function () {
                self.handleSendOTP();
            });

            // OTP: Verify button
            this.$container.find('.swp-btn-verify-otp').on('click', function () {
                self.handleVerifyOTP();
            });

            // OTP: Resend button
            this.$container.find('.swp-btn-resend-otp').on('click', function () {
                self.handleSendOTP();
            });

            // OTP: Input enter key
            this.$container.find('.swp-otp-input').on('keypress', function (e) {
                if (e.which === 13) {
                    e.preventDefault();
                    self.handleVerifyOTP();
                }
            });

            // Responsive resize handler - Disabled to prevent re-initialization on resize
            // The wheel uses CSS max-width and responsive dimensions, no need to re-init
            /*
            let resizeTimer;
            $(window).on('resize', function() {
                clearTimeout(resizeTimer);
                resizeTimer = setTimeout(function() {
                    if (self.wheelInstance) {
                        self.handleResize();
                    }
                }, 250);
            });
            */
        }

        handleResize() {
            // Note: This method is currently disabled to prevent wheel re-initialization on resize
            // The wheel canvas is responsive via CSS and doesn't need re-initialization
            // If you need to enable resize handling, uncomment the resize listener in bindEvents()
            return;
        }

        handleFormSubmit($form) {
            // Check if OTP is enabled and required
            const otpEnabled = this.wheelData && this.wheelData.formSettings && this.wheelData.formSettings.enable_otp;

            if (otpEnabled && !this.otpVerified) {
                this.showAlert(swpData.i18n.verifyOtpFirst, 'error');
                return;
            }

            if (this.isSpinning) {
                return;
            }

            // Validate form
            if (!this.validateForm($form)) {
                return;
            }

            // Get form data
            this.userData = {
                name: $form.find('[name="user_name"]').val(),
                email: $form.find('[name="user_email"]').val(),
                phone: $form.find('[name="user_phone"]').val()
            };

            // Show button loader and activate overlay
            this.isSpinning = true;
            this.showButtonLoader();

            // Trigger the spin
            if (this.wheelInstance && this.wheelInstance.play) {
                // Directly call handleSpin since we have the user data now
                this.handleSpin();
            } else {
                this.isSpinning = false;
                this.showAlert(swpData.i18n.wheelNotInitialized, 'error');
                this.hideButtonLoader();
            }
        }

        validateForm($form) {
            let isValid = true;

            // Remove previous errors
            $form.find('.swp-form-control').removeClass('error');
            $form.find('.swp-error-message').remove();
            $form.find('.swp-accept-terms').removeClass('error');
            $form.find('.swp-terms-group .swp-error-message').remove();

            const fs = this.wheelData && this.wheelData.formSettings ? this.wheelData.formSettings : {};
            const showName = parseInt(fs.show_name, 10) === 1;
            const showEmail = parseInt(fs.show_email, 10) === 1;
            const showPhone = parseInt(fs.show_phone, 10) === 1;
            const requireName = parseInt(fs.require_name, 10) === 1;
            const requireEmail = parseInt(fs.require_email, 10) === 1;
            const termsOn = parseInt(fs.enable_terms_checkbox, 10) === 1;

            if (showName) {
                const $nameField = $form.find('[name="user_name"]');
                const name = ($nameField.val() || '').trim();
                if (requireName && !name) {
                    $nameField.addClass('error');
                    $nameField.parent().append('<span class="swp-error-message">' + (swpData.i18n.required || 'This field is required') + '</span>');
                    isValid = false;
                }
            }

            if (showEmail) {
                const $emailField = $form.find('[name="user_email"]');
                const email = ($emailField.val() || '').trim();
                if (requireEmail && !email) {
                    $emailField.addClass('error');
                    $emailField.parent().append('<span class="swp-error-message">' + (swpData.i18n.required || 'This field is required') + '</span>');
                    isValid = false;
                } else if (email && !this.isValidEmail(email)) {
                    $emailField.addClass('error');
                    $emailField.parent().append('<span class="swp-error-message">' + (swpData.i18n.invalidEmail || 'Please enter a valid email address') + '</span>');
                    isValid = false;
                }
            }

            if (showPhone) {
                const $phone = $form.find('[name="user_phone"]');
                if ($phone.length && $phone.prop('required') && !($phone.val() || '').trim()) {
                    $phone.addClass('error');
                    $phone.parent().append('<span class="swp-error-message">' + (swpData.i18n.required || 'This field is required') + '</span>');
                    isValid = false;
                }
            }

            if (termsOn) {
                const $terms = $form.find('[name="swp_accept_terms"]');
                if ($terms.length && !$terms.is(':checked')) {
                    $terms.addClass('error');
                    $terms.closest('.swp-terms-group').append('<span class="swp-error-message">' + (swpData.i18n.termsRequired || 'You must accept the terms and conditions.') + '</span>');
                    isValid = false;
                }
            }

            return isValid;
        }

        isValidEmail(email) {
            return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
        }

        initWheel() {
            const canvasId = `swp-canvas-${this.wheelId}`;
            const $canvas = this.$container.find(`#${canvasId}`);

            if (!$canvas.length || !window.LuckyCanvas) {
                console.error('Canvas or LuckyCanvas library not found');
                return;
            }

            // Set CSS custom property for desktop max-width (section too — External Spin layout max-width)
            const maxWidth = this.wheelData.styles?.width || 460;
            $canvas[0].style.setProperty('--swp-canvas-max-width', maxWidth);
            const $wheelSection = $canvas.closest('.swp-wheel-section');
            if ($wheelSection.length) {
                $wheelSection[0].style.setProperty('--swp-canvas-max-width', maxWidth);
            }

            // Prepare prizes data
            const prizes = this.preparePrizesData();

            // Initialize wheel based on type
            switch (this.wheelType) {
                case 'big_wheel':
                    this.initBigWheel(`#${canvasId}`, prizes);
                    break;
                case 'nine_squares':
                    this.initNineSquares(`#${canvasId}`, prizes);
                    break;
                case 'slot_machine':
                    this.initSlotMachine(`#${canvasId}`, prizes);
                    break;
                default:
                    this.initBigWheel(`#${canvasId}`, prizes);
            }

        }

        preparePrizesData() {
            const prizes = this.wheelData.prizes || [];
            const prizeRepresentation = this.wheelData.settings?.prize_representation || 'label'; // label, image, both3
            const st0 = this.wheelData.styles || {};
            const isNineGrid = this.wheelType === 'nine_squares';
            const narrowRadialLabels = isNineGrid && this.isCircleStyleRadialLayout(st0);

            // Big wheel / slot: char-based prewrap. Nine-squares grid: raw line; initNineSquares
            // applies measureText word-wrap, ellipsis, and vertical centering per cell.
            const maxCharsPerLine = narrowRadialLabels ? 5 : 18;
            const wrapLabelWords = (text) => {
                const raw = (text || '').toString().replace(/\s+/g, ' ').trim();
                if (!raw) return '';

                const words = raw.split(' ');
                const lines = [];
                let line = '';

                for (let w = 0; w < words.length; w++) {
                    const word = words[w];
                    const candidate = line ? (line + ' ' + word) : word;

                    if (candidate.length <= maxCharsPerLine) {
                        line = candidate;
                        continue;
                    }

                    if (line) {
                        lines.push(line);
                    }

                    // Whole word longer than one line: keep full word on its own line (no truncation).
                    if (word.length > maxCharsPerLine) {
                        lines.push(word);
                        line = '';
                    } else {
                        line = word;
                    }
                }

                if (line) {
                    lines.push(line);
                }

                return lines.join('\n');
            };

            let maxLabelLines = 1;

            const mapped = prizes.map((prize, index) => {
                const prizeData = {
                    ...prize,
                    index: index,
                    background: prize.color || this.getRandomColor()
                };

                // Get prize styling settings
                const imageSize = this.wheelData.styles?.prize_image_size || this.wheelData.styles?.prize_image_width || 70;
                const imageTopPx = parseInt(this.wheelData.styles?.prize_image_top, 10) || 0;
                const imageLeftPx = parseInt(this.wheelData.styles?.prize_image_left, 10) || 0;
                const textTop = this.wheelData.styles?.prize_text_top || 15;
                const prizeFontSize = parseInt(this.wheelData.styles?.prize_font_size) || 16;
                const prizeFontWeight = this.wheelData.styles?.prize_font_weight || '600';
                const prizeFontColor = this.wheelData.styles?.prize_font_color || '#000000';
                const prizeLineHeight = parseInt(this.wheelData.styles?.prize_line_height) || 14;
                const prizeTextTopNoImage = parseInt(this.wheelData.styles?.prize_text_top_no_image) || 30;
                const prizeTextRotation = parseInt(this.wheelData.styles?.prize_text_rotation) || 0;

                const prizeImgLayout = {
                    width: imageSize + 'px',
                    height: imageSize + 'px',
                    left: imageLeftPx + 'px',
                    top: imageTopPx + 'px',
                };

                // Handle different prize representations
                if (prizeRepresentation === 'image' && prize.image && prize.image.trim()) {
                    // Image Only - no text label
                    prizeData.imgs = [{
                        src: prize.image,
                        ...prizeImgLayout,
                    }];
                } else if (prizeRepresentation === 'both' && prize.image && prize.image.trim()) {
                    // Both - image with text label
                    prizeData.imgs = [{
                        src: prize.image,
                        ...prizeImgLayout,
                    }];
                    const labelText = isNineGrid
                        ? (prize.label || '').toString().replace(/\s+/g, ' ').trim()
                        : wrapLabelWords(prize.label);
                    if (!isNineGrid) {
                        maxLabelLines = Math.max(maxLabelLines, labelText.split('\n').length);
                    }

                    prizeData.fonts = [{
                        text: labelText,
                        top: textTop + '%',
                        fontSize: prizeFontSize + 'px',
                        fontWeight: prizeFontWeight,
                        fontColor: prizeFontColor,
                        lineHeight: prizeLineHeight + 'px'
                    }];
                    // Store rotation for custom rendering
                    prizeData.textRotation = prizeTextRotation;
                } else {
                    // Label Only (default) - text only, centered
                    const labelText = isNineGrid
                        ? (prize.label || '').toString().replace(/\s+/g, ' ').trim()
                        : wrapLabelWords(prize.label);
                    if (!isNineGrid) {
                        maxLabelLines = Math.max(maxLabelLines, labelText.split('\n').length);
                    }

                    prizeData.fonts = [{
                        text: labelText,
                        top: prizeTextTopNoImage + '%',
                        fontSize: prizeFontSize + 'px',
                        fontWeight: prizeFontWeight,
                        fontColor: prizeFontColor,
                        lineHeight: prizeLineHeight + 'px'
                    }];
                    // Store rotation for custom rendering
                    prizeData.textRotation = prizeTextRotation;
                }

                return prizeData;
            });

            this._prizeLabelMaxLines = isNineGrid
                ? (parseInt(st0.line_clamp, 10) || 3)
                : maxLabelLines;

            this.applyNineSquaresPresetPrizeBackgrounds(mapped);

            return mapped;
        }

        /**
         * Sync nine-squares prize cell colors to the active admin preset (ring, external spin, radial, or theme).
         */
        applyNineSquaresPresetPrizeBackgrounds(mappedPrizes) {
            if (this.wheelType !== 'nine_squares' || !mappedPrizes || !mappedPrizes.length) {
                return;
            }
            const styles = this.wheelData.styles || {};
            const rawPrizes = this.wheelData.prizes || [];

            const applyPool = (pool) => {
                const n = pool.length;
                if (!n) {
                    return;
                }
                mappedPrizes.forEach((p, i) => {
                    p.background = pool[i % n];
                });
            };

            if (this.isGrid2x3Preset(styles)) {
                applyPool(SWP_NINE_SQUARE_GRID2X3_PRESET_COLORS);
                return;
            }
            if (this.isRingGridPreset(styles)) {
                applyPool(SWP_NINE_SQUARE_RING_PRESET_COLORS);
                return;
            }

            const presetKey = String(styles.active_preset || 'default').trim() || 'default';
            const radial = this.isCircleStyleRadialLayout(styles);
            const hexes = rawPrizes.map((p) => (p && p.color ? String(p.color).toLowerCase().replace(/\s/g, '') : ''))
                .filter((c) => c.length > 0);
            const allSameSavedColor = hexes.length > 0 && new Set(hexes).size === 1;
            const circleByPreset = presetKey === 'circleStyle';

            if (radial && (circleByPreset || allSameSavedColor)) {
                applyPool(SWP_NINE_SQUARE_RADIAL_PLAYFUL_COLORS);
                mappedPrizes.forEach((p) => {
                    if (p.fonts && p.fonts.length) {
                        p.fonts.forEach((f) => {
                            f.fontColor = '#ffffff';
                        });
                    }
                });
                return;
            }

            const cycle = SWP_NINE_SQUARE_THEME_PRESET_PRIZE_COLORS[presetKey]
                || SWP_NINE_SQUARE_THEME_PRESET_PRIZE_COLORS.default;
            applyPool(cycle);
        }

        /**
         * Match LuckyGrid geometry: cellHeight = (innerH - gutter * (rows - 1)) / rows
         * with one block and uniform padding (same as initNineSquares blocks[0].padding).
         */
        getGridCellMetrics(dimensions, styles, rows, cols, gutterOverride) {
            const gutter = gutterOverride !== undefined && gutterOverride !== null
                ? gutterOverride
                : (parseInt(styles.grid_gutter, 10) || 5);
            const pad = parseInt(styles.padding, 10) || 0;
            const innerW = Math.max(1, dimensions.width - 2 * pad);
            const innerH = Math.max(1, dimensions.height - 2 * pad);
            return {
                cellW: Math.max(1, (innerW - gutter * (cols - 1)) / cols),
                cellH: Math.max(1, (innerH - gutter * (rows - 1)) / rows),
            };
        }

        /**
         * Ring layout (not a filled 3×3): Circle Style preset, or highly rounded cells (saved preset may be missing).
         */
        isRingGridPreset(styles) {
            return ((styles && styles.active_preset) || '').toString() === 'ringGrid';
        }

        /**
         * External Spin preset (`grid2x3`): lucky-canvas-style blocks + spin control as HTML below the canvas.
         * Rows/columns come from styles (default 2×3); every cell can hold a prize; grid grows to fit prize count.
         */
        isGrid2x3Preset(styles) {
            return ((styles && styles.active_preset) || '').toString() === 'grid2x3';
        }

        /** lucky-canvas 3×3 eight-cell ring (perimeter order, clockwise from top-left). */
        getRingGridEightCoords() {
            return this.getRingGridAllPrizeCoords(3);
        }

        /**
         * One rectangular ring in cell space: inclusive bounds [lo, hi] on both axes.
         * Clockwise from top-left; size must be ≥ 2 or returns [].
         */
        getRingGridPerimeterCoordsForBounds(lo, hi) {
            const coords = [];
            const size = hi - lo + 1;
            if (size < 2) {
                return coords;
            }
            for (let x = lo; x <= hi; x++) {
                coords.push([x, lo]);
            }
            for (let y = lo + 1; y <= hi; y++) {
                coords.push([hi, y]);
            }
            for (let x = hi - 1; x >= lo; x--) {
                coords.push([x, hi]);
            }
            for (let y = hi - 1; y > lo; y--) {
                coords.push([lo, y]);
            }
            return coords;
        }

        /**
         * External Spin / doc-style rectangle: clockwise walk on the outer ring
         * (top L→R, right T→B, bottom R→L, left B→T). x = column, y = row.
         */
        getRectPerimeterCoordsClockwise(rows, cols) {
            const coords = [];
            const R = Math.max(1, Math.floor(Number(rows)) || 1);
            const C = Math.max(1, Math.floor(Number(cols)) || 1);
            if (R === 1) {
                for (let x = 0; x < C; x++) {
                    coords.push([x, 0]);
                }
                return coords;
            }
            if (C === 1) {
                for (let y = 0; y < R; y++) {
                    coords.push([0, y]);
                }
                return coords;
            }
            for (let x = 0; x < C; x++) {
                coords.push([x, 0]);
            }
            for (let y = 1; y < R; y++) {
                coords.push([C - 1, y]);
            }
            for (let x = C - 2; x >= 0; x--) {
                coords.push([x, R - 1]);
            }
            for (let y = R - 2; y >= 1; y--) {
                coords.push([0, y]);
            }
            return coords;
        }

        /**
         * Full grid: perimeter clockwise, then interior cells row-major (e.g. 3×3 center after ring).
         */
        getFullGridPrizeVisitOrderClockwise(rows, cols) {
            const R = Math.max(1, Math.floor(Number(rows)) || 1);
            const C = Math.max(1, Math.floor(Number(cols)) || 1);
            const perimeter = this.getRectPerimeterCoordsClockwise(R, C);
            const seen = new Set(perimeter.map(([x, y]) => `${x},${y}`));
            const out = perimeter.slice();
            for (let y = 0; y < R; y++) {
                for (let x = 0; x < C; x++) {
                    const k = `${x},${y}`;
                    if (!seen.has(k)) {
                        out.push([x, y]);
                        seen.add(k);
                    }
                }
            }
            return out;
        }

        /**
         * Ring Grid: every cell except center (spin button).
         * 3×3: classic clockwise outer ring (unchanged layout).
         * 5×5 / 7×7: row-major (left→right, top→bottom) so each row/column reads in order — layer-wise order scrambled the “middle”.
         */
        getRingGridAllPrizeCoords(n) {
            n = Math.floor(Number(n));
            if (!Number.isFinite(n) || n < 3) {
                n = 3;
            }
            if (n === 3) {
                return this.getRingGridPerimeterCoordsForBounds(0, n - 1);
            }
            const c = Math.floor(n / 2);
            const coords = [];
            for (let y = 0; y < n; y++) {
                for (let x = 0; x < n; x++) {
                    if (x === c && y === c) {
                        continue;
                    }
                    coords.push([x, y]);
                }
            }
            return coords;
        }

        /** Ring Grid: square size locked to 3, 5, or 7 (match rows/cols intent). */
        normalizeRingGridSize(rows, cols) {
            const allowed = [3, 5, 7];
            const r = parseInt(rows, 10);
            const c = parseInt(cols, 10);
            let n = Math.max(
                Number.isFinite(r) ? r : 3,
                Number.isFinite(c) ? c : 3
            );
            n = Math.min(7, Math.max(3, n));
            return allowed.reduce((best, a) => (
                Math.abs(a - n) < Math.abs(best - n) ? a : best
            ), 3);
        }

        /**
         * Grow rows/cols (alternating wider/taller) until the grid can show every prize.
         * Used by External Spin preset so saved 2×3 is a starting layout, not a hard cap of six cells.
         */
        expandGridDimsToFitPrizeCount(rows, cols, prizeCount) {
            let r = Math.max(1, parseInt(rows, 10) || 1);
            let c = Math.max(1, parseInt(cols, 10) || 1);
            const n = Math.max(0, parseInt(prizeCount, 10) || 0);
            while (r * c < n) {
                if (c <= r) {
                    c++;
                } else {
                    r++;
                }
            }
            return { rows: r, cols: c };
        }

        sumBlocksConfigPadding(styles) {
            if (!styles || !styles.blocks_config) {
                return Math.max(0, parseInt(styles.padding, 10) || 0);
            }
            try {
                const arr = JSON.parse(styles.blocks_config);
                if (!Array.isArray(arr) || arr.length === 0) {
                    return Math.max(0, parseInt(styles.padding, 10) || 0);
                }
                return arr.reduce((sum, b) => {
                    const p = b && b.padding !== undefined && b.padding !== null
                        ? parseInt(String(b.padding).replace(/px/g, '').trim(), 10)
                        : 0;
                    return sum + (Number.isFinite(p) ? p : 0);
                }, 0);
            } catch (e) {
                return Math.max(0, parseInt(styles.padding, 10) || 0);
            }
        }

        parseNineSquaresBlocksFromConfig(styles, fallbackPadPx, fallbackBorderRadius) {
            if (styles && styles.blocks_config) {
                try {
                    const arr = JSON.parse(styles.blocks_config);
                    if (Array.isArray(arr) && arr.length > 0) {
                        return arr.map((b) => ({
                            padding: b.padding != null ? b.padding : (fallbackPadPx + 'px'),
                            background: b.background || styles.background_color || '#ffffff',
                            borderRadius: b.borderRadius !== undefined && b.borderRadius !== null
                                ? b.borderRadius
                                : fallbackBorderRadius
                        }));
                    }
                } catch (e) {
                    /* fall through */
                }
            }
            return [{
                padding: fallbackPadPx + 'px',
                background: styles.background_color || '#ffffff',
                borderRadius: fallbackBorderRadius
            }];
        }

        isCircleStyleRadialLayout(styles) {
            if (this.isRingGridPreset(styles) || this.isGrid2x3Preset(styles)) {
                return false;
            }
            if (((styles && styles.active_preset) || '').toString() === 'circleStyle') {
                return true;
            }
            const br = parseInt(styles && styles.grid_border_radius, 10) || 0;
            return br >= 48;
        }

        /**
         * LuckyGrid official fine-grid ring: each prize spans col×row micro-cells (lucky-canvas docs).
         */
        getLuckyGridDocRingCellLayouts(z, s, m, n) {
            const hz = z / 2;
            const hs = s / 2;
            return [
                { x: hz - hs, y: 0, col: s, row: s },
                { x: z - m - hs, y: n - hs, col: s, row: s },
                { x: z - n - hs, y: m - hs, col: s, row: s },
                { x: z - s, y: hz - hs, col: s, row: s },
                { x: z - n - hs, y: z - m - hs, col: s, row: s },
                { x: z - m - hs, y: z - n - hs, col: s, row: s },
                { x: hz - hs, y: z - s, col: s, row: s },
                { x: m - hs, y: z - n - hs, col: s, row: s },
                { x: n - hs, y: z - m - hs, col: s, row: s },
                { x: 0, y: hz - hs, col: s, row: s },
                { x: n - hs, y: m - hs, col: s, row: s },
                { x: m - hs, y: n - hs, col: s, row: s }
            ];
        }

        /**
         * Evenly space `count` prize cells around the ring (fine z×z grid, each spans s×s).
         * Used when empty / placeholder prizes are omitted so the ring tightens automatically.
         */
        getLuckyGridRadialRingLayoutsEven(z, s, count) {
            const n = Math.max(0, parseInt(count, 10) || 0);
            if (n < 1) {
                return [];
            }
            const hz = z / 2;
            const hs = s / 2;
            const R = hz - hs;
            const maxCoord = z - s;
            const layouts = [];
            for (let i = 0; i < n; i++) {
                const angle = -Math.PI / 2 + (2 * Math.PI * i) / n;
                const cx = hz + R * Math.cos(angle);
                const cy = hz + R * Math.sin(angle);
                let x = Math.round(cx - hs);
                let y = Math.round(cy - hs);
                x = Math.max(0, Math.min(maxCoord, x));
                y = Math.max(0, Math.min(maxCoord, y));
                layouts.push({ x, y, col: s, row: s });
            }
            return layouts;
        }

        /**
         * Map ring slot → wheel prize index (legacy; radial ring now uses slot i → prize i, extras blank).
         */
        radialRingPrizeIndex(ringIdx, ringCount, prizeCount) {
            const n = Math.max(1, prizeCount);
            return ringIdx % n;
        }

        /**
         * LuckyGrid draws the first text line at baseline = cellY + top + lineHeight
         * (see fillText y + top + (lineIndex + 1) * lineHeight). So top must compensate
         * for lineHeight; using 50% alone pushes text too low and looks off-center.
         */
        luckyGridFirstLineTopPx(cellH, fontSizePx, lineHeightPx) {
            const h = Math.max(1, cellH);
            const fs = Math.max(1, fontSizePx);
            const lh = Math.max(1, lineHeightPx);
            let topPx = (h / 2) + 0.35 * fs - lh;
            topPx = Math.max(0, Math.min(topPx, h - lh - 1));
            return Math.round(topPx);
        }

        /** Center cell spin label: LuckyGrid baseline math + small nudge for caps / bold optical center. */
        luckyGridCenterButtonTopPx(cellH, fontSizePx, lineHeightPx) {
            const h = Math.max(1, cellH);
            const fs = Math.max(1, fontSizePx);
            const lh = Math.max(1, lineHeightPx);
            let topPx = this.luckyGridFirstLineTopPx(h, fs, lh) + Math.round(Math.min(fs * 0.2, h * 0.06));
            topPx = Math.max(0, Math.min(topPx, h - lh - 1));
            return topPx;
        }

        /**
         * LuckyGrid cells: re-map prize fonts/imgs from LuckyWheel-style % tops to cell-aware px tops.
         */
        normalizePrizeForGridCell(prize, cellH, cellW) {
            const st = this.wheelData.styles || {};
            const defaultFs = parseInt(st.prize_font_size, 10) || 16;
            const defaultLh = parseInt(st.prize_line_height, 10) || defaultFs;
            const h = Math.max(1, parseInt(cellH, 10) || 0);
            const w = Number.isFinite(parseInt(cellW, 10)) ? Math.max(1, parseInt(cellW, 10)) : h;
            const radialRing = this.wheelType === 'nine_squares' && this.isCircleStyleRadialLayout(st);
            /** Word wrap / ellipsis are applied in finalizeNineSquaresPrizeFontLayout (measureText). */
            const fontWordWrapDefault = false;

            const px = (val, fb) => {
                if (val === undefined || val === null) {
                    return fb;
                }
                const n = parseInt(String(val).replace(/px/g, '').trim(), 10);
                return Number.isFinite(n) ? n : fb;
            };

            const scaleFsLh = (fs, lh) => {
                if (!radialRing || h <= 1) {
                    return { fs, lh };
                }
                const span = Math.min(h, w);
                const cap = Math.max(7, Math.round(span * 0.24));
                const fs2 = Math.min(cap, Math.max(7, Math.round(fs * 0.82)));
                const lh2 = Math.max(fs2 + 1, Math.round(fs2 * 1.12));
                return { fs: fs2, lh: lh2 };
            };

            const out = { ...prize };
            const hasFonts = out.fonts && out.fonts.length;
            const hasImgs = out.imgs && out.imgs.length;
            const imgOffX = parseInt(st.prize_image_left, 10) || 0;
            const imgOffY = parseInt(st.prize_image_top, 10) || 0;

            if (hasFonts && hasImgs) {
                out.imgs = out.imgs.map((img) => {
                    const baseTop = h > 1 ? Math.round(h * 0.14) : 15;
                    return {
                        ...img,
                        top: (baseTop + imgOffY) + 'px',
                        left: imgOffX + 'px',
                    };
                });
                out.fonts = out.fonts.map((f) => {
                    let fs = px(f.fontSize, defaultFs);
                    let lhRaw = px(f.lineHeight, defaultLh);
                    const scaled = scaleFsLh(fs, lhRaw);
                    fs = scaled.fs;
                    lhRaw = scaled.lh;
                    const lhUse = Math.max(lhRaw || 0, fs);
                    let textTopPx = h > 1
                        ? Math.round(h * 0.56 + 0.35 * fs - lhUse)
                        : NaN;
                    if (Number.isFinite(textTopPx)) {
                        textTopPx = Math.max(0, Math.min(textTopPx, h - lhUse - 1));
                    }
                    return {
                        ...f,
                        top: Number.isFinite(textTopPx) ? (textTopPx + 'px') : '58%',
                        lineHeight: lhUse + 'px',
                        fontSize: f.fontSize || (fs + 'px'),
                        wordWrap: f.wordWrap !== undefined ? f.wordWrap : fontWordWrapDefault,
                    };
                });
            } else if (hasFonts && !hasImgs) {
                out.fonts = out.fonts.map((f) => {
                    let fs = px(f.fontSize, defaultFs);
                    let lhRaw = px(f.lineHeight, defaultLh);
                    const scaled = scaleFsLh(fs, lhRaw);
                    fs = scaled.fs;
                    lhRaw = scaled.lh;
                    const lhUse = Math.max(lhRaw || 0, fs);
                    const lineCount = ((f.text || '') + '').split('\n').filter(Boolean).length;
                    let topPx = h > 1 ? this.luckyGridFirstLineTopPx(h, fs, lhUse) : NaN;
                    if (radialRing && lineCount > 1 && Number.isFinite(topPx)) {
                        topPx = Math.max(0, Math.round(topPx - (lineCount - 1) * lhUse * 0.35));
                    }
                    return {
                        ...f,
                        top: Number.isFinite(topPx) ? (topPx + 'px') : '42%',
                        lineHeight: lhUse + 'px',
                        fontSize: f.fontSize || (fs + 'px'),
                        wordWrap: f.wordWrap !== undefined ? f.wordWrap : fontWordWrapDefault,
                    };
                });
            } else if (hasImgs && !hasFonts) {
                out.imgs = out.imgs.map((img) => {
                    const iw = px(img.width, Math.min(h, w));
                    const cy = h > 1 ? Math.max(0, Math.round(h / 2 - iw / 2) + imgOffY) : 0;
                    const cx = w > 1 ? Math.max(0, Math.round(w / 2 - iw / 2) + imgOffX) : 0;
                    return { ...img, top: cy + 'px', left: cx + 'px' };
                });
            }

            return out;
        }

        /**
         * Word-wrap (spaces only), ellipsis overflow, vertically center label block in LuckyGrid cell.
         */
        finalizeNineSquaresPrizeFontLayout(prize, cellW, cellH, styles) {
            if (!prize || !prize.fonts || !prize.fonts.length) {
                return;
            }
            const f = prize.fonts[0];
            const raw = String(f.text || '').replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
            if (!raw) {
                return;
            }
            const st = styles || {};
            const fontSize = parseInt(String(f.fontSize || '').replace(/px/g, '').trim(), 10)
                || parseInt(st.prize_font_size, 10)
                || parseInt(st.font_size, 10)
                || 16;
            let lineHeight = parseInt(String(f.lineHeight || '').replace(/px/g, '').trim(), 10);
            if (!Number.isFinite(lineHeight) || lineHeight < 1) {
                lineHeight = parseInt(st.prize_line_height, 10) || fontSize;
            }
            lineHeight = Math.max(fontSize, lineHeight);
            const fontWeight = f.fontWeight || st.prize_font_weight || st.font_weight || '400';
            const fontFamily = f.fontStyle || st.font_style || 'Arial';

            const radialLayout = this.isCircleStyleRadialLayout(st);
            const maxLines = radialLayout
                ? 2
                : Math.max(1, parseInt(st.line_clamp, 10) || 3);
            const limitPct = Math.min(100, Math.max(30, parseInt(st.length_limit, 10) || 90));
            const maxWidth = Math.max(32, (Math.max(1, cellW) * limitPct) / 100 - 6);

            const ctx = swpGetGridLabelMeasureContext();
            if (ctx) {
                ctx.font = fontWeight + ' ' + fontSize + 'px ' + fontFamily;
            }
            const lines = swpBuildGridLabelLines(ctx, raw, maxWidth, maxLines);
            f.text = lines.join('\n');
            f.wordWrap = false;
            f.lineHeight = lineHeight + 'px';

            const hasImage = prize.imgs && prize.imgs.length;
            const h = Math.max(1, cellH);
            let topPx;
            if (hasImage) {
                const imgTopBase = Math.round(h * 0.14);
                const imgTop = imgTopBase + (parseInt(st.prize_image_top, 10) || 0);
                const imgSize = parseInt(st.prize_image_size, 10) || parseInt(st.prize_image_width, 10) || 70;
                const reservedTop = Math.min(h - lineHeight, imgTop + imgSize + Math.max(4, Math.round(fontSize * 0.22)));
                const zoneH = Math.max(lineHeight, h - reservedTop);
                topPx = reservedTop + Math.max(0, (zoneH - lines.length * lineHeight) / 2);
                topPx = Math.max(0, Math.min(topPx, h - lines.length * lineHeight));
            } else if (radialLayout && lines.length === 1) {
                topPx = this.luckyGridFirstLineTopPx(h, fontSize, lineHeight);
            } else {
                topPx = swpLuckyGridTextBlockTopPx(h, lineHeight, lines.length);
            }
            f.top = Math.round(topPx) + 'px';
        }

        getResponsiveDimensions(styles) {
            const baseWidth = parseInt(styles.width) || 460;
            const baseHeight = parseInt(styles.height) || 460;
            const containerWidth = this.$container.find('.swp-wheel-section').width();

            // Check if we're on mobile device
            const isMobile = window.innerWidth <= 768;
            const isSmallScreen = window.innerWidth <= 480;

            if (isMobile) {
                // For mobile devices, use viewport width as base calculation
                const viewportWidth = window.innerWidth;
                const availableWidth = containerWidth && containerWidth > 0 ? containerWidth : viewportWidth;
                
                // Calculate size with proper padding consideration
                // Account for modal padding (20px on mobile) and some margin
                const paddingOffset = 40; // 20px padding on each side
                const safeWidth = availableWidth - paddingOffset;
                
                // Set minimum and maximum sizes
                const minSize = isSmallScreen ? 280 : 300;
                const maxSize = Math.min(safeWidth * 0.95, baseWidth);
                const finalSize = Math.max(minSize, Math.min(maxSize, safeWidth));

                return {
                    width: finalSize,
                    height: finalSize
                };
            }

            // On desktop or larger screens, scale down to fit container if needed
            if (containerWidth && containerWidth < baseWidth) {
                const scale = containerWidth / baseWidth;
                return {
                    width: Math.floor(containerWidth * 0.95),
                    height: Math.floor(baseHeight * scale * 0.95)
                };
            }

            return {
                width: baseWidth,
                height: baseHeight
            };
        }

        initBigWheel(canvasSelector, prizes) {
            const styles = this.wheelData.styles || {};
            const settings = this.wheelData.settings || {};
            const self = this;
            const dimensions = this.getResponsiveDimensions(styles);

            // Calculate scale factor for responsive preset elements
            const baseWidth = parseInt(styles.width) || 460;
            const scaleFactor = dimensions.width / baseWidth;

            // Check if animations are enabled
            const enableAnimations = settings.enable_animations != 0 && settings.enable_animations !== "0";

            // Use configured button radius (no need to force larger on mobile)
            const buttonRadius = parseInt(styles.button_radius) || 50;
            const finalButtonRadius = buttonRadius;
            
            // Store canvas internal dimensions and button radius for click detection
            self.canvasWidth = dimensions.width;
            self.buttonRadius = finalButtonRadius;

            // Apply text rotation BEFORE creating wheel instance to prevent flash
            const textRotation = parseInt(styles.prize_text_rotation) || 0;
            const textOffsetX = parseInt(styles.prize_text_offset_x) || 0;
            const textOffsetY = parseInt(styles.prize_text_offset_y) || 0;
            
            const prizeImgRot = parseInt(styles.prize_image_rotation, 10) || 0;
            if (textRotation !== 0 || textOffsetX !== 0 || textOffsetY !== 0 || prizeImgRot !== 0) {
                self.prePatchCanvasContext(canvasSelector, textRotation, textOffsetX, textOffsetY);
            }

            // Helper function to scale preset values
            const scalePresetValue = (value) => {
                if (typeof value === 'string' && value.endsWith('px')) {
                    const numValue = parseInt(value);
                    return Math.round(numValue * scaleFactor) + 'px';
                }
                return value;
            };

            // Helper function to scale preset configuration
            const scalePresetConfig = (config) => {
                if (!config || typeof config !== 'object') return config;
                
                const scaled = Array.isArray(config) ? [] : {};
                
                for (const key in config) {
                    const value = config[key];
                    
                    if (key === 'padding' || key === 'width' || key === 'height') {
                        scaled[key] = scalePresetValue(value);
                    } else if (key === 'imgs' && Array.isArray(value)) {
                        scaled[key] = value.map(img => ({
                            ...img,
                            width: scalePresetValue(img.width),
                            height: scalePresetValue(img.height),
                            top: typeof img.top === 'string' && img.top.endsWith('px')
                                ? scalePresetValue(img.top)
                                : img.top,
                            left: typeof img.left === 'string' && img.left.endsWith('px')
                                ? scalePresetValue(img.left)
                                : img.left,
                        }));
                    } else if (typeof value === 'object' && value !== null) {
                        scaled[key] = scalePresetConfig(value);
                    } else {
                        scaled[key] = value;
                    }
                }
                
                return scaled;
            };

            // Store rotation values for custom rendering
            const textRotations = prizes.map(p => p.textRotation || 0);

            // Lucky-canvas configuration for Big Wheel
            this.wheelInstance = new window.LuckyCanvas.LuckyWheel(canvasSelector, {
                width: dimensions.width,
                height: dimensions.height,
                blocks: (() => {
                    // Check if we have saved blocks configuration from presets
                    if (styles.blocks_config) {
                        try {
                            const blocksConfig = JSON.parse(styles.blocks_config);
                            if (Array.isArray(blocksConfig) && blocksConfig.length > 0) {
                                // Scale the preset blocks configuration for mobile
                                return scalePresetConfig(blocksConfig);
                            }
                        } catch (e) {
                            console.warn('Failed to parse blocks_config:', e);
                        }
                    }
                    // Fallback to default block
                    return [
                        {
                            padding: scalePresetValue((styles.padding || 0) + 'px'),
                            background: styles.background_color || '#ffffff'
                        }
                    ];
                })(),
                prizes: prizes,
                buttons: (() => {
                    // Check if we have saved buttons configuration from presets
                    if (styles.buttons_config) {
                        try {
                            const buttonsConfig = JSON.parse(styles.buttons_config);
                            if (Array.isArray(buttonsConfig) && buttonsConfig.length > 0) {
                                // Scale the preset buttons configuration for mobile
                                return scalePresetConfig(buttonsConfig);
                            }
                        } catch (e) {
                            console.warn('Failed to parse buttons_config:', e);
                        }
                    }
                    // Fallback to default button
                    return [
                        {
                            radius: finalButtonRadius + 'px',
                            pointer: !!parseInt(styles.show_pointer),
                            background: styles.button_color || '#ff6b6b',
                            cursor: 'pointer',
                            fonts: [
                                {
                                    text: styles.button_text || 'SPIN',
                                    top: (styles.button_text_top !== undefined ? parseInt(styles.button_text_top) : -10) + 'px',
                                    fontSize: (parseInt(styles.button_font_size) || 18) + 'px',
                                    lineHeight: (parseInt(styles.button_font_size) || 18) + 'px',
                                    fontWeight: styles.font_weight || '700',
                                    fontColor: styles.button_text_color || '#ffffff',
                                }
                            ]
                        }
                    ];
                })(),
                start: function () {
                    // Prevent multiple spins
                    if (self.isSpinning) {
                        return;
                    }

                    // Find the form and trigger form submission logic
                    const $form = self.$container.find('.swp-entry-form');

                    // Validate form
                    if (!self.validateForm($form)) {
                        return; // Stop if validation fails
                    }

                    // Check if OTP is enabled and required
                    const otpEnabled = self.wheelData && self.wheelData.formSettings && self.wheelData.formSettings.enable_otp;

                    if (otpEnabled && !self.otpVerified) {
                        self.showAlert(swpData.i18n.verifyOtpFirst, 'error');
                        return;
                    }

                    // Get form data
                    self.userData = {
                        name: $form.find('[name="user_name"]').val(),
                        email: $form.find('[name="user_email"]').val(),
                        phone: $form.find('[name="user_phone"]').val()
                    };

                    // Set spinning flag
                    self.isSpinning = true;

                    // Show loader on spin button
                    self.showButtonLoader();

                    // Trigger the spin with proper prize calculation
                    self.handleSpin();
                },
                defaultConfig: {
                    gutter: parseInt(styles.gutter) || 0,
                    stopRange: parseFloat(styles.stop_range) || 0,
                    offsetDegree: parseInt(styles.offset_degree) || 0,
                    // Control animation parameters based on enable_animations setting
                    speed: enableAnimations ? (parseInt(styles.speed) || 20) : 0,
                    accelerationTime: enableAnimations ? (parseInt(styles.acceleration_time) || 2500) : 0,
                    decelerationTime: enableAnimations ? (parseInt(styles.deceleration_time) || 2500) : 0
                },
                defaultStyle: {
                    fontColor: styles.font_color || '#000000',
                    fontSize: (parseInt(styles.font_size) || 18) + 'px',
                    fontWeight: styles.font_weight || '400',
                    wordWrap: false,
                    lengthLimit: (parseInt(styles.length_limit) || 90) + '%',
                    lineClamp: Math.max(
                        1,
                        parseInt(styles.line_clamp, 10) || 3,
                        parseInt(self._prizeLabelMaxLines, 10) || 3
                    )
                },
                // Callback when wheel animation ends
                end: (prize) => {
                    // Stop sound and show result when wheel animation completely ends
                    self.stopSound('spin');
                    
                    // Add 500ms delay before showing result for better readability
                    setTimeout(() => {
                        self.showResult(self.wheelData.prizes[prize.index]);
                    }, 200);
                },
                // Custom drawing hook for text rotation
                afterDraw: function(ctx) {
                    if (!textRotations || textRotations.every(r => r === 0)) return;
                    
                    const prizeRotation = parseInt(self.wheelData.styles?.prize_text_rotation) || 0;
                    if (prizeRotation === 0) return;
                    
                    // Apply custom text rotation via canvas transform
                    self.applyTextRotation(ctx, this, textRotations);
                }
            });

            // Make canvas responsive - ensure it scales properly
            setTimeout(() => {
                const $canvas = $(canvasSelector);
                const canvas = $canvas.find('canvas')[0];
                if (canvas) {
                    // Set canvas to be responsive
                    canvas.style.width = '100%';
                    canvas.style.height = 'auto';
                    canvas.style.maxWidth = '100%';
                    canvas.style.display = 'block';
                }
            }, 100);

            // Universal button click handler for both desktop and mobile
            setTimeout(() => {
                const $canvas = $(canvasSelector);
                const canvas = $canvas.find('canvas')[0];

                if (canvas) {
                    // Handle all interaction types: click, touch, pointer
                    const handleButtonClick = function(e) {
                        e.preventDefault();
                        e.stopPropagation();
                        
                        // Get coordinates from event
                        let clientX, clientY;
                        if (e.type.startsWith('touch')) {
                            if (e.touches && e.touches.length > 0) {
                                clientX = e.touches[0].clientX;
                                clientY = e.touches[0].clientY;
                            } else {
                                return; // No touch data
                            }
                        } else {
                            clientX = e.clientX;
                            clientY = e.clientY;
                        }

                        const rect = canvas.getBoundingClientRect();
                        const clickX = clientX - rect.left;
                        const clickY = clientY - rect.top;

                        // Calculate center of canvas in CSS pixels
                        const centerX = rect.width / 2;
                        const centerY = rect.height / 2;
                        
                        // Calculate distance from center in CSS pixels
                        const distanceFromCenter = Math.sqrt(
                            Math.pow(clickX - centerX, 2) + 
                            Math.pow(clickY - centerY, 2)
                        );
                        
                        // Use button radius directly - it's already in visual pixels
                        // The canvas scales automatically based on DPR, but click coordinates 
                        // and the configured button radius are both in CSS pixels
                        const buttonRadiusInCSSPixels = self.buttonRadius || 50;
                        
                        // Check if click is within the button
                        if (distanceFromCenter <= buttonRadiusInCSSPixels) {
                            if (!self.isSpinning) {
                                const $form = self.$container.find('.swp-entry-form');
                                if (self.validateForm($form)) {
                                    const otpEnabled = self.wheelData && self.wheelData.formSettings && self.wheelData.formSettings.enable_otp;
                                    if (otpEnabled && !self.otpVerified) {
                                        self.showAlert(swpData.i18n.verifyOtpFirst, 'error');
                                        return;
                                    }
                                    self.userData = {
                                        name: $form.find('[name="user_name"]').val(),
                                        email: $form.find('[name="user_email"]').val(),
                                        phone: $form.find('[name="user_phone"]').val()
                                    };
                                    self.isSpinning = true;
                                    self.handleSpin();
                                }
                            }
                        }
                    };

                    // Add event listeners for all interaction types
                    canvas.addEventListener('click', handleButtonClick, { passive: false });
                    canvas.addEventListener('touchstart', handleButtonClick, { passive: false });
                    canvas.addEventListener('touchend', function(e) {
                        e.preventDefault();
                        e.stopPropagation();
                    }, { passive: false });
                }
            }, 200);

            // Start auto-rotation after wheel is created
            const enableAutoRotation = this.wheelData.settings && this.wheelData.settings.enable_auto_rotation;

            // Convert to boolean properly - handle 0, "0", false, null, undefined as false
            const shouldAutoRotate = !!(enableAutoRotation && enableAutoRotation != 0 && enableAutoRotation !== "0");

            if (shouldAutoRotate) {
                // Delay slightly to ensure wheel is fully initialized
                setTimeout(function () {
                    self.startAutoRotation();
                }, 500);
            }
        }

        initNineSquares(canvasSelector, prizes) {
            const styles = this.wheelData.styles || {};
            const settings = this.wheelData.settings || {};
            const self = this;
            const dimensions = this.getResponsiveDimensions(styles);

            // Check if animations are enabled
            const enableAnimations = settings.enable_animations != 0 && settings.enable_animations !== "0";

            const radial = this.isCircleStyleRadialLayout(styles);
            const ringGrid = this.isRingGridPreset(styles);
            const grid2x3 = this.isGrid2x3Preset(styles);
            /* lucky-canvas doc: fine z×z grid, prizes span s×s cells, center button s×k. */
            const DOC_Z = 60;
            const DOC_S = 12;
            const DOC_K = 2.8;
            const DOC_RING_PAD_OUT = 10;
            const DOC_RING_PAD_IN = 10;

            let rows;
            let cols;
            if (radial) {
                rows = DOC_Z;
                cols = DOC_Z;
            } else {
                rows = parseInt(styles.grid_rows, 10) || 3;
                cols = parseInt(styles.grid_cols, 10) || 3;
                if (ringGrid) {
                    const rn = this.normalizeRingGridSize(rows, cols);
                    rows = rn;
                    cols = rn;
                } else if (grid2x3) {
                    const grown = this.expandGridDimsToFitPrizeCount(rows, cols, prizes.length);
                    rows = grown.rows;
                    cols = grown.cols;
                } else {
                    const rn = this.normalizeRingGridSize(rows, cols);
                    rows = rn;
                    cols = rn;
                }
            }
            const totalCells = rows * cols;
            const buttonCell = Math.floor(totalCells / 2); // Classic 3×3 center only; unused when radial
            const gridGutter = radial ? 0 : (parseInt(styles.grid_gutter, 10) || 5);
            const blockPad = radial ? 0 : (parseInt(styles.padding, 10) || 0);
            const gridPadForMetrics = radial
                ? (DOC_RING_PAD_OUT + DOC_RING_PAD_IN)
                : ((ringGrid || grid2x3) ? this.sumBlocksConfigPadding(styles) : (parseInt(styles.padding, 10) || 0));
            const gridMetrics = this.getGridCellMetrics(
                dimensions,
                (radial || ringGrid || grid2x3) ? Object.assign({}, styles, { padding: gridPadForMetrics }) : styles,
                rows,
                cols,
                gridGutter
            );
            const rawGridBr = parseInt(styles.grid_border_radius, 10);
            const cellBorderRadiusPx = Number.isFinite(rawGridBr) ? rawGridBr : 0;
            /* Circle Style radial: circular cells (Infinity). Ring Grid uses saved border radius (px). */
            const gridCellBorderRadius = radial ? Infinity : cellBorderRadiusPx;
            const blockBorderRadiusPx = radial
                ? Infinity
                : (cellBorderRadiusPx + 'px');

            const gridPrizes = [];
            this._radialPrizeToCells = null;
            this._grid2x3LuckyArrayIndexByWheelIndex = null;

            if (radial) {
                const visibleRadial = prizes
                    .map((p, idx) => ({ p, idx }))
                    .filter(({ p }) => swpNineSquaresPrizeIsRenderable(p));
                const nVisible = visibleRadial.length;
                const ringLayouts = this.getLuckyGridRadialRingLayoutsEven(DOC_Z, DOC_S, nVisible);
                const prizeToCells = {};
                const prizeSpanH = gridMetrics.cellH * DOC_S;
                const prizeSpanW = gridMetrics.cellW * DOC_S;
                const ringShadow = styles.grid_shadow || '';
                for (let i = 0; i < nVisible; i++) {
                    const layout = ringLayouts[i];
                    const mapIdx = visibleRadial[i].idx;
                    const cellPrize = this.normalizePrizeForGridCell(
                        visibleRadial[i].p,
                        prizeSpanH,
                        prizeSpanW
                    );
                    gridPrizes.push(Object.assign({}, cellPrize, layout, {
                        index: mapIdx,
                        background: cellPrize.background,
                        borderRadius: Infinity,
                        shadow: ringShadow
                    }));
                    if (!prizeToCells[mapIdx]) {
                        prizeToCells[mapIdx] = [];
                    }
                    prizeToCells[mapIdx].push(gridPrizes.length - 1);
                }
                this._radialPrizeToCells = prizeToCells;
            } else if (ringGrid) {
                const coordsList = this.getRingGridAllPrizeCoords(rows);
                for (let c = 0; c < coordsList.length && c < prizes.length; c++) {
                    const xy = coordsList[c];
                    const cellPrize = this.normalizePrizeForGridCell(prizes[c], gridMetrics.cellH);
                    gridPrizes.push({
                        x: xy[0],
                        y: xy[1],
                        ...cellPrize,
                        borderRadius: gridCellBorderRadius,
                        shadow: styles.grid_shadow || ''
                    });
                }
            } else if (grid2x3) {
                const cells = [];
                let prizeIdx = 0;
                for (let y = 0; y < rows && prizeIdx < prizes.length; y++) {
                    for (let x = 0; x < cols && prizeIdx < prizes.length; x++) {
                        const cellPrize = this.normalizePrizeForGridCell(prizes[prizeIdx], gridMetrics.cellH);
                        cells.push({
                            x: x,
                            y: y,
                            ...cellPrize,
                            borderRadius: gridCellBorderRadius,
                            shadow: styles.grid_shadow || ''
                        });
                        prizeIdx++;
                    }
                }
                const totalCells = rows * cols;
                let ordered = cells;
                if (cells.length === totalCells && totalCells > 0) {
                    const visitOrder = this.getFullGridPrizeVisitOrderClockwise(rows, cols);
                    if (visitOrder && visitOrder.length === cells.length) {
                        const key = (x, y) => `${x},${y}`;
                        const coordMap = new Map(cells.map((c) => [key(c.x, c.y), c]));
                        const mapped = visitOrder.map(([x, y]) => coordMap.get(key(x, y))).filter(Boolean);
                        if (mapped.length === cells.length) {
                            ordered = mapped;
                            const luckyIdxByWheel = [];
                            mapped.forEach((entry, arrIdx) => {
                                const w = entry.index;
                                if (w !== undefined && w !== null) {
                                    luckyIdxByWheel[w] = arrIdx;
                                }
                            });
                            this._grid2x3LuckyArrayIndexByWheelIndex = luckyIdxByWheel;
                        }
                    }
                }
                gridPrizes.push(...ordered);
            } else {
                let prizeIdx = 0;
                for (let y = 0; y < rows; y++) {
                    for (let x = 0; x < cols; x++) {
                        const cellIdx = y * cols + x;
                        if (cellIdx === buttonCell) {
                            continue;
                        }
                        if (prizeIdx < prizes.length) {
                            const cellPrize = this.normalizePrizeForGridCell(prizes[prizeIdx], gridMetrics.cellH);
                            gridPrizes.push({
                                x: x,
                                y: y,
                                ...cellPrize,
                                borderRadius: gridCellBorderRadius,
                                shadow: styles.grid_shadow || ''
                            });
                            prizeIdx++;
                        }
                    }
                }
            }

            let finalizeCellW = gridMetrics.cellW;
            let finalizeCellH = gridMetrics.cellH;
            if (radial) {
                finalizeCellW = gridMetrics.cellW * DOC_S;
                finalizeCellH = gridMetrics.cellH * DOC_S;
            }
            const selfFinalize = this;
            gridPrizes.forEach((gp) => {
                selfFinalize.finalizeNineSquaresPrizeFontLayout(gp, finalizeCellW, finalizeCellH, styles);
            });

            // Grid center button: big-wheel default button_text_top is -10px (arc layout); that clips text in square cells.
            const gridBtnFontSize = parseInt(styles.button_font_size, 10) || parseInt(styles.font_size, 10) || 18;
            const gridBtnLhPx = Math.max(1, parseInt(styles.button_line_height, 10) || gridBtnFontSize);
            const gridBtnLine = gridBtnLhPx + 'px';
            const rawGridBtnTop = styles.button_text_top;
            const parsedGridBtnTop = rawGridBtnTop !== undefined && rawGridBtnTop !== null && String(rawGridBtnTop).trim() !== ''
                ? parseInt(rawGridBtnTop, 10)
                : NaN;
            // 0 and -10 are not valid vertical centers for grid cells (0 pins text to the top; -10 is big-wheel default).
            const useGridCenteredButtonText = !Number.isFinite(parsedGridBtnTop)
                || parsedGridBtnTop === -10
                || parsedGridBtnTop === 0;
            const gridBtnTop = radial
                ? (useGridCenteredButtonText ? '43%' : ((Number.isFinite(parsedGridBtnTop) ? parsedGridBtnTop : 0) + 'px'))
                : (useGridCenteredButtonText
                    ? (this.luckyGridCenterButtonTopPx(gridMetrics.cellH, gridBtnFontSize, gridBtnLhPx) + 'px')
                    : (parsedGridBtnTop + 'px'));

            const panelBrPx = parseInt(styles.grid_panel_border_radius, 10);
            const blockRadiusFallback = Number.isFinite(panelBrPx) && panelBrPx >= 0 ? (panelBrPx + 'px') : '0';
            const luckyBlocks = radial
                ? [
                    {
                        padding: DOC_RING_PAD_OUT + 'px',
                        background: styles.button_color || '#869cfa',
                        borderRadius: Infinity
                    },
                    {
                        padding: DOC_RING_PAD_IN + 'px',
                        background: styles.background_color || '#e9e8fe',
                        borderRadius: Infinity
                    }
                ]
                : ((ringGrid || grid2x3)
                    ? this.parseNineSquaresBlocksFromConfig(styles, 10, blockRadiusFallback)
                    : [
                        {
                            padding: blockPad + 'px',
                            background: styles.background_color || '#ffffff',
                            borderRadius: blockBorderRadiusPx
                        }
                    ]);

            const radialButtonDiameterPx = Math.max(0, (parseInt(styles.button_radius, 10) || 0) * 2);
            const radialBaseCanvasPx = Math.max(1, Math.min(
                parseInt(dimensions.width, 10) || 0,
                parseInt(dimensions.height, 10) || 0
            ));
            const radialButtonSpan = radialButtonDiameterPx > 0
                ? Math.max(DOC_S * 1.2, Math.min(DOC_Z * 0.9, (radialButtonDiameterPx / radialBaseCanvasPx) * DOC_Z))
                : (DOC_S * DOC_K);
            const luckyButtons = radial
                ? [
                    {
                        x: DOC_Z / 2 - radialButtonSpan / 2,
                        y: DOC_Z / 2 - radialButtonSpan / 2,
                        col: radialButtonSpan,
                        row: radialButtonSpan,
                        background: styles.button_color || '#ff6b6b',
                        borderRadius: Infinity,
                        fonts: [
                            {
                                text: styles.button_text || 'START',
                                top: gridBtnTop,
                                fontSize: gridBtnFontSize + 'px',
                                lineHeight: gridBtnLine,
                                fontWeight: styles.font_weight || '700',
                                fontColor: styles.button_text_color || '#ffffff',
                                wordWrap: false
                            }
                        ]
                    }
                ]
                : (grid2x3
                    ? []
                    : [
                        {
                            x: Math.floor(cols / 2),
                            y: Math.floor(rows / 2),
                            background: styles.button_color || '#ff6b6b',
                            borderRadius: gridCellBorderRadius,
                            fonts: [
                                {
                                    text: styles.button_text || 'START',
                                    top: gridBtnTop,
                                    fontSize: gridBtnFontSize + 'px',
                                    lineHeight: gridBtnLine,
                                    fontWeight: styles.font_weight || '700',
                                    fontColor: styles.button_text_color || '#ffffff',
                                    wordWrap: false
                                }
                            ]
                        }
                    ]);

            const textRotationGrid = parseInt(styles.prize_text_rotation, 10) || 0;
            const textOffsetXGrid = parseInt(styles.prize_text_offset_x, 10) || 0;
            const textOffsetYGrid = parseInt(styles.prize_text_offset_y, 10) || 0;
            const prizeImgRotGrid = parseInt(styles.prize_image_rotation, 10) || 0;
            if (textRotationGrid !== 0 || textOffsetXGrid !== 0 || textOffsetYGrid !== 0 || prizeImgRotGrid !== 0) {
                self.prePatchCanvasContext(canvasSelector, textRotationGrid, textOffsetXGrid, textOffsetYGrid);
            }

            const requestNineSquaresSpin = function () {
                if (self.isSpinning) {
                    return;
                }
                const $form = self.$container.find('.swp-entry-form');
                if (!self.validateForm($form)) {
                    return;
                }
                const otpEnabled = self.wheelData && self.wheelData.formSettings && self.wheelData.formSettings.enable_otp;
                if (otpEnabled && !self.otpVerified) {
                    self.showAlert(swpData.i18n.verifyOtpFirst, 'error');
                    return;
                }
                self.userData = {
                    name: $form.find('[name="user_name"]').val(),
                    email: $form.find('[name="user_email"]').val(),
                    phone: $form.find('[name="user_phone"]').val()
                };
                self.isSpinning = true;
                self.showButtonLoader();
                self.handleSpin();
            };

            // Lucky-canvas configuration for Nine Squares (LuckyGrid: start/end are config callbacks)
            this.wheelInstance = new window.LuckyCanvas.LuckyGrid(canvasSelector, {
                width: dimensions.width,
                height: dimensions.height,
                rows: rows,
                cols: cols,
                blocks: luckyBlocks,
                prizes: gridPrizes,
                buttons: luckyButtons,
                start: requestNineSquaresSpin,
                end: function (prize) {
                    self.stopSound('spin');
                    const idx = prize && prize.index !== undefined ? prize.index : 0;
                    const won = self.wheelData.prizes[idx];
                    if (!won) {
                        self.isSpinning = false;
                        return;
                    }
                    setTimeout(function () {
                        self.showResult(won);
                    }, 200);
                },
                defaultConfig: {
                    gutter: gridGutter,
                    // Control animation parameters based on enable_animations setting
                    speed: enableAnimations ? (parseInt(styles.speed) || 20) : 0,
                    accelerationTime: enableAnimations ? (parseInt(styles.acceleration_time) || 2500) : 0,
                    decelerationTime: enableAnimations ? (parseInt(styles.deceleration_time) || 2500) : 0
                },
                defaultStyle: {
                    borderRadius: gridCellBorderRadius,
                    background: radial
                        ? '#bac5ee'
                        : ((ringGrid || grid2x3) ? (styles.grid_default_background || '#b8c5f2') : '#ffffff'),
                    shadow: styles.grid_shadow || '',
                    fontColor: styles.font_color || '#000000',
                    fontSize: (parseInt(styles.font_size) || 18) + 'px',
                    fontWeight: styles.font_weight || '400',
                    wordWrap: false,
                    lengthLimit: radial
                        ? '82%'
                        : ((parseInt(styles.length_limit) || 90) + '%'),
                    lineClamp: radial
                        ? 2
                        : Math.max(
                            1,
                            parseInt(styles.line_clamp, 10) || 3,
                            parseInt(self._prizeLabelMaxLines, 10) || 3
                        )
                },
                activeStyle: {
                    background: styles.active_background || '#ffd700',
                    fontColor: styles.active_font_color || '#000000',
                    shadow: styles.active_shadow || '0 4px 16px rgba(255,215,0,0.5)'
                }
            });

            if (grid2x3) {
                self.$container.find('.swp-grid-external-spin')
                    .off('click.swpGrid2x3')
                    .on('click.swpGrid2x3', function (e) {
                        e.preventDefault();
                        requestNineSquaresSpin();
                    });
            }

            // Make canvas responsive
            setTimeout(() => {
                const $canvas = $(canvasSelector);
                const canvas = $canvas.find('canvas')[0];
                if (canvas) {
                    canvas.style.width = '100%';
                    canvas.style.height = 'auto';
                    canvas.style.maxWidth = '100%';
                    canvas.style.display = 'block';
                }
            }, 100);
        }

        initSlotMachine(canvasSelector, prizes) {
            const styles = this.wheelData.styles || {};
            const settings = this.wheelData.settings || {};
            const self = this;
            const dimensions = this.getResponsiveDimensions(styles);

            // Check if animations are enabled
            const enableAnimations = settings.enable_animations != 0 && settings.enable_animations !== "0";

            const slotBlocks = parseInt(styles.slot_blocks) || 3;
            const slotSpeed = enableAnimations ? (parseInt(styles.speed) || 10) : 0;
            const slotDirection = styles.slot_direction || 'down';

            // Create slots configuration
            const slots = [];
            for (let i = 0; i < slotBlocks; i++) {
                slots.push({
                    speed: slotSpeed,
                    direction: slotDirection === 'up' ? 1 : -1
                });
            }

            // Lucky-canvas configuration for Slot Machine
            this.wheelInstance = new window.LuckyCanvas.SlotMachine(canvasSelector, {
                width: dimensions.width,
                height: dimensions.height,
                blocks: [
                    {
                        padding: (styles.padding || 0) + 'px',
                        background: styles.background_color || '#ffffff'
                    }
                ],
                slots: slots,
                prizes: prizes.map(prize => ({
                    ...prize,
                    imgs: prize.imgs || []
                })),
                defaultConfig: {
                    rowSpacing: parseInt(styles.slot_row_spacing) || 0,
                    colSpacing: parseInt(styles.slot_col_spacing) || 10,
                    // Control animation parameters based on enable_animations setting
                    speed: slotSpeed,
                    accelerationTime: enableAnimations ? (parseInt(styles.acceleration_time) || 2500) : 0,
                    decelerationTime: enableAnimations ? (parseInt(styles.deceleration_time) || 2500) : 0
                },
                defaultStyle: {
                    background: '#ffffff',
                    fontColor: styles.font_color || '#000000',
                    fontSize: (parseInt(styles.font_size) || 18) + 'px',
                    fontWeight: styles.font_weight || '400',
                    wordWrap: false,
                    lengthLimit: (parseInt(styles.length_limit) || 90) + '%',
                    lineClamp: parseInt(styles.line_clamp) || 3
                }
            });

            // For slot machine, clicking the wheel canvas itself won't work
            // The form submit button will trigger the spin
            
            // Make canvas responsive
            setTimeout(() => {
                const $canvas = $(canvasSelector);
                const canvas = $canvas.find('canvas')[0];
                if (canvas) {
                    canvas.style.width = '100%';
                    canvas.style.height = 'auto';
                    canvas.style.maxWidth = '100%';
                    canvas.style.display = 'block';
                }
            }, 100);
        }

        handleSpin() {
            // Clear any previous alerts
            this.clearAlert();

            // Stop auto-rotation if active
            if (this.isAutoRotating) {
                this.stopAutoRotation();
            }

            this.$container.find('.swp-form-control').prop('readonly', true);

            // Save to server first (server will calculate the prize)
            this.saveEntry(null, (response) => {
                // Hide loader after response
                this.hideButtonLoader();

                if (response && response.success) {
                    // Get the server-calculated prize index from response
                    const serverPrizeIndex = response.data && response.data.prize && response.data.prize.index !== undefined
                        ? response.data.prize.index
                        : this.calculatePrize(); // Fallback to client calculation
                    
                    // Start spinning animation with server's prize
                    this.spinToWin(serverPrizeIndex);
                } else {
                    // Reset spinning flag on error
                    this.isSpinning = false;
                    // Show error alert
                    const errorMessage = (response && response.data && response.data.message)
                        ? response.data.message
                        : (swpData.i18n.error || 'An error occurred. Please try again.');
                    this.showAlert(errorMessage, 'error');
                }
            });
        }

        calculatePrize() {
            const prizes = this.wheelData.prizes;
            const random = Math.random() * 100;
            let cumulative = 0;

            for (let i = 0; i < prizes.length; i++) {
                cumulative += parseFloat(prizes[i].probability || 0);
                if (random <= cumulative) {
                    return i;
                }
            }

            return 0;
        }

        spinToWin(prizeIndex) {
            const self = this;
            
            if (!this.wheelInstance || !this.wheelInstance.play) {
                console.error('Wheel instance not properly initialized');
                return;
            }

            // Play spinning sound (loop it)
            if (this.sounds.spin) {
                this.sounds.spin.loop = true;
            }
            this.playSound('spin');

            this.wheelInstance.play();
            
            // Calculate actual spin duration from wheel settings
            const styles = this.wheelData.settings || {};
            const enableAnimations = styles.enable_animations != 0;
            const accelerationTime = enableAnimations ? (parseInt(styles.acceleration_time) || 2500) : 0;

            /* LuckyGrid stop(n) expects the n-th entry in the configured prizes[] array, not the wheel prize id. */
            let stopCellIndex = prizeIndex;
            if (this.wheelType === 'nine_squares' && this._radialPrizeToCells) {
                const bucket = this._radialPrizeToCells[prizeIndex];
                if (bucket && bucket.length) {
                    stopCellIndex = bucket[Math.floor(Math.random() * bucket.length)];
                }
            } else if (this.wheelType === 'nine_squares' && this._grid2x3LuckyArrayIndexByWheelIndex) {
                const mapped = this._grid2x3LuckyArrayIndexByWheelIndex[prizeIndex];
                if (mapped !== undefined && mapped !== null) {
                    stopCellIndex = mapped;
                }
            }
            
            // Stop the wheel after acceleration phase
            setTimeout(() => {
                self.wheelInstance.stop(stopCellIndex);
            }, accelerationTime);
        }

        saveEntry(prizeIndex, callback) {
            const self = this;
            const postData = {
                action: 'swp_spin_wheel',
                nonce: swpData.nonce,
                wheel_id: this.wheelId,
                user_name: this.userData.name,
                user_email: this.userData.email,
                user_phone: this.userData.phone,
            };
            if (this.wheelData.enableUtmCapture) {
                swpUtmMergeUrlIntoSessionStorage();
                const utm = swpUtmReadFromSessionStorage();
                if (utm && Object.keys(utm).length) {
                    postData.utm_payload = JSON.stringify(utm);
                }
            }
            const $terms = this.$container.find('[name="swp_accept_terms"]');
            if ($terms.length) {
                postData.swp_accept_terms = $terms.is(':checked') ? '1' : '0';
            }
            $.ajax({
                url: swpData.ajaxurl,
                type: 'POST',
                contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
                data: postData,
                success: callback,
                error: function (xhr, status, error) {
                    console.log('AJAX Error:', {
                        status: status,
                        error: error,
                        response: xhr.responseText
                    });
                    // Hide loader on error
                    self.hideButtonLoader();
                    // Reset spinning flag on error
                    self.isSpinning = false;
                    // Show error message
                    self.showAlert(swpData.i18n.error || 'An error occurred. Please try again.', 'error');
                    callback({ success: false, data: { message: swpData.i18n.error } });
                }
            });
        }

        showResult(prize) {
            // Reset spinning flag
            this.isSpinning = false;

            const isWinner = prize.is_winning == 1;
            
            // Play win sound if user won
            if (isWinner) {
                this.playSound('win');
            }
            
            const $result = this.$container.find('.swp-result-section');
            const $icon = $result.find('.swp-result-icon');
            const $title = $result.find('.swp-result-title');
            const $message = $result.find('.swp-result-message');
            const $prizeDiv = $result.find('.swp-result-prize');
            const $participantInfo = $result.find('.swp-participant-info');

            // Set content
            $icon.removeClass('win lose').addClass(isWinner ? 'win' : 'lose');
            $title.removeClass('win lose').addClass(isWinner ? 'win' : 'lose')
                .text(isWinner ? swpData.i18n.congratulations : swpData.i18n.sorry);

            if (isWinner) {
                $message.text(swpData.i18n.wonAmazingPrize);

                // Get prize label and code, handle undefined
                const prizeLabel = prize.label || prize.text || (swpData.i18n.prizeLabelFallback || 'Prize');
                const prizeCode = prize.code || '';
                const codeLabel = swpData.i18n.prizeCodeLabel || 'Code:';

                $prizeDiv.html(`
                    <h4>${swpData.i18n.yourPrize}</h4>
                    <div class="prize-label">${prizeLabel}</div>
                    ${prizeCode ? `<div class="prize-code">${codeLabel} ${prizeCode}</div>` : ''}
                `);
                $prizeDiv.show();

                // Show confetti
                if (this.wheelData.settings && this.wheelData.settings.enable_confetti) {
                    this.showConfetti();
                }
            } else {
                $message.text(swpData.i18n.betterLuckNextTime);
                $prizeDiv.hide();
            }

            // Display participant information if enabled by admin
            const showParticipantInfo = this.formSettings && this.formSettings.show_participant_info !== 0;

            if (showParticipantInfo && isWinner) {
                const i18n = swpData.i18n || {};
                let participantHtml = '<div class="swp-participant-details">';
                participantHtml += '<div class="swp-participant-header">' + (i18n.winnerInformation || 'Winner Information') + '</div>';

                if (this.userData.name) {
                    participantHtml += `
                        <div class="swp-participant-item">
                            <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
                                <path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6m2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0m4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4m-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664z"/>
                            </svg>
                            <span class="swp-participant-label">${i18n.participantName || 'Name:'}</span>
                            <span class="swp-participant-value">${this.userData.name}</span>
                        </div>
                    `;
                }

                if (this.userData.email) {
                    participantHtml += `
                        <div class="swp-participant-item">
                            <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
                                <path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1zm13 2.383-4.708 2.825L15 11.105zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741M1 11.105l4.708-2.897L1 5.383z"/>
                            </svg>
                            <span class="swp-participant-label">${i18n.participantEmail || 'Email:'}</span>
                            <span class="swp-participant-value">${this.userData.email}</span>
                        </div>
                    `;
                }

                if (this.userData.phone) {
                    participantHtml += `
                        <div class="swp-participant-item">
                            <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
                                <path d="M3.654 1.328a.678.678 0 0 0-1.015-.063L1.605 2.3c-.483.484-.661 1.169-.45 1.77a17.568 17.568 0 0 0 4.168 6.608 17.569 17.569 0 0 0 6.608 4.168c.601.211 1.286.033 1.77-.45l1.034-1.034a.678.678 0 0 0-.063-1.015l-2.307-1.794a.678.678 0 0 0-.58-.122l-2.19.547a1.745 1.745 0 0 1-1.657-.459L5.482 8.062a1.745 1.745 0 0 1-.46-1.657l.548-2.19a.678.678 0 0 0-.122-.58L3.654 1.328zM1.884.511a1.745 1.745 0 0 1 2.612.163L6.29 2.98c.329.423.445.974.315 1.494l-.547 2.19a.678.678 0 0 0 .178.643l2.457 2.457a.678.678 0 0 0 .644.178l2.189-.547a1.745 1.745 0 0 1 1.494.315l2.306 1.794c.829.645.905 1.87.163 2.611l-1.034 1.034c-.74.74-1.846 1.065-2.877.702a18.634 18.634 0 0 1-7.01-4.42 18.634 18.634 0 0 1-4.42-7.009c-.362-1.03-.037-2.137.703-2.877L1.885.511z"/>
                            </svg>
                            <span class="swp-participant-label">${i18n.participantPhone || 'Phone:'}</span>
                            <span class="swp-participant-value">${this.userData.phone}</span>
                        </div>
                    `;
                }

                participantHtml += '</div>';
                $participantInfo.html(participantHtml).show();
            } else {
                // Hide participant info if disabled or not a winner
                $participantInfo.hide();
            }

            // Show modal
            $result.fadeIn(300);
        }

        closeResult() {
            this.$container.find('.swp-result-section').fadeOut(300);

            // Option 1: Reload page (fresh start)
            setTimeout(() => {
                location.reload();
            }, 500);

            // Option 2: Reset without reload (uncomment if preferred)
            // this.userData = {};
            // const $form = this.$container.find('.swp-entry-form');
            // $form[0].reset();
        }

        showAlert(message, type = 'error') {
            const $alertContainer = this.$container.find('.swp-alert-container');

            // Set message and type
            $alertContainer
                .removeClass('error success warning info')
                .addClass(type)
                .text(message)
                .show();
        }

        clearAlert() {
            const $alertContainer = this.$container.find('.swp-alert-container');
            $alertContainer
                .removeClass('error success warning info')
                .text('')
                .hide();
        }

        /**
         * Spin overlay mount: big wheel keeps legacy full-wrapper cover; grid/slot only cover the canvas column.
         */
        _getSpinLoaderMount() {
            if (this.wheelType === 'big_wheel') {
                return this.$container.find('.swp-wheel-wrapper');
            }
            return this.$container.find('.swp-wheel-section');
        }

        showButtonLoader() {
            // Disable submit button
            const $submitBtn = this.$container.find('.swp-btn-submit');
            $submitBtn.prop('disabled', true);
            this.$container.find('.swp-grid-external-spin').prop('disabled', true);

            // Store the original button configuration for canvas
            if (!this.originalButtonConfig) {
                this.originalButtonConfig = {
                    text: this.wheelData.settings.button_text || 'SPIN',
                    background: this.wheelData.settings.button_color || '#ff6b6b',
                    radius: this.wheelData.settings.button_radius
                };
            }

            const $mount = this._getSpinLoaderMount();
            let $overlay = $mount.find('.swp-canvas-overlay');
            const spinLabel = (swpData.i18n && swpData.i18n.spinning) ? swpData.i18n.spinning : 'Spinning...';
            const safeLabel = $('<div/>').text(spinLabel).html();

            if (!$overlay.length) {
                $mount.append(
                    '<div class="swp-canvas-overlay" role="status" aria-live="polite" aria-busy="true">' +
                    '<span class="swp-canvas-overlay-inner">' +
                    '<span class="swp-canvas-overlay-spinner" aria-hidden="true"></span>' +
                    '<span class="swp-canvas-overlay-text">' + safeLabel + '</span>' +
                    '</span></div>'
                );
                $overlay = $mount.find('.swp-canvas-overlay');
            } else {
                $overlay.attr({ role: 'status', 'aria-live': 'polite', 'aria-busy': 'true' });
                let $text = $overlay.find('.swp-canvas-overlay-text');
                if (!$text.length) {
                    $overlay.html(
                        '<span class="swp-canvas-overlay-inner">' +
                        '<span class="swp-canvas-overlay-spinner" aria-hidden="true"></span>' +
                        '<span class="swp-canvas-overlay-text">' + safeLabel + '</span>' +
                        '</span>'
                    );
                } else {
                    $text.text(spinLabel);
                }
            }

            if (this.wheelType === 'nine_squares') {
                this.$container.find('.swp-wheel-section').addClass('is-spinning');
            }
            $overlay.addClass('active');
        }

        hideButtonLoader() {
            // Re-enable submit button
            const $submitBtn = this.$container.find('.swp-btn-submit');
            $submitBtn.prop('disabled', false);
            this.$container.find('.swp-grid-external-spin').prop('disabled', false);

            const $mount = this._getSpinLoaderMount();
            if (this.wheelType === 'nine_squares') {
                this.$container.find('.swp-wheel-section').removeClass('is-spinning');
            }
            $mount.find('.swp-canvas-overlay').removeClass('active').attr('aria-busy', 'false');
        }

        showConfetti() {
            const $canvas = $(`#swp-confetti-${this.wheelId}`);
            if (!$canvas.length) return;

            const canvas = $canvas[0];
            const ctx = canvas.getContext('2d');
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;

            const confetti = [];
            const confettiCount = 100;
            const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7', '#dfe6e9'];

            for (let i = 0; i < confettiCount; i++) {
                confetti.push({
                    x: Math.random() * canvas.width,
                    y: Math.random() * canvas.height - canvas.height,
                    r: Math.random() * 6 + 4,
                    d: Math.random() * confettiCount,
                    color: colors[Math.floor(Math.random() * colors.length)],
                    tilt: Math.floor(Math.random() * 10) - 10,
                    tiltAngleIncremental: Math.random() * 0.07 + 0.05,
                    tiltAngle: 0
                });
            }

            function draw() {
                ctx.clearRect(0, 0, canvas.width, canvas.height);

                confetti.forEach((c, i) => {
                    ctx.beginPath();
                    ctx.lineWidth = c.r / 2;
                    ctx.strokeStyle = c.color;
                    ctx.moveTo(c.x + c.tilt + c.r, c.y);
                    ctx.lineTo(c.x + c.tilt, c.y + c.tilt + c.r);
                    ctx.stroke();

                    c.tiltAngle += c.tiltAngleIncremental;
                    c.y += (Math.cos(c.d) + 3 + c.r / 2) / 2;
                    c.tilt = Math.sin(c.tiltAngle - i / 3) * 15;

                    if (c.y > canvas.height) {
                        confetti[i] = {
                            ...c,
                            x: Math.random() * canvas.width,
                            y: -10
                        };
                    }
                });

                requestAnimationFrame(draw);
            }

            draw();

            // Remove confetti after 5 seconds
            setTimeout(() => {
                $canvas.fadeOut(1000, () => {
                    ctx.clearRect(0, 0, canvas.width, canvas.height);
                });
            }, 5000);
        }

        getRandomColor() {
            const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7'];
            return colors[Math.floor(Math.random() * colors.length)];
        }

        applyTextRotation(ctx, wheelInstance, rotations) {
            // This is a placeholder for custom text rotation
            // Lucky-canvas doesn't support direct text rotation in fonts config
            // The rotation will be handled via CSS transform on the canvas element
            // Or we can hook into the draw cycle to manually rotate text
            
            // Note: Due to lucky-canvas architecture, direct canvas manipulation
            // in afterDraw may cause visual issues. 
            // Better approach: Apply CSS transform to specific text elements if possible
            // or use the defaultConfig rotation if available in newer versions
        }

        getPrizeImageSrcAllowlist() {
            const set = new Set();
            (this.wheelData?.prizes || []).forEach((p) => {
                const u = (p.image || '').trim();
                if (u) {
                    set.add(u);
                }
            });
            return set;
        }

        prizeImageDrawImageIsPrizeImg(imgEl) {
            if (!imgEl || !imgEl.src) {
                return false;
            }
            const allow = this.getPrizeImageSrcAllowlist();
            if (!allow.size) {
                return false;
            }
            const s = imgEl.src.split('?')[0];
            let hit = false;
            allow.forEach((u) => {
                if (!u || hit) {
                    return;
                }
                const uu = u.split('?')[0];
                if (imgEl.src === u || s === uu || s.indexOf(uu) !== -1 || uu.indexOf(s.split('/').pop()) !== -1) {
                    hit = true;
                }
            });
            return hit;
        }

        prePatchCanvasContext(canvasSelector, rotation, offsetX, offsetY) {
            const self = this;
            const $canvas = $(canvasSelector);

            if (!$canvas.length) {
                return;
            }

            let canvas = $canvas.find('canvas')[0];
            if (!canvas) {
                const originalCreateElement = document.createElement.bind(document);
                document.createElement = function (tagName) {
                    const element = originalCreateElement(tagName);

                    if (tagName.toLowerCase() === 'canvas' && !element.__swpLuckyCanvasHooked) {
                        element.__swpLuckyCanvasHooked = true;
                        const originalGetContext = element.getContext.bind(element);
                        element.getContext = function (contextType, ...args) {
                            const ctx = originalGetContext(contextType, ...args);

                            if (contextType === '2d' && !ctx.__swpLuckyPatched) {
                                self.patchCanvasContext(ctx, rotation, offsetX, offsetY);
                            }

                            return ctx;
                        };
                    }

                    return element;
                };

                setTimeout(() => {
                    document.createElement = originalCreateElement;
                }, 100);
            } else {
                const ctx = canvas.getContext('2d');
                if (ctx) {
                    self.patchCanvasContext(ctx, rotation, offsetX, offsetY);
                }
            }
        }

        patchCanvasContext(ctx, rotation, offsetX, offsetY) {
            if (ctx.__swpLuckyPatched) {
                return;
            }
            ctx.__swpLuckyPatched = true;

            const self = this;
            const buttonText = self.wheelData?.styles?.button_text || self.wheelData?.settings?.button_text || 'SPIN';

            if (rotation || offsetX || offsetY) {
                const rotationRad = (rotation * Math.PI) / 180;
                const originalFillText = ctx.fillText.bind(ctx);
                const originalStrokeText = ctx.strokeText.bind(ctx);

                ctx.fillText = function (text, x, y, maxWidth) {
                    if (text === buttonText || text === 'START' || y < 50) {
                        originalFillText(text, x, y, maxWidth);
                        return;
                    }

                    ctx.save();
                    ctx.translate(x + offsetX, y + offsetY);
                    ctx.rotate(rotationRad);
                    const metrics = ctx.measureText(text);
                    const textWidth = metrics.width;
                    originalFillText(text, -textWidth / 2, 0, maxWidth);
                    ctx.restore();
                };

                ctx.strokeText = function (text, x, y, maxWidth) {
                    if (text === buttonText || text === 'START' || y < 50) {
                        originalStrokeText(text, x, y, maxWidth);
                        return;
                    }

                    ctx.save();
                    ctx.translate(x + offsetX, y + offsetY);
                    ctx.rotate(rotationRad);
                    const metrics = ctx.measureText(text);
                    const textWidth = metrics.width;
                    originalStrokeText(text, -textWidth / 2, 0, maxWidth);
                    ctx.restore();
                };
            }

            const originalDrawImage = ctx.drawImage.bind(ctx);
            ctx.drawImage = function () {
                const rotNow = parseInt(self.wheelData?.styles?.prize_image_rotation, 10) || 0;
                const args = arguments;
                if (!rotNow || !self.prizeImageDrawImageIsPrizeImg(args[0])) {
                    return originalDrawImage.apply(ctx, args);
                }
                const rotRad = (rotNow * Math.PI) / 180;
                if (args.length === 5) {
                    const img = args[0];
                    const dx = args[1];
                    const dy = args[2];
                    const dw = args[3];
                    const dh = args[4];
                    ctx.save();
                    ctx.translate(dx + dw / 2, dy + dh / 2);
                    ctx.rotate(rotRad);
                    originalDrawImage(img, -dw / 2, -dh / 2, dw, dh);
                    ctx.restore();
                    return;
                }
                if (args.length === 9) {
                    const img = args[0];
                    const sx = args[1];
                    const sy = args[2];
                    const sw = args[3];
                    const sh = args[4];
                    const dx = args[5];
                    const dy = args[6];
                    const dw = args[7];
                    const dh = args[8];
                    ctx.save();
                    ctx.translate(dx + dw / 2, dy + dh / 2);
                    ctx.rotate(rotRad);
                    originalDrawImage(img, sx, sy, sw, sh, -dw / 2, -dh / 2, dw, dh);
                    ctx.restore();
                    return;
                }
                return originalDrawImage.apply(ctx, args);
            };
        }

        startAutoRotation() {
            if (!this.wheelInstance || !this.wheelInstance.play || this.isAutoRotating) {
                return;
            }

            const self = this;
            this.isAutoRotating = true;

            // Create a smooth continuous rotation using play() and stop() in sequence
            let currentPrizeIndex = 0;
            const prizesCount = this.wheelData.prizes.length;
            const rotationDuration = 1500; // Time to rotate to next prize (in ms) - adjust for speed

            function continuousRotate() {
                if (!self.isAutoRotating || !self.wheelInstance) {
                    return;
                }

                // Move to next prize
                currentPrizeIndex = (currentPrizeIndex + 1) % prizesCount;

                // Start spinning
                self.wheelInstance.play();

                // Stop at the next prize position after a short duration
                setTimeout(function () {
                    if (self.isAutoRotating && self.wheelInstance && self.wheelInstance.stop) {
                        const luckyIdx = (self._grid2x3LuckyArrayIndexByWheelIndex
                            && self._grid2x3LuckyArrayIndexByWheelIndex[currentPrizeIndex] !== undefined
                            && self._grid2x3LuckyArrayIndexByWheelIndex[currentPrizeIndex] !== null)
                            ? self._grid2x3LuckyArrayIndexByWheelIndex[currentPrizeIndex]
                            : currentPrizeIndex;
                        self.wheelInstance.stop(luckyIdx);

                        // Immediately continue to next position for continuous effect
                        setTimeout(function () {
                            if (self.isAutoRotating) {
                                continuousRotate();
                            }
                        }, 100); // Small delay before next rotation
                    }
                }, rotationDuration);
            }

            // Start the continuous rotation
            continuousRotate();
        }

        stopAutoRotation() {
            this.isAutoRotating = false;
            if (this.autoRotationInterval) {
                cancelAnimationFrame(this.autoRotationInterval);
                this.autoRotationInterval = null;
            }
        }

        /**
         * Rotate wheel to a specific prize by label
         * @param {string} label - Prize label to rotate to
         * @param {boolean} skipValidation - Skip form validation (default: false)
         * @return {boolean} - Success status
         */
        rotateByLabel(label, skipValidation = false) {
            // Find prize index by label
            const prizeIndex = this.wheelData.prizes.findIndex(prize => prize.label === label);
            
            if (prizeIndex === -1) {
                console.error(`Prize with label "${label}" not found`);
                console.log('Available prizes:', this.wheelData.prizes.map(p => p.label));
                return false;
            }
            
            return this.rotateByIndex(prizeIndex, skipValidation);
        }

        /**
         * Rotate wheel to a specific prize by index
         * @param {number} prizeIndex - Prize index (0-based)
         * @param {boolean} skipValidation - Skip form validation (default: false)
         * @return {boolean} - Success status
         */
        rotateByIndex(prizeIndex, skipValidation = false) {
            // Validate prize index
            if (prizeIndex < 0 || prizeIndex >= this.wheelData.prizes.length) {
                console.error(`Invalid prize index: ${prizeIndex}. Valid range: 0-${this.wheelData.prizes.length - 1}`);
                return false;
            }

            // Check if already spinning
            if (this.isSpinning) {
                console.warn('Wheel is already spinning');
                return false;
            }

            // Stop auto-rotation if active
            if (this.isAutoRotating) {
                this.stopAutoRotation();
            }

            // If not skipping validation, validate form
            if (!skipValidation) {
                const $form = this.$container.find('.swp-entry-form');
                if (!this.validateForm($form)) {
                    console.error('Form validation failed. Use skipValidation=true to bypass.');
                    return false;
                }

                // Check OTP if enabled
                const otpEnabled = this.wheelData && this.wheelData.formSettings && this.wheelData.formSettings.enable_otp;
                if (otpEnabled && !this.otpVerified) {
                    console.error('OTP verification required. Use skipValidation=true to bypass.');
                    return false;
                }

                // Get form data
                this.userData = {
                    name: $form.find('[name="user_name"]').val(),
                    email: $form.find('[name="user_email"]').val(),
                    phone: $form.find('[name="user_phone"]').val()
                };
            } else {
                // Use default data if skipping validation
                if (!this.userData || !this.userData.name) {
                    this.userData = {
                        name: 'Test User',
                        email: 'test@example.com',
                        phone: ''
                    };
                }
            }

            // Set spinning flag
            this.isSpinning = true;

            // Show loader
            this.showButtonLoader();

            // Save entry and spin
            this.saveEntry(prizeIndex, (response) => {
                this.hideButtonLoader();

                if (response && response.success) {
                    this.spinToWin(prizeIndex);
                } else {
                    this.isSpinning = false;
                    const errorMessage = (response && response.data && response.data.message)
                        ? response.data.message
                        : (typeof swpData !== 'undefined' && swpData.i18n && swpData.i18n.error ? swpData.i18n.error : 'An error occurred. Please try again.');
                    this.showAlert(errorMessage, 'error');
                    console.error('Rotation failed:', errorMessage);
                }
            });

            return true;
        }

        /**
         * Get all available prize labels
         * @return {array} - Array of prize labels
         */
        getPrizeLabels() {
            return this.wheelData.prizes.map(prize => prize.label);
        }

        /**
         * Get prize information by label
         * @param {string} label - Prize label
         * @return {object|null} - Prize object or null if not found
         */
        getPrizeByLabel(label) {
            return this.wheelData.prizes.find(prize => prize.label === label) || null;
        }

        // OTP Methods
        handleSendOTP() {
            const self = this;
            const $form = this.$container.find('.swp-entry-form');
            const $otpSection = this.$container.find('.swp-otp-section');
            const $status = $otpSection.find('.swp-otp-status');
            const $sendBtn = $otpSection.find('.swp-btn-send-otp');

            // Get form values
            const fs = this.wheelData && this.wheelData.formSettings ? this.wheelData.formSettings : {};
            const showName = parseInt(fs.show_name, 10) === 1;
            const showEmail = parseInt(fs.show_email, 10) === 1;
            const userName = ($form.find('[name="user_name"]').val() || '').trim();
            const userEmail = ($form.find('[name="user_email"]').val() || '').trim();
            const userPhone = ($form.find('[name="user_phone"]').val() || '').trim();

            // Determine method
            let method = 'email';
            const $methodSelector = $otpSection.find('input[name="otp_method"]:checked');
            if ($methodSelector.length) {
                method = $methodSelector.val();
            }

            if (showName && parseInt(fs.require_name, 10) === 1 && !userName) {
                $status.removeClass('success').addClass('error')
                    .text(swpData.i18n.required || 'This field is required').show();
                return;
            }

            if (method === 'email') {
                if (showEmail && parseInt(fs.require_email, 10) === 1 && !userEmail) {
                    $status.removeClass('success').addClass('error')
                        .text(swpData.i18n.fillNameEmailFirst).show();
                    return;
                }
            } else if (method === 'sms') {
                if (!userPhone) {
                    $status.removeClass('success').addClass('error')
                        .text(swpData.i18n.enterPhoneForSms).show();
                    return;
                }
            } else if (method === 'both') {
                const sub = $otpSection.find('input[name="otp_method"]:checked').val() || 'email';
                if (sub === 'sms') {
                    if (!userPhone) {
                        $status.removeClass('success').addClass('error')
                            .text(swpData.i18n.enterPhoneForSms).show();
                        return;
                    }
                } else if (showEmail && parseInt(fs.require_email, 10) === 1 && !userEmail) {
                    $status.removeClass('success').addClass('error')
                        .text(swpData.i18n.fillNameEmailFirst).show();
                    return;
                }
            }

            let identifier = userEmail;
            if (method === 'sms') {
                identifier = userPhone;
            } else if (method === 'both') {
                const sub = $otpSection.find('input[name="otp_method"]:checked').val() || 'email';
                identifier = sub === 'sms' ? userPhone : userEmail;
            }

            // Disable button and show loading
            $sendBtn.prop('disabled', true).html('<span class="spinner"></span> ' + (swpData.i18n.sending || 'Sending...'));
            $status.removeClass('error success').hide();

            // Prepare AJAX data
            var ajaxData = {
                action: 'swp_send_otp',
                nonce: swpData.nonce,
                wheel_id: this.wheelId,
                method: method,
                identifier: identifier,
                user_name: userName
            };

            // Send AJAX request
            $.ajax({
                url: swpData.ajaxurl,
                type: 'POST',
                data: ajaxData,
                success: function (response) {

                    if (response && response.success) {
                        // Show success message
                        var message = (response.data && response.data.message) ? response.data.message : (swpData.i18n.otpSentSuccess || 'Verification code sent successfully!');
                        $status.removeClass('error').addClass('success')
                            .text(message).show();

                        // Hide send button, show verify section
                        $sendBtn.closest('.swp-otp-send-wrapper').hide();
                        $otpSection.find('.swp-otp-verify-wrapper').show();

                        // Start countdown timer
                        var expiresIn = (response.data && response.data.expires_in) ? response.data.expires_in : 300;
                        self.startOTPTimer(expiresIn);

                        // Store verification status
                        self.otpVerified = false;
                        self.otpIdentifier = identifier;
                        self.otpMethod = method;
                    } else {
                        var errorMsg = (response && response.data && response.data.message) ? response.data.message : (swpData.i18n.otpSendFailed || 'Failed to send verification code.');
                        $status.removeClass('success').addClass('error')
                            .text(errorMsg).show();
                        var sendCodeText = swpData.i18n.sendVerificationCode || 'Send Verification Code';
                        $sendBtn.prop('disabled', false).html(`
                            <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
                                <path d="M15.854.146a.5.5 0 0 1 .11.54l-5.819 14.547a.75.75 0 0 1-1.329.124l-3.178-4.995L.643 7.184a.75.75 0 0 1 .124-1.33L15.314.037a.5.5 0 0 1 .54.11ZM6.636 10.07l2.761 4.338L14.13 2.576zm6.787-8.201L1.591 6.602l4.339 2.76z"/>
                            </svg>
                            ${sendCodeText}
                        `);
                    }
                },
                error: function (jqXHR, textStatus, errorThrown) {

                    $status.removeClass('success').addClass('error')
                        .text(swpData.i18n.networkError || 'Network error. Please try again.').show();
                    var sendCodeText = swpData.i18n.sendVerificationCode || 'Send Verification Code';
                    $sendBtn.prop('disabled', false).html(`
                        <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
                            <path d="M15.854.146a.5.5 0 0 1 .11.54l-5.819 14.547a.75.75 0 0 1-1.329.124l-3.178-4.995L.643 7.184a.75.75 0 0 1 .124-1.33L15.314.037a.5.5 0 0 1 .54.11ZM6.636 10.07l2.761 4.338L14.13 2.576zm6.787-8.201L1.591 6.602l4.339 2.76z"/>
                        </svg>
                        ${sendCodeText}
                    `);
                }
            });
        }

        handleVerifyOTP() {
            const self = this;
            const $otpSection = this.$container.find('.swp-otp-section');
            const $status = $otpSection.find('.swp-otp-status');
            const $verifyBtn = $otpSection.find('.swp-btn-verify-otp');
            const $otpInput = $otpSection.find('.swp-otp-input');
            const $form = this.$container.find('.swp-entry-form');
            const code = $otpInput.val().trim();

            // Validate code
            if (!code || code.length !== 6) {
                $status.removeClass('success').addClass('error')
                    .text(swpData.i18n.enterValid6DigitCode || 'Please enter a valid 6-digit code').show();
                return;
            }

            // Disable button and show loading
            $verifyBtn.prop('disabled', true).text(swpData.i18n.verifying || 'Verifying...');
            $status.removeClass('error success').hide();

            // Determine which field was verified (email or phone)
            const method = this.otpMethod || 'email';

            // Send AJAX request
            $.ajax({
                url: swpData.ajaxurl,
                type: 'POST',
                data: {
                    action: 'swp_verify_otp',
                    nonce: swpData.nonce,
                    identifier: this.otpIdentifier,
                    code: code
                },
                success: function (response) {
                    if (response && response.success) {
                        // Show success message
                        var verifiedText = swpData.i18n.verified || 'Verified';
                        var message = (response.data && response.data.message) ? response.data.message : (swpData.i18n.otpVerifySuccess || 'Verification successful!');
                        $status.removeClass('error').addClass('success')
                            .text(message).show();

                        // Mark as verified
                        self.otpVerified = true;

                        // Add verified badge and make field readonly
                        if (method === 'email') {
                            const $emailGroup = $form.find('[name="user_email"]').closest('.swp-form-group');
                            $form.find('[name="user_email"]').prop('readonly', true);

                            // Add verified badge if it doesn't exist
                            if (!$emailGroup.find('.swp-verified-badge').length) {
                                $emailGroup.find('label').append(`
                                    <span class="swp-verified-badge">
                                        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
                                            <path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01zm.287 5.984-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7 8.793l2.646-2.647a.5.5 0 0 1 .708.708"/>
                                        </svg>
                                        ${verifiedText}
                                    </span>
                                `);
                            }
                        } else if (method === 'sms') {
                            const $phoneGroup = $form.find('[name="user_phone"]').closest('.swp-form-group');
                            $form.find('[name="user_phone"]').prop('readonly', true);

                            // Add verified badge if it doesn't exist
                            if (!$phoneGroup.find('.swp-verified-badge').length) {
                                $phoneGroup.find('label').append(`
                                    <span class="swp-verified-badge">
                                        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
                                            <path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01zm.287 5.984-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7 8.793l2.646-2.647a.5.5 0 0 1 .708.708"/>
                                        </svg>
                                        ${verifiedText}
                                    </span>
                                `);
                            }
                        }

                        // Clear timer
                        if (self.otpTimer) {
                            clearInterval(self.otpTimer);
                        }

                        // Hide OTP section
                        setTimeout(function () {
                            $otpSection.hide();
                            // Enable form submission
                            self.$container.find('.swp-entry-form button[type="submit"]').prop('disabled', false);
                        }, 1500);
                    } else {
                        var errorMsg = (response && response.data && response.data.message) ? response.data.message : (swpData.i18n.invalidVerificationCode || 'Invalid verification code.');
                        $status.removeClass('success').addClass('error')
                            .text(errorMsg).show();
                        $verifyBtn.prop('disabled', false).text(swpData.i18n.verify || 'Verify');
                        $otpInput.addClass('error');
                    }
                },
                error: function () {
                    $status.removeClass('success').addClass('error')
                        .text(swpData.i18n.networkError || 'Network error. Please try again.').show();
                    $verifyBtn.prop('disabled', false).text(swpData.i18n.verify || 'Verify');
                }
            });
        }

        startOTPTimer(seconds) {
            const self = this;
            const $otpSection = this.$container.find('.swp-otp-section');
            const $timer = $otpSection.find('.swp-otp-timer');
            const $resendBtn = $otpSection.find('.swp-btn-resend-otp');

            let remaining = seconds;

            function updateTimer() {
                const minutes = Math.floor(remaining / 60);
                const secs = remaining % 60;
                const timeStr = minutes + ':' + secs.toString().padStart(2, '0');
                const expiresText = (swpData.i18n.codeExpiresIn || 'Code expires in %s').replace('%s', timeStr);
                $timer.text(expiresText).show();

                if (remaining <= 0) {
                    clearInterval(self.otpTimer);
                    $timer.text(swpData.i18n.codeExpired || 'Code expired').addClass('expired');
                    $resendBtn.show();
                }

                remaining--;
            }

            updateTimer();
            this.otpTimer = setInterval(updateTimer, 1000);
        }
    }

    // Initialize on page load
    $(document).ready(function () {
        $('.swp-wheel-container').each(function () {
            const $container = $(this);
            const displayTrigger = $container.data('display-trigger') || 'page_load';
            const timeDelay = parseInt($container.data('time-delay')) || 0;
            const reappearDelay = parseInt($container.data('reappear-delay')) || 0;
            const wheelId = $container.data('wheel-id');

            // Check if wheel should be hidden based on reappear delay
            if (reappearDelay > 0 && checkWheelCookie(wheelId, reappearDelay)) {
                return; // Don't display wheel
            }

            // Handle display triggers
            switch (displayTrigger) {
                case 'page_load':
                    // Page load shows immediately (no delay)
                    showWheel($container);
                    break;

                case 'after_time':
                    // After time delay uses time_delay setting
                    setTimeout(function () {
                        showWheel($container);
                    }, timeDelay * 1000);
                    break;

                case 'scroll':
                    // Scroll uses time_delay as percentage
                    handleScrollTrigger($container, timeDelay);
                    break;

                case 'exit_intent':
                    // Exit intent shows on mouse leave
                    handleExitIntent($container);
                    break;

                default:
                    showWheel($container);
            }
        });
    });

    // Apply popup settings
    function applyPopupSettings($container) {
        const wheelData = JSON.parse($container.find('.swp-wheel-data').text());
        const styles = wheelData.styles || {};

        const maxWidth = styles.popup_max_width || 980;
        const widthPercent = styles.popup_width_percent || 90;
        const backgroundColor = styles.popup_background_color || '#ffffff';
        const backgroundImage = styles.popup_background_image || '';
        const borderRadius = styles.popup_border_radius || 20;
        const padding = styles.popup_padding || 40;
        const mobilePadding = Math.round(padding * 0.5); // 50% of desktop padding

        // Apply max-width and width to modal content
        const $modalContent = $container.find('.swp-modal-content');
        $modalContent.css({
            'max-width': maxWidth + 'px',
            'width': widthPercent + '%'
        });

        // Build background CSS for popup wrapper
        let backgroundCSS = {
            'background-color': backgroundColor,
            'border-radius': borderRadius + 'px',
            'padding': padding + 'px'
        };

        if (backgroundImage && backgroundImage.trim()) {
            backgroundCSS['background-image'] = `url('${backgroundImage}')`;
            backgroundCSS['background-size'] = 'cover';
            backgroundCSS['background-position'] = 'center';
            backgroundCSS['background-repeat'] = 'no-repeat';
        }

        // Apply settings to popup wrapper
        const $popupWrapper = $container.find('.swp-wheel-wrapper-popup');
        $popupWrapper.css(backgroundCSS);

        // Add responsive padding via CSS
        const styleId = 'swp-popup-responsive-' + $container.data('wheel-id');
        // Remove old style if exists
        $('#' + styleId).remove();

        $('<style id="' + styleId + '">')
            .text(`
                @media screen and (max-width: 768px) {
                    .swp-wheel-wrapper-popup {
                        padding: ${mobilePadding}px !important;
                    }
                }
            `)
            .appendTo('head');
    }

    // Apply form style settings
    function applyFormStyles($container) {
        const wheelData = JSON.parse($container.find('.swp-wheel-data').text());
        const styles = wheelData.styles || {};
        const wheelId = $container.data('wheel-id');

        // Get form style colors
        const titleColor = styles.form_title_color || '#1d2327';
        const descriptionColor = styles.form_description_color || '#50575e';
        const labelColor = styles.form_label_color || '#1d2327';
        const inputColor = styles.form_input_color || '#2c3338';
        const inputBackgroundColor = styles.form_input_background_color || '#ffffff';
        const inputBorderColor = styles.form_input_border_color || '#e6e6e8';
        const inputFocusBorderColor = styles.form_input_focus_border_color || '#f24101';

        // Remove old style if exists
        const styleId = 'swp-form-styles-' + wheelId;
        $('#' + styleId).remove();

        // Create dynamic CSS for form styles
        $('<style id="' + styleId + '">')
            .text(`
                /* Form Title Color */
                .swp-wheel-container[data-wheel-id="${wheelId}"] .swp-form-title {
                    color: ${titleColor} !important;
                }
                
                /* Form Description Color */
                .swp-wheel-container[data-wheel-id="${wheelId}"] .swp-form-description {
                    color: ${descriptionColor} !important;
                }
                
                /* Input Label Color */
                .swp-wheel-container[data-wheel-id="${wheelId}"] .swp-form-group label {
                    color: ${labelColor} !important;
                }
                
                /* Input Field Styles */
                .swp-wheel-container[data-wheel-id="${wheelId}"] .swp-form-control {
                    color: ${inputColor} !important;
                    background-color: ${inputBackgroundColor} !important;
                    border-color: ${inputBorderColor} !important;
                }
                
                /* Input Field Focus Border Color */
                .swp-wheel-container[data-wheel-id="${wheelId}"] .swp-form-control:focus {
                    border-color: ${inputFocusBorderColor} !important;
                    box-shadow: 0 0 0 1px ${inputFocusBorderColor} !important;
                }
            `)
            .appendTo('head');
    }

    // Store wheel instances globally for external access
    window.spinWheelInstances = window.spinWheelInstances || {};

    // Show wheel container
    function showWheel($container) {
        const displayMode = $container.data('display-mode') || 'inline';

        // Apply form styles (for both popup and inline)
        applyFormStyles($container);

        if (displayMode === 'popup') {
            // Apply popup settings
            applyPopupSettings($container);

            // Show popup modal
            $container.addClass('active').css('display', 'flex');
            // Prevent body scroll when popup is open
            $('body').css('overflow', 'hidden');
        } else {
            // Show inline
            $container.fadeIn(300);
        }

        // Initialize wheel and store instance
        const wheelId = $container.data('wheel-id');
        const wheelInstance = new SpinWheel($container[0]);
        window.spinWheelInstances[wheelId] = wheelInstance;

        // Set cookie for reappear delay
        const reappearDelay = parseInt($container.data('reappear-delay')) || 0;
        if (reappearDelay > 0) {
            setWheelCookie(wheelId, reappearDelay);
        }

        // Bind close button for popup
        if (displayMode === 'popup') {
            $container.find('.swp-modal-close').on('click', function () {
                closePopup($container);
            });

            // Close on overlay click
            $container.find('.swp-modal-overlay').on('click', function () {
                closePopup($container);
            });

            // Close on ESC key
            $(document).on('keydown.swpModal', function (e) {
                if (e.key === 'Escape') {
                    closePopup($container);
                }
            });
        }
    }

    // Close popup modal
    function closePopup($container) {
        $container.fadeOut(300, function () {
            $container.removeClass('active');
        });
        $('body').css('overflow', '');
        $(document).off('keydown.swpModal');
    }

    // Check cookie for reappear delay
    function checkWheelCookie(wheelId, days) {
        const cookieName = 'swp_wheel_' + wheelId + '_seen';
        const cookies = document.cookie.split(';');

        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();
            if (cookie.indexOf(cookieName + '=') === 0) {
                return true; // Cookie exists, don't show wheel
            }
        }

        return false; // Cookie doesn't exist, show wheel
    }

    // Set cookie when wheel is shown
    function setWheelCookie(wheelId, days) {
        if (days > 0) {
            const d = new Date();
            d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
            const expires = "expires=" + d.toUTCString();
            document.cookie = 'swp_wheel_' + wheelId + '_seen=1;' + expires + ';path=/';
        }
    }

    // Handle scroll trigger
    function handleScrollTrigger($container, scrollPercentage) {
        scrollPercentage = scrollPercentage || 50;

        $(window).on('scroll', function () {
            const scrollTop = $(window).scrollTop();
            const docHeight = $(document).height();
            const winHeight = $(window).height();
            const scrollPercent = (scrollTop / (docHeight - winHeight)) * 100;

            if (scrollPercent >= scrollPercentage && !$container.is(':visible')) {
                showWheel($container);
                $(window).off('scroll'); // Remove listener after showing
            }
        });
    }

    // Handle exit intent
    function handleExitIntent($container) {
        let mouseLeaveTimer;

        $(document).on('mouseleave', function (e) {
            if (e.clientY < 10 && !$container.is(':visible')) {
                clearTimeout(mouseLeaveTimer);
                mouseLeaveTimer = setTimeout(function () {
                    showWheel($container);
                    $(document).off('mouseleave'); // Remove listener after showing
                }, 100);
            }
        });
    }

    // Global helper functions for easy access
    window.SpinWheel = window.SpinWheel || {};
    
    /**
     * Rotate a specific wheel by prize label
     * @param {number} wheelId - Wheel ID
     * @param {string} prizeLabel - Prize label to rotate to
     * @param {boolean} skipValidation - Skip form validation (default: false)
     * @return {boolean} - Success status
     */
    window.SpinWheel.rotateByLabel = function(wheelId, prizeLabel, skipValidation = false) {
        const instance = window.spinWheelInstances[wheelId];
        if (!instance) {
            console.error(`Wheel with ID ${wheelId} not found. Available wheels:`, Object.keys(window.spinWheelInstances));
            return false;
        }
        return instance.rotateByLabel(prizeLabel, skipValidation);
    };

    /**
     * Rotate a specific wheel by prize index
     * @param {number} wheelId - Wheel ID
     * @param {number} prizeIndex - Prize index (0-based)
     * @param {boolean} skipValidation - Skip form validation (default: false)
     * @return {boolean} - Success status
     */
    window.SpinWheel.rotateByIndex = function(wheelId, prizeIndex, skipValidation = false) {
        const instance = window.spinWheelInstances[wheelId];
        if (!instance) {
            console.error(`Wheel with ID ${wheelId} not found. Available wheels:`, Object.keys(window.spinWheelInstances));
            return false;
        }
        return instance.rotateByIndex(prizeIndex, skipValidation);
    };

    /**
     * Get all prize labels for a specific wheel
     * @param {number} wheelId - Wheel ID
     * @return {array|null} - Array of prize labels or null if wheel not found
     */
    window.SpinWheel.getPrizeLabels = function(wheelId) {
        const instance = window.spinWheelInstances[wheelId];
        if (!instance) {
            console.error(`Wheel with ID ${wheelId} not found`);
            return null;
        }
        return instance.getPrizeLabels();
    };

    /**
     * Get prize information by label
     * @param {number} wheelId - Wheel ID
     * @param {string} prizeLabel - Prize label
     * @return {object|null} - Prize object or null if not found
     */
    window.SpinWheel.getPrizeByLabel = function(wheelId, prizeLabel) {
        const instance = window.spinWheelInstances[wheelId];
        if (!instance) {
            console.error(`Wheel with ID ${wheelId} not found`);
            return null;
        }
        return instance.getPrizeByLabel(prizeLabel);
    };

    /**
     * Get wheel instance
     * @param {number} wheelId - Wheel ID
     * @return {object|null} - Wheel instance or null if not found
     */
    window.SpinWheel.getInstance = function(wheelId) {
        return window.spinWheelInstances[wheelId] || null;
    };

})(jQuery);
