package fr.cenra.rhomeo.protocol.p05;

import fr.cenra.rhomeo.api.data.TrackingPoint;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.core.data.reference.OrthopteraIndicatorReference;
import fr.cenra.rhomeo.core.list.OrthoptereZoneBio;
import fr.cenra.rhomeo.protocol.TaxonOperator;
import java.beans.IntrospectionException;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import fr.cenra.rhomeo.api.data.Dataset;
import fr.cenra.rhomeo.api.process.Process;
import fr.cenra.rhomeo.api.process.ProcessContext;
import fr.cenra.rhomeo.api.result.Index;
import fr.cenra.rhomeo.core.RhomeoRuntimeException;
import java.util.HashSet;
import java.util.Set;

import fr.cenra.rhomeo.core.data.reference.Taxref;
import java.util.Collections;

/**
 * Computes {@link P05} related indicators (see {@link I05} and {@link I09}).
 * This indicator is a bit special, because it computes a single index for
 * multiple years, and send it back as the result for only one year, the most
 * recent from input. For more information, see <a href="https://gitlab.geomatys.com/rhomeo-group/rhomeo/issues/87">issue 87</a>.
 *
 * @author Alexis Manin (Geomatys)
 */
class Processor extends Process {

    static final String USED_YEARS = "Années prises en compte";

    final TaxonOperator operator;
    final AbstractP05Indicator indic;

    public Processor(final ProcessContext ctx, final AbstractP05Indicator indicator) {
        super(ctx);
        indic = indicator;
        operator = TaxonOperator.getOrCreate(ctx);
    }

    /**
     * Search for orthoptera references matching given observations. The mapping
     * is based on {@link Taxref#getCd_ref() } code. For each observation, we
     * try to acquire a cd_ref, and for each distinct cd_ref found, we'll take
     * the corresponding reference in {@link OrthopteraIndicatorReference} list.
     *
     * @param statements Observations to get a referent for.
     * @return Stream of available references, no doublon.
     */
    Stream<OrthopteraIndicatorReference> findMatchingReferences(final Stream<OrthopteraStatement> statements) {
        return statements
                // Get distinct rhomeo species codes for each observation
                .mapToInt(OrthopteraStatement::getCd_nom)
                .distinct()
                // Find distinct species identifiers corresponding to above rhomeo codes
                .map(this::getCdRef)
                .filter(cdRef -> cdRef > 0)
                .distinct()
                // Find orthoptera reference corresponding to given cd_refs
                .mapToObj(this::getReference)
                .filter(Objects::nonNull);
    }

    /**
     * Filter input references to keep only the ones valid in given zone, and
     * then perform a sum of their indicator values.
     *
     * @param references References to use in computing.
     * @param zone Zone to filter references with.
     * @return Sum of retained refrence indicator values
     */
    int sum(final Stream<OrthopteraIndicatorReference> references, final OrthoptereZoneBio zone) {
        return references
                // Keep only references consistent with given zone
                .filter(ref -> AbstractP05Indicator.checkZoneConsistency(ref, zone))
                // Extract indicator value from retained references
                .mapToInt(indic::getIndicatorValue)
                .sum();
    }

    private int getCdRef(final int cdNom) {
        try {
            final Integer tmp = operator.getCdRef(cdNom);
            return tmp == null ? -1 : tmp;
        } catch (ReflectiveOperationException | IntrospectionException ex) {
            throw new RuntimeException("Cannot perform cd_nom -> cd_ref mapping", ex);
        }
    }

    private OrthopteraIndicatorReference getReference(final int cdRef) {
        try {
            return operator.getOrthopteraReference(cdRef);
        } catch (ReflectiveOperationException | IntrospectionException ex) {
            throw new RuntimeException("Cannot perform cd_ref -> orthoptera mapping", ex);
        }
    }

    @Override
    public Indicator getIndicator() {
        return indic;
    }

