/*
 * Copyright (C) 2023-2024  Yomitan Authors
 * Copyright (C) 2021-2022  Yomichan Authors
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

import {getDisambiguations, getGroupedPronunciations, getPronunciationsOfType, getTermFrequency, groupTermTags} from '../dictionary/dictionary-data-util.js';
import {distributeFurigana, distributeFuriganaInflected} from '../language/ja/japanese.js';

/**
 * Creates a compatibility representation of the specified data.
 * @param {string} marker The marker that is being used for template rendering.
 * @param {import('anki-templates-internal').CreateDetails} details Information which is used to generate the data.
 * @returns {import('anki-templates').NoteData} An object used for rendering Anki templates.
 */
export function createAnkiNoteData(marker, {
    dictionaryEntry,
    resultOutputMode,
    mode,
    glossaryLayoutMode,
    compactTags,
    context,
    media
}) {
    const definition = createCachedValue(getDefinition.bind(null, dictionaryEntry, context, resultOutputMode));
    const uniqueExpressions = createCachedValue(getUniqueExpressions.bind(null, dictionaryEntry));
    const uniqueReadings = createCachedValue(getUniqueReadings.bind(null, dictionaryEntry));
    const context2 = createCachedValue(getPublicContext.bind(null, context));
    const pitches = createCachedValue(getPitches.bind(null, dictionaryEntry));
    const pitchCount = createCachedValue(getPitchCount.bind(null, pitches));
    const phoneticTranscriptions = createCachedValue(getPhoneticTranscriptions.bind(null, dictionaryEntry));

    if (typeof media !== 'object' || media === null || Array.isArray(media)) {
        media = {
            audio: void 0,
            screenshot: void 0,
            clipboardImage: void 0,
            clipboardText: void 0,
            selectionText: void 0,
            textFurigana: [],
            dictionaryMedia: {}
        };
    }
    /** @type {import('anki-templates').NoteData} */
    const result = {
        marker,
        get definition() { return getCachedValue(definition); },
        glossaryLayoutMode,
        compactTags,
        group: (resultOutputMode === 'group'),
        merge: (resultOutputMode === 'merge'),
        modeTermKanji: (mode === 'term-kanji'),
        modeTermKana: (mode === 'term-kana'),
        modeKanji: (mode === 'kanji'),
        compactGlossaries: (glossaryLayoutMode === 'compact'),
        get uniqueExpressions() { return getCachedValue(uniqueExpressions); },
        get uniqueReadings() { return getCachedValue(uniqueReadings); },
        get pitches() { return getCachedValue(pitches); },
        get pitchCount() { return getCachedValue(pitchCount); },
        get phoneticTranscriptions() { return getCachedValue(phoneticTranscriptions); },
        get context() { return getCachedValue(context2); },
        media,
        dictionaryEntry
    };
    Object.defineProperty(result, 'dictionaryEntry', {
        configurable: false,
        enumerable: false,
        writable: false,
        value: dictionaryEntry
    });
    return result;
}

/**
 * Creates a deferred-evaluation value.
 * @template [T=unknown]
 * @param {() => T} getter The function to invoke to get the return value.
 * @returns {import('anki-templates-internal').CachedValue<T>} An object which can be passed into `getCachedValue`.
 */
export function createCachedValue(getter) {
    return {getter, hasValue: false, value: void 0};
}

/**
 * Gets the value of a cached object.
 * @template [T=unknown]
 * @param {import('anki-templates-internal').CachedValue<T>} item An object that was returned from `createCachedValue`.
 * @returns {T} The result of evaluating the getter, which is cached after the first invocation.
 */
export function getCachedValue(item) {
    if (item.hasValue) { return /** @type {T} */ (item.value); }
    const value = item.getter();
    item.value = value;
    item.hasValue = true;
    return value;
}

// Private

/**
 * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry
 * @returns {?import('dictionary').TermSource}
 */
