// A template's layout tree, content resolution, and selectability contract. See
// docs/implementation.md#template-layout-tree.
const GAP = {type: "empty", "min-width": "1mm", "min-height": "1mm"};

// Generated matrix of "just the code" templates covering every QR_LEAF_TYPES combo. See
// docs/implementation.md#generated-qr-only-template-matrix.
const QR_ONLY_TEMPLATES = [
    {
        id: "qr-l", name: "QR code only (low error correction)",
        description: "Just the code, at QR's lowest error-correction level - fits more data (or a "
            + "smaller code) for the same text, but less tolerant of damage.",
    },
    {id: "qr", name: "QR code only", description: "Just the code - smallest label, prints fastest."},
    {
        id: "qr-q", name: "QR code only (quartile error correction)",
        description: "Just the code, at QR's second-highest (quartile) error-correction level - a "
            + "middle ground between code size and damage tolerance.",
    },
    {
        id: "qr-h", name: "QR code only (high error correction)",
        description: "Just the code, at QR's highest error-correction level - still scans if scuffed "
            + "or partly obscured, at the cost of a bigger code for the same text.",
    },
    {
        id: "mqr-d", name: "Micro QR code only (detection only)",
        description: "Just the code, in the more compact Micro QR format at its lowest, M1-only level - "
            + "the smallest QR-family code there is, but can only tell a scan is corrupted, not "
            + "recover from it.",
    },
    {
        id: "mqr-l", name: "Micro QR code only (low error correction)",
        description: "Just the code, in the more compact Micro QR format at its low error-correction level.",
    },
    {
        id: "mqr", name: "Micro QR code only",
        description: "Just the code, in the more compact Micro QR format - smallest label, prints fastest.",
    },
    {
        id: "mqr-q", name: "Micro QR code only (quartile error correction)",
        description: "Just the code, in the more compact Micro QR format at its highest (quartile) "
            + "error-correction level.",
    },
    {
        id: "rmqr", name: "rMQR code only",
        description: "Just the code, in the rectangular rMQR format, sized for the smallest total area "
            + "that fits the text - smallest label, prints fastest.",
    },
    {
        id: "rmqr-min", name: "rMQR code only (shortest, widest)",
        description: "Just the code, in the rectangular rMQR format, preferring the flattest/widest "
            + "symbol that fits the text - shortest across the tape, longest along it.",
    },
    {
        id: "rmqr-max", name: "rMQR code only (tallest, narrowest)",
        description: "Just the code, in the rectangular rMQR format, preferring the tallest/narrowest "
            + "symbol that fits the text - tallest across the tape, shortest along it.",
    },
    {
        id: "rmqr-h", name: "rMQR code only (high error correction)",
        description: "Just the code, in the rectangular rMQR format at its high error-correction level, "
            + "sized for the smallest total area that fits the text - still scans if scuffed or partly "
            + "obscured, at the cost of a bigger code for the same text.",
    },
    {
        id: "rmqr-h-min", name: "rMQR code only (high error correction, shortest/widest)",
        description: "Just the code, in the rectangular rMQR format at its high error-correction level, "
            + "preferring the flattest/widest symbol that fits the text.",
    },
    {
        id: "rmqr-h-max", name: "rMQR code only (high error correction, tallest/narrowest)",
        description: "Just the code, in the rectangular rMQR format at its high error-correction level, "
            + "preferring the tallest/narrowest symbol that fits the text.",
    },
].map(t => ({...t, required_vars: ["text"], layout: [{type: t.id, content: c => c.text}]}));

