/**
 * Copyright © CENRA (2016)
 *
 * remi.clement@espaces-naturels.fr
 *
 * This software is a multi-platform and portable application based on
 * the scientifical toolbox to monitor wetlands. With this toolbox it’s
 * possible to calculate indicators «RhoMéO» to monitor quality,
 * fonctionalities and urban and agricultural pressures of wetlands.
 * Indicators are calculated using naturalist data entered and/or imported,
 * and geographic data downloaded from a geographic server.
 *
 * This software is governed by the CeCILL license under French law and
 * abiding by the rules of distribution of free software.  You can  use,
 * modify and/ or redistribute the software under the terms of the CeCILL
 * license as circulated by CEA, CNRS and INRIA at the following URL
 * "http://www.cecill.info".
 *
 * As a counterpart to the access to the source code and  rights to copy,
 * modify and redistribute granted by the license, users are provided only
 * with a limited warranty  and the software's author,  the holder of the
 * economic rights,  and the successive licensors  have only  limited
 * liability.
 *
 * In this respect, the user's attention is drawn to the risks associated
 * with loading,  using,  modifying and/or developing or reproducing the
 * software by the user in light of its specific status of free software,
 * that may mean  that it is complicated to manipulate,  and  that  also
 * therefore means  that it is reserved for developers  and  experienced
 * professionals having in-depth computer knowledge. Users are therefore
 * encouraged to load and test the software's suitability as regards their
 * requirements in conditions enabling the security of their systems and/or
 * data to be ensured and,  more generally, to use and operate it in the
 * same conditions as regards security.
 *
 * The fact that you are presently reading this means that you have had
 * knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.protocol.p06;

import fr.cenra.rhomeo.api.data.Protocol;
import fr.cenra.rhomeo.api.data.Site;
import fr.cenra.rhomeo.api.process.Process;
import fr.cenra.rhomeo.api.process.ProcessContext;
import fr.cenra.rhomeo.core.list.HumidZoneType;
import fr.cenra.rhomeo.api.process.AbstractIndicator;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.api.result.Index;
import fr.cenra.rhomeo.core.RhomeoRuntimeException;
import fr.cenra.rhomeo.core.data.reference.OdonataDepartmentReference;
import fr.cenra.rhomeo.core.data.reference.OdonataDepartmentReferenceDescription;
import fr.cenra.rhomeo.core.data.reference.OdonataHabitatReference;
import fr.cenra.rhomeo.core.data.reference.OdonataHabitatReferenceDescription;
import fr.cenra.rhomeo.core.data.reference.TaxrefDescription;
import fr.cenra.rhomeo.core.list.OdonateZoneBio;
import fr.cenra.rhomeo.protocol.TaxonOperator;
import static fr.cenra.rhomeo.protocol.p05.AbstractP05Indicator.getReferences;
import fr.cenra.rhomeo.protocol.p06.validation.BehaviorCoverageValidator;
import java.beans.IntrospectionException;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import org.apache.sis.util.ArgumentChecks;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

/**
 *
 * @author Samuel Andrés (Geomatys)
 */
@Component(I10.NAME)
public class I10 extends AbstractIndicator {

    public static final String NAME = "I10";
    public static final String REMARKS = "Intégrité du peuplement d'odonates";

    @Autowired
    @Qualifier(P06.NAME)
    private Protocol protocol;

    @Autowired
    protected AnnualRatePopulationIntegrity indexSpi;

    @Autowired
    protected AnnualNbOfExpectedSpecies nbSpeciesIndexSpi;

    @Autowired
    protected AnnualRateRichness annualRateRichness;

    @Autowired
    protected BehaviorSpi behaviorSpi;

    @Autowired
    protected OdonataHabitatReferenceDescription habitatReferenceDescription;

    @Autowired
    protected OdonataDepartmentReferenceDescription departmentReferenceDescription;

    @Autowired
    protected TaxrefDescription taxrefDescription;

    public I10() {
        super(NAME, REMARKS);
    }

