LedgerSectionWriter.java

/*
 * Copyright © 2026 IKE Network (support@ike.network)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package network.ike.plugin.schema;

import network.ike.plugin.schema.SchemaSignature.Enumeration;
import network.ike.plugin.schema.SchemaSignature.Position;
import network.ike.plugin.schema.SchemaSignature.PositionKind;
import network.ike.plugin.schema.SchemaSignature.TypeDefinition;
import network.ike.plugin.schema.SchemaSignature.TypeReference;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

/**
 * Writes a {@link SchemaSignature} as a ledger section in the KnowledgeSet DSL, a node catalog, the same
 * shape the hand-written sections of a starter set have (IKE-Network/ike-issues#1104):
 * one concept per type with the schema's own documentation as its definition, is-a from
 * the extension base, one concept per argument position, per schema primitive, and per
 * type of another schema the catalog refers to, one concept per enumeration and per
 * enumerated value, and a type-position pattern whose semantics record, on each type, its
 * positions with their value type and cardinality.
 *
 * <p>Every label carries a prefix chosen per namespace, {@code ELM } for the expression
 * nodes and {@code ELM System } for the System types, because koncept identifiers are
 * derived from labels alone, a foundation already holds concepts named Concept, Code,
 * Quantity, Interval, and List, and ELM itself declares an Interval in both of its
 * namespaces. The generated class is a source file to check in and regenerate, never to
 * edit.
 */
public final class LedgerSectionWriter {

    private static final int FRAGMENT_WIDTH = 86;
    private static final String INDENT = "                        + \"";

    /**
     * What the writer needs beyond the signature.
     *
     * @param packageName          the generated class's package
     * @param className            the generated class's simple name
     * @param tag                  the fully-qualified-name tag, {@code ELM}, written in
     *                             parentheses after every label
     * @param labelPrefix          the prefix labels carry when their namespace has no prefix
     *                             of its own, {@code ELM }, also the prefix of the family's
     *                             own concepts
     * @param prefixByNamespace    the label prefix per namespace, {@code ELM System } for
     *                             the System types; a namespace absent here uses
     *                             {@code labelPrefix}
     * @param attribution          who the definitions come from, for example {@code the ELM
     *                             specification}
     * @param pin                  the version the schemas were taken from, for example a
     *                             repository tag
     * @param stampExpression      the Java expression of the inception stamp, {@code
     *                             Ike.INCEPTION}
     * @param termsClass           the class holding field data-type constants, {@code IkeTerm}
     * @param rootParentExpression the Java expression of the family root's parent, {@code
     *                             IkeTerm.MODEL_CONCEPT}
     */
    public record Options(String packageName, String className, String tag, String labelPrefix,
                          Map<String, String> prefixByNamespace, String attribution, String pin,
                          String stampExpression, String termsClass, String rootParentExpression) {
        /**
         * Creates the options, copying the prefix map.
         *
         * @param packageName          the package
         * @param className            the class name
         * @param tag                  the tag
         * @param labelPrefix          the default label prefix
         * @param prefixByNamespace    the prefix per namespace
         * @param attribution          the attribution
         * @param pin                  the pin
         * @param stampExpression      the stamp expression
         * @param termsClass           the terms class
         * @param rootParentExpression the root parent expression
         */
        public Options {
            labelPrefix = spaced(labelPrefix);
            Map<String, String> spacedPrefixes = new LinkedHashMap<>();
            for (Map.Entry<String, String> entry : prefixByNamespace.entrySet()) {
                spacedPrefixes.put(entry.getKey(), spaced(entry.getValue()));
            }
            prefixByNamespace = Map.copyOf(spacedPrefixes);
        }

        /** A prefix ends with one space, so that a label reads as two words whether or not the caller typed the space. */
        private static String spaced(String prefix) {
            String trimmed = prefix.strip();
            return trimmed.isEmpty() ? "" : trimmed + " ";
        }

        String prefixFor(String namespace) {
            return prefixByNamespace.getOrDefault(namespace, labelPrefix);
        }
    }

    private LedgerSectionWriter() {
    }

