/**
 * 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.p02;

import fr.cenra.rhomeo.protocol.p02.index.AnnualFloreFertile;
import fr.cenra.rhomeo.protocol.p02.index.AnnualFloreFertilePoint;
import fr.cenra.rhomeo.protocol.p02.index.AnnualFloreEngorgement;
import fr.cenra.rhomeo.protocol.p02.index.AnnualFloreEngorgementPoint;
import fr.cenra.rhomeo.api.Version;
import fr.cenra.rhomeo.api.data.DataContext;
import fr.cenra.rhomeo.api.data.TrackingPoint;
import fr.cenra.rhomeo.api.process.Indicator;
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.api.result.IndexSpi;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.RhomeoRuntimeException;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.data.ReferenceManager;
import fr.cenra.rhomeo.core.data.TrackingPointValue;
import fr.cenra.rhomeo.core.data.reference.FloreBassinReference;
import fr.cenra.rhomeo.core.data.reference.FloreBassinReferenceDescription;
import fr.cenra.rhomeo.protocol.TaxonOperator;
import fr.cenra.rhomeo.protocol.p02.list.AbundanceFlore;

import java.beans.IntrospectionException;
import java.util.ArrayList;
import java.util.Collections;
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.stream.Collectors;
import org.apache.sis.util.collection.Cache;

/**
 * Processes to use for {@link I02} and {@link I06}.
 *
 * @author Cédric Briançon (Geomatys)
 */
class I02I06Process extends Process {

    private final Indicator indicator;
    private final Set<Integer> availableSpecies;
    private final ValueType type;
    private final boolean byYear;
    private final TaxonOperator operator;

    /**
     * A cache to put computed tracking point indices, as they're used both for
     * results by year and by tracking point.
     */
    private final Cache<TrackingPoint, Double> tpIndices = new Cache(12, 0, true);

    I02I06Process(final ProcessContext ctx, final Indicator indicator, final Set<Integer> availableSpecies, final ValueType type, final boolean byYear, final TaxonOperator operator){
        super(ctx);
        this.indicator = indicator;
        this.availableSpecies = availableSpecies;
        this.type = type;
        this.byYear = byYear;
        this.operator = operator;
    }

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

    @Override
    protected Set<? extends Index> call() throws Exception {
        return byYear ? algorithmByYear() : algorithmByTrackingPoint();
    }

    /**
     * Generates indexes filtered by year.
     *
     * @return indexes of results, never {@code null}
     */
    private Set<? extends Index> algorithmByYear() {
        updateTitle("Calcul par année");
        final int totalProgress = 2;
        updateProgress(0, totalProgress);
        updateMessage("Tri par année");

        final Set<Index> indices = new HashSet<>();
        final List<TrackingPoint> tPoints = new ArrayList<>(ctx.getTrackingPoints());
        final Map<Integer,List<TrackingPointValue>> valuesForTrackingPointByYear = new HashMap<>();

        // Sort tracking point values by year.
        for (final TrackingPoint tp : tPoints) {
            final double value = getTrackingPointValue(tp);
            final TrackingPointValue tpValue = new TrackingPointValue(tp, value);
            if (valuesForTrackingPointByYear.containsKey(tp.getYear())) {
                valuesForTrackingPointByYear.get(tp.getYear()).add(new TrackingPointValue(tp, value));
            } else {
                final List<TrackingPointValue> newList = new ArrayList<>();
                newList.add(tpValue);
                valuesForTrackingPointByYear.put(tp.getYear(), newList);
            }
        }

        updateMessage("Génération des indices de calcul");
        updateProgress(1, totalProgress);

        // generates index by year, keeping only the median value if several for the same year.
        final IndexSpi indexSpi = (ValueType.Humidité.equals(type)) ? Session.getInstance().getBean(AnnualFloreEngorgement.class) :
                Session.getInstance().getBean(AnnualFloreFertile.class);

        for (final Map.Entry<Integer,List<TrackingPointValue>> valuesForYear : valuesForTrackingPointByYear.entrySet()) {
            final List<TrackingPointValue> values = valuesForYear.getValue();
            if (values.isEmpty()) {
                continue;
            }

            if (values.size() == 1) {
                final TrackingPointValue tempVal = values.get(0);
                indices.add(indexSpi.createIndex(tempVal.getValue(), valuesForYear.getKey()));
            } else {
                final List<Double> allValuesForYear = values.stream().map(TrackingPointValue::getValue).collect(Collectors.toList());
                Collections.sort(allValuesForYear);
                final int size = allValuesForYear.size();
                final double[] vals = new double[size];
                for (int i=0; i<size; i++) {
                    vals[i] = allValuesForYear.get(i);
                }

                final double quantileVal = RhomeoCore.quantile(vals, 0.5f);
                indices.add(indexSpi.createIndex(Math.rint(quantileVal * 100) / 100d, valuesForYear.getKey()));
            }
        }
        updateProgress(2, totalProgress);

        return indices;
    }

