package it.unibz.inf.ontop.rdf4j.predefined.parsing.impl;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.inject.Injector;
import com.moandjiezana.toml.Toml;
import it.unibz.inf.ontop.query.RDF4JConstructQuery;
import it.unibz.inf.ontop.query.RDF4JQueryFactory;
import it.unibz.inf.ontop.injection.OntopSystemConfiguration;
import it.unibz.inf.ontop.rdf4j.predefined.PredefinedGraphQuery;
import it.unibz.inf.ontop.rdf4j.predefined.PredefinedQueries;
import it.unibz.inf.ontop.rdf4j.predefined.PredefinedTupleQuery;
import it.unibz.inf.ontop.rdf4j.predefined.impl.PredefinedGraphQueryImpl;
import it.unibz.inf.ontop.rdf4j.predefined.parsing.PredefinedQueryConfigEntry;
import it.unibz.inf.ontop.rdf4j.predefined.parsing.PredefinedQueryConfigException;
import it.unibz.inf.ontop.rdf4j.predefined.parsing.PredefinedQueryParser;
import it.unibz.inf.ontop.utils.ImmutableCollectors;
import org.eclipse.rdf4j.query.Query;
import org.eclipse.rdf4j.query.QueryLanguage;
import org.eclipse.rdf4j.query.impl.MapBindingSet;
import org.eclipse.rdf4j.query.parser.ParsedQuery;
import org.eclipse.rdf4j.query.parser.QueryParserUtil;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;

public class PredefinedQueryParserImpl implements PredefinedQueryParser {

    private final RDF4JQueryFactory inputQueryFactory;

    public PredefinedQueryParserImpl(OntopSystemConfiguration configuration) {
        Injector injector = configuration.getInjector();
        inputQueryFactory = injector.getInstance(RDF4JQueryFactory.class);
    }

    @Override
    public PredefinedQueries parse(@Nonnull Reader configReader, @Nonnull Reader queryReader) throws PredefinedQueryConfigException {
        return parse(configReader, queryReader, null);
    }

    /**
     * TODO: handle exceptions from TOML and Jackson
     */
    @Override
    public PredefinedQueries parse(@Nonnull Reader configReader, @Nonnull Reader queryReader, @Nullable Reader contextReader)
            throws PredefinedQueryConfigException {
        try {
            Toml toml = new Toml().read(queryReader);
            ImmutableMap<String, String> queryMap = toml.entrySet().stream()
                    .filter(e -> e.getValue() instanceof Toml)
                    .flatMap(e -> Optional.ofNullable(((Toml) e.getValue()).getString("query"))
                            .map(v -> Maps.immutableEntry(e.getKey(), v))
                            .map(Stream::of)
                            .orElseGet(Stream::empty))
                    .collect(ImmutableCollectors.toMap());

            ObjectMapper mapper = new ObjectMapper();
            ImmutableMap<String, PredefinedQueryConfigEntry> configEntries = mapper.readValue(configReader, Config.class)
                    .getQueries();

            Sets.SetView<String> missingQueries = Sets.difference(configEntries.keySet(), queryMap.keySet());
            if (!missingQueries.isEmpty())
                throw new PredefinedQueryConfigException("Missing query entries for " + missingQueries);

            ImmutableMap<String, Object> contextMap = contextReader == null
                    ? ImmutableMap.of()
                    : ImmutableMap.copyOf(mapper.readValue(contextReader, new TypeReference<HashMap<String, Object>>() {}));

            ImmutableMap<String, PredefinedGraphQuery> graphQueries = configEntries.entrySet().stream()
                    .filter(e -> e.getValue().getQueryType().equals(Query.QueryType.GRAPH))
                    .collect(ImmutableCollectors.toMap(
                            Map.Entry::getKey,
                            e -> createPredefinedGraphQuery(e.getKey(), e.getValue(),
                                    queryMap.get(e.getKey()))));

            ImmutableMap<String, PredefinedTupleQuery> tupleQueries = configEntries.entrySet().stream()
                    .filter(e -> e.getValue().getQueryType().equals(Query.QueryType.TUPLE))
                    .collect(ImmutableCollectors.toMap(
                            Map.Entry::getKey,
                            e -> createPredefinedTupleQuery(e.getKey(), e.getValue(),
                                    queryMap.get(e.getKey()))));

            return PredefinedQueries.defaultPredefinedQueries(tupleQueries, graphQueries, contextMap);

        } catch (IOException e) {
            throw new PredefinedQueryConfigException(e);
        }
    }

    private PredefinedGraphQuery createPredefinedGraphQuery(String id, PredefinedQueryConfigEntry queryConfigEntry,
                                                            String queryString) {
        ParsedQuery parsedTree = QueryParserUtil.parseQuery(QueryLanguage.SPARQL, queryString, null);
        RDF4JConstructQuery graphQuery = inputQueryFactory.createConstructQuery(queryString, parsedTree, new MapBindingSet());

        return new PredefinedGraphQueryImpl(id, graphQuery, queryConfigEntry);
    }

    private PredefinedTupleQuery createPredefinedTupleQuery(String id, PredefinedQueryConfigEntry queryConfigEntry,
                                                            String queryString) {
        throw new RuntimeException("TODO: support tuple queries");
    }

    private static class Config {
        private final ImmutableMap<String, PredefinedQueryConfigEntry> queries;

        @JsonCreator
        public Config(@JsonProperty("queries") Map<String, ParsedPredefinedQueryConfigEntry> queries) {
            this.queries = ImmutableMap.copyOf(queries);
        }

        public ImmutableMap<String, PredefinedQueryConfigEntry> getQueries() {
            return queries;
        }
    }
}