    /**
     * Renders the signature as the source text of a ledger section.
     *
     * @param signature the signature to write
     * @param options   the naming and wiring choices
     * @return the Java source of the section class
     */
    public static String write(SchemaSignature signature, Options options) {
        Naming naming = new Naming(options);
        List<TypeDefinition> ordered = topological(signature.types());
        Set<TypeReference> declared = new LinkedHashSet<>();
        for (TypeDefinition type : signature.types()) {
            declared.add(type.reference());
        }
        for (Enumeration enumeration : signature.enumerations()) {
            declared.add(new TypeReference(enumeration.namespace(), enumeration.name()));
        }
        Map<String, List<TypeDefinition>> positionsByName = new TreeMap<>();
        Set<String> primitives = new TreeSet<>();
        Map<String, TypeReference> externals = new TreeMap<>();
        int positionCount = 0;
        for (TypeDefinition type : signature.types()) {
            for (Position position : type.positions()) {
                positionsByName.computeIfAbsent(position.name(), ignored -> new ArrayList<>()).add(type);
                TypeReference value = position.valueType();
                if (value.isPrimitive()) {
                    primitives.add(value.name());
                } else if (!declared.contains(value)) {
                    externals.put(value.name(), value);
                }
                positionCount++;
            }
            type.base().ifPresent(base -> {
                if (!declared.contains(base) && !base.isPrimitive()) {
                    externals.put(base.name(), base);
                }
            });
        }
        int valueCount = 0;
        for (Enumeration enumeration : signature.enumerations()) {
            valueCount += enumeration.values().size();
        }

        naming.spellApart(positionsByName.keySet());
        StringBuilder src = new StringBuilder();
        header(src, options, signature, positionsByName.size(), primitives.size(), externals.size(),
                positionCount, valueCount);
        src.append("final class ").append(options.className()).append(" {\n\n");
        src.append("    /** The family root: the catalog itself. */\n");
        src.append("    static final String ROOT_FQN = ").append(quote(naming.fqn("node catalog"))).append(";\n\n");
        src.append("    /** The pattern that records, on each type, its positions. */\n");
        src.append("    static final String TYPE_POSITION_PATTERN_FQN = ")
                .append(quote(naming.fqn("type position pattern"))).append(";\n\n");
        src.append("    private ").append(options.className()).append("() {\n    }\n\n");
        src.append("    /**\n     * Composes this section's declarations into the session.\n");
        src.append("     *\n     * @param set the knowledge set (the session)\n     */\n");
        src.append("    static void compose(KnowledgeSet set) {\n");
        src.append("        ActiveStamp inception = ").append(options.stampExpression()).append(";\n\n");

        family(src, naming, options, signature, positionsByName.size(), primitives.size(), externals.size());
        pattern(src, naming, options);
        positions(src, naming, positionsByName);
        primitives(src, naming, primitives, options);
        externals(src, naming, externals, options);
        types(src, naming, options, ordered, declared);
        enumerations(src, naming, options, signature.enumerations());

        src.append("    }\n}\n");
        return src.toString();
    }

    private static void header(StringBuilder src, Options options, SchemaSignature signature,
                               int positionNames, int primitiveCount, int externalCount, int positionCount,
                               int valueCount) {
        src.append("""
                /*
                 * Copyright © 2026 IKE Network (support@ike.network)
                 *
                 * Licensed under the Apache License, Version 2.0 (the "License");
                 * you may not use this file except in compliance with the License.
                 * You may obtain a copy of the License at
                 *
                 *     http://www.apache.org/licenses/LICENSE-2.0
                 *
                 * Unless required by applicable law or agreed to in writing, software
                 * distributed under the License is distributed on an "AS IS" BASIS,
                 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
                 * See the License for the specific language governing permissions and
                 * limitations under the License.
                 */
                """);
        src.append("package ").append(options.packageName()).append(";\n\n");
        src.append("import dev.ikm.tinkar.common.id.PublicIds;\n");
        src.append("import dev.ikm.tinkar.entity.builder.ActiveStamp;\n");
        src.append("import dev.ikm.tinkar.entity.builder.KnowledgeSet;\n");
        src.append("import dev.ikm.tinkar.terms.EntityProxy;\n\n");
        src.append("/**\n");
        src.append(" * The ").append(options.tag()).append(" node catalog as a ledger section, imported from ")
                .append(options.attribution()).append(", ").append(options.pin())
                .append(", by {@code ike:schema-import} (IKE-Network/ike-issues#1104).\n");
        src.append(" * GENERATED FROM THE SCHEMAS: regenerate, never edit.\n");
        src.append(" * <p>").append(signature.types().size()).append(" types, ")
                .append(positionNames).append(" position names over ").append(positionCount)
                .append(" type positions, ").append(primitiveCount).append(" schema primitives, ")
                .append(externalCount).append(" types of other schemas referred to, ")
                .append(signature.enumerations().size()).append(" enumerations with ")
                .append(valueCount).append(" values.\n");
        src.append(" */\n");
    }