function getPrimarySource(dictionaryEntry) {
    for (const headword of dictionaryEntry.headwords) {
        for (const source of headword.sources) {
            if (source.isPrimary) { return source; }
        }
    }
    return null;
}

/**
 * @param {import('dictionary').DictionaryEntry} dictionaryEntry
 * @returns {string[]}
 */
function getUniqueExpressions(dictionaryEntry) {
    if (dictionaryEntry.type === 'term') {
        const results = new Set();
        for (const {term} of dictionaryEntry.headwords) {
            results.add(term);
        }
        return [...results];
    } else {
        return [];
    }
}

/**
 * @param {import('dictionary').DictionaryEntry} dictionaryEntry
 * @returns {string[]}
 */
function getUniqueReadings(dictionaryEntry) {
    if (dictionaryEntry.type === 'term') {
        const results = new Set();
        for (const {reading} of dictionaryEntry.headwords) {
            results.add(reading);
        }
        return [...results];
    } else {
        return [];
    }
}

/**
 * @param {import('anki-templates-internal').Context} context
 * @returns {import('anki-templates').Context}
 */
function getPublicContext(context) {
    let {documentTitle, query, fullQuery} = context;
    if (typeof documentTitle !== 'string') { documentTitle = ''; }
    return {
        query,
        fullQuery,
        document: {
            title: documentTitle
        }
    };
}

/**
 * @param {import('dictionary').TermDictionaryEntry|import('dictionary').KanjiDictionaryEntry} dictionaryEntry
 * @returns {number[]}
 */
function getFrequencyNumbers(dictionaryEntry) {
    let previousDictionary;
    const frequencies = [];
    for (const {dictionary, frequency, displayValue} of dictionaryEntry.frequencies) {
        if (dictionary === previousDictionary) {
            continue;
        }
        previousDictionary = dictionary;

        if (displayValue !== null) {
            const frequencyMatch = displayValue.match(/\d+/);
            if (frequencyMatch !== null) {
                frequencies.push(Number.parseInt(frequencyMatch[0], 10));
                continue;
            }
        }
        frequencies.push(frequency);
    }
    return frequencies;
}

/**
 * @param {import('dictionary').TermDictionaryEntry|import('dictionary').KanjiDictionaryEntry} dictionaryEntry
 * @returns {number}
 */
function getFrequencyHarmonic(dictionaryEntry) {
    const frequencies = getFrequencyNumbers(dictionaryEntry);

    if (frequencies.length === 0) {
        return -1;
    }

    let total = 0;
    for (const frequency of frequencies) {
        total += 1 / frequency;
    }
    return Math.floor(frequencies.length / total);
}

/**
 * @param {import('dictionary').TermDictionaryEntry|import('dictionary').KanjiDictionaryEntry} dictionaryEntry
 * @returns {number}
 */
function getFrequencyAverage(dictionaryEntry) {
    const frequencies = getFrequencyNumbers(dictionaryEntry);

    if (frequencies.length === 0) {
        return -1;
    }

    let total = 0;
    for (const frequency of frequencies) {
        total += frequency;
    }
    return Math.floor(total / frequencies.length);
}

/**
 * @param {import('dictionary').DictionaryEntry} dictionaryEntry
 * @returns {import('anki-templates').PitchGroup[]}
 */
function getPitches(dictionaryEntry) {
    /** @type {import('anki-templates').PitchGroup[]} */
    const results = [];
    if (dictionaryEntry.type === 'term') {
        for (const {dictionary, pronunciations} of getGroupedPronunciations(dictionaryEntry)) {
            /** @type {import('anki-templates').Pitch[]} */
            const pitches = [];
            for (const groupedPronunciation of pronunciations) {
                const {pronunciation} = groupedPronunciation;
                if (pronunciation.type !== 'pitch-accent') { continue; }
                const {position, nasalPositions, devoicePositions, tags} = pronunciation;
                const {terms, reading, exclusiveTerms, exclusiveReadings} = groupedPronunciation;
                pitches.push({
                    expressions: terms,
                    reading,
                    position,
                    nasalPositions,
                    devoicePositions,
                    tags: convertPitchTags(tags),
                    exclusiveExpressions: exclusiveTerms,
                    exclusiveReadings
                });
            }
            results.push({dictionary, pitches});
        }
    }
    return results;
}

