<template>
    <div class="card">
        <div class="card-header">
            <h5 class="card-title mb-0">Label layout</h5>
        </div>
        <div class="card-body">
            <div class="mb-3 btn-group" role="group">
                <input type="radio" class="btn-check" id="layout-filter-all"
                       autocomplete="off" value="all" v-model="activeTag">
                <label class="btn btn-outline-secondary" for="layout-filter-all">All</label>
                <template v-for="tag in labelTags" :key="tag">
                    <input type="radio" class="btn-check" :id="'layout-filter-' + tag"
                           autocomplete="off" :value="tag" v-model="activeTag">
                    <label class="btn btn-outline-secondary" :for="'layout-filter-' + tag">
                        {{ tagLabel(tag) }}
                    </label>
                </template>
            </div>
            <div class="template-grid d-flex flex-wrap align-items-start">
                <div v-for="t in labelTemplates" :key="t.id" class="template-option d-flex flex-column text-center"
                     :class="{'template-option-disabled': !isSelectable(t)}"
                     :title="unavailableReason(t)"
                     role="button" @click="isSelectable(t) && $emit('input', t.id)">
                    <div class="template-thumb-wrap">
                        <canvas :ref="el => setTemplateCanvasRef(t.id, el)"
                               class="img-thumbnail template-thumb-canvas"
                               :class="{'border-primary': value === t.id}"></canvas>
                        <span v-if="t.id in failed" class="template-thumb-warning" :title="failed[t.id].message">
                            {{ failed[t.id].short }}
                        </span>
                        <span v-else-if="t.id in warned" class="template-thumb-warning template-thumb-warning-soft"
                              :title="warned[t.id].map(w => w.message).join(' ')">
                            {{ warned[t.id].map(w => w.short).join(", ") }}
                        </span>
                    </div>
                    <div class="small"
                        :class="{'fw-bold text-primary': value === t.id}">
                        {{ t.name }}
                        <span v-for="tag in t.tags" :key="tag" class="badge bg-secondary">{{ tagLabel(tag) }}</span>
                    </div>
                    <div class="small text-muted">{{ t.description }}</div>
                    <!-- Debugging aid: every QR-family/text leaf's raw size numbers, shown regardless
                         of whether either tripped a warning above - see label.js's qrInfo/textInfo. -->
                    <div v-if="t.id in qrInfo || t.id in textInfo" class="small text-secondary">
                        <div v-for="(info, i) in qrInfo[t.id]" :key="'qr' + i">
                            mm-per-mod: {{ info.moduleMm.toFixed(2) }}, px-per-mod: {{ info.scale }}
                        </div>
                        <div v-for="(info, i) in textInfo[t.id]" :key="'text' + i">
                            text-mm: {{ info.fontMm.toFixed(2) }}, text-px: {{ info.fontPx.toFixed(1) }}
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<style scoped>
/* This build pins Bootstrap 4 (no gap-* utilities, those are 5.1+) - hence plain CSS gap here. */
.template-grid {
    gap: 1rem;
}

.template-option {
    width: 20rem;
}

.template-option-disabled {
    opacity: .45;
    cursor: not-allowed;
}

.template-option .badge {
    font-size: .65rem;
    vertical-align: middle;
    margin-left: .35rem;
}

.template-thumb-wrap {
    position: relative;
}

.template-thumb-canvas {
    display: block;
    width: 100%;
    height: 10rem;
    /* Canvas draws at whatever size fits its content (see redraw/drawFallbackLabel); object-fit
       scales it into the thumbnail box like it would an <img>, no manual zoom math needed. */
    object-fit: contain;
    background: #fff;
}

/* Corner badge flagging *why* a layout failed to render - a capacity error (label.js's "bigger
   code than the tape allows"/"doesn't fit on this tape") or label-layouts.js's templateContent
   throwing "missing data" because a field it needs isn't filled in yet (see redraw()'s catch).
   Shows the short label directly (e.g. "too big", "missing data") rather than just an icon, so the
   reason reads at a glance; the full sentence is still the title tooltip. Layered on top of the
   greyed-out/unselectable state isSelectable/unavailableReason already apply for the same case. */
.template-thumb-warning {
    position: absolute;
    top: .35rem;
    right: .35rem;
    max-width: calc(100% - .7rem);
    padding: .15rem .45rem;
    border-radius: 1rem;
    background: rgba(220, 53, 69, .9);
    color: #fff;
    font-size: .7rem;
    font-weight: 600;
    line-height: 1.2;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, .15);
    cursor: help;
}

