/* FichothequeLib_API - Copyright (c) 2017-2023 Vincent Calame - Exemole
 * Logiciel libre donné sous triple licence :
 * 1) selon les termes de la CeCILL V2
 * 2) selon les termes de l’EUPL V.1.1
 * 3) selon les termes de la GNU GPLv3
 * Voir le fichier licences.txt
 */


package net.fichotheque.utils.selection;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import net.fichotheque.Fichotheque;
import net.fichotheque.SubsetItem;
import net.fichotheque.SubsetKey;
import net.fichotheque.corpus.Corpus;
import net.fichotheque.corpus.FicheMeta;
import net.fichotheque.croisement.Croisements;
import net.fichotheque.selection.CroisementCondition;
import net.fichotheque.selection.FicheCondition;
import net.fichotheque.selection.FicheQuery;
import net.fichotheque.selection.FicheSelector;
import net.fichotheque.selection.SelectionContext;
import net.fichotheque.utils.SelectionUtils;
import net.mapeadores.util.conditions.ConditionsConstants;


/**
 *
 * @author Vincent Calame
 */
public final class FicheConditionPredicateFactory {

    private final static Test ALWAYS_TEST = new ConstantTest(true);

    private FicheConditionPredicateFactory() {
    }

    public static Predicate<SubsetItem> newInstance(SelectionContext selectionContext, FicheCondition ficheCondition) {
        Test[] testArray;
        List<Test> testList = new ArrayList<Test>();
        for (FicheCondition.Entry entry : ficheCondition.getEntryList()) {
            Test test = toTest(selectionContext, entry.getFicheQuery(), entry.getCroisementCondition(), entry.includeSatellites());
            if (test != null) {
                testList.add(test);
            }
        }
        int size = testList.size();
        if (size == 0) {
            testArray = new Test[1];
            testArray[0] = ALWAYS_TEST;
        } else {
            testArray = testList.toArray(new Test[size]);
        }
        return new ResultPredicate(ficheCondition.getLogicalOperator(), testArray);
    }

    private static Test toTest(SelectionContext selectionContext, FicheQuery ficheQuery, CroisementCondition croisementCondition, boolean includeSatellites) {
        boolean invert = false;
        if ((croisementCondition == null) && (ficheQuery.isOnlyCorpusCondition())) {
            if (ficheQuery.isEmpty()) {
                return null;
            } else {
                return new CorpusListTest(invert, selectionContext, ficheQuery, includeSatellites);
            }
        } else {
            return new FicheSelectorTest(invert, selectionContext, ficheQuery, croisementCondition, includeSatellites);
        }
    }

    private static Set<SubsetKey> toSubsetKeySet(Corpus[] corpusArray) {
        Set<SubsetKey> subsetKeySet = new HashSet<SubsetKey>();
        for (Corpus corpus : corpusArray) {
            subsetKeySet.add(corpus.getSubsetKey());
        }
        return subsetKeySet;
    }


    private static class ResultPredicate implements Predicate<SubsetItem> {

        private final boolean isOr;
        private final Test[] testArray;

        private ResultPredicate(String logicalOperator, Test[] testArray) {
            this.isOr = (logicalOperator.equals(ConditionsConstants.LOGICALOPERATOR_OR));
            this.testArray = testArray;
        }

        @Override
        public boolean test(SubsetItem subsetItem) {
            if (isOr) {
                for (Test test : testArray) {
                    if (test.isSelected(subsetItem)) {
                        return true;
                    }
                }
                return false;
            } else {
                for (Test test : testArray) {
                    if (!test.isSelected(subsetItem)) {
                        return false;
                    }
                }
                return true;
            }
        }

    }


    private static abstract class Test {

        private final boolean invert;

        protected Test(boolean invert) {
            this.invert = invert;
        }

        public boolean isSelected(SubsetItem subsetItem) {
            boolean test = test(subsetItem);
            if (invert) {
                return !test;
            } else {
                return test;
            }
        }

        protected abstract boolean test(SubsetItem subsetItem);

    }


    private static class ConstantTest extends Test {

        private ConstantTest(boolean constant) {
            super(constant);
        }

        @Override
        public boolean test(SubsetItem subsetItem) {
            return true;
        }

    }


