package it.unibz.inf.ontop.spec.fact.impl;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import it.unibz.inf.ontop.exception.MinorOntopInternalBugException;
import it.unibz.inf.ontop.injection.OntopMappingSettings;
import it.unibz.inf.ontop.model.term.BNode;
import it.unibz.inf.ontop.model.term.IRIConstant;
import it.unibz.inf.ontop.model.term.ObjectConstant;
import it.unibz.inf.ontop.model.term.TermFactory;
import it.unibz.inf.ontop.model.vocabulary.OWL;
import it.unibz.inf.ontop.model.vocabulary.RDF;
import it.unibz.inf.ontop.model.vocabulary.RDFS;
import it.unibz.inf.ontop.spec.ontology.*;
import it.unibz.inf.ontop.utils.ImmutableCollectors;

import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Stream;

import static java.util.Objects.isNull;

@Singleton
public class FactExtractorWithSaturatedTBox extends AbstractFactExtractor {

    private static final String AUX_ROLE = "urn:AUX.ROLE";

    private final TermFactory termFactory;
    private final OntopMappingSettings settings;
    private final IRIConstant someValuesFrom, subClassOf, subPropertyOf, domain, range, rdfType;
    private final IRIConstant rdfsClass, owlClass, owlRestriction, onProperty, owlThing;
    private final IRIConstant rdfProperty, objectProperty, dataProperty;
    private final IRIConstant inverseOf;

    @Inject
    protected FactExtractorWithSaturatedTBox(TermFactory termFactory, OntopMappingSettings settings) {
        super(settings);
        this.settings = settings;
        this.termFactory = termFactory;
        someValuesFrom = termFactory.getConstantIRI(OWL.SOME_VALUES_FROM);
        subClassOf = termFactory.getConstantIRI(RDFS.SUBCLASSOF);
        subPropertyOf = termFactory.getConstantIRI(RDFS.SUBPROPERTYOF);
        domain = termFactory.getConstantIRI(RDFS.DOMAIN);
        range = termFactory.getConstantIRI(RDFS.RANGE);
        inverseOf = termFactory.getConstantIRI(OWL.INVERSE_OF);
        rdfType = termFactory.getConstantIRI(RDF.TYPE);
        rdfsClass = termFactory.getConstantIRI(RDFS.CLASS);
        owlClass = termFactory.getConstantIRI(OWL.CLASS);
        owlRestriction = termFactory.getConstantIRI(OWL.RESTRICTION);
        onProperty = termFactory.getConstantIRI(OWL.ON_PROPERTY);
        owlThing = termFactory.getConstantIRI(OWL.THING);
        rdfProperty = termFactory.getConstantIRI(RDF.PROPERTY);
        dataProperty = termFactory.getConstantIRI(OWL.DATATYPE_PROPERTY);
        objectProperty = termFactory.getConstantIRI(OWL.OBJECT_PROPERTY);
    }

    @Override
    protected Stream<RDFFact> extractTBox(ClassifiedTBox tbox) {
        // TODO: also consider simple RDF properties

        ImmutableMap<DescriptionBT, ObjectConstant> expressionIdMap = Stream.concat(Stream.concat(
                generateIdEntries(tbox.classesDAG(), this::generateIdFromClassExpression),
                generateIdEntries(tbox.dataPropertiesDAG(), this::generateIdFromDataPropertyExpression)),
                generateIdEntries(tbox.objectPropertiesDAG(), this::generateIdFromObjectPropertyExpression))
                .filter(v -> !(v.getValue() instanceof BNode)) // Remove BNodes
                .filter(v -> !((v.getValue().getValue().contains(AUX_ROLE)))) // Remove auxiliary object property
                .collect(ImmutableCollectors.toMap());

        // TODO: also consider class disjointness

        EquivalencesDAG<ClassExpression> classDag = tbox.classesDAG();

        return Stream.concat(
                Stream.concat(
                        extractFromDAG(
                                classDag,
                                e -> convertClassExpression(e, expressionIdMap),
                                expressionIdMap,
                                ClassExpression.class,
                                subClassOf),
                        extractFromDAG(
                                tbox.dataPropertiesDAG(),
                                e -> convertDataPropertyExpression(e, expressionIdMap, classDag),
                                expressionIdMap,
                                DataPropertyExpression.class,
                                subPropertyOf)),
                extractFromDAG(
                        tbox.objectPropertiesDAG(),
                        e -> convertObjectPropertyExpression(e, expressionIdMap, classDag),
                        expressionIdMap,
                        ObjectPropertyExpression.class,
                        subPropertyOf));
    }

    private <T extends DescriptionBT> Stream<Map.Entry<DescriptionBT, ObjectConstant>> generateIdEntries(
            EquivalencesDAG<T> dag, Function<T, ObjectConstant> idGenerator) {
        return dag.stream()
                .flatMap(e -> e.getMembers().stream())
                .map(m -> Maps.immutableEntry(m, idGenerator.apply(m)));
    }