    @Override
    public Protocol getProtocol() {
        return protocol;
    }

    @Override
    public Set<Process> createProcesses(ProcessContext ctx) {
        ArgumentChecks.ensureNonNull("processContext", ctx);
        final TaxonOperator operator = TaxonOperator.getOrCreate(ctx);
        return new HashSet<>(Arrays.asList(new Process[]{new I10Process(operator), new RichnessProcess(operator, this)}));
    }

    @Override
    public String getDefaultIndex() {
        return AnnualRatePopulationIntegrity.NAME;
    }

    /**
     *
     * @return
     */
    @Override
    public Set<String> getZoneTypeCodes() {
        final Set<String> result = new HashSet<>();
        for(final HumidZoneType type : HumidZoneType.values()){
            if(!type.equals(HumidZoneType.GRANDS_ESTUAIRES)
                    && !type.equals(HumidZoneType.BAIES_ESTUAIRES)
                    && !type.equals(HumidZoneType.ZH_BAS_FOND_ALTITUDE))
            result.add(type.getCode());
        }
        return result;
    }

    private static OdonateZoneBio getOdonateZoneBio(final Site site){
        return OdonateZoneBio.getByCode(site.getOdonateType());
    }

    /**
     * "Expected" {@link OdonataHabitatReference} computed from observations.
     *
     * A) Consistency constraints for the {@link OdonataHabitatReference}s :
     * ---------------------------------------------------------------------
     *
     * - the observed habitats of the species must be referenced;
     * - the {@link OdonataHabitatReference#zbio} must be coherent with the {@link Site#getOdonateType() };
     * - the department of the site must be referenced;
     * - the {@link OdonataHabitatReference#cd_ref} and {@link OdonataDepartmentReference#zbio} must be coherent.
     *
     * B) Consistency constraints for the {@link OdonataHabitatReference}s :
     * ---------------------------------------------------------------------
     * - the {@link OdonataDepartmentReference#valeur} must be < 2.
     * - the {@link OdonataHabitatReference#valeur} must be > 3.
     *
     *
     * C) Query
     * --------
     *
     * -- t1 restreint les observations d'habitats d'odonates aux types d'habitat des relevés.
     * -- De plus, ces types d'habitats doivent être compatibles avec la 'zone bio odonates' du site et doivent concerner des espèces potentiellement localisées dans le département du site,
     * -- Enfin, les valeurs des références 'odo_especes_par_dept' et 'odo_dependance_habitat' doivent respecter certaines contraintes.
     *
     * WITH t1 AS (
     *
     * -- t récupère les valeurs d'habitat pour les relevés d'odonates.
     * WITH t AS (
     *
     * SELECT
     * sites.dept, -- 1 seul site / group by indirect
     * sites.zbio, -- 1 seul site / group by indirect
     * […]
     * releves_valeurs.id_attribut, -- 1 seul valeur filtrée / group by
     * releves_valeurs.valeur AS habitat, -- group by
     * "left"(releves.date::text, 4) AS annee, -- group by
     * […]
     *
     * FROM sites
     * JOIN points_de_suivis ON sites.id_site = points_de_suivis.id_site
     * JOIN releves ON points_de_suivis.id_point_suivi = releves.id_point_suivi
     * JOIN releves_valeurs ON releves.id_releve = releves_valeurs.id_releve
     *
     * WHERE
     * releves_valeurs.id_attribut = 'habitats_odo'::text
     *
     * GROUP BY
     * […]
     * releves_valeurs.id_attribut,
     * releves_valeurs.valeur,
     * ("left"(releves.date::text, 4)),
     * […]
     * )
     *
     *
     * -- récupère la valeur minimale de l'habitat de l'espèce en filtrant sur les valeurs de présence par département et sur la valeur d'habitat
     * -- il s'avère que la valeur minimale semble inutilisée : ce sont les espèces filtrées qui sont utilisées
     * SELECT
     * t.dept, -- group by site
     * […]
     * t.zbio, -- group by site
     * […]
     * odo_dependance_habitat.espece AS sp_attendue, --group by
     * min(odo_dependance_habitat.valeur::text) AS min_valeur,
     * odo_dependance_habitat.cd_ref, -- group by
     * t.annee, -- group by
     * […]
     * FROM t,
     * odo_dependance_habitat,
     * odo_especes_par_dept
     *
     * WHERE
     * odo_especes_par_dept.valeur > 2::double precision -- filtre sur odo_especes_par_dept
     * AND odo_dependance_habitat.valeur::integer < 3 -- filtre sur odo_dependance_habitat
     *
     * AND t.dept = odo_especes_par_dept.id_dept::text  -- jointure -> odo_especes_par_dept
     * AND t.zbio = odo_dependance_habitat.zbio::text -- jointure -> odo_dependance_habitat
     * AND t.habitat = odo_dependance_habitat.habitat::text -- jointure -> odo_dependance_habitat
     * AND odo_dependance_habitat.cd_ref = odo_especes_par_dept.cd_ref -- jointure odo_dependance_habitat <-> odo_especes_par_dept pour être cohérent au niveau espèce.
     *
     * GROUP BY
     * t.dept, -- vient du site
     * […]
     * t.zbio, -- vient du site
     * […]
     * odo_dependance_habitat.espece,
     * odo_dependance_habitat.cd_ref,
     * t.annee,
     * […]
     * )
     *
     * @param statements
     * @param site
     * @return
     * @throws IntrospectionException
     */
    protected Map<Integer, Set<OdonataHabitatReference>> t1(final Iterable<OdonataStatement> statements, final Site site) throws IntrospectionException{

        final Map<Integer, Set<OdonataHabitatReference>> expectedSpeciesByYear = new HashMap<>();

        // Get the consistent pair of habitat reference / departement reference.
        // "JOIN" t.dept = odo_especes_par_dept.id_dept::text
        // "JOIN" t.zbio = odo_dependance_habitat.zbio::text
        // "JOIN" odo_dependance_habitat.cd_ref = odo_especes_par_dept.cd_ref
        final Map<String, Set<Map.Entry<OdonataHabitatReference, OdonataDepartmentReference>>> consistents = getConsistentReferences(site);

        // Loop over statements
        final Function<Integer, Set<OdonataHabitatReference>> creator = input -> new HashSet();
        for(final OdonataStatement statement : statements){

            final int year = statement.getDate().getYear();

            final Set<Map.Entry<OdonataHabitatReference, OdonataDepartmentReference>> consistentsSet = consistents.get(statement.getHabitat());

            if(consistentsSet!=null){

                // Loop over consistent reference pairs for the habitat of the current statement.
                // ("JOIN" t.habitat = odo_dependance_habitat.habitat::text)
                for(final Map.Entry<OdonataHabitatReference, OdonataDepartmentReference> consistent : consistentsSet){

                    // Get the referencies.
                    final OdonataHabitatReference habReference = consistent.getKey();
                    final OdonataDepartmentReference deptReference = consistent.getValue();

                    // filter on reference values :
                    // odo_especes_par_dept.valeur > 2::double precision -- filtre sur odo_especes_par_dept
                    // AND odo_dependance_habitat.valeur::integer < 3 -- filtre sur odo_dependance_habitat
                    if(habReference.getValeur()<3
                            && deptReference.getValeur()>2){
                        expectedSpeciesByYear.computeIfAbsent(year, creator).add(habReference);
                    }
                }
            }
        }
        return expectedSpeciesByYear;
    }

