package org.jabref.logic.openoffice.style;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import org.jabref.logic.formatter.bibtexfields.RemoveEnclosingBracesFormatter;
import org.jabref.logic.util.strings.StringUtil;
import org.jabref.model.database.BibDatabase;
import org.jabref.model.entry.Author;
import org.jabref.model.entry.AuthorList;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.field.Field;
import org.jabref.model.entry.field.OrFields;
import org.jabref.model.openoffice.ootext.OOText;
import org.jabref.model.openoffice.style.CitationLookupResult;
import org.jabref.model.openoffice.style.CitationMarkerEntry;
import org.jabref.model.openoffice.style.CitationMarkerNormEntry;
import org.jabref.model.openoffice.style.CitationType;
import org.jabref.model.openoffice.style.NonUniqueCitationMarker;
import org.jabref.model.openoffice.style.PageInfo;

import org.jspecify.annotations.NonNull;

class JStyleGetCitationMarker {

    private static final RemoveEnclosingBracesFormatter REMOVE_BRACES_FORMATTER = new RemoveEnclosingBracesFormatter();

    private JStyleGetCitationMarker() {
    }

    /// Look up the nth author and return the "proper" last name for
    /// citation markers.
    ///
    /// Note: "proper" in the sense that it includes the "von" part
    /// of the name (followed by a space) if there is one.
    ///
    /// @param authorList The author list.
    /// @param number     The number of the author to return.
    /// @return The author name, or an empty String if inapplicable.
    private static String getAuthorLastName(AuthorList authorList, int number) {
        StringBuilder stringBuilder = new StringBuilder();

        if (authorList.getNumberOfAuthors() > number) {
            Author author = authorList.getAuthor(number);
            // "von " if von exists
            Optional<String> von = author.getNamePrefix();
            if (von.isPresent() && !von.get().isEmpty()) {
                stringBuilder.append(von.get());
                stringBuilder.append(' ');
            }
            // last name if it exists
            stringBuilder.append(author.getFamilyName().map(REMOVE_BRACES_FORMATTER::format).orElse(""));
        }

        return stringBuilder.toString();
    }

    private static String markupAuthorName(JStyle style, String name) {
        return style.getAuthorNameMarkupBefore()
                + name
                + style.getAuthorNameMarkupAfter();
    }