/**
 * @param {import('dictionary').DictionaryEntry} dictionaryEntry
 * @returns {import('anki-templates').TranscriptionGroup[]}
 */
function getPhoneticTranscriptions(dictionaryEntry) {
    const results = [];
    if (dictionaryEntry.type === 'term') {
        for (const {dictionary, pronunciations} of getGroupedPronunciations(dictionaryEntry)) {
            const phoneticTranscriptions = [];
            for (const groupedPronunciation of pronunciations) {
                const {pronunciation} = groupedPronunciation;
                if (pronunciation.type !== 'phonetic-transcription') { continue; }
                const {ipa, tags} = pronunciation;
                const {terms, reading, exclusiveTerms, exclusiveReadings} = groupedPronunciation;
                phoneticTranscriptions.push({
                    expressions: terms,
                    reading,
                    ipa,
                    tags,
                    exclusiveExpressions: exclusiveTerms,
                    exclusiveReadings
                });
            }
            results.push({dictionary, phoneticTranscriptions});
        }
    }
    return results;
}

/**
 * @param {import('anki-templates-internal').CachedValue<import('anki-templates').PitchGroup[]>} cachedPitches
 * @returns {number}
 */
function getPitchCount(cachedPitches) {
    const pitches = getCachedValue(cachedPitches);
    return pitches.reduce((i, v) => i + v.pitches.length, 0);
}

/**
 * @param {import('dictionary').DictionaryEntry} dictionaryEntry
 * @param {import('anki-templates-internal').Context} context
 * @param {import('settings').ResultOutputMode} resultOutputMode
 * @returns {import('anki-templates').DictionaryEntry}
 */
function getDefinition(dictionaryEntry, context, resultOutputMode) {
    switch (dictionaryEntry.type) {
        case 'term':
            return getTermDefinition(dictionaryEntry, context, resultOutputMode);
        case 'kanji':
            return getKanjiDefinition(dictionaryEntry, context);
        default:
            return /** @type {import('anki-templates').UnknownDictionaryEntry} */ ({});
    }
}

/**
 * @param {import('dictionary').KanjiDictionaryEntry} dictionaryEntry
 * @param {import('anki-templates-internal').Context} context
 * @returns {import('anki-templates').KanjiDictionaryEntry}
 */
function getKanjiDefinition(dictionaryEntry, context) {
    const {character, dictionary, onyomi, kunyomi, definitions} = dictionaryEntry;

    let {url} = context;
    if (typeof url !== 'string') { url = ''; }

    const stats = createCachedValue(getKanjiStats.bind(null, dictionaryEntry));
    const tags = createCachedValue(convertTags.bind(null, dictionaryEntry.tags));
    const frequencies = createCachedValue(getKanjiFrequencies.bind(null, dictionaryEntry));
    const frequencyHarmonic = createCachedValue(getFrequencyHarmonic.bind(null, dictionaryEntry));
    const frequencyAverage = createCachedValue(getFrequencyAverage.bind(null, dictionaryEntry));
    const cloze = createCachedValue(getCloze.bind(null, dictionaryEntry, context));

    return {
        type: 'kanji',
        character,
        dictionary,
        onyomi,
        kunyomi,
        glossary: definitions,
        get tags() { return getCachedValue(tags); },
        get stats() { return getCachedValue(stats); },
        get frequencies() { return getCachedValue(frequencies); },
        get frequencyHarmonic() { return getCachedValue(frequencyHarmonic); },
        get frequencyAverage() { return getCachedValue(frequencyAverage); },
        url,
        get cloze() { return getCachedValue(cloze); }
    };
}

