package eu.dnetlib.validator2.engine.builtins;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import eu.dnetlib.validator2.engine.RuleEvaluationException;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.xpath.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;

public class XPathExpressionHelper {

    private static final org.slf4j.Logger logger = LoggerFactory.getLogger(XPathExpressionHelper.class);

    // XPath expression compilation
    private static final XPath XPATH = XPathFactory.newInstance().newXPath();
    private static final ConcurrentHashMap<String, XPathExpression> COMPILED_EXPRESSIONS = new ConcurrentHashMap<>();

    // XPath expression evaluation
    private static final int MAX_CACHE_ENTRIES = 100; // TODO: Make this configurable
    private static final LoadingCache<Document, ConcurrentMap<XPathExpression, NodeList>>
        EVALUATED_EXPRESSIONS =
            CacheBuilder.
                newBuilder().
                maximumSize(MAX_CACHE_ENTRIES).
                build(new CacheLoader<Document, ConcurrentMap<XPathExpression, NodeList>>() {
                    @Override
                    public ConcurrentMap<XPathExpression, NodeList> load(Document key) {
                        return new ConcurrentHashMap<>();
                    }
                });

    public static XPathExpression compile(String xpath) {
        return COMPILED_EXPRESSIONS.computeIfAbsent(xpath, s -> {
            try {
                if ( logger.isTraceEnabled() )
                    logger.trace("Compiling {}", s);
                XPathExpression expr = XPATH.compile(s);
                return expr;
            } catch (XPathExpressionException e) {
                logger.error("Compilation failure", e);
                throw new RuntimeException(e.getMessage(), e);
            }
        });
    }

    public static NodeList nodeList(String xpath, Document doc) {
        logger.debug("Evaluating nodeList {}", xpath);
        return nodeList(compile(xpath), doc);
    }

    public static NodeList nodeList(XPathExpression expr, Document doc) {
        try {
            return EVALUATED_EXPRESSIONS.get(doc).computeIfAbsent(expr, (XPathExpression xpath) -> {
                try {
                    NodeList nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
                    return nodes;
                }
                catch(XPathExpressionException ex) {
                    throw new RuleEvaluationException(ex.getMessage(), ex);
                }
            });
        } catch (ExecutionException e) {
            throw new RuleEvaluationException(e.getMessage(), e);
        }
    }

    public static String attr(String xpath, Node node) {
        logger.debug("Evaluating attr {}", xpath);
        return attr(compile(xpath), node);
    }

    public static String attr(XPathExpression expr, Node node) {
        try {
            String value = expr.evaluate(node);
            return value;
        }
        catch(XPathExpressionException ex) {
            throw new RuleEvaluationException(ex.getMessage(), ex);
        }
    }

    public static Node node(String xpath, Node node) {
        logger.debug("Evaluating node {}", xpath);
        return node(compile(xpath), node);
    }

    public static Node node(XPathExpression expr, Node node) {
        try {
            Node resultNode = (Node) expr.evaluate(node, XPathConstants.NODE);
            return resultNode;
        }
        catch(XPathExpressionException ex) {
            throw new RuleEvaluationException(ex.getMessage(), ex);
        }
    }
}