    ///
    /// Note:
    /// - The "Au[AS]Bu[AS]Cu" (or the "Au") part may be empty (maxAuthors==0 or nAuthors==0).
    /// - OXFORD_COMMA is only emitted if nAuthors is at least 3.
    /// - andString  is only emitted if nAuthors is at least 2.
    ///
    /// @param authorList Parsed list of authors.
    /// @param maxAuthors The maximum number of authors to write out. If there are more authors, then ET_AL_STRING is emitted to mark their omission. Set to -1 to write out all authors. maxAuthors=0 is pointless, now throws IllegalArgumentException.  (Earlier it behaved as maxAuthors=1). maxAuthors less than -1 : throw IllegalArgumentException
    /// @param andString  For "A, B[and]C"
    /// @return "Au[AS]Bu[AS]Cu[OXFORD_COMMA][andString]Du[yearSep]" or "Au[etAlString][yearSep]",  where AS = AUTHOR_SEPARATOR, Au, Bu, Cu, Du are last names of authors.
    private static String formatAuthorList(JStyle style,
                                           @NonNull AuthorList authorList,
                                           int maxAuthors,
                                           String andString) {
        // Apparently maxAuthorsBeforeEtAl is always 1 for in-text citations.
        // In reference lists can be for example 7,
        // (https://www.chicagomanualofstyle.org/turabian/turabian-author-date-citation-quick-guide.html)
        // but those are handled elsewhere.
        //
        // There is also
        // https://apastyle.apa.org/style-grammar-guidelines/ ...
        //          ... citations/basic-principles/same-year-first-author
        // suggesting the to avoid ambiguity, we may need more than one name
        // before "et al.". We do not currently do this kind of disambiguation,
        // but we might, one day.
        //
        final int maxAuthorsBeforeEtAl = 1;

        // The String to represent authors that are not mentioned,
        // e.g. " et al."
        String etAlString = style.getEtAlString();

        // getItalicEtAl is not necessary now, since etAlString could
        // itself contain the markup.
        // This is for backward compatibility.
        if (style.getItalicEtAl()) {
            etAlString = "<i>" + etAlString + "</i>";
        }

        // The String to add between author names except the last two,
        // e.g. ", ".
        String authorSep = style.getAuthorSeparator();

        // The String to put after the second to last author in case
        // of three or more authors: (A, B[,] and C)
        String oxfordComma = style.getOxfordComma();

        StringBuilder stringBuilder = new StringBuilder();

        final int nAuthors = authorList.getNumberOfAuthors();

        // To reduce ambiguity, throw on unexpected values of maxAuthors
        if (maxAuthors == 0 && nAuthors != 0) {
            throw new IllegalArgumentException("maxAuthors = 0 in formatAuthorList");
        }
        if (maxAuthors < -1) {
            throw new IllegalArgumentException("maxAuthors < -1 in formatAuthorList");
        }

        // emitAllAuthors == false means use "et al."
        boolean emitAllAuthors = (nAuthors <= maxAuthors) || (maxAuthors == -1);

        int nAuthorsToEmit;
        if (emitAllAuthors) {
            nAuthorsToEmit = nAuthors;
        } else {
            // If we use "et al." maxAuthorsBeforeEtAl also limits the number of authors emitted.
            nAuthorsToEmit = Math.min(maxAuthorsBeforeEtAl, nAuthors);
        }

        if (nAuthorsToEmit >= 1) {
            stringBuilder.append(style.getAuthorsPartMarkupBefore());
            stringBuilder.append(style.getAuthorNamesListMarkupBefore());
            // The first author
            String name = getAuthorLastName(authorList, 0);
            stringBuilder.append(markupAuthorName(style, name));
        }

        if (nAuthors >= 2) {
            if (emitAllAuthors) {
                // Emit last names, except for the last author
                int j = 1;
                while (j < (nAuthors - 1)) {
                    stringBuilder.append(authorSep);
                    String name = getAuthorLastName(authorList, j);
                    stringBuilder.append(markupAuthorName(style, name));
                    j++;
                }
                // oxfordComma if at least 3 authors
                if (nAuthors >= 3) {
                    stringBuilder.append(oxfordComma);
                }
                // Emit " and "+"LastAuthor"
                stringBuilder.append(andString);
                String name = getAuthorLastName(authorList, nAuthors - 1);
                stringBuilder.append(markupAuthorName(style, name));
            } else {
                // Emit last names up to nAuthorsToEmit.
                //
                // The (maxAuthorsBeforeEtAl > 1) test is intended to
                // make sure the compiler eliminates this block as
                // long as maxAuthorsBeforeEtAl is fixed to 1.
                if (maxAuthorsBeforeEtAl > 1) {
                    int j = 1;
                    while (j < nAuthorsToEmit) {
                        stringBuilder.append(authorSep);
                        String name = getAuthorLastName(authorList, j);
                        stringBuilder.append(markupAuthorName(style, name));
                        j++;
                    }
                }
            }
        }

        if (nAuthorsToEmit >= 1) {
            stringBuilder.append(style.getAuthorNamesListMarkupAfter());
        }

        if (nAuthors >= 2 && !emitAllAuthors) {
            stringBuilder.append(etAlString);
        }

        stringBuilder.append(style.getAuthorsPartMarkupAfter());
        return stringBuilder.toString();
    }

    /// On success, getRawCitationMarkerField returns content,
    /// but we also need to know which field matched, because
    /// for some fields (actually: for author names) we need to
    /// reproduce the surrounding braces to inform AuthorList.parse
    /// not to split up the content.
    private static class FieldAndContent {
        Field field;
        String content;

        FieldAndContent(Field field, String content) {
            this.field = field;
            this.content = content;
        }
    }

    /// @return the field and the content of the first nonempty (after trimming)
    /// field (or alias) from `fields` found in `entry`.
    /// Return `Optional.empty()` if found nothing.
    private static Optional<FieldAndContent> getRawCitationMarkerField(@NonNull BibEntry entry,
                                                                       @NonNull BibDatabase database,
                                                                       @NonNull OrFields fields) {
        for (Field field : fields.getFields() /* FieldFactory.parseOrFields(fields)*/) {
            // NOT LaTeX free, because there is some latextohtml in org.jabref.logic.openoffice.style.OOPreFormatter
            Optional<String> optionalContent = entry.getResolvedFieldOrAlias(field, database);
            final boolean foundSomething = !StringUtil.isBlank(optionalContent);
            if (foundSomething) {
                return Optional.of(new FieldAndContent(field, optionalContent.get()));
            }
        }
        return Optional.empty();
    }