    private ObjectConstant generateIdFromClassExpression(ClassExpression expression) {
        if (expression instanceof OClass)
            return termFactory.getConstantIRI(((OClass) expression).getIRI());
        else
            // TODO: use a better name (make sure expression.toString() is safe
            return termFactory.getConstantBNode(UUID.randomUUID().toString());
    }

    private ObjectConstant generateIdFromDataPropertyExpression(DataPropertyExpression expression) {
        return termFactory.getConstantIRI(expression.getIRI());
    }

    private ObjectConstant generateIdFromObjectPropertyExpression(ObjectPropertyExpression expression) {
        return expression.isInverse()
                ? termFactory.getConstantBNode("op-inv" + expression.getIRI())
                : termFactory.getConstantIRI(expression.getIRI());
    }

    private <T extends DescriptionBT> Stream<RDFFact> extractFromDAG(EquivalencesDAG<T> dag,
                                                                     Function<T, Stream<RDFFact>> expressionConverter,
                                                                     ImmutableMap<DescriptionBT, ObjectConstant> expressionIdMap,
                                                                     Class<T> expressionClass,
                                                                     IRIConstant subClassOrSubProperty) {
        return Stream.concat(
                expressionIdMap.keySet().stream()
                        .filter(expressionClass::isInstance)
                        .filter(v -> !(expressionIdMap.get(v) instanceof BNode))
                        .flatMap(e -> expressionConverter.apply((T) e)),
                extractSub(dag, subClassOrSubProperty, expressionIdMap));
    }

    private <T extends DescriptionBT> Stream<RDFFact> extractSub(EquivalencesDAG<T> dag, IRIConstant subPredicateProperty,
                                                                 ImmutableMap<DescriptionBT, ObjectConstant> expressionIdMap) {
        return dag.stream()
                    .flatMap(supEq -> supEq.getMembers().stream()
                            .flatMap(sup -> dag.getSub(supEq).stream()
                                    .flatMap(subEq -> subEq.getMembers().stream()
                                            // Necessary to remove dag elements whose corresponding value from the
                                            // expressionIdMap were BNodes. ExpressionIdMap was filtered.
                                            .filter(sub -> !(isNull(expressionIdMap.get(sub)))
                                                    && !(isNull(expressionIdMap.get(sup))))
                                            .map(sub -> RDFFact.createTripleFact(
                                                    expressionIdMap.get(sub),
                                                    subPredicateProperty,
                                                    expressionIdMap.get(sup))))));
    }

    /**
     * Everything except subClassOf
     */
    private Stream<RDFFact> convertClassExpression(ClassExpression e,
                                                   ImmutableMap<DescriptionBT, ObjectConstant> expressionIdMap) {
        ObjectConstant classId = expressionIdMap.get(e);

        Stream<RDFFact> common = Stream.of(
                RDFFact.createTripleFact(classId, rdfType, rdfsClass),
                RDFFact.createTripleFact(classId, rdfType, owlClass));

        if (e instanceof OClass)
            return common;
        else if (e instanceof ObjectSomeValuesFrom) {
            return Stream.concat(
                    common,
                    extractFactsFromRestriction(
                            classId,
                            expressionIdMap.get(((ObjectSomeValuesFrom) e).getProperty())));
        }
        else if (e instanceof DataSomeValuesFrom) {
            return Stream.concat(
                    common,
                    extractFactsFromRestriction(
                            classId,
                            expressionIdMap.get(((DataSomeValuesFrom) e).getProperty())));
        }
        else
            throw new MinorOntopInternalBugException("Unexpected class expression");
    }

    private Stream<RDFFact> extractFactsFromRestriction(ObjectConstant classId, ObjectConstant propertyId) {
        return Stream.of(
                RDFFact.createTripleFact(classId, rdfType, owlRestriction),
                RDFFact.createTripleFact(classId, onProperty, propertyId),
                RDFFact.createTripleFact(classId, someValuesFrom, owlThing));
    }

    private Stream<RDFFact> convertDataPropertyExpression(DataPropertyExpression e,
                                                          ImmutableMap<DescriptionBT, ObjectConstant> expressionIdMap,
                                                          EquivalencesDAG<ClassExpression> classDag) {
        ObjectConstant propertyId = expressionIdMap.get(e);
        Stream<RDFFact> basics = Stream.of(
                RDFFact.createTripleFact(propertyId, rdfType, rdfProperty),
                RDFFact.createTripleFact(propertyId, rdfType, dataProperty));

        // TODO: consider range?
        return Stream.concat(
                basics,
                e.getAllDomainRestrictions().stream()
                        .flatMap(c -> extractSubDomainOrRange(propertyId, c, domain, expressionIdMap, classDag)));

    }

