package org.jabref.logic.importer.fileformat;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.time.DateTimeException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.jabref.logic.importer.AuthorListParser;
import org.jabref.logic.importer.ParseException;
import org.jabref.logic.importer.Parser;
import org.jabref.logic.util.StandardFileType;
import org.jabref.logic.util.URLUtil;
import org.jabref.logic.util.strings.StringUtil;
import org.jabref.model.entry.AuthorList;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.Date;
import org.jabref.model.entry.LinkedFile;
import org.jabref.model.entry.field.Field;
import org.jabref.model.entry.field.StandardField;
import org.jabref.model.entry.types.StandardEntryType;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/// A parser for the bavarian flavour (Bibliotheksverbund Bayern) of the marc xml standard
///
/// See [Feldbeschreibung der Titeldaten bei der Deutschen Nationalbibliothek](https://www.dnb.de/DE/Professionell/Metadatendienste/Exportformate/MARC21/marc21_node.html)
///
/// For further information see
///
///   - https://www.bib-bvb.de/web/kkb-online/rda-felderverzeichnis-des-b3kat-aseq
///   - https://www.loc.gov/marc/bibliographic/ for detailed documentation
///   - for modifications in B3Kat https://www.bib-bvb.de/documents/10792/9f51a033-5ca1-42e2-b2d3-a75e7f1512d4
///   - https://www.dnb.de/DE/Professionell/Metadatendienste/Exportformate/MARC21/marc21_node.html
///   - https://www.dnb.de/SharedDocs/Downloads/DE/Professionell/Standardisierung/AGV/marc21VereinbarungDatentauschTeil1.pdf?__blob=publicationFile&v=2
///   - about multiple books in a series https://www.dnb.de/SharedDocs/Downloads/DE/Professionell/Standardisierung/marc21FormatumstiegAbbildungBegrenzterWerke2008.pdf?__blob=publicationFile&v=2>
///
public class MarcXmlParser implements Parser {
    private static final Logger LOGGER = LoggerFactory.getLogger(MarcXmlParser.class);
    private static final String DISALLOW_DOCTYPE_DECLARATION = "http://apache.org/xml/features/disallow-doctype-decl";

    @Override
    public List<BibEntry> parseEntries(InputStream inputStream) throws ParseException {
        try {
            DocumentBuilder documentBuilder = createDocumentBuilder();
            Document content = documentBuilder.parse(inputStream);
            return this.parseEntries(content);
        } catch (ParserConfigurationException | SAXException | IOException exception) {
            throw new ParseException(exception);
        }
    }

    private static DocumentBuilder createDocumentBuilder() throws ParserConfigurationException {
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        documentBuilderFactory.setFeature(DISALLOW_DOCTYPE_DECLARATION, true);
        return documentBuilderFactory.newDocumentBuilder();
    }

    private List<BibEntry> parseEntries(Document content) {
        List<BibEntry> result = new LinkedList<>();

        Element root = (Element) content.getElementsByTagName("zs:searchRetrieveResponse").item(0);
        Element srwrecords = getChild("zs:records", root);
        if (srwrecords == null) {
            // no records found, so return the empty list
            return result;
        }
        List<Element> records = getChildren("zs:record", srwrecords);
        for (Element element : records) {
            Element e = getChild("zs:recordData", element);
            if (e != null) {
                e = getChild("record", e);
                if (e != null) {
                    result.add(parseEntry(e));
                }
            }
        }
        return result;
    }

    private BibEntry parseEntry(Element element) {
        BibEntry bibEntry = new BibEntry(BibEntry.DEFAULT_TYPE);

        List<Element> datafields = getChildren("datafield", element);
        for (Element datafield : datafields) {
            String tag = datafield.getAttribute("tag");
            LOGGER.debug("tag: {}", tag);

            if ("020".equals(tag)) {
                putIsbn(bibEntry, datafield);
            } else if ("100".equals(tag) || "700".equals(tag) || "710".equals(tag)) {
                putPersonalName(bibEntry, datafield); // Author, Editor, Publisher
            } else if ("111".equals(tag)) {
                // FixMe: Conference Information also in Subtitle (245) & Author (710)
                putConferenceDetail(bibEntry, datafield);
            } else if ("245".equals(tag)) {
                putTitle(bibEntry, datafield);
            } else if ("250".equals(tag)) {
                putEdition(bibEntry, datafield);
            } else if ("264".equals(tag)) {
                putPublication(bibEntry, datafield);
            } else if ("300".equals(tag)) {
                putPhysicalDescription(bibEntry, datafield);
            } else if ("490".equals(tag) || "830".equals(tag)) {
                putSeries(bibEntry, datafield);
            } else if ("502".equals(tag)) {
                putThesisDescription(bibEntry, datafield); // Master's thesis, PhD thesis, Thesis
            } else if ("520".equals(tag)) {
                putSummary(bibEntry, datafield);
            } else if ("653".equals(tag)) {
                putKeywords(bibEntry, datafield);
            } else if ("773".equals(tag)) {
                putIssue(bibEntry, datafield);
            } else if ("856".equals(tag)) {
                putElectronicLocation(bibEntry, datafield);
            } else if ("966".equals(tag)) {
                putDoi(bibEntry, datafield);
            } else if (Integer.parseInt(tag) >= 546 && Integer.parseInt(tag) <= 599) {
                // Notes
                // FixMe: Some notes seem to have tags lower than 546
                putNotes(bibEntry, datafield);
            } else {
                LOGGER.debug("Unparsed tag: {}", tag);
            }
        }
        return bibEntry;
    }

