/*
 * @Copyright 2018-2026 HardBackNutter
 * @License GNU General Public License
 *
 * This file is part of NeverTooManyBooks.
 *
 * NeverTooManyBooks 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.
 *
 * NeverTooManyBooks 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 NeverTooManyBooks. If not, see <http://www.gnu.org/licenses/>.
 */
package com.hardbacknutter.nevertoomanybooks.searchengines.kbnl;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import com.hardbacknutter.nevertoomanybooks.entities.codes.ISBN;
import com.hardbacknutter.nevertoomanybooks.entities.codes.ISNI;
import com.hardbacknutter.nevertoomanybooks.database.DBKey;
import com.hardbacknutter.nevertoomanybooks.entities.Author;
import com.hardbacknutter.nevertoomanybooks.entities.AuthorRole;
import com.hardbacknutter.nevertoomanybooks.entities.Book;
import com.hardbacknutter.nevertoomanybooks.entities.Identifier;
import com.hardbacknutter.nevertoomanybooks.entities.Publisher;
import com.hardbacknutter.nevertoomanybooks.entities.Series;
import com.hardbacknutter.nevertoomanybooks.searchengines.ParserHelper;
import com.hardbacknutter.nevertoomanybooks.searchengines.SearchEngineUtils;

import org.xml.sax.SAXException;