export const LABEL_TEMPLATES = [
    {
        id: "mqr-token", name: "MQR Token", description: "The code with the encoded text printed next to it.",
        required_vars: ["shortId"], tags: ["internal"],
        layout: [{type: "mqr", content: c => c.shortId}]
    },
    {
        id: "qr-url", name: "MQR Token", description: "The code with the encoded text printed next to it.",
        required_vars: ["shortUrl"], tags: ["external"],
        layout: [{type: "qr-h", content: c => c.shortUrl}]
    },
    {
        id: "rmqr-url", name: "rMQR Token", description: "The code with the encoded text printed next to it.",
        required_vars: ["shortUrl", "itemId"], tags: ["external"],
        layout: [{type: "rmqr", content: c => c.shortUrl}, GAP, {
            type: "text",
            content: c => c.itemId?.toString().padStart(4, "0")
        }]
    },
    {
        id: "mqr-tokem-id", name: "rMQR Token", description: "The code with the encoded text printed next to it.",
        required_vars: ["shortId", "itemId"], tags: ["internal"],
        layout: [{type: "mqr", content: c => c.shortId}, GAP, {
            type: "text",
            content: c => c.itemId?.toString().padStart(4, "0")
        }]
    },
    {
        id: "location-rmqr-url", name: "rMQR Token", description: "The code with the encoded text printed next to it.",
        required_vars: ["shortUrl", "locationId"], tags: ["external"],
        layout: [{type: "rmqr", content: c => c.shortUrl}, GAP, {
            type: "text",
            content: c => c.locationId?.toString().padStart(4, "0")
        }]
    },
    {
        id: "location-mqr-tokem-id",
        name: "rMQR Token",
        description: "The code with the encoded text printed next to it.",
        required_vars: ["shortId", "locationId"],
        tags: ["internal"],
        layout: [{type: "mqr", content: c => c.shortId}, GAP, {
            type: "text",
            content: c => c.locationId?.toString().padStart(4, "0")
        }]
    }, ...QR_ONLY_TEMPLATES,

    {
        id: "qr-text", name: "QR code + text", description: "The code with the encoded text printed next to it.",
        required_vars: ["text"],
        layout: [{type: "qr", content: c => c.text}, GAP, {type: "text", content: c => c.text?.split("\n")}]
    },
    {
        id: "qr-text-below", name: "QR code + text below",
        description: "The code with the encoded text printed below it.",
        required_vars: ["text"],
        layout: [[{type: "qr", content: c => c.text}, GAP, {type: "text", content: c => c.text?.split("\n")}]]
    },
    {
        id: "id-qr-text-vertical", name: "ID + QR code + text below",
        description: "The code with the encoded text printed below it.",
        required_vars: ["itemId", "text", "userHandle"],
        layout: [[{type: "text", content: c => "Item: " + c.itemId}, GAP, {
            type: "qr",
            content: c => c.text
        }, GAP, {type: "text", content: c => c.userHandle}]]
    },
    {
        id: "text", name: "Text only", description: "No code, just the text itself, as large as it fits.",
        required_vars: ["text"],
        layout: [{type: "text", content: c => c.text?.split("\n")}]
    },
    {
        id: "item-handle", name: "Item handle",
        description: "The compact owner@domain:id handle - meaningful in-app, not scannable on its own.",
        required_vars: ["itemHandle"], tags: ["internal"],
        layout: [{type: "text", content: c => c.itemHandle}]
    },
    {
        id: "item-url", name: "Item URL",
        description: "The full item URL as text, with no code - for copying rather than scanning.",
        required_vars: ["itemUrl"], tags: ["external"],
        layout: [{type: "text", content: c => c.itemUrl}]
    },
    {
        id: "owner-handle", name: "Owner handle", description: "Just the owning user's handle, as text only.",
        required_vars: ["userHandle"], tags: ["internal"],
        layout: [{type: "text", content: c => c.userHandle}]
    },
    {
        id: "item-id", name: "Item ID", description: "Just the bare item id, as text only.",
        required_vars: ["itemId"], tags: ["internal"],
        layout: [{type: "text", content: c => c.itemId}]
    },
    {
        id: "owner-id-text", name: "Owner + item ID",
        description: "The owner's handle and the item id, as two lines of text - no code.",
        required_vars: ["userHandle", "itemId"], tags: ["internal"],
        layout: [{type: "text", content: c => [c.userHandle, c.itemId]}]
    },
    {
        id: "location-id", name: "Location ID", description: "Just the bare storage location id, as text only.",
        required_vars: ["locationId"], tags: ["internal"],
        layout: [{type: "text", content: c => c.locationId}]
    },
    {
        id: "owner-id-text-location", name: "Owner + location ID",
        description: "The owner's handle and the location id, as two lines of text - no code.",
        required_vars: ["userHandle", "locationId"], tags: ["internal"],
        layout: [{type: "text", content: c => [c.userHandle, c.locationId]}]
    },
    {
        id: "item-url-qr-handle", name: "Item URL + handle",
        description: "Scannable item URL, with the item's compact handle printed alongside.",
        required_vars: ["itemUrl", "itemHandle"], tags: ["external"],
        layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => c.itemHandle}]
    },
    {
        id: "item-url-qr-owner", name: "Item URL + owner",
        description: "Scannable item URL, with the owner's handle printed alongside.",
        required_vars: ["itemUrl", "userHandle"], tags: ["external"],
        layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => c.userHandle}]
    },
    {
        id: "item-url-qr-id", name: "Item URL + item ID",
        description: "Scannable item URL, with the bare item id printed alongside.",
        required_vars: ["itemUrl", "itemId"], tags: ["external"],
        layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => c.itemId}]
    },
    {
        id: "item-url-qr-owner-id", name: "Item URL + owner + ID",
        description: "Scannable item URL, with the owner's handle and the item id on two lines alongside.",
        required_vars: ["itemUrl", "userHandle", "itemId"], tags: ["external"],
        layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => [c.userHandle, c.itemId]}]
    },
    {
        id: "short-url-qr", name: "Short link (QR code)",
        description: "Scannable short link for this item or storage location - more compact than "
            + "the full URL. Available for any element with a resolvable short link, not just items.",
        required_vars: ["shortUrl"], tags: ["external"],
        layout: [{type: "qr", content: c => c.shortUrl}]
    },
    {
        id: "item-url-qr-owner-id2", name: "Item URL + owner + ID",
        description: "Scannable item URL, with the owner's handle and the item id on two lines alongside.",
        required_vars: ["itemUrl", "userHandle", "itemId"], tags: ["external"],
        layout: [{type: "qr", content: c => c.itemUrl}, GAP, [{
            type: "text",
            content: c => c.userHandle
        }, GAP, {type: "text", content: c => c.itemId}]]
    },
];