    private void putIsbn(BibEntry bibEntry, Element datafield) {
        String isbn = getSubfield("a", datafield);
        if (StringUtil.isNullOrEmpty(isbn)) {
            LOGGER.debug("Empty ISBN received");
            return;
        }

        int length = isbn.length();
        if (length != 10 && length != 13) {
            LOGGER.debug("Malformed ISBN received, length: {}", length);
            return;
        }

        Optional<String> field = bibEntry.getField(StandardField.ISBN);
        if (field.isPresent()) {
            // Only overwrite the field, if it's ISBN13
            if (field.get().length() == 13) {
                bibEntry.setField(StandardField.ISBN, isbn);
            }
        } else {
            bibEntry.setField(StandardField.ISBN, isbn);
        }
    }

    private void putPersonalName(BibEntry bibEntry, Element datafield) {
        String author = getSubfield("a", datafield);
        String relation = getSubfield("4", datafield);
        AuthorList name;

        if (StringUtil.isNotBlank(author) && StringUtil.isNotBlank(relation)) {
            name = new AuthorListParser().parse(author);
            Optional<StandardField> field = Optional.ofNullable(
                    switch (relation) {
                        case "aut" ->
                                StandardField.AUTHOR;
                        case "edt" ->
                                StandardField.EDITOR;
                        case "pbl" ->
                                StandardField.PUBLISHER;
                        default ->
                                null;
                    });

            if (field.isPresent()) {
                String ind1 = datafield.getAttribute("ind1");
                String brackedName;
                if (field.get() == StandardField.PUBLISHER && StringUtil.isNotBlank(ind1) && "2".equals(ind1)) {
                    // ind == 2 -> Corporate publisher
                    brackedName = "{" + name.getAsFirstLastNamesWithAnd() + "}";
                } else {
                    brackedName = name.getAsLastFirstNamesWithAnd(false);
                }

                if (bibEntry.getField(field.get()).isPresent()) {
                    bibEntry.setField(field.get(), bibEntry.getField(field.get()).get().concat(" and " + brackedName));
                } else {
                    bibEntry.setField(field.get(), brackedName);
                }
            }
        }
    }

    private void putConferenceDetail(BibEntry bibEntry, Element datafield) {
        String conference = getSubfield("a", datafield);
        bibEntry.setType(StandardEntryType.Proceedings);

        if (StringUtil.isNotBlank(conference)) {
            bibEntry.setField(StandardField.EVENTTITLE, conference);
        }
    }

    private void putTitle(BibEntry bibEntry, Element datafield) {
        String title = getSubfield("a", datafield);
        String subtitle = getSubfield("b", datafield);
        String responsibility = getSubfield("c", datafield);
        String number = getSubfield("n", datafield);
        String part = getSubfield("p", datafield);

        if (StringUtil.isNotBlank(title)) {
            bibEntry.setField(StandardField.TITLE, title);
        }

        if (StringUtil.isNotBlank(subtitle)) {
            bibEntry.setField(StandardField.SUBTITLE, subtitle);
        }

        if (StringUtil.isNotBlank(responsibility)) {
            bibEntry.setField(StandardField.TITLEADDON, responsibility);
        }

        if (StringUtil.isNotBlank(number)) {
            bibEntry.setField(StandardField.NUMBER, number);
        }

        if (StringUtil.isNotBlank(part)) {
            bibEntry.setField(StandardField.PART, part);
        }
    }