    /**
     * Count the result of {@link I10#t1(java.lang.Iterable, fr.cenra.rhomeo.api.data.Site) }, by year.
     *
     * -- Compte le nombre d'espèces attendues selon le filtrage de t1... et... et... et... ? Quoi d'autre ???
     * -- En fait, on ne voi pas ce qu'il y a à compter…
     * t2 AS (
     *
     * SELECT
     * […]
     * count(t1.sp_attendue) AS nb_attendues,
     * t1.annee, -- group by
     * […]
     *
     * FROM t1
     *
     * GROUP BY
     * […]
     * t1.annee,
     * […]
     * )
     *
     * @param t1 Result of {@link I10#t1(java.lang.Iterable, fr.cenra.rhomeo.api.data.Site) }.
     * @return
     *
     * @see I10#t1(java.lang.Iterable, fr.cenra.rhomeo.api.data.Site)
     */
    protected static Map<Integer, Integer> t2(final Map<Integer, Set<OdonataHabitatReference>> t1){
        final Map<Integer, Integer> result = new HashMap<>();
        for(final Map.Entry<Integer, Set<OdonataHabitatReference>> entry : t1.entrySet()){
            final Set<Integer> diffSpecies = new HashSet<>();
            for(final OdonataHabitatReference habRef : entry.getValue()){
                diffSpecies.add(habRef.getCd_ref());
            }
            result.put(entry.getKey(), diffSpecies.size());
        }
        return result;
    }