/**
 * @param {import('dictionary').KanjiDictionaryEntry} dictionaryEntry
 * @returns {import('anki-templates').KanjiStatGroups}
 */
function getKanjiStats(dictionaryEntry) {
    /** @type {import('anki-templates').KanjiStatGroups} */
    const results = {};
    for (const [key, value] of Object.entries(dictionaryEntry.stats)) {
        results[key] = value.map(convertKanjiStat);
    }
    return results;
}

/**
 * @param {import('dictionary').KanjiStat} kanjiStat
 * @returns {import('anki-templates').KanjiStat}
 */
function convertKanjiStat({name, category, content, order, score, dictionary, value}) {
    return {
        name,
        category,
        notes: content,
        order,
        score,
        dictionary,
        value
    };
}

/**
 * @param {import('dictionary').KanjiDictionaryEntry} dictionaryEntry
 * @returns {import('anki-templates').KanjiFrequency[]}
 */
function getKanjiFrequencies(dictionaryEntry) {
    /** @type {import('anki-templates').KanjiFrequency[]} */
    const results = [];
    for (const {index, dictionary, dictionaryIndex, dictionaryPriority, character, frequency, displayValue} of dictionaryEntry.frequencies) {
        results.push({
            index,
            dictionary,
            dictionaryOrder: {
                index: dictionaryIndex,
                priority: dictionaryPriority
            },
            character,
            frequency: displayValue !== null ? displayValue : frequency
        });
    }
    return results;
}

/**
 * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry
 * @param {import('anki-templates-internal').Context} context
 * @param {import('settings').ResultOutputMode} resultOutputMode
 * @returns {import('anki-templates').TermDictionaryEntry}
 */
function getTermDefinition(dictionaryEntry, context, resultOutputMode) {
    /** @type {import('anki-templates').TermDictionaryEntryType} */
    let type = 'term';
    switch (resultOutputMode) {
        case 'group': type = 'termGrouped'; break;
        case 'merge': type = 'termMerged'; break;
    }

    const {inflectionRuleChainCandidates, score, dictionaryIndex, dictionaryPriority, sourceTermExactMatchCount, definitions} = dictionaryEntry;

    let {url} = context;
    if (typeof url !== 'string') { url = ''; }

    const primarySource = getPrimarySource(dictionaryEntry);

    const dictionaryNames = createCachedValue(getTermDictionaryNames.bind(null, dictionaryEntry));
    const commonInfo = createCachedValue(getTermDictionaryEntryCommonInfo.bind(null, dictionaryEntry, type));
    const termTags = createCachedValue(getTermTags.bind(null, dictionaryEntry, type));
    const expressions = createCachedValue(getTermExpressions.bind(null, dictionaryEntry));
    const frequencies = createCachedValue(getTermFrequencies.bind(null, dictionaryEntry));
    const frequencyHarmonic = createCachedValue(getFrequencyHarmonic.bind(null, dictionaryEntry));
    const frequencyAverage = createCachedValue(getFrequencyAverage.bind(null, dictionaryEntry));
    const pitches = createCachedValue(getTermPitches.bind(null, dictionaryEntry));
    const phoneticTranscriptions = createCachedValue(getTermPhoneticTranscriptions.bind(null, dictionaryEntry));
    const glossary = createCachedValue(getTermGlossaryArray.bind(null, dictionaryEntry, type));
    const cloze = createCachedValue(getCloze.bind(null, dictionaryEntry, context));
    const furiganaSegments = createCachedValue(getTermFuriganaSegments.bind(null, dictionaryEntry, type));
    const sequence = createCachedValue(getTermDictionaryEntrySequence.bind(null, dictionaryEntry));

    return {
        type,
        id: (type === 'term' && definitions.length > 0 ? definitions[0].id : void 0),
        source: (primarySource !== null ? primarySource.transformedText : null),
        rawSource: (primarySource !== null ? primarySource.originalText : null),
        sourceTerm: (type !== 'termMerged' ? (primarySource !== null ? primarySource.deinflectedText : null) : void 0),
        inflectionRuleChainCandidates,
        score,
        isPrimary: (type === 'term' ? dictionaryEntry.isPrimary : void 0),
        get sequence() { return getCachedValue(sequence); },
        get dictionary() { return getCachedValue(dictionaryNames)[0]; },
        dictionaryOrder: {
            index: dictionaryIndex,
            priority: dictionaryPriority
        },
        get dictionaryNames() { return getCachedValue(dictionaryNames); },
        get expression() {
            const {uniqueTerms} = getCachedValue(commonInfo);
            return (type === 'term' || type === 'termGrouped' ? uniqueTerms[0] : uniqueTerms);
        },
        get reading() {
            const {uniqueReadings} = getCachedValue(commonInfo);
            return (type === 'term' || type === 'termGrouped' ? uniqueReadings[0] : uniqueReadings);
        },
        get expressions() { return getCachedValue(expressions); },
        get glossary() { return getCachedValue(glossary); },
        get definitionTags() { return type === 'term' ? getCachedValue(commonInfo).definitionTags : void 0; },
        get termTags() { return getCachedValue(termTags); },
        get definitions() { return getCachedValue(commonInfo).definitions; },
        get frequencies() { return getCachedValue(frequencies); },
        get frequencyHarmonic() { return getCachedValue(frequencyHarmonic); },
        get frequencyAverage() { return getCachedValue(frequencyAverage); },
        get pitches() { return getCachedValue(pitches); },
        get phoneticTranscriptions() { return getCachedValue(phoneticTranscriptions); },
        sourceTermExactMatchCount,
        url,
        get cloze() { return getCachedValue(cloze); },
        get furiganaSegments() { return getCachedValue(furiganaSegments); }
    };
}

