package eu.dnetlib.validator2.engine;

import eu.dnetlib.validator2.validation.guideline.Builders;
import eu.dnetlib.validator2.validation.guideline.Builders.ElementSpecBuilder;
import eu.dnetlib.validator2.validation.guideline.Cardinality;
import eu.dnetlib.validator2.validation.guideline.RequirementLevel;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public class Helper {

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


    private static final String EMPTY = "";

    public static boolean isEmpty(String s) {
        return (canonicalize(s).isEmpty());
    }

    public static String ensurePropertyIsPresent(String propertyName, Supplier<String> propertySupplier) {
        return ensureNonEmpty(propertySupplier.get(), "Empty property: " + propertyName);
    }

    public static String readPropertyAndErrIfNotFound(String propertyName, Map<String, String> map) {
        return ensurePropertyIsPresent(propertyName, () -> map.get(propertyName));
    }

    public static String ensureNonEmpty(String value, String errorMessage) {
        return ensureNonEmpty(value, () -> new RuntimeException(errorMessage));
    }

    public static String ensureNonEmpty(String value, Supplier<? extends RuntimeException> exceptionSupplier) {
        String canonical = canonicalize(value);
        if (canonical.isEmpty()) {
            throw exceptionSupplier.get();
        }
        return canonical;
    }

    public static String canonicalize(String s) {
        if (s == null) return EMPTY;
        return s.trim();
    }

    private static <T> String asString(T t) {
        return t == null ? "<null>" : t.toString();
    }

    public static String stringify(Rule<?> rule) {
        return rule.getClass().getSimpleName() + "[" +
                rule.getContext().getProperties().
                        stream().
                        map(prop -> prop.getName() + "=" + prop.getValue()).
                        collect(Collectors.joining(",")) + "]";
    }

    public static <T> boolean predicateSucceedsForAllNodes(NodeList nodes,
                                                           Function<Node, T> nodeReader,
                                                           Predicate<T> predicate) {
        int len = nodes.getLength();
        if (len == 0) return false; // Is used in original implementation
        for (int i = 0; i < len; i++) {
            T t = nodeReader.apply(nodes.item(i));
            if (!predicate.test(t)) return false;
        }
        return true;
    }

    public static <T> boolean predicateSucceedsForAtLeastOneNode(NodeList nodes,
                                                                 Function<Node, T> nodeReader,
                                                                 Predicate<T> predicate) {
        int len = nodes.getLength();
        for (int i = 0; i < len; i++) {
            T t = nodeReader.apply(nodes.item(i));
            if (predicate.test(t)) return true;
        }
        return false;
    }

    public static <T> boolean predicateFailsForAllNodes(NodeList nodes,
                                                        Function<Node, T> nodeReader,
                                                        Predicate<T> predicate) {
        int len = nodes.getLength();
        for (int i = 0; i < len; i++) {
            T t = nodeReader.apply(nodes.item(i));
            if (predicate.test(t)) return false;
        }
        return true;
    }

    public static <T> boolean predicateSucceedsForExactlyOneNode(NodeList nodes,
                                                                 Function<Node, T> nodeReader,
                                                                 Predicate<T> predicate) {
        int len = nodes.getLength();
        return len == 1 && predicate.test(nodeReader.apply(nodes.item(0)));
    }

    public static <T> NodeList nodesThatMatchThePredicate(NodeList nodes,
                                                          Function<Node, T> nodeReader,
                                                          Predicate<T> predicate) {
        List<Node> filtered = new ArrayList<>();
        if (nodes != null) {
            int len = nodes.getLength();
            for (int i = 0; i < len; i++) {
                Node node = nodes.item(i);
                //logger.debug("Getting node: " + node);
                T t = nodeReader.apply(node);
                //logger.debug("Read node value: " + t);
                if (predicate.test(t)) {
                    filtered.add(node);
                }
            }
        }
        //logger.debug(filtered.size() + " nodes matched the predicate");
        return new ListOfNodes(filtered);
    }

    public static String getAttributeValue(Node node, String attrName) {
        if (node == null || !node.hasAttributes()) { return null; }
        Node attr = node.getAttributes().getNamedItem(attrName);
        if (attr == null) { return null; }
        return attr.getNodeValue();
    }

    public static Predicate<Integer> createCardinalityPredicate(long min, long max, boolean isInclusive) {
        return isInclusive
                ? (Integer len) -> min <= len && len <= max
                : (Integer len) -> min < len && len < max;
    }


    public static ElementSpecBuilder buildElement(String elementName,
                                                  RequirementLevel requirementLevel,
                                                  Cardinality cardinality) {

        if (requirementLevel == RequirementLevel.MANDATORY) {
            return Builders.forMandatoryElement(elementName, cardinality);
        }
        else if (requirementLevel == RequirementLevel.MANDATORY_IF_APPLICABLE) {
            Builders.forMandatoryIfApplicableElement(elementName, cardinality, null);
        }
        else if (requirementLevel == RequirementLevel.OPTIONAL) {
            if (cardinality == Cardinality.ONE_TO_N) {
                return Builders.forOptionalRepeatableElement(elementName);
            }
            else {
                return Builders.forOptionalElement(elementName);
            }
        }
        else if (requirementLevel == RequirementLevel.RECOMMENDED) {
            if (cardinality == Cardinality.ONE_TO_N) {
                return Builders.forRecommendedRepeatableElement(elementName);
            }
            else {
                return Builders.forRecommendedElement(elementName);
            }
        }
        throw new RuntimeException("Not reachable");
    }

    public static ElementSpecBuilder addAttribute(ElementSpecBuilder builder,
                                                  String attrName,
                                                  RequirementLevel requirementLevel) {

        if (requirementLevel == RequirementLevel.MANDATORY) {
            return builder.withMandatoryAttribute(attrName);
        }
        else if (requirementLevel == RequirementLevel.OPTIONAL) {
            return builder.withOptionalAttribute(attrName);
        }
        else if (requirementLevel == RequirementLevel.RECOMMENDED) {
            return builder.withRecommendedAttribute(attrName);
        }
        throw new RuntimeException("Not reachable");
    }

    public static class URLResolver {
        /*
        TODO
         In case of network error all subsequent evaluations of same url string will be false (incorrectly)
        */
        private static final ConcurrentHashMap<String, Boolean> resolvedURLs = new ConcurrentHashMap<>();

        public static boolean resolve(String URLString) {
            return resolvedURLs.computeIfAbsent(URLString, u -> {
                HttpURLConnection huc = null;
                try {
                    URL url = new URL(u);
                    huc = (HttpURLConnection) url.openConnection();
                    huc.setRequestMethod("HEAD");
                    return huc.getResponseCode() == HttpURLConnection.HTTP_OK;
                } catch (IOException e) {
                    return false;
                } finally {
                    if (huc != null) huc.disconnect();
                }
            });
        }

    }

    public static class Diagnostics {

        public static <T, R extends Rule<T>> RuleDiagnostics<T, R> systemOut() {
            return new PrintStreamDiagnostics<>(System.out);
        }

        public static <T, R extends Rule<T>> RuleDiagnostics<T, R> systemErr() {
            return new PrintStreamDiagnostics<>(System.err);
        }

    }

    private static class PrintStreamDiagnostics<T, R extends Rule<T>> implements RuleDiagnostics<T, R> {

        private final PrintStream printStream;

        private PrintStreamDiagnostics(PrintStream printStream) {
            this.printStream = printStream;
        }

        @Override
        public void success(R rule, T t) {
//            LEONIDAS
//            printStream.println("Success: value " + asString(t) + " passes the " + rule.getContext().getIdProperty().getValue());
        }

        @Override
        public void failure(R rule, T t) {
//            LEONIDAS
//            printStream.println("Failure: value " + asString(t) + " fails the " + rule.getContext().getIdProperty().getValue());
        }

        @Override
        public void error(R rule, T t, Throwable err) {
            String trace = stackTraceOf(err);
            printStream.println(
                "Error: value " + asString(t) +
                " raised an error to the" + rule.getContext().getIdProperty().getValue() + ": " + trace
            );
        }
    }

    private static class ListOfNodes implements NodeList {

        private final List<Node> nodes;

        private ListOfNodes(List<Node> nodes) {
            this.nodes = nodes;
        }

        @Override
        public Node item(int index) {
            return nodes.get(index);
        }

        @Override
        public int getLength() {
            return nodes.size();
        }
    }

    private static String stackTraceOf(Throwable t) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        t.printStackTrace(pw);
        return sw.toString();
    }

    public static void forEachZipEntry(File zipFile,
                                       int bufferSize,
                                       Predicate<ZipEntry> filter,
                                       BiConsumer<String, InputStream> zipEntryConsumer) throws IOException {
        if (zipEntryConsumer == null) return;
        try (ZipInputStream zip = new ZipInputStream(new FileInputStream(zipFile))) {
            ZipEntry entry;
            while ((entry = zip.getNextEntry()) != null) {
                if (filter == null || filter.test(entry)) {
                    ByteArrayOutputStream out = new ByteArrayOutputStream(bufferSize);
                    byte[] bytesIn = new byte[bufferSize];
                    int read = 0;
                    while((read = zip.read(bytesIn)) != -1) {
                        out.write(bytesIn, 0, read);
                    }
                    zipEntryConsumer.accept(entry.getName(), new ByteArrayInputStream(out.toByteArray()));
                }
                zip.closeEntry();
            }
        }
    }

    public static final Predicate<ZipEntry> ZIP_ENTRY_IS_FILE = zipEntry -> !zipEntry.isDirectory();
}