    /**
     * Observed species by year.
     *
     * WITH t4 AS (
     *
     * SELECT
     * […]
     * CASE
     * WHEN taxref.rang = 'SSES'::text THEN taxref.cd_taxsup::text
     * ELSE taxref.cd_ref::text
     * END AS cd_ref,
     * taxref.nom_complet, -- group by
     * taxref.rang, -- group by
     * "left"(releves.date::text, 4) AS annee, -- group by
     * taxref.cd_taxsup -- group by
     *
     * FROM
     * […]
     * points_de_suivis […]
     * JOIN releves ON releves.id_point_suivi = points_de_suivis.id_point_suivi
     * JOIN obs_especes ON releves.id_releve = obs_especes.id_releve
     * JOIN taxref ON obs_especes.cd_ref = taxref.cd_nom::text
     *
     * -- Filtrage sur les observations d'odonates
     * WHERE obs_especes.code_thematique = 'odonates'::text
     *
     * GROUP BY
     * […]
     * taxref.cd_ref,
     * taxref.nom_complet,
     * taxref.rang,
     * ("left"(releves.date::text, 4)),
     * taxref.cd_taxsup
     * )
     *
     * (Source : base issue du script RhoMéO_Geomatys.backup)
     *
     * @param statements
     * @return
     * @throws IntrospectionException
     */
    protected Map<Integer, Set<Integer>> t4(final Iterable<OdonataStatement> statements, final TaxonOperator operator) throws IntrospectionException, ReflectiveOperationException {
        final Map<Integer, Set<Integer>> observedSpeciesByYear = new HashMap<>();
        final Function<Integer, Set<Integer>> creator = input -> new HashSet<>();
        for (final OdonataStatement statement : statements) {

            final Integer taxref = operator.getCdRef(statement.getCd_nom());
            // "INNER JOIN" taxref
            if (taxref != null) {
                observedSpeciesByYear.computeIfAbsent(statement.getDate().getYear(), creator).add(taxref);
            }
        }
        return observedSpeciesByYear;
    }

    /**
     * Get the {@link OdonataHabitatReference#cd_ref}s present in the result of
     * {@link I10#t1(java.lang.Iterable, fr.cenra.rhomeo.api.data.Site) }.
     *
     * @param t1 Result of {@link I10#t1(java.lang.Iterable, fr.cenra.rhomeo.api.data.Site) }.
     * @return
     *
     * @see I10#t1(java.lang.Iterable, fr.cenra.rhomeo.api.data.Site)
     */
    protected static Set<Integer> getCdRefs(final Map<Integer, Set<OdonataHabitatReference>> t1){

        // Must be a set to avoid duplicated.
        final Set<Integer> result = new HashSet<>();

        for(final Collection<OdonataHabitatReference> set : t1.values()){
            for(final OdonataHabitatReference ref : set){
                result.add(ref.getCd_ref());
            }
        }
        return result;
    }