/**
 * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry
 * @returns {string[]}
 */
function getTermDictionaryNames(dictionaryEntry) {
    const dictionaryNames = new Set();
    for (const {dictionary} of dictionaryEntry.definitions) {
        dictionaryNames.add(dictionary);
    }
    return [...dictionaryNames];
}

/**
 * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry
 * @param {import('anki-templates').TermDictionaryEntryType} type
 * @returns {import('anki-templates').TermDictionaryEntryCommonInfo}
 */
function getTermDictionaryEntryCommonInfo(dictionaryEntry, type) {
    const merged = (type === 'termMerged');
    const hasDefinitions = (type !== 'term');

    /** @type {Set<string>} */
    const allTermsSet = new Set();
    /** @type {Set<string>} */
    const allReadingsSet = new Set();
    for (const {term, reading} of dictionaryEntry.headwords) {
        allTermsSet.add(term);
        allReadingsSet.add(reading);
    }
    const uniqueTerms = [...allTermsSet];
    const uniqueReadings = [...allReadingsSet];

    /** @type {import('anki-templates').TermDefinition[]} */
    const definitions = [];
    /** @type {import('anki-templates').Tag[]} */
    const definitionTags = [];
    for (const {tags, headwordIndices, entries, dictionary, sequences} of dictionaryEntry.definitions) {
        const definitionTags2 = [];
        for (const tag of tags) {
            definitionTags.push(convertTag(tag));
            definitionTags2.push(convertTag(tag));
        }
        if (!hasDefinitions) { continue; }
        const only = merged ? getDisambiguations(dictionaryEntry.headwords, headwordIndices, allTermsSet, allReadingsSet) : void 0;
        definitions.push({
            sequence: sequences[0],
            dictionary,
            glossary: entries,
            definitionTags: definitionTags2,
            only
        });
    }

    return {
        uniqueTerms,
        uniqueReadings,
        definitionTags,
        definitions: hasDefinitions ? definitions : void 0
    };
}

/**
 * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry
 * @returns {import('anki-templates').TermFrequency[]}
 */