    private void putEdition(BibEntry bibEntry, Element datafield) {
        String edition = getSubfield("a", datafield); // e.g. '1st ed. 2020'
        String editionAddendum = getSubfield("b", datafield); // e.g. 'revised by N.N.'

        if (StringUtil.isNullOrEmpty(edition)) {
            return;
        }

        if (StringUtil.isNotBlank(editionAddendum)) {
            edition = edition.concat(", " + editionAddendum);
        }

        bibEntry.setField(StandardField.EDITION, edition);
    }

    private void putPublication(BibEntry bibEntry, Element datafield) {
        String ind2 = datafield.getAttribute("ind2");
        if (StringUtil.isNotBlank(ind2) && "1".equals(ind2)) { // Publisher
            String place = getSubfield("a", datafield);
            String name = getSubfield("b", datafield);
            String date = getSubfield("c", datafield);

            if (StringUtil.isNotBlank(place)) {
                bibEntry.setField(StandardField.ADDRESS, place);
            }

            if (StringUtil.isNotBlank(name)) {
                bibEntry.setField(StandardField.PUBLISHER, "{" + name + "}");
            }

            if (StringUtil.isNotBlank(date)) {
                String strippedDate = StringUtil.stripBrackets(date);
                try {
                    Date.parse(strippedDate).ifPresent(bibEntry::setDate);
                } catch (DateTimeException exception) {
                    // cannot read date value, just copy it in plain text
                    LOGGER.info("Cannot parse date '{}'", strippedDate);
                    bibEntry.setField(StandardField.DATE, StringUtil.stripBrackets(strippedDate));
                }
            }
        }
    }

    private void putPhysicalDescription(BibEntry bibEntry, Element datafield) {
        String pagetotal = getSubfield("a", datafield);

        if (StringUtil.isNotBlank(pagetotal) && (pagetotal.contains("pages") || pagetotal.contains("p.") || pagetotal.contains("S") || pagetotal.contains("Seiten"))) {
            pagetotal = pagetotal.replaceAll(".*?(\\d+)(?:\\s*Seiten|\\s*S|\\s*pages|\\s*p).*", "$1");
            bibEntry.setField(StandardField.PAGETOTAL, pagetotal);
        }
    }

    private void putSeries(BibEntry bibEntry, Element datafield) {
        // tag 490 - Series
        // tag 830 - Series Added Entry

        String name = getSubfield("a", datafield);
        String volume = getSubfield("v", datafield);
        String issn = getSubfield("x", datafield);

        if (StringUtil.isNotBlank(name)) {
            bibEntry.setField(StandardField.SERIES, name);
        }

        if (StringUtil.isNotBlank(volume)) {
            bibEntry.setField(StandardField.VOLUME, volume);
        }

        if (StringUtil.isNotBlank(issn)) {
            bibEntry.setField(StandardField.ISSN, issn);
        }
    }

    private void putThesisDescription(BibEntry bibEntry, Element datafield) {
        String thesisDegree = getSubfield("b", datafield);
        String school = getSubfield("c", datafield);
        bibEntry.setType(StandardEntryType.MastersThesis);

        if (StringUtil.isNotBlank(school)) {
            bibEntry.setField(StandardField.SCHOOL, school);
        }

        if ("Dissertation".equals(thesisDegree)) {
            bibEntry.setType(StandardEntryType.PhdThesis);
        }
    }

    private void putSummary(BibEntry bibEntry, Element datafield) {
        String summary = getSubfield("a", datafield);

        String ind1 = datafield.getAttribute("ind1");
        if (StringUtil.isNotBlank(summary) && StringUtil.isNotBlank(ind1) && "3".equals(ind1)) { // Abstract
            if (bibEntry.getField(StandardField.ABSTRACT).isPresent()) {
                bibEntry.setField(StandardField.ABSTRACT, bibEntry.getField(StandardField.ABSTRACT).get().concat(summary));
            } else {
                bibEntry.setField(StandardField.ABSTRACT, summary);
            }
        }
    }

    private void putKeywords(BibEntry bibEntry, Element datafield) {
        String keyword = getSubfield("a", datafield);

        if (StringUtil.isNotBlank(keyword)) {
            Optional<String> keywords = bibEntry.getField(StandardField.KEYWORDS);
            if (keywords.isPresent()) {
                bibEntry.setField(StandardField.KEYWORDS, keywords.get() + ", " + keyword);
            } else {
                bibEntry.setField(StandardField.KEYWORDS, keyword);
            }
        }
    }