    /**
     * Everything except subPropertyOf
     */
    private Stream<RDFFact> extractSubDomainOrRange(ObjectConstant propertyId, ClassExpression classExpression,
                                                    IRIConstant rangeOrDomainProperty,
                                                    ImmutableMap<DescriptionBT, ObjectConstant> expressionIdMap,
                                                    EquivalencesDAG<ClassExpression> classDag) {
        Equivalences<ClassExpression> eq = classDag.getVertex(classExpression);

        if (settings.areSuperClassesOfDomainRangeInferred()) {
            return classDag.getSuper(eq).stream()
                    .flatMap(supEq -> supEq.stream()
                            .filter(sup -> sup instanceof OClass)
                            .map(sup -> RDFFact.createTripleFact(propertyId, rangeOrDomainProperty, expressionIdMap.get(sup))));
        } else {
            return classDag.getDirectSuper(eq).stream()
                    .flatMap(supEq -> supEq.stream()
                            .filter(sup -> sup instanceof OClass)
                            .map(sup -> RDFFact.createTripleFact(propertyId, rangeOrDomainProperty, expressionIdMap.get(sup))));
        }

    }

    /**
     * Everything except subPropertyOf
     */
    private Stream<RDFFact> convertObjectPropertyExpression(ObjectPropertyExpression e,
                                                            ImmutableMap<DescriptionBT, ObjectConstant> expressionIdMap,
                                                            EquivalencesDAG<ClassExpression> classDag) {
        ObjectConstant propertyId = expressionIdMap.get(e);

        return Stream.concat(
                Stream.concat(
                Stream.concat(
                Stream.concat(
                        Stream.of(
                            RDFFact.createTripleFact(propertyId, rdfType, rdfProperty),
                            RDFFact.createTripleFact(propertyId, rdfType, objectProperty)),
                extractSubDomainOrRange(propertyId, e.getDomain(), domain, expressionIdMap, classDag)),
                extractSubDomainOrRange(propertyId, e.getRange(), range, expressionIdMap, classDag)),
                extractFactsFromSubClassRestriction(propertyId, e.getRange(), expressionIdMap, classDag)),
                extractInverse(propertyId, e, expressionIdMap, classDag));
    }

    private Stream<RDFFact> extractFactsFromSubClassRestriction(ObjectConstant propertyId, ClassExpression classExpression,
                                                                ImmutableMap<DescriptionBT, ObjectConstant> expressionIdMap,
                                                                EquivalencesDAG<ClassExpression> classDag) {
        Equivalences<ClassExpression> eq = classDag.getVertex(classExpression);

        boolean restrictionPresent = (classDag.getDirectSub(eq).stream()
                .map(c -> c.toString())
                .anyMatch(c -> c.contains(AUX_ROLE))) &&
                // Avoid scenarios of functional property declaration by checking already defined domain/range
                // Only auxiliary object property connects restriction to graph
                (eq.getMembers().size()==1) &&
                (classDag.getDirectSuper(eq).size()==0);

        if (restrictionPresent) {
            BNode newBNode = termFactory.getConstantBNode(UUID.randomUUID().toString());
            Stream<RDFFact> restriction = Stream.of(RDFFact.createTripleFact(newBNode, rdfType, owlRestriction));
            Stream<RDFFact> restrictionOnProperty = Stream.of(RDFFact.createTripleFact(newBNode, onProperty, propertyId));
            Stream<RDFFact> restrictionSomeValuesFrom = classDag.getDirectSub(eq).stream()
                    .flatMap(sup -> classDag.getSuper(sup).stream()
                            .flatMap(supEq2 -> supEq2.stream()
                                    .filter(sup2 -> sup2 instanceof OClass)
                                    .map(sup3 -> RDFFact.createTripleFact(newBNode, someValuesFrom, expressionIdMap.get(sup3)))
                            )
                    );

            Stream<RDFFact> newBNodeclasses = Stream.of(
                    RDFFact.createTripleFact(newBNode, rdfType, rdfsClass),
                    RDFFact.createTripleFact(newBNode, rdfType, owlClass));

            Stream<RDFFact> restrictionSubClasses = classDag.getSub(eq).stream()
                    .flatMap(supEq -> supEq.stream()
                            .filter(sup -> sup instanceof OClass)
                            .map(sup -> RDFFact.createTripleFact(expressionIdMap.get(sup), subClassOf, newBNode)));

            return Stream.concat(restriction,
                    Stream.concat(restrictionOnProperty,
                            Stream.concat(restrictionSomeValuesFrom,
                                    Stream.concat(newBNodeclasses, restrictionSubClasses))));
        }

        return Stream.of();
    }

    private Stream<RDFFact> extractInverse(ObjectConstant propertyId, ObjectPropertyExpression e,
                                                                ImmutableMap<DescriptionBT, ObjectConstant> expressionIdMap,
                                                                EquivalencesDAG<ClassExpression> classDag) {
        Equivalences<ClassExpression> eq = classDag.getVertex(e.getInverse().getDomain());

        return eq.getMembers().stream()
                .filter(m -> m instanceof ObjectSomeValuesFrom)
                .map(m -> (ObjectSomeValuesFrom) m)
                .filter(m -> !m.getProperty().isInverse()) //Drop the inverse of itself generated by Ontop
                .filter(m -> !m.getProperty().toString().contains(AUX_ROLE)) // Drop auxiliary object property
                .map(m -> RDFFact.createTripleFact(propertyId, inverseOf, expressionIdMap.get(m.getProperty())));
    }
}