    /**
     * Number of observed {@link T4ObservedSpecies} among expected {@link OdonataHabitatReference} by year.
     *
     * t3 AS (
     *
     * -- Récupération de l'espèce des odonates observés (on remonte à cd_taxsup pour les sous-espèces de manière à avoir l'espèce)
     * WITH t4 AS (
     *
     * SELECT
     * […]
     * CASE
     * WHEN taxref.rang = 'SSES'::text THEN taxref.cd_taxsup::text
     * ELSE taxref.cd_ref::text
     * END AS cd_ref,
     * taxref.nom_complet, -- group by
     * […]
     * "left"(releves.date::text, 4) AS annee, -- group by
     * […]
     *
     * FROM
     * […]
     * JOIN points_de_suivis  […]
     * JOIN releves ON releves.id_point_suivi = points_de_suivis.id_point_suivi
     * JOIN obs_especes ON releves.id_releve = obs_especes.id_releve
     * JOIN taxref ON obs_especes.cd_ref = taxref.cd_nom::text
     *
     * -- Filtrage sur les observations d'odonates
     * WHERE obs_especes.code_thematique = 'odonates'::text
     *
     * GROUP BY
     * […]
     * taxref.cd_ref,
     * taxref.nom_complet,
     * taxref.rang,
     * ("left"(releves.date::text, 4)),
     * taxref.cd_taxsup
     * )
     *
     *
     * -- On compte le nombre d'espèces observées qui étaient attendues
     * SELECT
     * […]
     * t4.annee, -- group by
     * count(DISTINCT t4.nom_complet) AS nb_sp_vues
     *
     * FROM t4
     * -- Jointure sur l'espèce entre les habitats odonates observés et attendus
     * JOIN t1 ON […] AND t1.cd_ref::text = t4.cd_ref
     *
     * GROUP BY
     * […]
     * t4.annee
     * […]
     * )
     *
     * @param t1 Result of {@link I10#t1(java.lang.Iterable, fr.cenra.rhomeo.api.data.Site) }.
     * @param t4 Result of {@link I10#t4(java.lang.Iterable)  }.
     * @return
     *
     * @see I10#t1(java.lang.Iterable, fr.cenra.rhomeo.api.data.Site)
     * @see I10#t4(java.lang.Iterable)
     */
    static Map<Integer, Integer> t3(final Map<Integer, Set<OdonataHabitatReference>> t1,
            final Map<Integer, Set<Integer>> t4){

        final Map<Integer, Integer> result = new HashMap<>();
        final Set<Integer> expectedCdRefs = getCdRefs(t1);

        // For each year of observed species.
        for(final Map.Entry<Integer, Set<Integer>> observedSpecies : t4.entrySet()){

            final Collection<Integer> observedCol = observedSpecies.getValue();

            // Must be a set to avoid duplicated.
            final Set<Integer> obsRefs = new HashSet<>();

            // Loop over observed species of the current year.
            for(final Integer observed : observedCol){

                // Get the list of "expected" for the current observed species. (i.e. "JOIN" condition)
                if(expectedCdRefs.contains(observed)){

                    // Add "nom_complet"
                    obsRefs.add(observed);
                }
            }

            // Count distinct "nom_complet" of observed
            result.put(observedSpecies.getKey(), obsRefs.size());
        }
        return result;
    }

    /**
     * -- Pourcentage du nombre d'espèces vues qui étaient attendues par rappor au nombre d'espèces attendues, par site et par an.
     * SELECT
     * […]
     * t2.annee,
     * […]
     * round(t3.nb_sp_vues::numeric * 100::numeric / t2.nb_attendues::numeric, 2) AS pour_vues_attendues,
     * […]
     *
     * FROM t2
     * FULL JOIN […] t3.annee = t2.annee
     * […]
     *
     * @param t2
     * @param t3
     * @return
     */
    protected Set<Index> rate(final Map<Integer, Integer> t2, final Map<Integer, Integer> t3){

        final Set<Index> indices = new HashSet<>();

        for(final Integer year : t3.keySet()){

            final Integer expected = t2.get(year);

            // Get the nb of "expected" species (i.e. "JOIN")
            if(expected!=null){
                indices.add(indexSpi.createIndex(100.*(t3.get(year)/(double)t2.get(year)), year));
            }
        }
        return indices;
    }