    private static void family(StringBuilder src, Naming naming, Options options, SchemaSignature signature,
                               int positionNames, int primitiveCount, int externalCount) {
        src.append("        // ── The family root and its parents ──\n");
        concept(src, naming.fqn("node catalog"), naming.label("node catalog"),
                "The catalog of the node kinds of " + options.attribution() + ": each kind, what it"
                        + " holds, and what each thing it holds may be. Its " + signature.types().size()
                        + " node kinds, each with its base and its positions, its "
                        + positionNames + " position names, its " + primitiveCount
                        + " schema primitives, its " + externalCount
                        + (externalCount == 1 ? " type" : " types") + " of other schemas it refers to, and its "
                        + signature.enumerations().size()
                        + (signature.enumerations().size() == 1 ? " enumeration" : " enumerations")
                        + ", imported from " + options.pin()
                        + " and regenerated from the schemas, never edited. A node kind is a kind of node"
                        + " a tree in this language can have; a position is a named place in a node that"
                        + " holds a child or a value.",
                options.rootParentExpression());
        src.append("        EntityProxy.Concept root = set.conceptRef(ROOT_FQN);\n\n");
        concept(src, naming.fqn("position"), naming.label("position"),
                "A position of the catalog: a named place in a node that holds a child"
                        + " or a value, such as operand or dataType, shared by every node type that has a"
                        + " position of that name. Which types have it, with what value type and how"
                        + " many, is recorded on each type by the type position pattern.",
                "root");
        src.append("        EntityProxy.Concept positionParent = set.conceptRef(")
                .append(quote(naming.fqn("position"))).append(");\n\n");
        concept(src, naming.fqn("primitive"), naming.label("primitive"),
                "A value type the XML Schema language itself supplies, string or QName, rather"
                        + " than a node kind of the catalog: what a position holds when it holds a plain"
                        + " value and not a node.",
                "root");
        src.append("        EntityProxy.Concept primitiveParent = set.conceptRef(")
                .append(quote(naming.fqn("primitive"))).append(");\n\n");
        concept(src, naming.fqn("external type"), naming.label("external type"),
                "A type declared by a schema outside this catalog that a position or a base refers"
                        + " to; the catalog names it so that every reference resolves, and says no"
                        + " more about it than its name and its namespace.",
                "root");
        src.append("        EntityProxy.Concept externalParent = set.conceptRef(")
                .append(quote(naming.fqn("external type"))).append(");\n\n");
    }

    private static void pattern(StringBuilder src, Naming naming, Options options) {
        src.append("        // ── The type position pattern: which positions each type has ──\n");
        concept(src, naming.fqn("type position"), naming.label("type position"),
                "What a type position semantic is: one position of one node kind, with the"
                        + " type of value it holds, the fewest and the most values the schema allows,"
                        + " the schema's own note on that position for that kind, and its form: whether"
                        + " our vertex holds it as a property or as an edge.",
                "root");
        concept(src, naming.fqn("catalog structure"), naming.label("catalog structure"),
                "Why type positions are recorded: so that a tree can be checked against the"
                        + " catalog and a reader can know what each node may hold.",
                "root");
        src.append("        EntityProxy.Concept structure = set.conceptRef(")
                .append(quote(naming.fqn("catalog structure"))).append(");\n");
        concept(src, naming.fqn("property form"), naming.label("property form"),
                "The form of a position that holds a plain value, an attribute in the schema: our"
                        + " vertex holds it as a property keyed by the position.",
                "root");
        concept(src, naming.fqn("edge form"), naming.label("edge form"),
                "The form of a position that holds a node, an element in the schema: our vertex"
                        + " holds it as an edge to the vertex below, named by the position.",
                "root");
        String[][] fields = {
            {"position field", "The position a type position semantic is about."},
            {"value type field", "The type of value that position holds on that type: a node kind of"
                    + " the catalog, a schema primitive, or a type of another schema."},
            {"minimum field", "The fewest values the schema allows in that position, zero when it is"
                    + " optional."},
            {"maximum field", "The most values the schema allows in that position, or minus one when"
                    + " the schema sets no limit."},
            {"position note field", "The schema's own note on that position for that type, empty when"
                    + " the schema gives none."},
            {"form field", "How our vertex holds that position on that type: the property form when"
                    + " it holds a plain value, the edge form when it holds a node."},
        };
        for (String[] field : fields) {
            concept(src, naming.fqn(field[0]), naming.label(field[0]), field[1], "root");
        }
        src.append("        set.pattern(TYPE_POSITION_PATTERN_FQN).at(inception)\n");
        src.append("                .meaning(set.conceptRef(").append(quote(naming.fqn("type position"))).append("))\n");
        src.append("                .purpose(structure)\n");
        src.append("                .field(set.conceptRef(").append(quote(naming.fqn("position field")))
                .append("), structure, ").append(options.termsClass()).append(".COMPONENT_FIELD)\n");
        src.append("                .field(set.conceptRef(").append(quote(naming.fqn("value type field")))
                .append("), structure, ").append(options.termsClass()).append(".COMPONENT_FIELD)\n");
        src.append("                .field(set.conceptRef(").append(quote(naming.fqn("minimum field")))
                .append("), structure, ").append(options.termsClass()).append(".INTEGER_FIELD)\n");
        src.append("                .field(set.conceptRef(").append(quote(naming.fqn("maximum field")))
                .append("), structure, ").append(options.termsClass()).append(".INTEGER_FIELD)\n");
        src.append("                .field(set.conceptRef(").append(quote(naming.fqn("position note field")))
                .append("), structure, ").append(options.termsClass()).append(".STRING)\n");
        src.append("                .field(set.conceptRef(").append(quote(naming.fqn("form field")))
                .append("), structure, ").append(options.termsClass()).append(".COMPONENT_FIELD);\n");
        src.append("        EntityProxy.Concept propertyForm = set.conceptRef(")
                .append(quote(naming.fqn("property form"))).append(");\n");
        src.append("        EntityProxy.Concept edgeForm = set.conceptRef(")
                .append(quote(naming.fqn("edge form"))).append(");\n");
        src.append("        EntityProxy.Pattern typePositions = set.patternRef(TYPE_POSITION_PATTERN_FQN);\n\n");
    }