    private void putIssue(BibEntry bibEntry, Element datafield) {
        bibEntry.setType(StandardEntryType.Article);

        List<String> issues = getSubfields("g", datafield);

        for (String issue : issues) {
            String[] parts = issue.split(":");
            if (parts.length == 2) {
                String key = parts[0].trim();
                String value = parts[1].trim();

                if (StringUtil.isNotBlank(value)) {
                    switch (key) {
                        case "number" ->
                                bibEntry.setField(StandardField.NUMBER, value);
                        case "year" ->
                                bibEntry.setField(StandardField.YEAR, value);
                        case "pages" ->
                                bibEntry.setField(StandardField.PAGES, value);
                        case "volume" ->
                                bibEntry.setField(StandardField.VOLUME, value);
                        case "day" ->
                                bibEntry.setField(StandardField.DAY, value);
                        case "month" ->
                                bibEntry.setField(StandardField.MONTH, value);
                    }
                }
            }
        }
    }

    private void putDoi(BibEntry bibEntry, Element datafield) {
        String ind1 = datafield.getAttribute("ind1");
        String resource = getSubfield("u", datafield);

        if ("e".equals(ind1) && StringUtil.isNotBlank("u") && StringUtil.isNotBlank(resource)) { // DOI
            String fulltext = getSubfield("3", datafield);
            handleVolltext(bibEntry, fulltext, resource, StandardField.DOI);
        }
    }

    private void putElectronicLocation(BibEntry bibEntry, Element datafield) {
        // 856 - fulltext pdf url
        String ind1 = datafield.getAttribute("ind1");
        String ind2 = datafield.getAttribute("ind2");

        if ("4".equals(ind1) && "0".equals(ind2)) {
            String fulltext = getSubfield("3", datafield);
            String resource = getSubfield("u", datafield);
            handleVolltext(bibEntry, fulltext, resource, StandardField.URL);
        }
    }

    private static void handleVolltext(BibEntry bibEntry, String fieldName, String resource, Field fallBackField) {
        if ("Volltext".equals(fieldName) && StringUtil.isNotBlank(resource)) {
            try {
                LinkedFile linkedFile = new LinkedFile("", URLUtil.create(resource), StandardFileType.PDF.getName());
                bibEntry.setFiles(List.of(linkedFile));
            } catch (MalformedURLException | IllegalArgumentException e) {
                LOGGER.info("Malformed URL: {}", resource);
            }
        } else {
            bibEntry.setField(fallBackField, resource);
        }
    }

    private void putNotes(BibEntry bibEntry, Element datafield) {
        String[] notes = new String[] {
                getSubfield("a", datafield),
                getSubfield("0", datafield),
                getSubfield("h", datafield),
                getSubfield("S", datafield),
                getSubfield("c", datafield),
                getSubfield("f", datafield),
                getSubfield("i", datafield),
                getSubfield("k", datafield),
                getSubfield("l", datafield),
                getSubfield("z", datafield),
                getSubfield("3", datafield),
                getSubfield("5", datafield)
        };

        String notesJoined = Arrays.stream(notes)
                                   .filter(StringUtil::isNotBlank)
                                   .collect(Collectors.joining("\n\n"));

        if (bibEntry.getField(StandardField.NOTE).isPresent()) {
            bibEntry.setField(StandardField.NOTE, bibEntry.getField(StandardField.NOTE).get().concat(notesJoined));
        } else {
            bibEntry.setField(StandardField.NOTE, notesJoined);
        }
    }

    private String getSubfield(String a, Element datafield) {
        List<Element> subfields = getChildren("subfield", datafield);

        for (Element subfield : subfields) {
            if (subfield.getAttribute("code").equals(a)) {
                return subfield.getTextContent();
            }
        }

        return null;
    }

    private List<String> getSubfields(String a, Element datafield) {
        List<Element> subfields = getChildren("subfield", datafield);

        return subfields.stream().filter(field -> field.getAttribute("code").equals(a)).map(Node::getTextContent).toList();
    }

    private Element getChild(String name, Element e) {
        if (e == null) {
            return null;
        }
        NodeList children = e.getChildNodes();

        int j = children.getLength();
        for (int i = 0; i < j; i++) {
            Node test = children.item(i);
            if (test.getNodeType() == Node.ELEMENT_NODE) {
                Element entry = (Element) test;
                if (entry.getTagName().equals(name)) {
                    return entry;
                }
            }
        }
        return null;
    }

    private List<Element> getChildren(String name, Element e) {
        List<Element> result = new LinkedList<>();
        NodeList children = e.getChildNodes();

        int j = children.getLength();
        for (int i = 0; i < j; i++) {
            Node test = children.item(i);
            if (test.getNodeType() == Node.ELEMENT_NODE) {
                Element entry = (Element) test;
                if (entry.getTagName().equals(name)) {
                    result.add(entry);
                }
            }
        }

        return result;
    }
}