    /**
     * "Group by" clause of the t2 relation.
     *
     * GROUP BY […] odo_dependance_habitat.espece, odo_dependance_habitat.cd_ref
     *
     * @param t2
     * @return
     */
    protected Set<Index> expectedSpecies(final Map<Integer, Integer> t2){

        final Set<Index> indices = new HashSet<>();

        for(final Integer year : t2.keySet()){
            indices.add(nbSpeciesIndexSpi.createIndex(t2.get(year), year));
        }
        return indices;
    }

    /**
     *
     * Retrieves {@link List}s of pairs of {@link OdonataHabitatReference}
     * and {@link OdonataDepartmentReference} indexed by habitat. The pairs are
     * consistent with the given {@link Site} and consistent each other.
     *
     * In case no consistent pair is found, returns an empty {@link Optional}.
     *
     * @param habitat
     * @param site
     * @return
     * @throws IntrospectionException
     * @see I10#getConsistentReferences(fr.cenra.rhomeo.core.list.OdonateZoneBio, java.lang.String)
     */
    private Map<String, Set<Map.Entry<OdonataHabitatReference, OdonataDepartmentReference>>>
        getConsistentReferences(final Site site) throws IntrospectionException {
        return getConsistentReferences(getOdonateZoneBio(site), site.getCountyCode());
    }

    /**
     * -- t1 restreint les observations d'habitats d'odonates aux attentes en fonction du type d'habitat et du département
     * WITH t1 AS (
     *
     * -- t récupère les valeurs d'habitat pour les relevés d'odonates.
     * WITH t AS (
     *
     * SELECT
     * sites.dept, -- 1 seul site / group by indirect
     * sites.zbio, -- 1 seul site / group by indirect
     * […]
     *
     * FROM sites
     * […]
     *
     * GROUP BY
     * sites.id_site,
     * […]
     * )
     *
     * - récupère la valeur minimale de l'habitat de l'espèce en filtrant sur les valeurs de présence par département et sur la valeur d'habitat
     * -- il s'avère que la valeur minimale semble inutilisée : ce sont les espèces filtrées qui sont utilisées
     * SELECT
     * t.dept, -- group by site
     * […]
     * t.zbio, -- group by site
     * […]
     * odo_dependance_habitat.cd_ref, -- group by
     * […]
     * FROM t,
     * odo_dependance_habitat,
     * odo_especes_par_dept
     *
     * WHERE
     *
     * AND t.dept = odo_especes_par_dept.id_dept::text  -- jointure -> odo_especes_par_dept
     * AND t.zbio = odo_dependance_habitat.zbio::text -- jointure -> odo_dependance_habitat
     * […]
     * AND odo_dependance_habitat.cd_ref = odo_especes_par_dept.cd_ref -- jointure odo_dependance_habitat <-> odo_especes_par_dept pour être cohérent au niveau espèce.
     * […]
     * )
     *
     *
     * =========================================================================
     * Note on the following "skipped" join :
     *
     * AND t.habitat = odo_dependance_habitat.habitat::text -- jointure -> odo_dependance_habitat
     *
     * This method skip the following "JOIN", because t.habitat comes from
     * statements. So, in order to get all references only once by dataset,
     * this "JOIN" has to be made after the method has been called.
     *
     *
     * @param zoneBio
     * @param id_dept
     * @return
     * @throws IntrospectionException
     * @see I10#getConsistentReferences(fr.cenra.rhomeo.api.data.Site)
     */
    protected Map<String, Set<Map.Entry<OdonataHabitatReference, OdonataDepartmentReference>>>
        getConsistentReferences(final OdonateZoneBio zoneBio, final String id_dept) throws IntrospectionException {

        // Get the habitat references for the biozone of the site. ("JOIN" t.zbio = odo_dependance_habitat.zbio::text)
        final Map<String, Set<OdonataHabitatReference>> habReferences = getHabitatReferences(zoneBio);

        // Get the department references for the department of the site. ("JOIN" t.dept = odo_especes_par_dept.id_dept::text)
        final Map<Integer, Set<OdonataDepartmentReference>> deptReferences = getDepartmentReferences(id_dept);


        // Prepare the structure lists of consistent combinations of habitat references and department references, indexed by habitat.
        final Map<String, Set<Map.Entry<OdonataHabitatReference, OdonataDepartmentReference>>> consistent = new HashMap<>();

        // Loop over the habitat references.
        for(final Map.Entry<String, Set<OdonataHabitatReference>> habRefs : habReferences.entrySet()){

            // Create the consistent list of consistent pairs for the current habitat
            final Set<Map.Entry<OdonataHabitatReference, OdonataDepartmentReference>> cl = new HashSet<>();

            // Loop over habitat references of the current habitat
            for(final OdonataHabitatReference habReference : habRefs.getValue()){

                // Get the department references of the species for the current habitat reference, of the current habitat.
                // ("JOIN" odo_dependance_habitat.cd_ref = odo_especes_par_dept.cd_ref)
                final Collection<OdonataDepartmentReference> deptRefs = deptReferences.get(habReference.getCd_ref());

                if(deptRefs!=null){
                    // Loop over department references to create consistent pairs.
                    for(final OdonataDepartmentReference deptReference : deptRefs){
                        cl.add(new AbstractMap.SimpleImmutableEntry(habReference, deptReference));
                    }
                }
            }

            consistent.put(habRefs.getKey(), cl);
        }
        return consistent;
    }

