/*
 * Copyright (c) 2017-2022 XMLmind Software. All rights reserved.
 *
 * Author: Hussein Shafie
 *
 * This file is part of the XMLmind Assembly Processor project.
 * For conditions of distribution and use, see the accompanying legal.txt file.
 */
package com.xmlmind.assembly;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import java.util.HashMap;
import static javax.xml.XMLConstants.XML_NS_URI;
import org.w3c.dom.Node;
import org.w3c.dom.Element;
import org.w3c.dom.Document;
import com.xmlmind.util.ArrayUtil;
import com.xmlmind.util.URLUtil;
import com.xmlmind.util.XMLText;
import com.xmlmind.util.XMLResolver;
import com.xmlmind.domutil.ConsoleHelper;
import com.xmlmind.domutil.DOMUtil;
import com.xmlmind.domutil.NodeLocation;
import static com.xmlmind.transproc.Processor.XLINK_NS_URI;
import static com.xmlmind.transproc.Processor.DOCBOOK5_IDREF_LIST;
import static com.xmlmind.assembly.Processor.DOCBOOK_NS_URI;

/*package*/ final class Checker {
    private final ConsoleHelper console;

    protected XMLResolver resolver;

    private boolean hasErrors;

    private static final String SVG_NS_URI = "http://www.w3.org/2000/svg";

    private static final String[] RESOURCE_SPECS = {
        DOCBOOK_NS_URI, "imagedata", null, "fileref",
        SVG_NS_URI, "image", XLINK_NS_URI, "href",
        DOCBOOK_NS_URI, "textdata", null, "fileref",
        DOCBOOK_NS_URI, "audiodata", null, "fileref",
        DOCBOOK_NS_URI, "videodata", null, "fileref"
    };

    // -----------------------------------------------------------------------

    public Checker(ConsoleHelper console) {
        this.console = console;
    }

    public void setResolver(XMLResolver resolver) {
        this.resolver = resolver;
    }

    public XMLResolver getResolver() {
        return resolver;
    }

    public boolean check(Document doc) {
        hasErrors = false;

        Element root = doc.getDocumentElement();
        Map<String,Element[]> idMap = new HashMap<String,Element[]>();
        collectIds(root, idMap);

        checkInternalLinks(root, idMap);

        checkResources(root);

        return hasErrors;
    }

    private void collectIds(Element tree, Map<String,Element[]> idMap) {
        String id = DOMUtil.getAttribute(tree, XML_NS_URI, "id");
        if (id != null) {
            if (!XMLText.isName(id)) {
                // An ID must match the Name production.
                reportError(tree, Msg.msg("notAnID", id));
            } else {
                Element[] elements = idMap.get(id);
                if (elements == null) {
                    elements = new Element[] { tree };
                } else {
                    elements = ArrayUtil.append(elements, tree);

                    String firstOccurrence;
                    NodeLocation location = (NodeLocation) 
                        elements[0].getUserData(NodeLocation.USER_DATA_KEY);
                    if (location == null) {
                        firstOccurrence = "???";
                    } else {
                        firstOccurrence = location.toString();
                    }

                    reportError(tree,
                                Msg.msg("duplicateID", id, firstOccurrence));
                }
                idMap.put(id, elements);
            }
        }

        // ---

        Node child = tree.getFirstChild();
        while (child != null) {
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                collectIds((Element) child, idMap);
            }

            child = child.getNextSibling();
        }
    }

    private void checkInternalLinks(Element tree, Map<String,Element[]> idMap) {
        final int count = DOCBOOK5_IDREF_LIST.length;
        for (int i = 0; i < count; i += 3) {
            String nsURI = DOCBOOK5_IDREF_LIST[i];
            String localName = DOCBOOK5_IDREF_LIST[i+1];
            String valuePrefix = DOCBOOK5_IDREF_LIST[i+2];

            String value = DOMUtil.getAttribute(tree, nsURI, localName);
            if (value != null &&
                (valuePrefix == null || value.startsWith(valuePrefix))) {
                if (value.length() == 0 || "???".equals(value)) {
                    reportError(tree, Msg.msg("notAnIDREF", value));
                } else {
                    if (valuePrefix != null && value.startsWith(valuePrefix)) {
                        value = value.substring(valuePrefix.length());
                    }

                    String[] idRefs = XMLText.splitList(value);
                    for (String idRef : idRefs) {
                        if (!idMap.containsKey(idRef)) {
                            reportError(tree, Msg.msg("referenceToUndefinedID",
                                                      idRef));
                        }
                    }
                }
            }
        }

        // ---

        Node child = tree.getFirstChild();
        while (child != null) {
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                checkInternalLinks((Element) child, idMap);
            }

            child = child.getNextSibling();
        }
    }

    private void checkResources(Element tree) {
        final int count = RESOURCE_SPECS.length;
        for (int i = 0; i < RESOURCE_SPECS.length; i += 4) {
            String elementNS = RESOURCE_SPECS[i];
            String elementLocalName = RESOURCE_SPECS[i+1];
            String attributeNS = RESOURCE_SPECS[i+2];
            String attributeLocalName = RESOURCE_SPECS[i+3];

            if (DOMUtil.hasName(tree, elementNS, elementLocalName)) {
                String location =
                    DOMUtil.getAttribute(tree, attributeNS, attributeLocalName);
                if (location != null) {
                    URL url = null;

                    if (location.length() > 0 && !"???".equals(location)) {
                        try {
                            String resolved = null;
                            if (resolver != null) {
                                resolved = resolver.resolveURI(location);
                            }

                            if (resolved != null) {
                                url = URLUtil.createURL(resolved);
                            } else {
                                url = 
                                    URLUtil.createURL(DOMUtil.getBaseURL(tree),
                                                      location);
                            }
                        } catch (MalformedURLException ignored) {}
                    }

                    if (url == null) {
                        reportError(tree, Msg.msg("notAnURL", location));
                    } else {
                        if (!URLUtil.exists(url)) {
                            reportError(tree,
                                        Msg.msg("missingResource", 
                                                elementLocalName, 
                                                URLUtil.toDisplayForm(url)));
                        }
                    }
                }
            }
        }

        // ---

        Node child = tree.getFirstChild();
        while (child != null) {
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                checkResources((Element) child);
            }

            child = child.getNextSibling();
        }
    }

    private void reportError(Element element, String message) {
        console.warning(element, message);
        hasErrors = true;
    }
}