    /**
     * Generates indexes filtered by tracking points.
     *
     * @return indexes of results, never {@code null}
     * @throws IntrospectionException
     */
    private Set<? extends Index> algorithmByTrackingPoint() {
        updateTitle("Calcul par points de suivi");

        final Set<Index> indices = new HashSet<>();
        final List<TrackingPoint> tPoints = new ArrayList<>(ctx.getTrackingPoints());

        final int size = tPoints.size();
        int initialStep = 0;
        updateProgress(initialStep, size);
        updateMessage("Tri par points de suivi");

        final IndexSpi indexSpi = (ValueType.Humidité.equals(type)) ? Session.getInstance().getBean(AnnualFloreEngorgementPoint.class) :
                Session.getInstance().getBean(AnnualFloreFertilePoint.class);
        for (final TrackingPoint tp : tPoints) {
            final double value = getTrackingPointValue(tp);
            // Now round the value with only 4 decimal digits.
            indices.add(indexSpi.createIndex(Math.rint(value * 100) / 100d, tp));
            updateProgress(initialStep++, size);
        }

        return indices;
    }

    /**
     * Retrieve previously computed index for given tracking point, or compute
     * it if needed.
     * @param tp Tracking point to get index value for.
     * @return Index value.
     */
    private double getTrackingPointValue(final TrackingPoint tp) {
        try {
            return tpIndices.getOrCreate(tp, () -> computeTrackingPointValue(tp));
        } catch (Exception ex) {
            throw new RhomeoRuntimeException(ex);
        }
    }

    /**
     * Calculate the indicator value for the given tracking point.
     *
     * @param tp Tracking point to consider.
     * @return The value calculated for this tracking point.
     */
    private double computeTrackingPointValue(final TrackingPoint tp) throws IntrospectionException, ReflectiveOperationException {
        final Map<Integer, List<FloreStatement>> stmtsForSpecies = new HashMap<>();
        final List<FloreStatement> stmts = ctx.getData().getSubSet(tp);

        // Get available species name to ensure statements species names are valid and not empty
        // group statements on species names and keep only aquatic species
        stmts.stream()
                .filter(stmt -> availableSpecies.contains(stmt.getCd_nom()) &&
                        !("AQ".equals(stmt.getPhysionomy())))
                .forEach(stmt -> {
                    if (stmtsForSpecies.containsKey(stmt.getCd_nom())) {
                        stmtsForSpecies.get(stmt.getCd_nom()).add(stmt);
                    } else {
                        final List<FloreStatement> newList = new ArrayList<>();
                        newList.add(stmt);
                        stmtsForSpecies.put(stmt.getCd_nom(), newList);
                    }
                });

        // regroup species and maximum of overlap
        final Map<Integer, Double> maxOverlapSpecies = new HashMap<>();
        stmtsForSpecies.entrySet().forEach(entry -> {
            final Optional<Double> max = entry.getValue().stream()
                    .map(I02I06Process::getOverlap).max(Double::compareTo);
            max.ifPresent(m -> maxOverlapSpecies.put(entry.getKey(), m));
        });

        // calculate rs as the sum(max(overlaps))
        // and nrs as sum(humidity * maxOverlap)
        double nrs = 0;
        double rs = 0;

        for (final Map.Entry<Integer,Double> maxForSpecies : maxOverlapSpecies.entrySet()) {
            if (maxForSpecies.getValue() > 0) {
                final Integer humidOrNutriVal = getHumidityOrNutrimentForSpecies(maxForSpecies.getKey());
                if (humidOrNutriVal != null) {
                    rs += maxForSpecies.getValue();
                    nrs += humidOrNutriVal * maxForSpecies.getValue();
                }
            }
        }

        return nrs/rs;
    }

    /**
     * Get the overlap value for the given statement.
     *
     * @param stmt
     * @return
     */
    static double getOverlap(final FloreStatement stmt) {
        final Optional<AbundanceFlore> opt = RhomeoCore.getByCode(AbundanceFlore.class, stmt.getAbundance());
        if (opt.isPresent())
            return opt.get().getDoubleValue();

        return 0;
    }

    /**
     *
     * @param speciesName
     * @param type Indicator type
     * @return The humidity value for this species for I02, or the nutriment value for I06, may be null if not found.
     * @throws IntrospectionException
     */
    private Integer getHumidityOrNutrimentForSpecies(final Integer speciesName) throws IntrospectionException, ReflectiveOperationException {
        final FloreBassinReference ref = operator.getFloreReference(speciesName);
        if (ref != null)
            return type.extractor.apply(ref);
        return null;
    }

    static List<FloreBassinReference> getFloreBassinValues() throws IntrospectionException {
        final ReferenceManager<FloreBassinReference> referenceManager = ReferenceManager.getOrCreate(new FloreBassinReferenceDescription());
        final DataContext dc = Session.getInstance().getDataContext();
        final Version versionSel = dc.getReferences().get(FloreBassinReference.class);
        return referenceManager.getValues(versionSel != null ? versionSel : referenceManager.getInstalledVersions().iterator().next());
    }

    /**
     * Find all available species from the references list.
     *
     * @return
     * @throws IntrospectionException
     */
    static Set<Integer> getAvailableSpecies() throws IntrospectionException {
        final List<FloreBassinReference> references = getFloreBassinValues();
        return references.stream().map(FloreBassinReference::getCd_nom).collect(Collectors.toSet());
    }
}