    /// This method looks up a field for an entry in a database.
    ///
    /// Any number of backup fields can be used if the primary field is
    /// empty.
    ///
    /// @param fields A list of fields, to look up, using first nonempty hit. If backup fields are needed, separate field names by /. E.g. to use "author" with "editor" as backup, specify `FieldFactory.serializeOrFields(StandardField.AUTHOR, StandardField.EDITOR)`
    /// @return The resolved field content, or an empty string if the field(s) were empty.
    private static String getCitationMarkerField(JStyle style,
                                                 @NonNull CitationLookupResult citationLookupResult,
                                                 OrFields fields) {
        Optional<FieldAndContent> optionalFieldAndContent =
                getRawCitationMarkerField(citationLookupResult.entry, citationLookupResult.database, fields);

        if (optionalFieldAndContent.isEmpty()) {
            // No luck? Return an empty string:
            return "";
        }

        FieldAndContent fieldAndContent = optionalFieldAndContent.get();
        String result = style.getFieldFormatter().format(fieldAndContent.content);

        // If the field we found is mentioned in authorFieldNames and
        // content has a pair of braces around it, we add a pair of
        // braces around the result, so that AuthorList.parse does not split
        // the content.
        final OrFields fieldsToRebrace = style.getAuthorFieldNames();
        if (fieldsToRebrace.contains(fieldAndContent.field) && StringUtil.isInCurlyBrackets(fieldAndContent.content)) {
            result = "{" + result + "}";
        }
        return result;
    }

    private static AuthorList getAuthorList(JStyle style, CitationLookupResult citationLookupResult) {
        // The bibtex fields providing author names, e.g. "author" or
        // "editor".
        OrFields authorFieldNames = style.getAuthorFieldNames();

        String authorListAsString = getCitationMarkerField(style, citationLookupResult, authorFieldNames);
        return AuthorList.parse(authorListAsString);
    }

    private enum AuthorYearMarkerPurpose {
        IN_PARENTHESIS,
        IN_TEXT,
        NORMALIZED,
        /// "Doe, 2003" -- no brackets at all.
        NO_PARENTHESIS,
        /// "Doe" -- author part only.
        AUTHOR_ONLY,
        /// "2003a" -- year part only.
        YEAR_ONLY
    }

    private static AuthorYearMarkerPurpose purposeOf(CitationType citationType) {
        return switch (citationType) {
            // INVISIBLE_CIT never reaches marker generation, the value is irrelevant.
            case AUTHORYEAR_PAR,
                 INVISIBLE_CIT ->
                    AuthorYearMarkerPurpose.IN_PARENTHESIS;
            case AUTHORYEAR_INTEXT ->
                    AuthorYearMarkerPurpose.IN_TEXT;
            case AUTHORYEAR_NOPAR ->
                    AuthorYearMarkerPurpose.NO_PARENTHESIS;
            case AUTHOR_ONLY ->
                    AuthorYearMarkerPurpose.AUTHOR_ONLY;
            case YEAR_ONLY ->
                    AuthorYearMarkerPurpose.YEAR_ONLY;
        };
    }

    /// How many authors would be emitted for entry, considering
    /// style and entry.getIsFirstAppearanceOfSource()
    ///
    /// If entry is unresolved, return 0.
    private static int calculateNAuthorsToEmit(JStyle style, CitationMarkerEntry entry) {
        if (entry.getLookupResult().isEmpty()) {
            // unresolved
            return 0;
        }

        int maxAuthors = entry.getIsFirstAppearanceOfSource()
                         ? style.getMaxAuthorsFirst()
                         : style.getMaxAuthors();

        AuthorList authorList = getAuthorList(style, entry.getLookupResult().get());
        int nAuthors = authorList.getNumberOfAuthors();

        if (maxAuthors == -1) {
            return nAuthors;
        } else {
            return Integer.min(nAuthors, maxAuthors);
        }
    }