class KbNlBookHandler
        extends KbNlHandlerBase {

    private static final Pattern TRANSLATION_PATTERN = Pattern.compile(
            ".*?(?:Vert\\. van:|Vertaling van:)\\s*(.*)",
            Pattern.UNICODE_CASE);
    private static final Pattern ISBN_BOUNDARY_PATTERN = Pattern.compile("[;)]");
    /** Example: {@code "https://webggc.oclc.org/cbs/DB=2.37/XMLPRS=Y/PPN?PPN=852323123"}. */
    private static final Pattern PERMALINK_PATTERN = Pattern.compile(
            ".*?https.*?PPN=(\\d+).*", Pattern.UNICODE_CASE);
    private static final Pattern PAGES_PATTERN = Pattern.compile(".*?(\\d+) pagina.*",
                                                                 Pattern.UNICODE_CASE);

    /** Example: {@code REL?PPN=068561504}. */
    private static final Pattern AUTHOR_ID = Pattern.compile("REL\\?PPN=(\\d+)");
    /** Split the author name from the trailing bracketed sections. */
    private static final Pattern AUTHOR_NAME = Pattern.compile("(?=\\()");
    /** Loop over the bracketed sections after the author name. */
    private static final Pattern AUTHOR_BRACKETED_SUFFIX = Pattern.compile("\\((.*?)\\)");
    /** Parse the bracketed section with the author dates. */
    private static final Pattern BIRTH_DEATH_DATES =
            Pattern.compile("(\\d\\d\\d\\d)-(\\d\\d\\d\\d|)");

    @NonNull
    private final ParserHelper parserHelper;
    @Nullable
    private String tmpSeriesNr;

    /**
     * Constructor.
     *
     * @param parserHelper to use
     * @param data         Book to update
     */
    KbNlBookHandler(@NonNull final ParserHelper parserHelper,
                    @NonNull final Book data) {
        super(data);
        this.parserHelper = parserHelper;
    }

    @Override
    public void startDocument()
            throws SAXException {
        super.startDocument();

        tmpSeriesNr = null;
    }

    @Override
    public void endDocument()
            throws SAXException {
        super.endDocument();

        if (tmpSeriesNr != null) {
            final String title = book.getTitle();
            // should never happen, but paranoia...
            if (!title.isBlank()) {
                final String s = SearchEngineUtils.cleanName(title);
                if (!s.isBlank()) {
                    book.add(Series.from(s, tmpSeriesNr));
                }
            }
        }

        // There is no language field; e.g. French books data is the same as Dutch ones.
        // just add Dutch and hope for the best.
        if (!book.isEmpty() && !book.contains(DBKey.LANGUAGE)) {
            book.setLanguage("nld");
        }
    }

    @Override
    protected void processPermalink(@NonNull final String permaLink) {
        final Matcher matcher = PERMALINK_PATTERN.matcher(permaLink);
        if (matcher.find()) {
            final String ppn = matcher.group(1);
            book.setIdentifierValue(Identifier.SID_KBNL, ppn);
        }
    }

    /**
     * Labels for both Dutch (default) and English are listed.
     * <p>
     * Note that "Colorist" is also used in Dutch.
     * <p>
     * A lot of the data is unstructured. Parsing is best-effort.
     *
     * @param currentLabel the current {@code labelledLabel}
     * @param currentData  content of {@code labelledData}
     */
    protected void processEntry(@NonNull final String currentLabel,
                                @NonNull final List<CurrentData> currentData) {
        switch (currentLabel) {
            case "Title":
            case "Titel":
                processTitle(currentData);
                break;

            case "Author":
            case "Auteur":
                parseAuthor(currentData, AuthorRole.WRITER);
                break;

            case "Collaborator":
            case "Medewerker":
                parseAuthor(currentData, AuthorRole.CONTRIBUTOR);
                break;

            case "Artist":
            case "Kunstenaar":
                // artist is for comics etc
            case "Illustrator":
                // illustrator (label is same in Dutch) is for books
                // Just put them both down as artists
                parseAuthor(currentData, AuthorRole.ARTIST);
                break;

            case "Colorist":
                parseAuthor(currentData, AuthorRole.COLORIST);
                break;
            case "Translator":
            case "Vertaler":
                parseAuthor(currentData, AuthorRole.TRANSLATOR);
                break;

            case "Series":
            case "Reeks":
                processSeries(currentData);
                break;

            case "Part(s)":
            case "Deel / delen":
                processSeriesNumber(currentData);
                break;

            case "Publisher":
            case "Uitgever":
                parsePublisher(currentData);
                break;

            case "Year":
            case "Jaar":
                parseDatePublished(currentData);
                break;

            case "Extent":
            case "Omvang":
                processPages(currentData);
                break;

            case "ISBN":
            case "ISSN":
                // only one of the two will be present
                parseProductCode(currentData);
                break;

            case "Illustration":
            case "Illustratie":
                processIllustration(currentData);
                break;

            case "Size":
            case "Formaat":
                // seen, but skipped for now; it's one dimension, the height
                // so librarians know if it will fit on a shelf...
                // Formaat: 30 cm
                break;

            case "Physical information":
            case "Fysieke informatie":
                // "1 file (PDF)"
                break;

            case "Editie":
                parseEditionInfo(currentData);
                break;

            case "Contains":
            case "Bevat / omvat":
            case "Abstract":
            case "Samenvatting": {
                parseDescription(currentData);
                break;
            }
            // We concat these 3 fields into the description.
            // but as it can also contain semi-structured data
            // we parse twice.
            // Note/Noot
            // Note/Annotatie
            // Annotation edition / Annotatie editie
            case "Noot":
                // drop down to Note
            case "Note":
            case "Annotatie":
                // It's unclear if this is just an alternative field used
                // for the same type of information. But as usual,... it's unstructured.
            case "Annotation edition":
            case "Annotatie editie":
                // Parse semi-structured data
                parseAnnotation(currentData);
                // add to the description
                parseDescription(currentData);
                break;

            case "Annex":
            case "Bĳlage":
                // kleurenprent van oorspr. cover ?
                break;

            case "Numbering":
            case "Nummering":
                // ISSN .. but unstructured.
                break;

            default:
                // ignore
                break;
        }
    }

    private void parseEditionInfo(@NonNull final List<CurrentData> currentData) {
        final String data = currentData.stream()
                                       .map(cd -> cd.data)
                                       .filter(s -> !s.isEmpty())
                                       .collect(Collectors.joining(" "));
        final String s = SearchEngineUtils.cleanText(data);
        if (!s.isBlank()) {
            book.setEditionInfo(s);
        }
    }

    /**
     * "Annotation" field can contain some semi-structured data.
     *
     * <pre>{@code https://webggc.oclc.org/cbs/DB=2.37/XMLPRS=Y/PPN?PPN=306324296
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text>Opl. van 1000 ex</psi:text>
     *   </psi:line>
     *   <psi:line>
     *     <psi:text>Vert. van: </psi:text>
     *     <psi:text mark="highlight">Cromwell</psi:text>
     *     <psi:text> </psi:text>
     *     <psi:text mark="highlight">Stone,</psi:text>
     *     <psi:text> en Le retour de </psi:text>
     *     <psi:text mark="highlight">Cromwell</psi:text>
     *     <psi:text> </psi:text>
     *     <psi:text mark="highlight">Stone,</psi:text>
     *     <psi:text> en Le testament de </psi:text>
     *     <psi:text mark="highlight">Cromwell</psi:text>
     *     <psi:text> </psi:text>
     *     <psi:text mark="highlight">Stone</psi:text>
     *   </psi:line>
     *  </psi:labelledData>
     * }</pre>
     *
     * <pre>{@code sid: https://webggc.oclc.org/cbs/DB=2.37/XMLPRS=Y/PPN?PPN=435149695
     *  <psi:labelledData>
     *    <psi:line>
     *      <psi:text>Vertaling </psi:text>
     *      <psi:text mark="highlight">van</psi:text>
     *      <psi:text>: The secret keeper of </psi:text>
     *      <psi:text mark="highlight">Jaipur.</psi:text>
     *      <psi:text> - Toronto : Mira, ©2021</psi:text>
     *    </psi:line>
     *  </psi:labelledData>
     * }</pre>
     * <p>
     * For now, attempt to get the original title out... the above examples will work,
     * but don't expect too much though.
     *
     * @param currentData content of {@code labelledData}
     */
    private void parseAnnotation(@NonNull final List<CurrentData> currentData) {
        final String data = currentData.stream()
                                       .map(cd -> cd.data)
                                       .filter(s -> !s.isEmpty())
                                       .collect(Collectors.joining(" "));

        final Matcher matcher = TRANSLATION_PATTERN.matcher(data);
        if (matcher.find()) {
            book.setTranslatedFromTitle(matcher.group(1));
        }
    }

    /**
     * Title.
     *
     * <pre>{@code
     *  <psi:labelledData>
     *    <psi:line>
     *      <psi:text>De</psi:text>
     *      <psi:text href="CLK?IKT=4&amp;TRM=Foundation">Foundation</psi:text>
     *      <psi:text>/ Isaac Asimov ; [vert. uit het Engels door Jack Kröner]</psi:text>
     *    </psi:line>
     *  </psi:labelledData>
     *
     *  <psi:labelledData>
     *    <psi:line>
     *      <psi:text href="CLK?IKT=12&amp;TRM=422449059&amp;REC=*">
     *          De buitengewone reis /  Silvio Camboni
     *      </psi:text>
     *    </psi:line>
     *  </psi:labelledData>
     * }</pre>
     *
     * @param currentData content of {@code labelledData}
     */
    private void processTitle(@NonNull final List<CurrentData> currentData) {

        final String[] cleanedData = currentData
                .stream()
                .map(cd -> cd.data)
                .collect(Collectors.joining(" "))
                .split("/");

        final String s = SearchEngineUtils.cleanText(cleanedData[0]);
        if (!s.isBlank()) {
            book.setTitle(s);
        }
        // It's temping to decode [1], as this is the author as it appears on the cover,
        // but the data has proven to be very unstructured and mostly unusable.
    }

    /**
     * Author.
     *
     * <pre>{@code
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text href="REL?PPN=068561504">Isaak Judovič Ozimov (1920-1992)</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     *
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text href="REL?PPN=068870728">Jean-Pol is Jean-Paul Van den Broeck</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     *
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text href="REL?PPN=070047596">Dirk Stallaert (1955-)</psi:text>
     *     <psi:text>;</psi:text>
     *     <psi:text href="REL?PPN=073286796">Leo Loedts</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     *
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text href="REL?PPN=068852002">Ruurd Feenstra (1904-1974)
     *       (ISNI 0000 0000 2173 3650) </psi:text>
     *     </psi:line>
     *   </psi:labelledData>
     * }</pre>
     *
     * <p>
     * Note that the author name in the above example can be the "actual" name, and not
     * the publicly known/used name, i.e. in this case "Isaac Asimov"
     * The 2nd sample shows how the site is creative (hum) about authors using pen-names.
     * The 3rd example has a list of authors for the same author type.
     * The 4th sample shows that dates and other info can be added
     *
     * @param currentData content of {@code labelledData}
     * @param type        the author type
     */
    @VisibleForTesting
    void parseAuthor(@NonNull final Iterable<CurrentData> currentData,
                     @AuthorRole.Role final int type) {
        for (final CurrentData cd : currentData) {
            final String[] parts = AUTHOR_NAME.split(cd.data, 2);

            String name = parts[0].strip();
            // reject separators as for example: <psi:text>;</psi:text>
            if (name.length() == 1) {
                return;
            }

            name = SearchEngineUtils.cleanName(name);
            if (name.isBlank()) {
                return;
            }

            final Author author = Author.from(name);

            if (cd.url != null) {
                final Matcher matcher = AUTHOR_ID.matcher(cd.url);
                if (matcher.find()) {
                    final String siId = matcher.group(1);
                    if (siId != null) {
                        author.setIdentifierValue(Identifier.SID_KBNL, siId);
                    }
                }
            }
            if (parts.length > 1) {
                // "Isaak Judovič Ozimov (1920-1992) (ISNI 0000 0001 2259 0564)"
                // parts[1] == "(1920-1992) (ISNI 0000 0001 2259 0564)"
                // parts[1] == "(1976-) (ISNI 0000 0004 8004 6868)"
                final Matcher matcher = AUTHOR_BRACKETED_SUFFIX.matcher(parts[1]);
                while (matcher.find()) {
                    final String s = matcher.group(1);
                    if (s != null) {
                        if (s.startsWith("ISNI")) {
                            final ISNI isni = new ISNI(s.substring(4));
                            if (isni.isValid()) {
                                author.setIdentifierValue(Identifier.SID_ISNI, isni.asText());
                            }
                        } else {
                            final Matcher bddMatcher = BIRTH_DEATH_DATES.matcher(s);
                            if (bddMatcher.find()) {
                                author.setBirthDate(bddMatcher.group(1));
                                final String deathDate = bddMatcher.group(2);
                                if (deathDate != null) {
                                    author.setDeathDate(deathDate);
                                }
                            }
                        }
                    }
                }
            }

            parserHelper.addAuthor(author, type, book, false);
        }
    }

    /**
     * Series.
     *
     * <pre>{@code
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text href="CLK?IKT=12&amp;TRM=841288933&amp;REC=*">Foundation-trilogie</psi:text>
     *     <psi:text>;</psi:text>
     *     <psi:text href="CLK?IKT=12&amp;TRM=841288933&amp;REC=*">dl. 1</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     *
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text href="CLK?IKT=12&amp;TRM=851863523&amp;REC=*">Discus-serie</psi:text>
     *     <psi:text>; </psi:text>
     *     <psi:text href="CLK?IKT=12&amp;TRM=821611178&amp;REC=*">Kluitman jeugdserie</psi:text>
     *     <psi:text> ; </psi:text>
     *     <psi:text href="CLK?IKT=12&amp;TRM=821611178&amp;REC=*">J 1247</psi:text>
     *   </psi:line>
     *  </psi:labelledData>
     * }</pre>
     *
     * @param currentData content of {@code labelledData}
     */
    private void processSeries(@NonNull final List<CurrentData> currentData) {
        final String s = SearchEngineUtils.cleanName(currentData.get(0).data);
        if (!s.isBlank()) {
            book.add(Series.from(s));
            // the number part is totally unstructured
        }
    }

    /**
     * Book number in a Series.
     *
     * <pre>{@code
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text>Deel 1 / blah</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     * }</pre>
     *
     * @param currentData content of {@code labelledData}
     */
    private void processSeriesNumber(@NonNull final List<CurrentData> currentData) {
        // This element is listed BEFORE the Series ("reeks") itself so store it tmp.
        // Note it's often missing altogether
        final String[] nrStr = currentData.get(0).data.split("/")[0].split(" ");
        if (nrStr.length > 1) {
            tmpSeriesNr = nrStr[1];
        } else {
            tmpSeriesNr = nrStr[0];
        }
    }

    /**
     * ISBN or ISSN.
     *
     * <pre>{@code
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text mark="highlight">90-229-5335-1</psi:text>
     *     <psi:text>(geb.)</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     *
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text mark="highlight">978-94-6373145-4</psi:text>
     *     <psi:text> (paperback)</psi:text>
     *   </psi:line>
     *   <psi:line>
     *     <psi:text>&#xA0;</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     *
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text> (Geb.; f. 4,50)</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     *
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text>2-256-90374-5 : 40.00F</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     *
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text> : 42.00F</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     * }</pre>
     *
     * @param currentData content of {@code labelledData}
     */
    private void parseProductCode(@NonNull final List<CurrentData> currentData) {
        for (final CurrentData cd : currentData) {
            final String text = cd.data;
            if (Character.isDigit(text.charAt(0))) {
                if (!book.hasProductCode()) {
                    final String codeStr = ISBN.cleanText(text.split(":")[0]);
                    // Do a crude test on the length and hope for the best
                    // (don't do a full ISBN test here, no need)
                    // 8 for ISSN, 10/13 for ISBN-
                    if (codeStr.length() == 8
                        || codeStr.length() == 10
                        || codeStr.length() == 13) {
                        book.setRawProductCode(codeStr);
                    }
                }
            } else if (text.charAt(0) == '(') {
                if (!book.contains(DBKey.FORMAT)) {
                    // Skip the 1st bracket, and split either on closing or on semicolon
                    book.setFormat(ISBN_BOUNDARY_PATTERN.split(text.substring(1))[0].strip());
                }
            }
        }
    }

    /**
     * Publisher.
     *
     * <pre>{@code
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text href="CLK?IKT=3003&amp;TRM=Breda">Breda</psi:text>
     *     <psi:text> : </psi:text>
     *     <psi:text href="CLK?IKT=3003&amp;TRM=Dark">Dark</psi:text>
     *     <psi:text> </psi:text>
     *     <psi:text href="CLK?IKT=3003&amp;TRM=Dragon">Dragon</psi:text>
     *     <psi:text> </psi:text>
     *     <psi:text href="CLK?IKT=3003&amp;TRM=Books">Books</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     *
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text href="CLK?IKT=3003&amp;TRM=Utrecht">Utrecht</psi:text>
     *     <psi:text>[</psi:text>
     *     <psi:text href="CLK?IKT=3003&amp;TRM=etc.">etc.</psi:text>
     *     <psi:text>] :</psi:text>
     *     <psi:text href="CLK?IKT=3003&amp;TRM=Bruna">Bruna</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     * }</pre>
     *
     * @param currentData content of {@code labelledData}
     */
    private void parsePublisher(@NonNull final List<CurrentData> currentData) {
        String text = currentData.stream()
                                 .map(cd -> cd.data)
                                 .filter(name -> !name.isEmpty())
                                 .collect(Collectors.joining(" "));
        // the part before the ":" is (usually?) the city. 2nd part is the name
        if (text.contains(":")) {
            text = text.split(":")[1].strip();
        }
        text = SearchEngineUtils.cleanName(text);
        if (!text.isBlank()) {
            book.add(Publisher.from(text));
        }
    }

    /**
     * Process a year field. Once again the data is not structured, but at least
     * it's guessable. Some examples:
     *
     * <pre>{@code
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text>1983</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     *
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text>[2019]</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     *
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text>cop. 1986</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     *
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text>c1977, cover 1978</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     * }</pre>
     *
     * @param currentData content of {@code labelledData}
     */
    private void parseDatePublished(@NonNull final List<CurrentData> currentData) {
        if (!book.contains(DBKey.PUBLICATION_DATE)) {
            // Grab the first bit before a comma, and strip it for digits + hope for the best
            final String year = SearchEngineUtils.digits(currentData.get(0).data.split(",")[0]);
            if (!year.isEmpty()) {
                try {
                    book.setPublicationDate(Integer.parseInt(year));
                } catch (@NonNull final NumberFormatException ignore) {
                    // ignore
                }
            }
        }
    }

    /**
     * Process the number-of-pages field. Once again the data is not structured, but at least
     * it's guessable. Some examples:
     *
     * <pre>{@code
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text>48 pagina's</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     *
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text>1 online resource (ePub2, 352 pagina's)</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     *
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text>156 p</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     * }</pre>
     *
     * @param currentData content of {@code labelledData}
     */
    private void processPages(@NonNull final List<CurrentData> currentData) {
        if (!book.contains(DBKey.PAGES)) {
            final String data = currentData.get(0).data;

            final Matcher matcher = PAGES_PATTERN.matcher(data);
            if (matcher.find()) {
                try {
                    final String group = matcher.group(1);
                    if (group != null && !group.isEmpty()) {
                        book.setPages(Integer.parseInt(group));
                        return;
                    }
                } catch (@NonNull final NumberFormatException ignore) {
                    // ignore
                }
            }
            try {
                book.setPages(Integer.parseInt(data.split(" ")[0]));
                return;
            } catch (@NonNull final NumberFormatException ignore) {
                // ignore
            }

            // use source
            book.setPages(data);
        }
    }

    /**
     * Info on illustrations in the book.
     *
     * <pre>{@code
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text>gekleurde illustraties</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     *
     * <psi:labelledData>
     *   <psi:line>
     *     <psi:text>zw. ill</psi:text>
     *   </psi:line>
     * </psi:labelledData>
     * }</pre>
     *
     * @param currentData content of {@code labelledData}
     */
    private void processIllustration(@NonNull final List<CurrentData> currentData) {
        if (!book.contains(DBKey.COLOR)) {
            book.setColor(currentData.get(0).data);
        }
    }

    private void parseDescription(@NonNull final List<CurrentData> currentData) {
        // Merging the lines is not perfect.
        // The data does not contain one sentence/paragraph in each line.
        // Sometimes just words mid-sentence... oh well.
        String desc = currentData
                .stream()
                .map(cd -> cd.data)
                .filter(name -> !name.isEmpty())
                .collect(Collectors.joining(" "));

        desc = SearchEngineUtils.cleanText(desc);
        if (!desc.isBlank()) {
            // append... the site has (at least) two fields we want to collect
            final String previous = book.getDescription();
            if (!previous.isEmpty()) {
                desc = previous + (previous.endsWith("\n") ? "" : "\n")
                       + '\n' + desc;
            }
            book.setDescription(desc);
        }
    }
}