    private static void positions(StringBuilder src, Naming naming, Map<String, List<TypeDefinition>> byName) {
        src.append("        // ── Positions: one concept per name, shared across types ──\n");
        for (Map.Entry<String, List<TypeDefinition>> entry : byName.entrySet()) {
            String name = entry.getKey();
            List<TypeDefinition> owners = entry.getValue();
            Set<PositionKind> kinds = new TreeSet<>();
            for (TypeDefinition owner : owners) {
                for (Position position : owner.positions()) {
                    if (position.name().equals(name)) {
                        kinds.add(position.kind());
                    }
                }
            }
            String kindText = kinds.size() == 2 ? "an element on some types and an attribute on others"
                    : kinds.contains(PositionKind.ATTRIBUTE) ? "an attribute" : "a child element";
            String definition = "The argument position named " + name + ", " + kindText + ", on "
                    + owners.size() + (owners.size() == 1 ? " node kind" : " node kinds") + " of the catalog."
                    + naming.spellingNote(name);
            concept(src, naming.positionFqn(name), naming.positionLabel(name), definition, "positionParent");
        }
        src.append('\n');
    }

    private static void primitives(StringBuilder src, Naming naming, Set<String> primitives, Options options) {
        src.append("        // ── Schema primitives the positions use ──\n");
        for (String primitive : primitives) {
            concept(src, naming.primitiveFqn(primitive), naming.primitiveLabel(primitive),
                    "The XML Schema primitive " + primitive + ", a plain value a position of "
                            + options.attribution() + " holds.",
                    "primitiveParent");
        }
        src.append('\n');
    }

    private static void externals(StringBuilder src, Naming naming, Map<String, TypeReference> externals,
                                  Options options) {
        if (externals.isEmpty()) {
            return;
        }
        src.append("        // ── Types of other schemas the catalog refers to ──\n");
        for (TypeReference external : externals.values()) {
            concept(src, naming.externalFqn(external.name()), naming.externalLabel(external.name()),
                    "The type " + external.name() + " of the namespace " + external.namespace()
                            + ", which " + options.attribution() + " refers to but does not declare.",
                    "externalParent");
        }
        src.append('\n');
    }