    /// Produce (Author, year) or "Author (year)" style citation strings.
    ///
    /// Here we do not check for duplicate entries: those
    /// are handled by `getCitationMarker` by
    /// omitting them from the list.
    ///
    /// Unresolved citations recognized by
    /// entry.getBibEntry() and/or
    /// entry.getDatabase() returning empty, and
    /// emitted as "Unresolved${citationKey}".
    ///
    /// Neither uniqueLetter nor pageInfo are emitted
    /// for unresolved citations.
    ///
    /// @param purpose            IN_PARENTHESIS and NORMALIZED puts parentheses around the whole, IN_TEXT around each (year,uniqueLetter,pageInfo) part. NORMALIZED omits uniqueLetter and pageInfo,  ignores isFirstAppearanceOfSource (always style.getMaxAuthors, not getMaxAuthorsFirst)
    /// @param entries            The list of CitationMarkerEntry values to process.
    /// @param startsNewGroup     Should have the same length as `entries`, and contain true for entries starting a new group, false for those that only add a uniqueLetter tothe grouped presentation.
    /// @param maxAuthorsOverride If not empty, always show this number of authors. Added to allow NORMALIZED to use maxAuthors value that differs from style.getMaxAuthors()
    /// @return The formatted citation.
    private static OOText getAuthorYearParenthesisMarker2(JStyle style,
                                                          AuthorYearMarkerPurpose purpose,
                                                          List<CitationMarkerEntry> entries,
                                                          boolean[] startsNewGroup,
                                                          Optional<Integer> maxAuthorsOverride) {

        boolean inParenthesis = purpose == AuthorYearMarkerPurpose.IN_PARENTHESIS
                || purpose == AuthorYearMarkerPurpose.NORMALIZED;

        boolean bracketsAroundYear = purpose == AuthorYearMarkerPurpose.IN_TEXT;

        boolean emitAuthors = purpose != AuthorYearMarkerPurpose.YEAR_ONLY;
        boolean emitYear = purpose != AuthorYearMarkerPurpose.AUTHOR_ONLY;

        boolean emitPageInfo = purpose != AuthorYearMarkerPurpose.NORMALIZED
                && purpose != AuthorYearMarkerPurpose.AUTHOR_ONLY;

        // The String to separate authors from year, e.g. "; ".
        String yearSep;
        if (!emitAuthors || !emitYear) {
            yearSep = "";
        } else if (bracketsAroundYear) {
            yearSep = style.getYearSeparatorInText();
        } else {
            yearSep = style.getYearSeparator();
        }

        // The opening parenthesis.
        String startBrace = style.getBracketBefore();

        // The closing parenthesis.
        String endBrace = style.getBracketAfter();

        // The String to separate citations from each other.
        String citationSeparator = style.getCitationSeparator();

        // The bibtex field providing the year, e.g. "year".
        OrFields yearFieldNames = style.getYearFieldNames();

        // The String to add between the two last author names, e.g. " & ".
        String andString = bracketsAroundYear
                           ? style.getAuthorLastSeparatorInTextWithFallBack()
                           : style.getAuthorLastSeparator();

        String pageInfoSeparator = style.getPageInfoSeparator();
        String uniquefierSeparator = style.getUniquefierSeparator();

        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(style.getCitationGroupMarkupBefore());

        if (inParenthesis) {
            stringBuilder.append(startBrace); // shared parenthesis
        }

        for (int j = 0; j < entries.size(); j++) {
            CitationMarkerEntry entry = entries.get(j);
            boolean startingNewGroup = startsNewGroup[j];
            boolean endingAGroup = (j + 1 == entries.size()) || startsNewGroup[j + 1];

            if (!startingNewGroup) {
                // Just add our uniqueLetter
                String uniqueLetter = entry.getUniqueLetter().orElse(null);
                if (uniqueLetter != null) {
                    stringBuilder.append(uniquefierSeparator);
                    stringBuilder.append(uniqueLetter);
                }

                // And close the brace, if we are the last in the group.
                if (bracketsAroundYear && endingAGroup) {
                    stringBuilder.append(endBrace);
                }
                continue;
            }

            if (j > 0) {
                stringBuilder.append(citationSeparator);
            }

            StringBuilder pageInfoPart = new StringBuilder();
            if (emitPageInfo) {
                Optional<OOText> pageInfo =
                        PageInfo.normalizePageInfo(entry.getPageInfo());
                if (pageInfo.isPresent()) {
                    pageInfoPart.append(pageInfoSeparator);
                    pageInfoPart.append(OOText.toString(pageInfo.get()));
                }
            }

            final boolean isUnresolved = entry.getLookupResult().isEmpty();
            if (isUnresolved) {
                stringBuilder.append("Unresolved(%s)".formatted(entry.getCitationKey()));
                if (emitPageInfo) {
                    stringBuilder.append(pageInfoPart);
                }
            } else {
                CitationLookupResult citationLookupResult = entry.getLookupResult().get();

                int maxAuthors = purpose == AuthorYearMarkerPurpose.NORMALIZED
                                 ? style.getMaxAuthors()
                                 : calculateNAuthorsToEmit(style, entry);

                if (maxAuthorsOverride.isPresent()) {
                    maxAuthors = maxAuthorsOverride.get();
                }

                if (emitAuthors) {
                    AuthorList authorList = getAuthorList(style, citationLookupResult);
                    String authorString = formatAuthorList(style, authorList, maxAuthors, andString);
                    stringBuilder.append(authorString);
                    stringBuilder.append(yearSep);
                }

                if (bracketsAroundYear) {
                    stringBuilder.append(startBrace); // parenthesis before year
                }

                if (emitYear) {
                    String year = getCitationMarkerField(style, citationLookupResult, yearFieldNames);
                    if (year != null) {
                        stringBuilder.append(year);
                    }

                    if (purpose != AuthorYearMarkerPurpose.NORMALIZED) {
                        entry.getUniqueLetter().ifPresent(stringBuilder::append);
                    }
                }

                if (emitPageInfo) {
                    stringBuilder.append(pageInfoPart);
                }

                if (bracketsAroundYear && endingAGroup) {
                    stringBuilder.append(endBrace);  // parenthesis after year
                }
            }
        } // for j

        if (inParenthesis) {
            stringBuilder.append(endBrace); // shared parenthesis
        }
        stringBuilder.append(style.getCitationGroupMarkupAfter());
        return OOText.fromString(stringBuilder.toString());
    }