    /**
     * Retrieves {@link Set}s of {@link OdonataHabitatReference}s, indexed by
     * {@link OdonataHabitatReference#habitat}, for a given {@link OdonateZoneBio}.
     *
     * The values of the result {@link Map} are {@link Collection}s because
     * {@link OdonataHabitatReference#habitat} is not a primary key of
     * {@link OdonataHabitatReference}s. So, it is possible to retrive several
     * {@link OdonataHabitatReference}s for a given {@link OdonataHabitatReference#habitat},
     * (which have different {@link OdonataHabitatReference#cd_nom}). Note that,
     * on the contrary, {@link OdonataHabitatReference#zbio} is fixed because of
     * the method paramater.
     *
     * @param zonebio
     * @return The values of {@link OdonataHabitatReference} for species which
     * {@link OdonataHabitatReference#zbio} is given as a parameter, indexed by
     * their {@link OdonataHabitatReference#habitat}.
     * @throws IntrospectionException
     */
    protected Map<String, Set<OdonataHabitatReference>> getHabitatReferences(final OdonateZoneBio zonebio) throws IntrospectionException {

        // Get habitat references.
        final List<OdonataHabitatReference> references = getReferences(habitatReferenceDescription);

        // Prepare the result structure.
        final Map<String, Set<OdonataHabitatReference>> result = new HashMap<>();
        final Function<String, Set<OdonataHabitatReference>> creator = input -> new HashSet<>();
        // Loop over habitat references.
        for (final OdonataHabitatReference reference : references) {
            if (zonebio.getZbio().equals(reference.getZbio())) {
                result.computeIfAbsent(reference.getHabitat(), creator).add(reference);
            }
        }
        return result;
    }