    private static void types(StringBuilder src, Naming naming, Options options, List<TypeDefinition> ordered,
                              Set<TypeReference> declared) {
        src.append("        // ── Types, each after its base ──\n");
        for (TypeDefinition type : ordered) {
            String parent = "root";
            if (type.base().isPresent()) {
                TypeReference base = type.base().get();
                parent = "set.conceptRef(" + quote(declared.contains(base)
                        ? naming.typeFqn(base) : naming.externalFqn(base.name())) + ")";
            }
            String abstractNote = type.isAbstract()
                    ? " The schema marks it abstract: a tree never holds it directly, only one of the"
                    + " types that extend it." : "";
            String definition = type.documentation().isEmpty()
                    ? "A type of " + options.attribution() + "; the schema gives no description." + abstractNote
                    : "From " + options.attribution() + ": " + type.documentation() + abstractNote;
            src.append("        set.concept(").append(quote(naming.typeFqn(type.reference()))).append(").at(inception)\n");
            src.append("                .synonym(").append(quote(naming.typeLabel(type.reference()))).append(")\n");
            definitionCall(src, definition);
            src.append("                .isA(").append(parent).append(')');
            for (Position position : type.positions()) {
                TypeReference value = position.valueType();
                String valueType = value.isPrimitive()
                        ? "set.conceptRef(" + quote(naming.primitiveFqn(value.name())) + ")"
                        : declared.contains(value)
                        ? "set.conceptRef(" + quote(naming.typeFqn(value)) + ")"
                        : "set.conceptRef(" + quote(naming.externalFqn(value.name())) + ")";
                src.append("\n                .semantic(typePositions, PublicIds.of(set.uuidFor(")
                        .append(quote("Type position: " + naming.typeLabel(type.reference()) + " " + position.name()))
                        .append(")),\n");
                src.append("                        set.conceptRef(").append(quote(naming.positionFqn(position.name())))
                        .append("), ").append(valueType).append(", ").append(position.minimum()).append(", ")
                        .append(position.maximum()).append(",\n");
                src.append("                        ").append(quote(position.documentation())).append(", ")
                        .append(position.kind() == PositionKind.ATTRIBUTE ? "propertyForm" : "edgeForm").append(')');
            }
            src.append(";\n");
        }
        src.append('\n');
    }

    private static void enumerations(StringBuilder src, Naming naming, Options options,
                                     List<Enumeration> enumerations) {
        if (enumerations.isEmpty()) {
            return;
        }
        src.append("        // ── Enumerations and their values ──\n");
        for (Enumeration enumeration : enumerations) {
            TypeReference reference = new TypeReference(enumeration.namespace(), enumeration.name());
            String definition = enumeration.documentation().isEmpty()
                    ? "An enumeration of " + options.attribution() + " with " + enumeration.values().size()
                    + " values; the schema gives no description."
                    : "From " + options.attribution() + ": " + enumeration.documentation();
            concept(src, naming.typeFqn(reference), naming.typeLabel(reference), definition, "root");
            for (String value : enumeration.values()) {
                concept(src, naming.valueFqn(reference, value), naming.valueLabel(reference, value),
                        "The " + enumeration.name() + " value " + value + " of " + options.attribution() + ".",
                        "set.conceptRef(" + quote(naming.typeFqn(reference)) + ")");
            }
        }
    }

    private static void concept(StringBuilder src, String fqn, String label, String definition, String parent) {
        src.append("        set.concept(").append(quote(fqn)).append(").at(inception)\n");
        src.append("                .synonym(").append(quote(label)).append(")\n");
        definitionCall(src, definition);
        src.append("                .isA(").append(parent).append(");\n");
    }

    private static void definitionCall(StringBuilder src, String definition) {
        List<String> fragments = fragments(definition);
        src.append("                .definition(").append(quote(fragments.get(0)));
        for (int i = 1; i < fragments.size(); i++) {
            src.append('\n').append(INDENT).append(escape(fragments.get(i))).append('"');
        }
        src.append(")\n");
    }

    /**
     * Splits a definition into fragments of at most {@link #FRAGMENT_WIDTH} characters on
     * spaces, each continuation fragment starting with the space that separates it, the
     * way the hand-written sections wrap their definitions.
     */
    static List<String> fragments(String text) {
        List<String> fragments = new ArrayList<>();
        String remaining = text;
        while (remaining.length() > FRAGMENT_WIDTH) {
            int cut = remaining.lastIndexOf(' ', FRAGMENT_WIDTH);
            if (cut <= 0) {
                cut = remaining.indexOf(' ', FRAGMENT_WIDTH);
                if (cut < 0) {
                    break;
                }
            }
            fragments.add(remaining.substring(0, cut));
            remaining = remaining.substring(cut);
        }
        fragments.add(remaining);
        return fragments;
    }