function getTermFrequencies(dictionaryEntry) {
    const results = [];
    const {headwords} = dictionaryEntry;
    for (const {headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, hasReading, frequency, displayValue} of dictionaryEntry.frequencies) {
        const {term, reading} = headwords[headwordIndex];
        results.push({
            index: results.length,
            expressionIndex: headwordIndex,
            dictionary,
            dictionaryOrder: {
                index: dictionaryIndex,
                priority: dictionaryPriority
            },
            expression: term,
            reading,
            hasReading,
            frequency: displayValue !== null ? displayValue : frequency
        });
    }
    return results;
}

/**
 * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry
 * @returns {import('anki-templates').TermPitchAccent[]}
 */
function getTermPitches(dictionaryEntry) {
    const results = [];
    const {headwords} = dictionaryEntry;
    for (const {headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, pronunciations} of dictionaryEntry.pronunciations) {
        const {term, reading} = headwords[headwordIndex];
        const pitches = getPronunciationsOfType(pronunciations, 'pitch-accent');
        const cachedPitches = createCachedValue(getTermPitchesInner.bind(null, pitches));
        results.push({
            index: results.length,
            expressionIndex: headwordIndex,
            dictionary,
            dictionaryOrder: {
                index: dictionaryIndex,
                priority: dictionaryPriority
            },
            expression: term,
            reading,
            get pitches() { return getCachedValue(cachedPitches); }
        });
    }
    return results;
}

/**
 * @param {import('dictionary').PitchAccent[]} pitches
 * @returns {import('anki-templates').PitchAccent[]}
 */
function getTermPitchesInner(pitches) {
    const results = [];
    for (const {position, tags} of pitches) {
        const cachedTags = createCachedValue(convertTags.bind(null, tags));
        results.push({
            position,
            get tags() { return getCachedValue(cachedTags); }
        });
    }
    return results;
}

/**
 * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry
 * @returns {import('anki-templates').TermPhoneticTranscription[]}
 */
function getTermPhoneticTranscriptions(dictionaryEntry) {
    const results = [];
    const {headwords} = dictionaryEntry;
    for (const {headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, pronunciations} of dictionaryEntry.pronunciations) {
        const {term, reading} = headwords[headwordIndex];
        const phoneticTranscriptions = getPronunciationsOfType(pronunciations, 'phonetic-transcription');
        const termPhoneticTranscriptions = getTermPhoneticTranscriptionsInner(phoneticTranscriptions);
        results.push({
            index: results.length,
            expressionIndex: headwordIndex,
            dictionary,
            dictionaryOrder: {
                index: dictionaryIndex,
                priority: dictionaryPriority
            },
            expression: term,
            reading,
            get phoneticTranscriptions() { return termPhoneticTranscriptions; }
        });
    }

    return results;
}

/**
 * @param {import('dictionary').PhoneticTranscription[]} phoneticTranscriptions
 * @returns {import('anki-templates').PhoneticTranscription[]}
 */
function getTermPhoneticTranscriptionsInner(phoneticTranscriptions) {
    const results = [];
    for (const {ipa, tags} of phoneticTranscriptions) {
        const cachedTags = createCachedValue(convertTags.bind(null, tags));
        results.push({
            ipa,
            get tags() { return getCachedValue(cachedTags); }
        });
    }
    return results;
}

/**
 * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry
 * @returns {import('anki-templates').TermHeadword[]}
 */