    /**
     * Retrieves {@link Set}s of {@link OdonataDepartmentReference}s, indexed by
     * {@link OdonataDepartmentReference#cd_ref}, for a given department id.
     *
     * The values of the result {@link Map} are {@link Collection}s because
     * {@link OdonataDepartmentReference#cd_ref} is not a primary key of
     * {@link OdonataDepartmentReference}s. So, it is theoretically possible to retrive several
     * {@link OdonataDepartmentReference}s for a given {@link OdonataDepartmentReference#cd_ref},
     * (which have different {@link OdonataDepartmentReference#cd_nom}). Note that,
     * on the contrary, {@link OdonataDepartmentReference#id_dept} is fixed because of
     * the method paramater.
     *
     * @param id_dept
     * @return The values of {@link OdonataDepartmentReference} for species
     * which {@link OdonataDepartmentReference#id_dept} is given as a parameter,
     * indexed by their {@link OdonataDepartmentReference#cd_ref}.
     * @throws IntrospectionException
     */
    protected Map<Integer, Set<OdonataDepartmentReference>> getDepartmentReferences(final String id_dept) throws IntrospectionException {

        // Get department references.
        final List<OdonataDepartmentReference> references = getReferences(departmentReferenceDescription);

        // Prepare the result structure.
        final Map<Integer, Set<OdonataDepartmentReference>> result = new HashMap<>();

        // Loop over departement refenreces.
        for (final OdonataDepartmentReference reference : references) {
            if (id_dept.equals(reference.getId_dept())) {
                if(result.get(reference.getCd_ref())==null){
                    result.put(reference.getCd_ref(), new HashSet<>());
                }
                result.get(reference.getCd_ref()).add(reference);
            }
        }
        return result;
    }

    private class I10Process extends Process {

        final TaxonOperator<OdonataStatement> operator;

        private I10Process(final TaxonOperator operator) {
            super(operator.getProcessContext());
            this.operator = operator;
        }

        @Override
        public Indicator getIndicator() {
            return I10.this;
        }

        @Override
        protected Set<? extends Index> call() throws Exception {
            updateTitle("Calcul ".concat(I10.this.getTitle()));
            final long totalProgress = 8;
            long progress = 0;

            updateMessage("Analyse du site");
            updateProgress(progress++, totalProgress);

            if (ctx.getDataCtx() == null)
                throw new IllegalStateException("The data context must not be null.");
            else if (ctx.getDataCtx().getSite() == null)
                throw new IllegalStateException("The data context must specify a site.");
            else if (ctx.getDataCtx().getSite().getOdonateType() == null)
                throw new IllegalStateException("The site of the data context must specify an type of odonata bio zone.");

            final Site site = ctx.getDataCtx().getSite();

            // Get the orthoptere biozone of the site
            final OdonateZoneBio siteBioZone = getOdonateZoneBio(site);

            if (siteBioZone == null) {
                throw new RhomeoRuntimeException("The odonata type of current site cannot be converted to an odonata biozone.");
            }

            final Iterable<OdonataStatement> statements = operator.getProcessData();

            /*
            0- Preliminary calculus.
            ======================*/
            updateMessage("Discrimination des espèces");
            updateProgress(progress++, totalProgress);
            final Map<Integer, Set<OdonataHabitatReference>> t1 = t1(statements, site);

            updateMessage("Regroupement des espèces observées par année");
            updateProgress(progress++, totalProgress);
            final Map<Integer, Set<Integer>> t4 = t4(statements, operator);

            /*
            1- Loop over statements to compute numbers of observed species, by year.
            ======================================================================*/
            updateMessage("Somme des observations annuelles");
            updateProgress(progress++, totalProgress);
            final Map<Integer, Integer> observedSumByYear = t3(t1, t4);

            /*
            2- Loop over indicators to compute number of expected species among observed, by year.
            ====================================================================================*/
            updateMessage("Calcul des sommes annuelles de référence");
            updateProgress(progress++, totalProgress);
            final Map<Integer, Integer> expectedSumByYear = t2(t1);

            /*
            4- Compute indices.
            =================*/
            updateMessage("Calcul d'intégrité de population");
            updateProgress(progress++, totalProgress);
            final Set<Index> indices = rate(expectedSumByYear, observedSumByYear);

            updateMessage("Calcul des espèces attendues");
            updateProgress(progress++, totalProgress);
            indices.addAll(expectedSpecies(expectedSumByYear));

            updateMessage("Calcul du niveau d'autochtonie");
            updateProgress(progress, totalProgress);
            indices.addAll(BehaviorCoverageValidator.computeBehaviorIndices(ctx, operator, behaviorSpi));

            return indices;
        }
    }
}