    /// Add / override methods for the purpose of creating a normalized citation marker.
    private static class CitationMarkerNormEntryWrap implements CitationMarkerEntry {

        CitationMarkerNormEntry inner;

        CitationMarkerNormEntryWrap(CitationMarkerNormEntry inner) {
            this.inner = inner;
        }

        @Override
        public String getCitationKey() {
            return inner.getCitationKey();
        }

        @Override
        public Optional<CitationLookupResult> getLookupResult() {
            return inner.getLookupResult();
        }

        @Override
        public Optional<String> getUniqueLetter() {
            return Optional.empty();
        }

        @Override
        public Optional<OOText> getPageInfo() {
            return Optional.empty();
        }

        @Override
        public boolean getIsFirstAppearanceOfSource() {
            return false;
        }
    }

    /// @param normEntry A citation to process.
    /// @return A normalized citation marker for deciding which
    /// citations need uniqueLetters.
    ///
    /// For details of what "normalized" means: See {@link JStyleGetCitationMarker#getAuthorYearParenthesisMarker2}
    ///
    /// Note: now includes some markup.
    static OOText getNormalizedCitationMarker(JStyle style,
                                              CitationMarkerNormEntry normEntry,
                                              Optional<Integer> maxAuthorsOverride) {
        boolean[] startsNewGroup = {true};
        CitationMarkerEntry entry = new CitationMarkerNormEntryWrap(normEntry);
        return getAuthorYearParenthesisMarker2(style,
                AuthorYearMarkerPurpose.NORMALIZED,
                List.of(entry),
                startsNewGroup,
                maxAuthorsOverride);
    }

    private static List<OOText>
    getNormalizedCitationMarkers(JStyle style,
                                 List<CitationMarkerEntry> citationMarkerEntries,
                                 Optional<Integer> maxAuthorsOverride) {

        List<OOText> normalizedMarkers = new ArrayList<>(citationMarkerEntries.size());
        for (CitationMarkerEntry citationMarkerEntry : citationMarkerEntries) {
            OOText normalized = getNormalizedCitationMarker(style,
                    citationMarkerEntry,
                    maxAuthorsOverride);
            normalizedMarkers.add(normalized);
        }
        return normalizedMarkers;
    }

