// Fixtures tab — universe-map slice (KL-154 originally, KL-164 split,
// KL-175 collision highlighter).
//
// Builds the visual address-occupancy map for each universe in use, and
// handles the gap-click flow that pre-fills the Add modal with the
// clicked universe + start address.
//
// Lives in the same `$store.fixtures` namespace as the list and editor
// slices; methods reach the editor's openAdd via `this.openAdd()` at
// runtime (single composed store).

// Universe is 512 channels (1..512). Used for the trailing-gap segment
// in buildUniverseSegments.
const UNIVERSE_MAX = 512;

// KL-196: precomputed tick scale for the channel map. The same scale
// applies to every universe (1..512), so we cache it once at module
// load and the per-universe template renders this static array. Each
// tick is positioned by left%, computed as (channel-1)/512*100 — the
// start-of-channel position in the flex-grow bar above. Endpoints (1
// and 512) anchor to 0% and 100% explicitly so their labels align
// flush with the bar edges instead of slightly inside.
function _buildUniverseTicks() {
    const ticks = [];
    // Endpoints — label flush-left (1) and flush-right (512).
    ticks.push({ pos: 0,   kind: 'endpoint-start', label: '1'   });
    ticks.push({ pos: 100, kind: 'endpoint-end',   label: '512' });
    // Majors every 50 (50..500) — labeled.
    for (let n = 50; n <= 500; n += 50) {
        ticks.push({ pos: ((n - 1) / 512) * 100, kind: 'major', label: String(n) });
    }
    // Minors every 10 (10..510) — unlabeled, skip multiples of 50
    // (those are already majors).
    for (let n = 10; n <= 510; n += 10) {
        if (n % 50 === 0) continue;
        ticks.push({ pos: ((n - 1) / 512) * 100, kind: 'minor', label: null });
    }
    return ticks;
}
const UNIVERSE_TICKS = _buildUniverseTicks();

function occupantsFor(allFixtures, universe) {
    const u = Number(universe);
    const out = [];
    allFixtures.forEach((fx, idx) => {
        const start = Number(fx.address || 0);
        const channels = Number(fx.channels || 0);
        if (start < 1 || channels < 1) return;
        if (Number(fx.universe || 0) !== u) return;
        out.push({fx, idx, start, channels, end: start + channels - 1});
    });
    out.sort((a, b) => a.start - b.start);
    return out;
}

function pairwiseCollisions(occupants) {
    const out = [];
    for (let i = 0; i < occupants.length; i++) {
        const a = occupants[i];
        for (let j = i + 1; j < occupants.length; j++) {
            const b = occupants[j];
            if (b.start > a.end) break; // sorted: no later fixture can reach back
            out.push({
                aIdx: a.idx,
                bIdx: b.idx,
                aName: a.fx.name || `Fixture #${a.idx + 1}`,
                bName: b.fx.name || `Fixture #${b.idx + 1}`,
                overlapStart: Math.max(a.start, b.start),
                overlapEnd: Math.min(a.end, b.end),
            });
        }
    }
    return out;
}

export function universemapSlice() {
    return {
        // KL-196: tick-scale for the channel map. Same array for every
        // universe (the scale is universe-agnostic), so this is a
        // precomputed module constant exposed via a getter for
        // reactive-template friendliness.
        get universeTicks() {
            return UNIVERSE_TICKS;
        },

        // KL-154: list of distinct universes that have at least one
        // fixture on them, sorted ascending. Drives the universe-map UI.
        // Empty when no fixtures exist; the map hides itself in that case.
        get universesInUse() {
            const u = new Set();
            for (const fx of this.allFixtures) {
                if (Number(fx.channels || 0) <= 0) continue;
                u.add(Number(fx.universe || 0));
            }
            return [...u].sort((a, b) => a - b);
        },

        // KL-175: pairwise collision detection, used by the segment overlap
        // flag, the universe-header pill count, and the anchored callouts.
        // Pure function of allFixtures + universe — getters in templates
        // re-evaluate on store mutation, so reactivity is automatic.
        getUniverseCollisions(universe) {
            return pairwiseCollisions(occupantsFor(this.allFixtures, universe));
        },

        universeCollisionCount(universe) {
            return this.getUniverseCollisions(universe).length;
        },

        // KL-154 (KL-175 corrected): build an ordered segment list for a
        // universe — alternating gap and fixture blocks, ending with a
        // trailing gap to channel 512 when the rig doesn't fill the
        // universe. Each segment carries `channels` so the renderer can
        // flex-size proportionally.
        //
        // KL-175: fixture segments also carry `bubbles` — the collision
        // callouts to render anchored under THIS segment. We attach the
        // callout to the b-side fixture (later-by-address) of each pair
        // so the visually-protruding red segment is the one with the
        // arrow pointing at it. Anchoring to the segment dodges the
        // problem that flex-basis:auto distorts flex-weight-to-pixel
        // mapping when buttons have text content.
        buildUniverseSegments(universe) {
            const occupants = occupantsFor(this.allFixtures, universe);
            const collisions = pairwiseCollisions(occupants);
            const overlappingIdx = new Set();
            const bubblesByBIdx = {};
            for (const c of collisions) {
                overlappingIdx.add(c.aIdx);
                overlappingIdx.add(c.bIdx);
                if (!bubblesByBIdx[c.bIdx]) bubblesByBIdx[c.bIdx] = [];
                bubblesByBIdx[c.bIdx].push({
                    id: `${c.aIdx}-${c.bIdx}`,
                    rangeText: c.overlapStart === c.overlapEnd
                        ? `ch ${c.overlapStart}`
                        : `ch ${c.overlapStart}-${c.overlapEnd}`,
                    label: `${c.aName} ↔ ${c.bName}`,
                    detail: `Channels ${c.overlapStart}-${c.overlapEnd}: ${c.aName} overlaps ${c.bName}. Re-address one of them.`,
                    partnerIdx: c.aIdx,
                });
            }

            const segments = [];
            let cursor = 1;
            for (const o of occupants) {
                if (o.start > cursor) {
                    segments.push({
                        type: 'gap',
                        start: cursor,
                        end: o.start - 1,
                        channels: o.start - cursor,
                    });
                }
                segments.push({
                    type: 'fixture',
                    start: o.start,
                    end: o.end,
                    channels: o.channels,
                    name: o.fx.name || `Fixture #${o.idx + 1}`,
                    role: o.fx.role || 'ambient',
                    index: o.idx,
                    overlap: overlappingIdx.has(o.idx),
                    bubbles: bubblesByBIdx[o.idx] || [],
                });
                cursor = Math.max(cursor, o.end + 1);
            }
            if (cursor <= UNIVERSE_MAX) {
                segments.push({
                    type: 'gap',
                    start: cursor,
                    end: UNIVERSE_MAX,
                    channels: UNIVERSE_MAX - cursor + 1,
                });
            }
            return segments;
        },

        // KL-154: clicking a gap segment opens the Add Fixture modal
        // pre-filled with that universe and start address. Saves the user
        // a round-trip through manual entry.
        openAddAtGap(universe, start) {
            this.openAdd();
            // openAdd() resets the form; queue our override after it
            // settles (it calls loadCatalog + refreshSuggestedAddress
            // asynchronously, but those don't touch universe/start).
            this.addForm.universe = Number(universe) || 0;
            this.addForm.startAddress = Number(start) || 1;
            this.addForm.startAddressAuto = false;
            this.addForm.universeFull = false;
        },
    };
}