function getTermExpressions(dictionaryEntry) {
    const results = [];
    const {headwords} = dictionaryEntry;
    for (let i = 0, ii = headwords.length; i < ii; ++i) {
        const {term, reading, tags, sources: [{deinflectedText}], wordClasses} = headwords[i];
        const termTags = createCachedValue(convertTags.bind(null, tags));
        const frequencies = createCachedValue(getTermExpressionFrequencies.bind(null, dictionaryEntry, i));
        const pitches = createCachedValue(getTermExpressionPitches.bind(null, dictionaryEntry, i));
        const termFrequency = createCachedValue(getTermExpressionTermFrequency.bind(null, termTags));
        const furiganaSegments = createCachedValue(getTermHeadwordFuriganaSegments.bind(null, term, reading));
        const item = {
            sourceTerm: deinflectedText,
            expression: term,
            reading,
            get termTags() { return getCachedValue(termTags); },
            get frequencies() { return getCachedValue(frequencies); },
            get pitches() { return getCachedValue(pitches); },
            get furiganaSegments() { return getCachedValue(furiganaSegments); },
            get termFrequency() { return getCachedValue(termFrequency); },
            wordClasses
        };
        results.push(item);
    }
    return results;
}

/**
 * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry
 * @param {number} i
 * @returns {import('anki-templates').TermFrequency[]}
 */
function getTermExpressionFrequencies(dictionaryEntry, i) {
    const results = [];
    const {headwords, frequencies} = dictionaryEntry;
    for (const {headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, hasReading, frequency, displayValue} of frequencies) {
        if (headwordIndex !== i) { continue; }
        const {term, reading} = headwords[headwordIndex];
        results.push({
            index: results.length,
            expressionIndex: headwordIndex,
            dictionary,
            dictionaryOrder: {
                index: dictionaryIndex,
                priority: dictionaryPriority
            },
            expression: term,
            reading,
            hasReading,
            frequency: displayValue !== null ? displayValue : frequency
        });
    }
    return results;
}

/**
 * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry
 * @param {number} i
 * @returns {import('anki-templates').TermPitchAccent[]}
 */
function getTermExpressionPitches(dictionaryEntry, i) {
    const results = [];
    const {headwords, pronunciations: termPronunciations} = dictionaryEntry;
    for (const {headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, pronunciations} of termPronunciations) {
        if (headwordIndex !== i) { continue; }
        const {term, reading} = headwords[headwordIndex];
        const pitches = getPronunciationsOfType(pronunciations, 'pitch-accent');
        const cachedPitches = createCachedValue(getTermPitchesInner.bind(null, pitches));
        results.push({
            index: results.length,
            expressionIndex: headwordIndex,
            dictionary,
            dictionaryOrder: {
                index: dictionaryIndex,
                priority: dictionaryPriority
            },
            expression: term,
            reading,
            get pitches() { return getCachedValue(cachedPitches); }
        });
    }
    return results;
}

/**
 * @param {import('anki-templates-internal').CachedValue<import('anki-templates').Tag[]>} cachedTermTags
 * @returns {import('anki-templates').TermFrequencyType}
 */
function getTermExpressionTermFrequency(cachedTermTags) {
    const termTags = getCachedValue(cachedTermTags);
    return getTermFrequency(termTags);
}

/**
 * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry
 * @param {import('anki-templates').TermDictionaryEntryType} type
 * @returns {import('dictionary-data').TermGlossary[]|undefined}
 */
function getTermGlossaryArray(dictionaryEntry, type) {
    if (type === 'term') {
        const results = [];
        for (const {entries} of dictionaryEntry.definitions) {
            results.push(...entries);
        }
        return results;
    }
    return void 0;
}

/**
 * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry
 * @param {import('anki-templates').TermDictionaryEntryType} type
 * @returns {import('anki-templates').Tag[]|undefined}
 */
function getTermTags(dictionaryEntry, type) {
    if (type !== 'termMerged') {
        const results = [];
        for (const {tag} of groupTermTags(dictionaryEntry)) {
            results.push(convertTag(tag));
        }
        return results;
    }
    return void 0;
}

/**
 * @param {import('dictionary').Tag[]} tags
 * @returns {import('anki-templates').Tag[]}
 */
function convertTags(tags) {
    const results = [];
    for (const tag of tags) {
        results.push(convertTag(tag));
    }
    return results;
}