    /**
     * Generic algorithm to compute both {@link I05} and {@link I09} rates.
     *
     * The computing for those indicators is a bit special. We do not make a
     * result by year, but aggregate multiple years in a single result (see
     * <a href="https://gitlab.geomatys.com/rhomeo-group/rhomeo/issues/87">issue
     * 87</a> for details).
     *
     * @return A set of {@link AnnualRateSedimentaryDynamics}.
     * @throws java.beans.IntrospectionException
     * @throws IllegalStateException if the data context is null, or if its site
     * is null, of if the "orthoptere type" of its site is null.
     * @throws RhomeoRuntimeException if the orthoptere type of the site cannot
     * be converted to an {@link OrthoptereZoneBio}.
     */
    @Override
    protected Set<? extends Index> call() throws Exception {
        updateTitle("Calcul ".concat(indic.getTitle()));
        final long totalProgress = 6;
        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().getOrthoptereType() == null)
            throw new IllegalStateException("The site of the data context must specify an type of orthoptera bio zone.");

        // Get the orthoptere biozone of the site
        final OrthoptereZoneBio siteBioZone = OrthoptereZoneBio.getByCode(ctx.getDataCtx().getSite().getOrthoptereType());

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

        /*
         * 1- Loop over statements to compute sum for observed species.
         * ==========================================================*/
        // By year
        updateMessage("Analyse des observations annuelles");
        updateProgress(progress++, totalProgress);
        final TaxonOperator<?> op = TaxonOperator.getOrCreate(ctx);

        Stream dataStream = ctx.getData().getItems().stream();
        dataStream = findMatchingReferences(dataStream);
        final int yearSum = sum(dataStream, siteBioZone);

        // By tracking point
        updateMessage("Analyse des observations par point de suivi");
        updateProgress(progress++, totalProgress);

        final Map<TrackingPoint, List<OrthopteraStatement>> dataByPoint
                = ((Dataset<OrthopteraStatement>) ctx.getData()).groupByPoint();

        /*
         * 2- Loop over indicators to compute sum for expected species.
         * ==========================================================*/
        updateMessage("Indice de zone");
        updateProgress(progress++, totalProgress);
        int expectedSum = sum(op.getOrthopteraReferences(), siteBioZone);

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

        /*
         * 3- Compute indices.
         * =================*/
        /* HACK (#87) : We have to determine the most recent year to use for
         * annual index. Thing is : We could parse input data to find maximum
         * year, or sort it and then get the year of the last element. But, it's
         * costly operations. So, an alternative is to wait tracking point
         * grouping, and then parse them to find the point with the most recent
         * year. It allows to parse less elements to find what year should be
         * retained.
         */
        updateMessage("Calcul de l'indicateur par point de suivi");
        updateProgress(progress++, totalProgress);

        final List<Integer> years = dataByPoint.keySet().stream()
                .mapToInt(TrackingPoint::getYear)
                .distinct()
                .sorted()
                .boxed()
                .collect(Collectors.toList());

        if (years.size() < 1)
            return Collections.EMPTY_SET;

        ctx.getDataCtx().getUserData().put(USED_YEARS, years);

        List<Index> pointIndices = dataByPoint.entrySet().parallelStream()
                .map(entry -> {
                    final Stream<OrthopteraStatement> pointData = entry.getValue().stream();
                    final Stream<OrthopteraIndicatorReference> pointRefs = findMatchingReferences(pointData);
                    final int pointSum = sum(pointRefs, siteBioZone);
                    return indic.getAnnualPointIndexSpi().createIndex(100 * (pointSum / (double) expectedSum), entry.getKey());
                })
                .collect(Collectors.toList());

        indices.addAll(pointIndices);

        updateMessage("Calcul annuel de l'indicateur");
        updateProgress(progress++, totalProgress);
        indices.add(indic.getAnnualIndexSpi()
                .createIndex(100 * (yearSum / (double) expectedSum), years.get(years.size() - 1)));

        return indices;
    }
}