    /// Produce citation marker for a citation group.
    ///
    /// Attempts to join consecutive citations: if normalized citations
    /// markers match and no pageInfo is present, the second entry
    /// can be presented by appending its uniqueLetter to the
    /// previous.
    ///
    /// If either entry has pageInfo, join is inhibited.
    /// If the previous entry has more names than we need
    /// we check with extended normalizedMarkers if they match.
    ///
    /// For consecutive identical entries, the second one is omitted.
    /// Identical requires same pageInfo here, we do not try to merge them.
    /// Note: notifying the user about them would be nice.
    /// Note: only consecutive citations are checked.
    ///
    /// @param citationMarkerEntries           A group of citations to process.
    /// @param inParenthesis                   If true, put parenthesis around the whole group, otherwise around each (year,uniqueLetter,pageInfo) part.
    /// @param nonUniqueCitationMarkerHandling What should happen if we stumble upon citations with identical normalized citation markers which cite different sources and are not distinguished by uniqueLetters.
    public static OOText
    createCitationMarker(JStyle style,
                         List<CitationMarkerEntry> citationMarkerEntries,
                         boolean inParenthesis,
                         NonUniqueCitationMarker nonUniqueCitationMarkerHandling) {
        return createCitationMarker(style,
                citationMarkerEntries,
                inParenthesis ? CitationType.AUTHORYEAR_PAR : CitationType.AUTHORYEAR_INTEXT,
                nonUniqueCitationMarkerHandling);
    }