/**
 * @param {import('dictionary').Tag} tag
 * @returns {import('anki-templates').Tag}
 */
function convertTag({name, category, content, order, score, dictionaries, redundant}) {
    return {
        name,
        category,
        notes: (content.length > 0 ? content[0] : ''),
        order,
        score,
        dictionary: (dictionaries.length > 0 ? dictionaries[0] : ''),
        redundant
    };
}

/**
 * @param {import('dictionary').Tag[]} tags
 * @returns {import('anki-templates').PitchTag[]}
 */
function convertPitchTags(tags) {
    const results = [];
    for (const tag of tags) {
        results.push(convertPitchTag(tag));
    }
    return results;
}

/**
 * @param {import('dictionary').Tag} tag
 * @returns {import('anki-templates').PitchTag}
 */
function convertPitchTag({name, category, content, order, score, dictionaries, redundant}) {
    return {
        name,
        category,
        order,
        score,
        content: [...content],
        dictionaries: [...dictionaries],
        redundant
    };
}

/**
 * @param {import('dictionary').DictionaryEntry} dictionaryEntry
 * @param {import('anki-templates-internal').Context} context
 * @returns {import('anki-templates').Cloze}
 */
function getCloze(dictionaryEntry, context) {
    let originalText = '';
    let term = '';
    let reading = '';
    switch (dictionaryEntry.type) {
        case 'term':
            {
                term = dictionaryEntry.headwords[0].term;
                reading = dictionaryEntry.headwords[0].reading;
                const primarySource = getPrimarySource(dictionaryEntry);
                if (primarySource !== null) { originalText = primarySource.originalText; }
            }
            break;
        case 'kanji':
            originalText = dictionaryEntry.character;
            break;
    }

    const {sentence} = context;
    let text;
    let offset;
    if (typeof sentence === 'object' && sentence !== null) {
        ({text, offset} = sentence);
    }
    if (typeof text !== 'string') { text = ''; }
    if (typeof offset !== 'number') { offset = 0; }

    const textSegments = [];
    for (const {text: text2, reading: reading2} of distributeFuriganaInflected(term, reading, text.substring(offset, offset + originalText.length))) {
        textSegments.push(reading2.length > 0 ? reading2 : text2);
    }

    return {
        sentence: text,
        prefix: text.substring(0, offset),
        body: text.substring(offset, offset + originalText.length),
        bodyKana: textSegments.join(''),
        suffix: text.substring(offset + originalText.length)
    };
}

/**
 * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry
 * @param {import('anki-templates').TermDictionaryEntryType} type
 * @returns {import('anki-templates').FuriganaSegment[]|undefined}
 */
function getTermFuriganaSegments(dictionaryEntry, type) {
    if (type === 'term') {
        for (const {term, reading} of dictionaryEntry.headwords) {
            return getTermHeadwordFuriganaSegments(term, reading);
        }
    }
    return void 0;
}

/**
 * @param {string} term
 * @param {string} reading
 * @returns {import('anki-templates').FuriganaSegment[]}
 */
function getTermHeadwordFuriganaSegments(term, reading) {
    /** @type {import('anki-templates').FuriganaSegment[]} */
    const result = [];
    for (const {text, reading: reading2} of distributeFurigana(term, reading)) {
        result.push({text, furigana: reading2});
    }
    return result;
}

/**
 * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry
 * @returns {number}
 */
function getTermDictionaryEntrySequence(dictionaryEntry) {
    let hasSequence = false;
    let mainSequence = -1;
    if (!dictionaryEntry.isPrimary) { return mainSequence; }
    for (const {sequences} of dictionaryEntry.definitions) {
        const sequence = sequences[0];
        if (!hasSequence) {
            mainSequence = sequence;
            hasSequence = true;
            if (mainSequence === -1) { break; }
        } else if (mainSequence !== sequence) {
            mainSequence = -1;
            break;
        }
    }
    return mainSequence;
}