// Every distinct tag value used across LABEL_TEMPLATES' tags, in first-seen order - drives
// LabelLayoutPreview.vue's filter buttons without hand-listing "internal"/"external" there.
export const LABEL_TAGS = [...new Set(LABEL_TEMPLATES.flatMap(t => t.tags ?? []))];

// Every field name any template's required_vars names, in first-seen order.
export const KNOWN_VARS = [...new Set(LABEL_TEMPLATES.flatMap(t => t.required_vars))];

// DERIVED_VARS shape and declaration-order invariant. See
// docs/implementation.md#derived-vars-shape-and-ordering.
export const DERIVED_VARS = {
    // Full user@domain handle, kept as separate user/domain base vars since that's how the
    // account is actually shaped (see federation.md's Unique Handles); this is just the
    // display/URL form.
    userHandle: {
        inputs: ["user", "domain"],
        calc: (f) => `${f.user}@${f.domain}`,
    },
    // Self-contained Item URL (see docs/design-in-progress/items-labels.md) - what a printed
    // label encodes, since scanning it must resolve everything with no other context. `webdomain`
    // defaults to this origin but is editable, since any frontend can resolve any handle.
    itemUrl: {
        inputs: ["webdomain", "userHandle", "itemId"],
        calc: (f) => `${f.webdomain}/i/${f.userHandle}/${f.itemId}`,
    },
    // Compact "owner handle + id" form (see docs/design-in-progress/items-labels.md) - meaningful
    // only where context already makes clear it's a Toolshed item, unlike itemUrl.
    itemHandle: {
        inputs: ["userHandle", "itemId"],
        calc: (f) => `${f.userHandle}:${f.itemId}`,
    },
};

// Text-input vars = template KNOWN_VARS minus derived ones, plus any var a DERIVED_VARS calc
// needs (e.g. "webdomain") even if no template names it directly.
export const BASE_VARS = [...new Set([
    ...KNOWN_VARS.filter(v => !(v in DERIVED_VARS)),
    ...Object.values(DERIVED_VARS).flatMap(d => d.inputs).filter(v => !(v in DERIVED_VARS)),
])];

// Runs each DERIVED_VARS calc against `fields`, adding the result wherever its inputs are
// present, so callers never need to know DERIVED_VARS' {inputs, calc} shape.
export function withDerivedVars(fields) {
    const result = {...fields};
    for (const [name, {inputs, calc}] of Object.entries(DERIVED_VARS)) {
        if (inputs.every(v => result[v])) {
            result[name] = calc(result);
        }
    }
    return result;
}

function walkLeaves(node, fn) {
    if (Array.isArray(node)) {
        node.forEach(child => walkLeaves(child, fn));
    } else {
        fn(node);
    }
}

function mapTree(node, fn) {
    return Array.isArray(node) ? node.map(child => mapTree(child, fn)) : fn(node);
}

// Resolved only if every part is - a single string for a QR-family/"text" leaf, every line for a
// multi-line "text" (see "owner-id-text" and "item-url-qr-owner-id" above).
function isResolved(value) {
    return Array.isArray(value) ? value.every(isResolved) : value !== undefined && value !== null;
}

// Shared by LabelLayoutPreview.vue's grid and Print.vue's preview, so neither reimplements
// leaf-walking/field-resolving itself.
export function templateIsAvailable(t, fields) {
    let available = true;
    walkLeaves(t.layout, leaf => {
        if (leaf.type !== "empty" && !isResolved(leaf.content(fields))) {
            available = false;
        }
    });
    return available;
}

// Resolves t's content leaves against fields into a tree for label.js's
// drawLabel/drawFallbackLabel. Throws the same {message, short} shape as label.js's own
// capacity errors (see docs/implementation.md#missing-data-is-a-templatecontent-error) when a
// leaf isn't resolved yet, rather than returning null, so a caller's single catch around
// drawLabel/drawFallbackLabel handles both without a separate isAvailable pre-check.
export function templateContent(t, fields) {
    if (!templateIsAvailable(t, fields)) {
        const err = new Error("This layout needs more fields filled in before it can be drawn.");
        err.short = "missing data";
        throw err;
    }
    return mapTree(t.layout, leaf => leaf.type === "empty" ? leaf : {...leaf, value: leaf.content(fields)});
}