    /** Orders types so that every type follows its base; ties keep declaration order. */
    static List<TypeDefinition> topological(List<TypeDefinition> types) {
        Map<TypeReference, TypeDefinition> byReference = new LinkedHashMap<>();
        for (TypeDefinition type : types) {
            byReference.put(type.reference(), type);
        }
        List<TypeDefinition> ordered = new ArrayList<>();
        Set<TypeReference> placed = new LinkedHashSet<>();
        for (TypeDefinition type : types) {
            place(type, byReference, placed, ordered, new LinkedHashSet<>());
        }
        return ordered;
    }

    private static void place(TypeDefinition type, Map<TypeReference, TypeDefinition> byReference,
                              Set<TypeReference> placed, List<TypeDefinition> ordered, Set<TypeReference> visiting) {
        if (placed.contains(type.reference())) {
            return;
        }
        if (!visiting.add(type.reference())) {
            throw new IllegalArgumentException("Type " + type.name() + " extends itself through " + visiting);
        }
        if (type.base().isPresent()) {
            TypeDefinition base = byReference.get(type.base().get());
            if (base != null) {
                place(base, byReference, placed, ordered, visiting);
            }
        }
        visiting.remove(type.reference());
        placed.add(type.reference());
        ordered.add(type);
    }

    private static String quote(String text) {
        return '"' + escape(text) + '"';
    }

    private static String escape(String text) {
        return text.replace("\\", "\\\\").replace("\"", "\\\"");
    }

    /**
     * The naming scheme: a prefix per namespace plus the schema name for labels, the tag in
     * parentheses for FQNs. Two position names that differ only by case, ELM's
     * {@code codeSystem} and {@code codesystem}, would collapse into one binding constant,
     * so such names are spelled apart in their labels.
     */
    private static final class Naming {
        private final Options options;
        private final Map<String, String> caseSuffixes = new LinkedHashMap<>();

        Naming(Options options) {
            this.options = options;
        }

        /** Finds position names that collide case-insensitively and gives each a suffix. */
        void spellApart(Set<String> positionNames) {
            Map<String, List<String>> byLowerCase = new TreeMap<>();
            for (String name : positionNames) {
                byLowerCase.computeIfAbsent(name.toLowerCase(java.util.Locale.ROOT), ignored -> new ArrayList<>())
                        .add(name);
            }
            for (List<String> group : byLowerCase.values()) {
                if (group.size() < 2) {
                    continue;
                }
                for (String name : group) {
                    String suffix = name.equals(name.toLowerCase(java.util.Locale.ROOT)) ? ", lower case"
                            : name.equals(name.toUpperCase(java.util.Locale.ROOT)) ? ", upper case" : ", mixed case";
                    if (caseSuffixes.containsValue(suffix) && group.stream().filter(other ->
                            caseSuffixes.getOrDefault(other, "").equals(suffix)).count() > 0) {
                        throw new IllegalArgumentException("Position names " + group
                                + " differ only by case in more than one mixed spelling; the writer cannot"
                                + " spell them apart");
                    }
                    caseSuffixes.put(name, suffix);
                }
            }
        }

        String spellingNote(String positionName) {
            String suffix = caseSuffixes.get(positionName);
            return suffix == null ? "" : " The schema spells it " + suffix.substring(2)
                    + ", and spells another position the same way but for case.";
        }

        String label(String word) {
            return options.labelPrefix() + word;
        }

        String fqn(String word) {
            return label(word) + " (" + options.tag() + ")";
        }

        String typeLabel(TypeReference type) {
            return options.prefixFor(type.namespace()) + type.name();
        }

        String typeFqn(TypeReference type) {
            return typeLabel(type) + " (" + options.tag() + ")";
        }

        String positionLabel(String name) {
            return options.labelPrefix() + name + " position" + caseSuffixes.getOrDefault(name, "");
        }

        String positionFqn(String name) {
            return positionLabel(name) + " (" + options.tag() + ")";
        }

        String primitiveLabel(String name) {
            return options.labelPrefix() + "primitive " + name;
        }

        String primitiveFqn(String name) {
            return primitiveLabel(name) + " (" + options.tag() + ")";
        }

        String externalLabel(String name) {
            return options.labelPrefix() + "external " + name;
        }

        String externalFqn(String name) {
            return externalLabel(name) + " (" + options.tag() + ")";
        }

        String valueLabel(TypeReference enumeration, String value) {
            return typeLabel(enumeration) + " " + value;
        }

        String valueFqn(TypeReference enumeration, String value) {
            return valueLabel(enumeration, value) + " (" + options.tag() + ")";
        }
    }
}