// Shared stage-card helpers used by Sig Select, Sea Select, and the Game Kit fan.
//
// Each call site handles its own DOM lookup, click wiring, and polarity decisions;
// this module owns the duplicated logic for normalizing a card source, painting
// the upright/reversal faces, populating the stat-block keyword lists, and
// rendering the FYI panel entries.
var StageCard = (function () {
    'use strict';

    function _parseCSV(str) {
        return (str || '').split(',').map(function (s) { return s.trim(); }).filter(Boolean);
    }
    function _parseJSON(str) {
        try { return JSON.parse(str || '[]'); } catch (e) { return []; }
    }

    // Normalize a `.sig-card` / `.fan-card` element's data-* attrs into the same
    // shape that sea.js receives via fetch (so all call sites can share populateCard).
    function fromDataset(el) {
        return {
            corner_rank:        el.dataset.cornerRank        || '',
            suit_icon:          el.dataset.suitIcon          || '',
            name_group:         el.dataset.nameGroup         || '',
            name_title:         el.dataset.nameTitle         || '',
            arcana:             el.dataset.arcana            || '',
            correspondence:     el.dataset.correspondence    || '',
            keywords_upright:   _parseCSV(el.dataset.keywordsUpright),
            keywords_reversed:  _parseCSV(el.dataset.keywordsReversed),
            energies:           _parseJSON(el.dataset.energies),
            operations:         _parseJSON(el.dataset.operations),
            levity_qualifier:   el.dataset.levityQualifier   || '',
            gravity_qualifier:  el.dataset.gravityQualifier  || '',
            reversal_qualifier: el.dataset.reversalQualifier || '',
            // Pattern B' marker — `data-reversal-drops-qualifier="true"`
            // means the reversal face renders the name swap (in
            // `reversal_qualifier`) WITHOUT a trailing polarity qualifier.
            // Set on cards 16-18 (Realms) via the Earthman deck seed.
            reversal_drops_qualifier:
                el.dataset.reversalDropsQualifier === 'true',
            // Polarity-split title overrides — non-blank for cards 48-49 only,
            // where each polarity (and within each polarity, each axis state)
            // has a fully distinct title rather than a shared name + qualifier.
            levity_emanation:   el.dataset.levityEmanation   || '',
            gravity_emanation:  el.dataset.gravityEmanation  || '',
            levity_reversal:    el.dataset.levityReversal    || '',
            gravity_reversal:   el.dataset.gravityReversal   || '',
            // Word(s) inside any title slot to wrap in <em> at render time
            // (e.g. "Stalking" for trumps 19-21). Blank for most cards.
            italic_word:        el.dataset.italicWord        || '',
            // Sprint A.3 — image-rendering mode. When `image_url` is non-empty,
            // the stage card renders an <img> instead of the text fan-card
            // scaffold (transparent-bg PNG over arcana-colored border per
            // [[project-image-based-deck-face-rendering]]). `arcana_key` is the
            // canonical model code (MAJOR/MINOR/MIDDLE) used to pick the
            // border-color CSS var (--terUser for major, --quiUser for the rest).
            image_url:          el.dataset.imageUrl          || '',
            arcana_key:         el.dataset.arcanaKey         || '',
        };
    }

    function _escape(s) {
        return String(s)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;');
    }

    // Wrap every occurrence of `word` in `text` with <em>...</em>, returning
    // an HTML-safe string. Both inputs are escaped before splicing.
    function _italicize(text, word) {
        if (!text) return '';
        var safeText = _escape(text);
        if (!word) return safeText;
        var safeWord = _escape(word);
        if (safeText.indexOf(safeWord) === -1) return safeText;
        return safeText.split(safeWord).join('<em>' + safeWord + '</em>');
    }

    // Set element text — uses innerHTML if the card has an italic_word that
    // appears in the string, otherwise textContent. Caller passes the full
    // card so we can pick up `italic_word` without re-passing it everywhere.
    function _setTitle(el, text, card) {
        if (!el) return;
        if (card.italic_word && text && text.indexOf(card.italic_word) !== -1) {
            el.innerHTML = _italicize(text, card.italic_word);
        } else {
            el.textContent = text || '';
        }
    }

    // Decide whether a card object represents Major Arcana — sig sources from
    // `data-arcana` (Django's `get_arcana_display`, e.g. "Major Arcana"), sea
    // from card.arcana (model code, e.g. "MAJOR"). Accept both.
    function _isMajor(card) {
        var a = (card.arcana || '').toUpperCase();
        return a === 'MAJOR' || a === 'MAJOR ARCANA';
    }

    // Map either form (model code or display) to the rendered label.
    function _arcanaDisplay(card) {
        var a = (card.arcana || '').toUpperCase();
        if (a === 'MAJOR'  || a === 'MAJOR ARCANA')  return 'Major Arcana';
        if (a === 'MINOR'  || a === 'MINOR ARCANA')  return 'Minor Arcana';
        if (a === 'MIDDLE' || a === 'MIDDLE ARCANA') return 'Middle Arcana';
        return '';
    }

    // Toggle image-mode on the stage card. When `card.image_url` is non-empty,
    // show the <img.sig-stage-card-img> child + add .sig-stage-card--image
    // marker class (CSS hides the text fan-card-* children + applies the
    // arcana-colored border). When image_url is empty (legacy text-only
    // decks: Earthman, RWS pre-images), strip the marker + hide the <img>
    // so the text scaffold takes over. Sprint A.3 of
    // [[project-image-based-deck-face-rendering]].
    function _setImageMode(stageCard, card) {
        if (!stageCard) return;
        var img = stageCard.querySelector('.sig-stage-card-img');
        if (card.image_url) {
            stageCard.classList.add('sig-stage-card--image');
            if (card.arcana_key) {
                stageCard.setAttribute('data-arcana-key', card.arcana_key);
            }
            if (img) {
                img.src = card.image_url;
                img.alt = card.name_title || '';
                img.style.display = '';
            }
        } else {
            stageCard.classList.remove('sig-stage-card--image');
            stageCard.removeAttribute('data-arcana-key');
            if (img) {
                img.style.display = 'none';
                img.removeAttribute('src');
            }
        }
    }

    // Paint the stage-card's upright + reversal faces from a normalized card
    // object + the active polarity ('levity' | 'gravity'). Reversal-qualifier
    // falls back to the current polarity's qualifier when blank (6F behavior).
    function populateCard(stageCard, card, polarity) {
        if (!stageCard) return;
        _setImageMode(stageCard, card);
        var isLevity  = polarity === 'levity';
        var qualifier = isLevity ? (card.levity_qualifier || '') : (card.gravity_qualifier || '');
        var isMajor   = _isMajor(card);
        var title     = card.name_title || '';
        var reversalQualifier = card.reversal_qualifier || '';
        // Polarity-split overrides (cards 48-49). When set, the full title
        // string already incorporates whatever qualifier the card carries
        // ("The Effulgent Mould of Man" etc.) — render as a single line and
        // leave the upright qualifier slots empty.
        var emanationOverride = isLevity ? (card.levity_emanation || '') : (card.gravity_emanation || '');
        var reversalOverride  = isLevity ? (card.levity_reversal  || '') : (card.gravity_reversal  || '');

        stageCard.querySelectorAll('.fan-corner-rank').forEach(function (el) {
            el.textContent = card.corner_rank || '';
        });
        stageCard.querySelectorAll('.stage-suit-icon').forEach(function (el) {
            if (card.suit_icon) {
                el.className = 'fa-solid ' + card.suit_icon + ' stage-suit-icon';
                el.style.display = '';
            } else {
                el.style.display = 'none';
            }
        });

        var nameGroupEl = stageCard.querySelector('.fan-card-name-group');
        if (nameGroupEl) nameGroupEl.textContent = emanationOverride ? '' : (card.name_group || '');

        var arcanaEl = stageCard.querySelector('.fan-card-arcana');
        if (arcanaEl) arcanaEl.textContent = _arcanaDisplay(card);

        var nameEl = stageCard.querySelector('.fan-card-name');
        var qAbove = stageCard.querySelector('.sig-qualifier-above');
        var qBelow = stageCard.querySelector('.sig-qualifier-below');

        if (emanationOverride) {
            // Cards 48-49 + trumps 19-21 — single-line title, no qualifier slots.
            _setTitle(nameEl, emanationOverride, card);
            if (qAbove) qAbove.textContent = '';
            if (qBelow) qBelow.textContent = '';
        } else {
            _setTitle(nameEl, isMajor ? title + ',' : title, card);
            if (qAbove) qAbove.textContent = isMajor ? '' : qualifier;
            if (qBelow) qBelow.textContent = isMajor ? qualifier : '';
        }

        // Reversal face — class always matches semantic content:
        //   .fan-card-reversal-name      → title-like text
        //   .fan-card-reversal-qualifier → qualifier-like text
        // DOM order controls visual layout after the 180° SPIN (DOM-second appears
        // visually on top). Sig + sea skeletons render a fixed slot pair so we
        // assign classes per-arcana here so each branch lands in the right slot:
        //   Major / polarity-split — title on top    → .name carried by DOM-second
        //   Non-major              — qualifier on top → .qualifier carried by DOM-second
        //
        // Pattern B / B' Majors (cards 2-5, 10-15, 16-18, 22-35, 41): `card.
        // reversal_qualifier` is the NAME SWAP for the reversal face (not a
        // qualifier). Polarity qualifier persists across both faces — unless
        // `reversal_drops_qualifier=true` (Realms — cards 16-18), in which
        // case the reversal face renders the name swap alone. See
        // `TarotCard.applet_face` docstring + [[feedback-reversal-qualifier-
        // dual-role]].
        var hasNameSwap = isMajor && reversalQualifier && qualifier;
        var slots = stageCard.querySelectorAll('.fan-card-face-reversal > p');
        if (slots.length === 2) {
            var bottomEl = slots[0];  // DOM-first  → visually bottom after spin
            var topEl    = slots[1];  // DOM-second → visually top after spin
            // Wipe any squeeze-class artifacts from a prior populate call before
            // re-stamping the base class — keeps the slot's CSS predictable.
            bottomEl.className = '';
            topEl.className    = '';

            if (reversalOverride) {
                // Polarity-split: single-line title on TOP; qualifier slot empty.
                topEl.className    = 'fan-card-reversal-name';
                _setTitle(topEl, reversalOverride, card);
                bottomEl.className = 'fan-card-reversal-qualifier';
                bottomEl.textContent = '';
            } else if (hasNameSwap) {
                // Pattern B / B' — swapped name on TOP.
                topEl.className = 'fan-card-reversal-name';
                if (card.reversal_drops_qualifier) {
                    // Pattern B' (Realms 16-18): single-line swap name, no qualifier.
                    _setTitle(topEl, reversalQualifier, card);
                    bottomEl.className = 'fan-card-reversal-qualifier';
                    bottomEl.textContent = '';
                } else {
                    // Pattern B (2-5, 10-15, 22-35, 41): swap name w. comma + polarity qualifier below.
                    _setTitle(topEl, reversalQualifier + ',', card);
                    bottomEl.className = 'fan-card-reversal-qualifier';
                    bottomEl.textContent = qualifier;
                }
            } else if (isMajor) {
                // Major w. qualifier (no name swap — cards 6-9 implicit virtues):
                // title-with-comma on TOP, polarity qualifier on BOTTOM.
                topEl.className    = 'fan-card-reversal-name';
                _setTitle(topEl, title + ',', card);
                bottomEl.className = 'fan-card-reversal-qualifier';
                bottomEl.textContent = qualifier;
            } else {
                // Non-major: qualifier on TOP, title on BOTTOM (inverted from
                // upright order by design — qualifier word reads first after spin).
                topEl.className    = 'fan-card-reversal-qualifier';
                topEl.textContent  = reversalQualifier || qualifier;
                bottomEl.className = 'fan-card-reversal-name';
                _setTitle(bottomEl, title, card);
            }
        }
    }

    // Fill the upright + reversed keyword <ul>s. Accepts either CSS-selector
    // overrides or the default `.stat-face--upright .stat-keywords` /
    // `.stat-face--reversed .stat-keywords`.
    function populateKeywords(statBlock, uprightItems, reversedItems, opts) {
        if (!statBlock) return;
        opts = opts || {};
        var upSel = opts.uprightSel || '.stat-face--upright .stat-keywords';
        var rvSel = opts.reversedSel || '.stat-face--reversed .stat-keywords';
        var ul = statBlock.querySelector(upSel);
        var rl = statBlock.querySelector(rvSel);
        if (ul) ul.innerHTML = (uprightItems || []).map(function (k) { return '<li>' + k + '</li>'; }).join('');
        if (rl) rl.innerHTML = (reversedItems || []).map(function (k) { return '<li>' + k + '</li>'; }).join('');
    }

    // Sprint A.7-polish-3 — fill the title + arcana fields per [[project-
    // image-based-deck-face-rendering]]'s locked Q3 spec: each stat face
    // gets a title (card name minus any comma+qualifier for Earthman-style
    // major arcana) + an arcana type label ("Major Arcana" / "Minor Arcana").
    // For text-mode decks the same info is on the card face too — duplicates,
    // not regressive. For image-mode decks the stat block is the only home
    // for textual metadata. Caller-side opt-out by passing opts.skipExtras=true.
    //
    // Sprint A.7.5 — also fills the top-left rank+suit chip (`.stat-chip-rank`
    // + `.stat-chip-icon`) on each stat-face header. Chip data reuses the same
    // `corner_rank` + `suit_icon` fields populated on the card's corners; for
    // image-mode decks where the card has no corners, the chip is the only
    // place the rank+suit appears.
    function populateStatExtras(statBlock, card, opts) {
        if (!statBlock) return;
        opts = opts || {};
        if (opts.skipExtras) return;
        // Strip any "Title, Qualifier" Earthman pattern → just "Title".
        var rawTitle = card.name_title || card.name || '';
        var title = rawTitle.split(',')[0].trim();
        var arcana = _arcanaDisplay(card);
        statBlock.querySelectorAll('.stat-face-title').forEach(function (el) {
            el.textContent = title;
        });
        statBlock.querySelectorAll('.stat-face-arcana').forEach(function (el) {
            el.textContent = arcana;
        });
        statBlock.querySelectorAll('.stat-chip-rank').forEach(function (el) {
            el.textContent = card.corner_rank || '';
        });
        statBlock.querySelectorAll('.stat-chip-icon').forEach(function (el) {
            if (card.suit_icon) {
                el.className = 'fa-solid ' + card.suit_icon + ' stat-chip-icon';
                el.style.display = '';
            } else {
                el.style.display = 'none';
            }
        });
        // Surface arcana on the stat-block parent so SCSS `[data-arcana-key]`
        // selectors can color-key the title (--quiUser for minor/middle,
        // --terUser for major) — mirrors the same hook used on the card.
        if (card.arcana_key) {
            statBlock.setAttribute('data-arcana-key', card.arcana_key);
        } else if (card.arcana) {
            // Fallback: card.arcana may be "MAJOR" (model code) or "Major
            // Arcana" (display) — uppercase + take first word for the key.
            statBlock.setAttribute('data-arcana-key',
                (card.arcana || '').toUpperCase().split(' ')[0]);
        }
    }

    // Concatenate energies + operations into a single FYI nav array.
    function buildInfoData(card) {
        var data = (card.energies || []).map(function (e) {
            return { type: e.type, effect: e.effect, category: 'energies' };
        });
        return data.concat((card.operations || []).map(function (o) {
            return { type: o.type, effect: o.effect, category: 'operations' };
        }));
    }

    // Paint a single FYI entry into the .sig-info panel. Caller owns idx + data.
    function renderFyi(infoPanel, data, idx) {
        if (!infoPanel) return;
        var title  = infoPanel.querySelector('.sig-info-title');
        var type   = infoPanel.querySelector('.sig-info-type');
        var effect = infoPanel.querySelector('.sig-info-effect');
        var index  = infoPanel.querySelector('.sig-info-index');
        if (!data || data.length === 0) {
            if (title)  { title.textContent = 'Energy';
                          title.className   = 'sig-info-title sig-info-title--energies'; }
            if (type)   type.textContent  = '';
            if (effect) effect.innerHTML  = '<em>No interactions defined.</em>';
            if (index)  index.textContent = '';
            return;
        }
        var entry = data[idx];
        var isEnergies = entry.category === 'energies';
        if (title)  { title.textContent = isEnergies ? 'Energy' : 'Operation';
                      title.className   = 'sig-info-title sig-info-title--' + entry.category; }
        if (type)   type.textContent  = entry.type   || '';
        if (effect) effect.innerHTML  = entry.effect || '';
        if (index)  index.textContent = data.length > 1
            ? (idx + 1) + ' / ' + data.length : '';
    }

    return {
        fromDataset:      fromDataset,
        populateCard:     populateCard,
        populateKeywords: populateKeywords,
        populateStatExtras: populateStatExtras,
        buildInfoData:    buildInfoData,
        renderFyi:        renderFyi,
    };
}());