    private static class CorpusListTest extends Test {

        private final Fichotheque fichotheque;
        private final Corpus[] corpusArray;
        private final boolean withSatelliteTest;
        private final Predicate<FicheMeta> fichePredicate;
        private final Set<SubsetKey> subsetKeySet;

        private CorpusListTest(boolean invert, SelectionContext selectionContext, FicheQuery ficheQuery, boolean includeSatellites) {
            super(invert);
            this.fichotheque = selectionContext.getFichotheque();
            this.corpusArray = SelectionUtils.toCorpusArray(fichotheque, ficheQuery, selectionContext.getSubsetAccessPredicate());
            this.withSatelliteTest = includeSatellites;
            this.fichePredicate = selectionContext.getFichePredicate();
            if (withSatelliteTest) {
                this.subsetKeySet = toSubsetKeySet(corpusArray);
            } else {
                this.subsetKeySet = null;
            }
        }

        @Override
        public boolean test(SubsetItem subsetItem) {
            if (withSatelliteTest) {
                for (Corpus corpus : subsetItem.getSubset().getSatelliteCorpusList()) {
                    if (subsetKeySet.contains(corpus.getSubsetKey())) {
                        FicheMeta ficheMeta = corpus.getFicheMetaById(subsetItem.getId());
                        if (ficheMeta != null) {
                            if (fichePredicate.test(ficheMeta)) {
                                return true;
                            }
                        }
                    }
                }
            }
            for (Corpus corpus : corpusArray) {
                Croisements croisements = fichotheque.getCroisements(subsetItem, corpus);
                if (!croisements.isEmpty()) {
                    for (Croisements.Entry entry : croisements.getEntryList()) {
                        FicheMeta ficheMeta = (FicheMeta) entry.getSubsetItem();
                        if (fichePredicate.test(ficheMeta)) {
                            return true;
                        }
                    }
                }
            }
            return false;
        }

    }


    private static class FicheSelectorTest extends Test {

        private final Fichotheque fichotheque;
        private final Corpus[] corpusArray;
        private final boolean withSatelliteTest;
        private final Set<SubsetKey> subsetKeySet;
        private final FicheSelector ficheSelector;
        private final boolean withCroisementCondition;

        private FicheSelectorTest(boolean invert, SelectionContext selectionContext, FicheQuery ficheQuery, CroisementCondition croisementCondition, boolean includeSatellites) {
            super(invert);
            this.fichotheque = selectionContext.getFichotheque();
            this.corpusArray = SelectionUtils.toCorpusArray(fichotheque, ficheQuery, selectionContext.getSubsetAccessPredicate());
            this.withSatelliteTest = includeSatellites;
            if (withSatelliteTest) {
                this.subsetKeySet = toSubsetKeySet(corpusArray);
            } else {
                this.subsetKeySet = null;
            }
            this.ficheSelector = FicheSelectorBuilder.init(selectionContext).add(ficheQuery, croisementCondition).toFicheSelector();
            this.withCroisementCondition = (croisementCondition != null);
        }

        @Override
        public boolean test(SubsetItem subsetItem) {
            if (withSatelliteTest) {
                for (Corpus corpus : subsetItem.getSubset().getSatelliteCorpusList()) {
                    if (subsetKeySet.contains(corpus.getSubsetKey())) {
                        FicheMeta satelliteFicheMeta = corpus.getFicheMetaById(subsetItem.getId());
                        if (satelliteFicheMeta != null) {
                            if (ficheSelector.test(satelliteFicheMeta)) {
                                return true;
                            }
                        }
                    }
                }
            }
            for (Corpus corpus : corpusArray) {
                Croisements croisements = fichotheque.getCroisements(subsetItem, corpus);
                for (Croisements.Entry entry : croisements.getEntryList()) {
                    FicheMeta ficheMeta = (FicheMeta) entry.getSubsetItem();
                    if (!withCroisementCondition) {
                        if (ficheSelector.test(ficheMeta)) {
                            return true;
                        }
                    } else {
                        if (ficheSelector.isSelected(ficheMeta, entry.getCroisement()) != null) {
                            return true;
                        }
                    }
                }
            }
            return false;
        }

    }

}