/* Soft variant for a template that rendered fine but tripped one of label.js's scan-reliability
   thresholds (MIN_RECOMMENDED_QR_PX_PER_MODULE/MIN_RECOMMENDED_QR_MODULE_MM) - still selectable,
   just flagged, so amber rather than the hard-failure badge's red. */
.template-thumb-warning-soft {
    background: rgba(255, 193, 7, .9);
    color: #000;
}
</style>

<script>
import {drawLabel, drawFallbackLabel, preloadQrEncoder} from "@/label.js";
import {LABEL_TEMPLATES, LABEL_TAGS, templateIsAvailable, templateContent} from "@/label-layouts.js";

export default {
    name: "LabelLayoutPreview",
    props: {
        // Named content fields templates draw from (see label.js's buildLabelFields), kept in
        // sync by the parent. A field's absence (not just empty) means a template needing it is
        // unavailable.
        fields: {
            type: Object,
            required: true
        },
        // The selected template id.
        value: {
            type: String,
            required: true
        },
        // Ids of the most-recently printed/downloaded templates, most recent first (see
        // Print.vue's rememberPrintedTemplate); bubbled to the front of the grid below.
        recentTemplateIds: {
            type: Array,
            default: () => []
        },
        // The connected printer's tape/resolution (see label.js's tapeFromStatus), or null when
        // no printer is connected. When present, redraw() renders every thumbnail with drawLabel
        // at this tape's real dpi/printArea/length instead of drawFallbackLabel's fixed reference
        // - so the grid's px/mm-per-module numbers and any length-limit failures reflect what
        // would actually come out of the connected printer.
        tape: {
            type: Object,
            default: null
        }
    },
    model: {
        prop: "value",
        event: "input"
    },
    emits: ["input"],
    data() {
        return {
            // Keyed by template id, holding the {short, message} error from its most recent
            // redraw() failure (e.g. text too long, or too many QR modules for the thumbnail's
            // fixed reference size - see label.js's snapQrToCrispSize/encodeQr). Absent, not just
            // falsy, for a template that last drew fine, so `t.id in failed` matches "has one".
            failed: {},
            // Keyed by template id, holding the array of {short, message} scan-reliability
            // warnings (see label.js's drawFallbackLabel) from its most recent successful
            // redraw() - present only when that render tripped
            // MIN_RECOMMENDED_QR_PX_PER_MODULE/MIN_RECOMMENDED_QR_MODULE_MM.
            warned: {},
            // Keyed by template id, holding every QR-family leaf's raw {scale, moduleMm} from its
            // most recent successful redraw(), regardless of whether it warned - a debugging aid
            // shown unconditionally below each thumbnail.
            qrInfo: {},
            // Same idea as qrInfo, but every text leaf's raw {fontPx, fontMm} (see label.js's
            // checkTextSizes).
            textInfo: {},
            // Which LABEL_TAGS entry (or "all") the grid below is narrowed to.
            activeTag: "all",
        };
    },
    computed: {
        labelTags() {
            return LABEL_TAGS;
        },
        // LABEL_TEMPLATES with recentTemplateIds' entries pulled to the front (most recent
        // first, everything else following in its original order), narrowed to activeTag, then
        // with anything currently in `failed` (not printable - see redraw()'s catch) stably
        // sorted to the back regardless of recency/tag order, so real errors don't crowd out
        // layouts that actually work.
        labelTemplates() {
            const recent = this.recentTemplateIds
                .map(id => LABEL_TEMPLATES.find(t => t.id === id))
                .filter(Boolean);
            const recentIds = new Set(recent.map(t => t.id));
            const ordered = [...recent, ...LABEL_TEMPLATES.filter(t => !recentIds.has(t.id))];
            const filtered = this.activeTag === "all" ? ordered : ordered.filter(t => t.tags?.includes(this.activeTag));
            return [...filtered].sort((a, b) => (a.id in this.failed ? 1 : 0) - (b.id in this.failed ? 1 : 0));
        }
    },
    watch: {
        fields() {
            this.redraw();
        },
        // Switching filters mounts fresh canvas elements for thumbnails hidden until now (see
        // labelTemplates); nextTick waits for those refs to land before drawing into them.
        activeTag() {
            this.$nextTick(() => this.redraw());
        },
        // Printer connect/disconnect/switch - every thumbnail's canvas already exists regardless
        // of tape (unlike Print.vue's own tape-fed preview), so no flush:'post' is needed here.
        tape() {
            this.redraw();
        }
    },
    methods: {
        // "internal" -> "Internal", for both the filter buttons and each thumbnail's badge.
        tagLabel(tag) {
            return tag.charAt(0).toUpperCase() + tag.slice(1);
        },

        setTemplateCanvasRef(id, el) {
            if (el) {
                this.templateCanvases[id] = el;
            } else {
                delete this.templateCanvases[id];
            }
        },

        isAvailable(t) {
            return templateIsAvailable(t, this.fields);
        },

        // Greyed out (and unclickable) either because a needed field isn't filled in yet
        // (isAvailable) or its content failed to render (see redraw()'s catch) - either way
        // there's nothing a click could select that would actually show anything.
        isSelectable(t) {
            return this.isAvailable(t) && !(t.id in this.failed);
        },

        // The disabled thumbnail's tooltip - empty once it's selectable again.
        unavailableReason(t) {
            if (!this.isAvailable(t)) {
                return "Not available - fill in the fields this layout needs above.";
            }
            return this.failed[t.id]?.message ?? "";
        },

        // .template-thumb-canvas's object-fit:contain scales the just-drawn bitmap into its fixed
        // CSS box (getBoundingClientRect, unlike canvas.width/height, reflects that box - object-fit
        // doesn't change it); when that's a magnification, switch to nearest-neighbor so the
        // print-accurate pixel edges stay crisp instead of blurring, same call Print.vue's own
        // fitZoom makes for its explicitly-sized preview.
        applyImageRendering(canvas) {
            const {width: boxWidth, height: boxHeight} = canvas.getBoundingClientRect();
            const scale = Math.min(boxWidth / canvas.width, boxHeight / canvas.height);
            canvas.style.imageRendering = scale >= 1 ? "pixelated" : "auto";
        },

        // Live per-template thumbnails. Renders each with drawLabel at the connected printer's
        // real tape/resolution when one's connected (see the `tape` prop), so the numbers/errors
        // shown match what would actually print; falls back to drawFallbackLabel's fixed
        // reference size otherwise. Either way, still illustrative previews via CSS object-fit,
        // not the to-be-printed-accurate canvas Print.vue's own main preview is.
        redraw() {
            for (const t of LABEL_TEMPLATES) {
                const canvas = this.templateCanvases[t.id];
                if (!canvas) {
                    continue;
                }
                try {
                    const content = templateContent(t, this.fields);
                    const {warnings, qrInfo, textInfo} = this.tape
                        ? drawLabel(canvas, this.tape, content, "along")
                        : drawFallbackLabel(canvas, content, "along");
                    this.applyImageRendering(canvas);
                    delete this.failed[t.id];
                    if (warnings.length) {
                        this.warned[t.id] = warnings;
                    } else {
                        delete this.warned[t.id];
                    }
                    if (qrInfo.length) {
                        this.qrInfo[t.id] = qrInfo;
                    } else {
                        delete this.qrInfo[t.id];
                    }
                    if (textInfo.length) {
                        this.textInfo[t.id] = textInfo;
                    } else {
                        delete this.textInfo[t.id];
                    }
                } catch (e) {
                    // Grey the thumbnail out (see isSelectable/unavailableReason) rather than
                    // leaving it blank - selecting a template that can't render here would only
                    // hand Print.vue's own redraw the exact same failure.
                    canvas.width = 1;
                    canvas.height = 1;
                    this.failed[t.id] = {short: e.short ?? "error", message: e.message};
                    delete this.warned[t.id];
                    delete this.qrInfo[t.id];
                    delete this.textInfo[t.id];
                }
            }
        }
    },
    created() {
        // Keyed by template id, populated by setTemplateCanvasRef() - a plain :ref="t.id" string
        // in v-for still gets Vue's refInFor array-collecting behavior despite distinct names per
        // iteration, turning this.$refs[t.id] into a one-element array; a function ref avoids that.
        this.templateCanvases = {};
    },
    async mounted() {
        // See Print.vue's mounted() - every thumbnail here has a qr/mqr/rmqr leaf, so nothing's
        // worth drawing before the wasm resource resolves.
        await preloadQrEncoder();
        this.redraw();
    }
}
</script>