    /// As above, but selects the presentation from the citation type of the group.
    ///
    /// @param citationType Decides brackets and which of the (author, year) parts are emitted.
    public static OOText
    createCitationMarker(JStyle style,
                         List<CitationMarkerEntry> citationMarkerEntries,
                         CitationType citationType,
                         NonUniqueCitationMarker nonUniqueCitationMarkerHandling) {

        final int nEntries = citationMarkerEntries.size();

        // Original:
        //
        // Look for groups of uniquefied entries that should be combined in the output.
        // E.g. (Olsen, 2005a, b) should be output instead of (Olsen, 2005a; Olsen, 2005b).
        //
        // Now:
        // - handle pageInfos
        // - allow duplicate entries with same or different pageInfos.
        //
        // We assume entries are already sorted, all we need is to
        // group consecutive entries if we can.
        //
        // We also assume, that identical entries have the same uniqueLetters.
        //

        List<OOText> normalizedMarkers = getNormalizedCitationMarkers(style,
                citationMarkerEntries,
                Optional.empty());

        // How many authors would be emitted without grouping.
        int[] nAuthorsToEmit = new int[nEntries];
        int[] nAuthorsToEmitRevised = new int[nEntries];
        for (int i = 0; i < nEntries; i++) {
            CitationMarkerEntry entry = citationMarkerEntries.get(i);
            int nAuthors = calculateNAuthorsToEmit(style, entry);
            nAuthorsToEmit[i] = nAuthors;
            nAuthorsToEmitRevised[i] = nAuthors;
        }

        boolean[] startsNewGroup = new boolean[nEntries];
        List<CitationMarkerEntry> filteredCitationMarkerEntries = new ArrayList<>(nEntries);
        int i_out = 0;

        if (nEntries > 0) {
            filteredCitationMarkerEntries.add(citationMarkerEntries.getFirst());
            startsNewGroup[i_out] = true;
            i_out++;
        }

        for (int i = 1; i < nEntries; i++) {
            final CitationMarkerEntry ce1 = citationMarkerEntries.get(i - 1);
            final CitationMarkerEntry ce2 = citationMarkerEntries.get(i);

            final String nm1 = OOText.toString(normalizedMarkers.get(i - 1));
            final String nm2 = OOText.toString(normalizedMarkers.get(i));

            final boolean isUnresolved1 = ce1.getLookupResult().isEmpty();
            final boolean isUnresolved2 = ce2.getLookupResult().isEmpty();

            boolean startingNewGroup;
            boolean sameAsPrev; /* true indicates ce2 may be omitted from output */
            if (isUnresolved2) {
                startingNewGroup = true;
                sameAsPrev = false; // keep it visible
            } else {
                // Does the number of authors to be shown differ?
                // Since we compared normalizedMarkers, the difference
                // between maxAuthors and maxAuthorsFirst may invalidate
                // our expectation that adding uniqueLetter is valid.

                boolean nAuthorsShownInhibitsJoin;
                if (isUnresolved1) {
                    nAuthorsShownInhibitsJoin = true; // no join for unresolved
                } else {
                    final boolean isFirst1 = ce1.getIsFirstAppearanceOfSource();
                    final boolean isFirst2 = ce2.getIsFirstAppearanceOfSource();

                    // nAuthorsToEmitRevised[i-1] may have been indirectly increased,
                    // we have to check that too.
                    if (!isFirst1 &&
                            !isFirst2 &&
                            (nAuthorsToEmitRevised[i - 1] == nAuthorsToEmit[i - 1])) {
                        // we can rely on normalizedMarkers
                        nAuthorsShownInhibitsJoin = false;
                    } else if (style.getMaxAuthors() == style.getMaxAuthorsFirst()) {
                        // we can rely on normalizedMarkers
                        nAuthorsShownInhibitsJoin = false;
                    } else {
                        final int prevShown = nAuthorsToEmitRevised[i - 1];
                        final int need = nAuthorsToEmit[i];

                        if (prevShown < need) {
                            // We do not retrospectively change the number of authors shown
                            // at the previous entry, take that as decided.
                            nAuthorsShownInhibitsJoin = true;
                        } else {
                            // prevShown >= need
                            // Check with extended normalizedMarkers.
                            OOText nmx1 =
                                    getNormalizedCitationMarker(style, ce1, Optional.of(prevShown));
                            OOText nmx2 =
                                    getNormalizedCitationMarker(style, ce2, Optional.of(prevShown));
                            boolean extendedMarkersDiffer = !nmx2.equals(nmx1);
                            nAuthorsShownInhibitsJoin = extendedMarkersDiffer;
                        }
                    }
                }

                final boolean citationKeysDiffer = !ce2.getCitationKey().equals(ce1.getCitationKey());
                final boolean normalizedMarkersDiffer = !nm2.equals(nm1);

                Optional<OOText> pageInfo2 = PageInfo.normalizePageInfo(ce2.getPageInfo());
                Optional<OOText> pageInfo1 = PageInfo.normalizePageInfo(ce1.getPageInfo());
                final boolean bothPageInfosAreEmpty = pageInfo2.isEmpty() && pageInfo1.isEmpty();
                final boolean pageInfosDiffer = !pageInfo2.equals(pageInfo1);

                Optional<String> ul2 = ce2.getUniqueLetter();
                Optional<String> ul1 = ce1.getUniqueLetter();
                final boolean uniqueLetterPresenceChanged = ul2.isPresent() != ul1.isPresent();
                final boolean uniqueLettersDiffer = !ul2.equals(ul1);

                final boolean uniqueLetterDoesNotMakeUnique = citationKeysDiffer
                        && !normalizedMarkersDiffer
                        && !uniqueLettersDiffer;

                if (uniqueLetterDoesNotMakeUnique &&
                        nonUniqueCitationMarkerHandling == NonUniqueCitationMarker.THROWS) {
                    throw new IllegalArgumentException("different citation keys,"
                            + " but same normalizedMarker and uniqueLetter");
                }

                final boolean pageInfoInhibitsJoin = bothPageInfosAreEmpty
                                                     ? false
                                                     : (citationKeysDiffer || pageInfosDiffer);

                startingNewGroup = normalizedMarkersDiffer
                        || nAuthorsShownInhibitsJoin
                        || pageInfoInhibitsJoin
                        || uniqueLetterPresenceChanged
                        || uniqueLetterDoesNotMakeUnique;

                if (!startingNewGroup) {
                    // inherit from first of group. Used at next i.
                    nAuthorsToEmitRevised[i] = nAuthorsToEmitRevised[i - 1];
                }

                sameAsPrev = !startingNewGroup
                        && !uniqueLettersDiffer
                        && !citationKeysDiffer
                        && !pageInfosDiffer;
            }

            if (!sameAsPrev) {
                filteredCitationMarkerEntries.add(ce2);
                startsNewGroup[i_out] = startingNewGroup;
                i_out++;
            }
        }

        if (citationType.isPartial()) {
            Arrays.fill(startsNewGroup, true);
        }

        return getAuthorYearParenthesisMarker2(style,
                purposeOf(citationType),
                filteredCitationMarkerEntries,
                startsNewGroup,
                Optional.empty());
    }
}
