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

import fr.cenra.rhomeo.api.annotations.RefersTo;
import fr.cenra.rhomeo.api.data.Statement;
import fr.cenra.rhomeo.api.process.ProcessContext;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.data.reference.FloreBassinReference;
import fr.cenra.rhomeo.core.data.reference.OrthopteraIndicatorReference;
import fr.cenra.rhomeo.core.data.reference.Taxref;
import fr.cenra.rhomeo.core.util.RelationResolver;
import java.beans.IntrospectionException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.stream.Stream;
import javafx.beans.InvalidationListener;
import org.apache.sis.util.ArgumentChecks;

/**
 *
 * @author Alexis Manin (Geomatys)
 * @param <T> Type of statements managed by current operator.
 */
public class TaxonOperator<T extends Statement> {

    protected static final WeakHashMap<ProcessContext, TaxonOperator> CACHED_OPERATORS = new WeakHashMap<>(2);

    protected final ProcessContext processContext;
    private List<T> processData;

    protected Map<Integer, Taxref> cdNomTaxrefMapping;
    protected Map<Integer, Integer> cdNomCdRefMapping;
    protected Map<String, Set<Integer>> odonataByCounty;
    protected Map<Integer, FloreBassinReference> floreRefByCdNom;
    protected Map<Integer, OrthopteraIndicatorReference> orthoRefByCdNom;

    protected TaxonOperator(ProcessContext processContext) {
        ArgumentChecks.ensureNonNull("Process context", processContext);
        this.processContext = processContext;
    }

    public ProcessContext getProcessContext() {
        return processContext;
    }

    /**
     * Return an ordered (by date) list of statements present in this operator's
     * process context.
     *
     * Returned list is a copy of actual dataset, no a view.
     *
     * @return List of data to base computing on.
     */
    public synchronized List<T> getProcessData() throws Exception {
        if (processData == null) {
            processData = new ArrayList<>(processContext.getData().getItems());
            processData.sort((o1, o2) -> o1.getDate().compareTo(o2.getDate()));
        }

        return processData;
    }

    /**
     * Return the species identifier (cd_ref) associated to given taxon identifier (cd_nom).
     * @param cdNom The name to get a species identifier for.
     * @return A species identifier. Can be null.
     * @throws ReflectiveOperationException
     * @throws IntrospectionException
     */
    @RefersTo(type = Taxref.class, property = "cd_nom")
    public synchronized Taxref getTaxref(final int cdNom) throws ReflectiveOperationException, IntrospectionException {
        if (cdNomTaxrefMapping == null) {
            final RefersTo reference = TaxonOperator.class.getMethod("getTaxref", Integer.TYPE).getAnnotation(RefersTo.class);
            Set<Taxref> refs = Session.getInstance().getBean(RelationResolver.class).getTargetObjects(reference);
            cdNomTaxrefMapping = new HashMap<>(refs.size());
            for (final Taxref ref : refs) {
                cdNomTaxrefMapping.put(ref.getCd_nom(), ref);
            }
        }

        return cdNomTaxrefMapping.get(cdNom);
    }

    /**
     * Return the species identifier (cd_ref) associated to given taxon identifier (cd_nom).
     * @param cdNom The name to get a species identifier for.
     * @return A species identifier. Can be null.
     * @throws ReflectiveOperationException
     * @throws IntrospectionException
     */
    @RefersTo(type = Taxref.class, property = "cd_ref")
    public synchronized Integer getCdRef(final int cdNom) throws ReflectiveOperationException, IntrospectionException {
        if (cdNomCdRefMapping == null) {
            final RefersTo reference = TaxonOperator.class.getMethod("getCdRef", Integer.TYPE).getAnnotation(RefersTo.class);
            Set<Taxref> refs = Session.getInstance().getBean(RelationResolver.class).getTargetObjects(reference);
            cdNomCdRefMapping = new HashMap<>(refs.size());
            for (final Taxref ref : refs) {
                cdNomCdRefMapping.put(ref.getCd_nom(), ref.getCd_ref());
            }
        }

        return cdNomCdRefMapping.get(cdNom);
    }

    @RefersTo(type = FloreBassinReference.class, property = "cd_nom")
    public synchronized FloreBassinReference getFloreReference(final int cdNom) throws ReflectiveOperationException, IntrospectionException {
        if (floreRefByCdNom == null) {
            final RefersTo reference = TaxonOperator.class.getMethod("getFloreReference", Integer.TYPE).getAnnotation(RefersTo.class);
            Set<FloreBassinReference> refs = Session.getInstance().getBean(RelationResolver.class).getTargetObjects(reference);
            floreRefByCdNom = new HashMap<>(refs.size());
            for (final FloreBassinReference ref : refs) {
                floreRefByCdNom.put(ref.getCd_nom(), ref);
            }
        }

        return floreRefByCdNom.get(cdNom);
    }

    private synchronized Map<Integer, OrthopteraIndicatorReference> getOrthopteraByCdRef() throws ReflectiveOperationException, IntrospectionException {
        if (orthoRefByCdNom == null) {
            final RefersTo reference = TaxonOperator.class.getMethod("getOrthopteraReference", Integer.TYPE).getAnnotation(RefersTo.class);
            Set<OrthopteraIndicatorReference> refs = Session.getInstance().getBean(RelationResolver.class).getTargetObjects(reference);
            orthoRefByCdNom = new HashMap<>(refs.size());
            for (final OrthopteraIndicatorReference ref : refs) {
                orthoRefByCdNom.put(ref.getCd_ref(), ref);
            }
        }

        return orthoRefByCdNom;
    }

    @RefersTo(type = OrthopteraIndicatorReference.class, property = "cd_ref")
    public synchronized OrthopteraIndicatorReference getOrthopteraReference(final int cdRef) throws ReflectiveOperationException, IntrospectionException {
        return getOrthopteraByCdRef().get(cdRef);
    }

    /**
     * Return all known orthoptera references.
     * 
     * @return A stream of references available for the zone. Never null, but
     * can be empty.
     * @throws ReflectiveOperationException
     * @throws IntrospectionException
     */
    public Stream<OrthopteraIndicatorReference> getOrthopteraReferences() throws ReflectiveOperationException, IntrospectionException {
        return getOrthopteraByCdRef().values().stream();
    }

    /**
     * Find or initialize an operator which will work with given processing context.
     * @param ctx The context to use this operator with.
     * @return An operator valid for given context, either new or previously initiated.
     */
    public static TaxonOperator getOrCreate(final ProcessContext ctx) {
        synchronized (CACHED_OPERATORS) {
            return CACHED_OPERATORS.computeIfAbsent(ctx, TaxonOperator::createOperator);
        }
    }

    private static TaxonOperator createOperator(final ProcessContext ctx) {
        // if source dataset or user selection change, we must create a new taxon operator to clear cached data.
        final InvalidationListener listener = obs -> CACHED_OPERATORS.remove(ctx);
        ctx.getSourceDataset().getItems().addListener(listener);
        ctx.filterProperty().addListener(listener);

        return new TaxonOperator(ctx);
    }
}
