package it.unibz.inf.ontop.iq.type.impl;

import com.google.common.collect.ImmutableList;
import it.unibz.inf.ontop.exception.MinorOntopInternalBugException;
import it.unibz.inf.ontop.injection.IntermediateQueryFactory;
import it.unibz.inf.ontop.iq.IQTree;
import it.unibz.inf.ontop.iq.node.*;
import it.unibz.inf.ontop.iq.transform.IQTreeTransformer;
import it.unibz.inf.ontop.iq.transform.impl.DefaultRecursiveIQTreeVisitingTransformer;
import it.unibz.inf.ontop.iq.type.SingleTermTypeExtractor;
import it.unibz.inf.ontop.model.term.*;
import it.unibz.inf.ontop.model.term.functionsymbol.FunctionSymbol;
import it.unibz.inf.ontop.model.term.functionsymbol.NotYetTypedEqualityFunctionSymbol;
import it.unibz.inf.ontop.model.type.DBTermType;
import it.unibz.inf.ontop.model.type.TermType;
import it.unibz.inf.ontop.iq.type.NotYetTypedEqualityTransformer;
import it.unibz.inf.ontop.substitution.ImmutableSubstitution;
import it.unibz.inf.ontop.substitution.SubstitutionFactory;
import it.unibz.inf.ontop.utils.ImmutableCollectors;

import javax.inject.Inject;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;

public class NotYetTypedEqualityTransformerImpl implements NotYetTypedEqualityTransformer {

    private final IQTreeTransformer expressionTransformer;

    @Inject
    protected NotYetTypedEqualityTransformerImpl(IntermediateQueryFactory iqFactory,
                                                 SingleTermTypeExtractor typeExtractor,
                                                 TermFactory termFactory) {
        this.expressionTransformer = new ExpressionTransformer(iqFactory,
                                                                typeExtractor,
                                                                termFactory);
    }

    @Override
    public IQTree transform(IQTree tree) {
        return expressionTransformer.transform(tree);
    }


    protected static class ExpressionTransformer extends AbstractExpressionTransformer {

        protected ExpressionTransformer(IntermediateQueryFactory iqFactory,
                                        SingleTermTypeExtractor typeExtractor,
                                        TermFactory termFactory) {
            super(iqFactory, typeExtractor, termFactory);
        }

        @Override
        protected boolean isFunctionSymbolToReplace(FunctionSymbol functionSymbol) {
            return functionSymbol instanceof NotYetTypedEqualityFunctionSymbol;
        }

        /**
         * NB: It tries to reduce equalities into strict equalities.
         *
         * Essential for integers and strings as these kinds of types are often used to build IRIs.
         */
        @Override
        protected ImmutableFunctionalTerm replaceFunctionSymbol(FunctionSymbol functionSymbol,
                                                                ImmutableList<ImmutableTerm> newTerms, IQTree tree) {
            if (newTerms.size() != 2)
                throw new MinorOntopInternalBugException("Was expecting the not yet typed equalities to be binary");

            ImmutableTerm term1 = newTerms.get(0);
            ImmutableTerm term2 = newTerms.get(1);

            ImmutableList<Optional<TermType>> extractedTypes = newTerms.stream()
                    .map(t -> typeExtractor.extractSingleTermType(t, tree))
                    .collect(ImmutableCollectors.toList());

            if (extractedTypes.stream()
                    .allMatch(type -> type
                            .filter(t -> t instanceof DBTermType)
                            .isPresent())) {
                ImmutableList<DBTermType> types = extractedTypes.stream()
                        .map(Optional::get)
                        .map(t -> (DBTermType) t)
                        .collect(ImmutableCollectors.toList());

                DBTermType type1 = types.get(0);
                DBTermType type2 = types.get(1);

                return type1.equals(type2)
                        ? transformSameTypeEquality(type1, term1, term2, tree)
                        : transformDifferentTypesEquality(type1, type2, term1, term2);
            }
            else
                return termFactory.getDBNonStrictDefaultEquality(term1, term2);
        }

        private ImmutableExpression transformSameTypeEquality(DBTermType type, ImmutableTerm term1, ImmutableTerm term2,
                                                              IQTree tree) {
            if (type.areEqualitiesStrict())
                return termFactory.getStrictEquality(term1, term2);

            if (areIndependentFromConstants(term1, term2, tree) && type.areEqualitiesBetweenTwoDBAttributesStrict()) {
                return termFactory.getStrictEquality(term1, term2);
            }

            /*
             * Tries to reuse an existing typed non-strict equality
             */
            switch (type.getCategory()) {
                case DECIMAL:
                case FLOAT_DOUBLE:
                    return termFactory.getDBNonStrictNumericEquality(term1, term2);
                case DATETIME:
                    return termFactory.getDBNonStrictDatetimeEquality(term1, term2);
                default:
                    return termFactory.getDBNonStrictDefaultEquality(term1, term2);
            }
        }

        protected ImmutableExpression transformDifferentTypesEquality(DBTermType type1, DBTermType type2,
                                                                      ImmutableTerm term1, ImmutableTerm term2) {
            /*
             * If not type declares that the equality cannot be reduced to a strict equality
             */
            if (areCompatibleForStrictEq(type1, type2)) {
                return termFactory.getStrictEquality(term1, term2);
            }

            /*
             * Tries to reuse an existing typed non-strict equality
             */
            DBTermType.Category category1 = type1.getCategory();
            DBTermType.Category category2 = type2.getCategory();

            switch (category1) {
                case STRING:
                case INTEGER:
                    switch (category2) {
                        case DECIMAL:
                        case FLOAT_DOUBLE:
                            return termFactory.getDBNonStrictNumericEquality(term1, term2);
                        default:
                            break;
                    }
                    break;
                case DECIMAL:
                case FLOAT_DOUBLE:
                    switch (category2) {
                        case INTEGER:
                        case DECIMAL:
                        case FLOAT_DOUBLE:
                            return termFactory.getDBNonStrictNumericEquality(term1, term2);
                        default:
                            break;
                    }
                    break;
                case DATETIME:
                    if (category2 == DBTermType.Category.DATETIME) {
                        return termFactory.getDBNonStrictDatetimeEquality(term1, term2);
                    }
                    break;
                case BOOLEAN:
                case OTHER:
                default:
                    break;
            }
            // By default
            return termFactory.getDBNonStrictDefaultEquality(term1, term2);
        }


        /*
         * TODO: make it robust so as to make sure the term does not depend on a constant introduced in the mapping.
         *
         * Constants in the mapping are indeed uncontrolled and may have a different lexical value
         * that the ones returned by the DB, which would make the test fail.
         */
        protected boolean areIndependentFromConstants(ImmutableTerm term1, ImmutableTerm term2, IQTree tree) {
            return !((term1 instanceof DBConstant) || (term2 instanceof DBConstant));
        }

        private boolean areCompatibleForStrictEq(DBTermType type1, DBTermType type2) {
            return Stream.of(type1.areEqualitiesStrict(type2), type2.areEqualitiesStrict(type1))
                    .filter(Optional::isPresent)
                    .map(Optional::get)
                    .reduce((b1, b2) -> b1 && b2)
                    .orElse(false);
        }
    }

}
