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

import fr.cenra.rhomeo.api.Version;
import fr.cenra.rhomeo.api.data.DataContext;
import fr.cenra.rhomeo.api.data.Dataset;
import fr.cenra.rhomeo.api.data.Reference;
import fr.cenra.rhomeo.api.data.ReferenceDescription;
import fr.cenra.rhomeo.api.data.TrackingPoint;
import fr.cenra.rhomeo.api.data.Warning;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.api.result.AdditionalValue;
import fr.cenra.rhomeo.api.result.AdditionalValueMapper;
import fr.cenra.rhomeo.api.result.Index;
import fr.cenra.rhomeo.api.result.TrackingPointIndex;
import fr.cenra.rhomeo.core.CSVEncoder;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.util.ExportUtils;
import fr.cenra.rhomeo.core.util.SerializableDataContext;
import java.beans.IntrospectionException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.util.ArgumentChecks;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 *
 * @author Alexis Manin (Geomatys)
 */
@Component
public class ResultWriter {

    static final String ADDITIONAL_RESULT_FILE_NAME = "additionalResults.csv";
    static final String RESULT_FILE_NAME = "results.csv";

    @Autowired
    Session session;

    @Autowired(required = false)
    Set<AdditionalValueMapper> avMappers;

    public WritingContext prepareWriting(final Path root) throws IOException {
        return new WritingContext(root, session, avMappers);
    }

    /**
     * An object independent from {@link Session} variations, designed to write
     * a snapshot of results stored in session at a precise time (creation of
     * an instance of this class).
     */
    public static class WritingContext {
        final Path root;
        final DataContext dataContext;
        final Map<ReferenceDescription, Version> referenceVersions;
        final Set<Indicator> indicators;
        final Set<TrackingPoint> trackingPoints;
        final List<Index> indices;
        final Validator validator;
        final Dataset ds;

        final Set<AdditionalValue> additional;
        final Set<AdditionalValueMapper> mappers;

        private WritingContext(final Path root, final Session session, final Set<AdditionalValueMapper> avMappers) throws IOException {
            ArgumentChecks.ensureNonNull("Root path", root);
            ArgumentChecks.ensureNonNull("Session", session);
            if (!Files.isDirectory(root)) {
                Files.createDirectories(root);
            }
            this.root = root;

            final DataContext dc = session.getDataContext();
            ArgumentChecks.ensureNonNull("Data context", dc);
            ArgumentChecks.ensureNonNull("Site", dc.getSite());
            ArgumentChecks.ensureNonNull("Protocol", dc.getProtocol());
            dataContext = dc;

            referenceVersions = new HashMap<>();
            if (dc.getReferences() != null) {
                for (final Map.Entry<Class<Reference>, Version> entry : dc.getReferences().entrySet()) {
                    for (final ReferenceDescription refDescriptor : dc.getProtocol().getReferenceTypes()) {
                        if (refDescriptor.getReferenceType().equals(entry.getKey()))
                            referenceVersions.put(refDescriptor, entry.getValue());
                    }
                }
            }

            indicators = new HashSet<>(session.getProcessContext().getIndicators());

            trackingPoints = new HashSet<>(session.getProcessContext().getTrackingPoints());

            indices = new ArrayList<>(session.getResults());
            // #40 : Sort results.
            indices.sort(new IndexComparator());

            validator = session.getBean(Validator.class);

            ds = session.getDataset();

            mappers = avMappers;
            if (mappers == null || mappers.isEmpty()) // If no writer is available, there's no need to keep a reference of additional values.
                additional = Collections.EMPTY_SET;
            else additional = session.getAdditionalValues();
        }

        public WritingContext writeSite() throws IOException, DataStoreException {
            ExportUtils.writeGeoJson(dataContext.getSite(), root);
            return this;
        }

        public WritingContext writeMetadataJSON() throws IOException {
            ExportUtils.writeMetadata(SerializableDataContext.fromDataContext(dataContext), root);
            return this;
        }

        public WritingContext writeMetadataText() throws IOException {
            final Set<ConstraintViolation<Dataset>> datasetErrors = validator.validate(ds);
            datasetErrors.removeIf(cv -> cv.getConstraintDescriptor().getPayload().contains(Warning.class));

            ExportUtils.writeMetadataText(root.resolve("metadata.txt"), dataContext.getSite(), dataContext.getProtocol(), referenceVersions, indicators, dataContext.getUserData(), datasetErrors);

            return this;
        }

        /**
         * Write all results in CSV files (results.csv for main ones,
         * additionalResults.csv for others).
         *
         * @return This context.
         * @throws IOException
         * @throws IntrospectionException
         */
        public WritingContext writeResults() throws IOException, IntrospectionException {
            return writeResults(null);
        }

        /**
         * Write results accepted by given predicate in CSV files (results.csv
         * for main ones, additionalResults.csv for others).
         *
         * @param filter A predicate to filter results.
         * @return This context.
         * @throws IOException
         * @throws IntrospectionException
         */
        public WritingContext writeResults(final Predicate<Index> filter) throws IOException, IntrospectionException {
            final ArrayList<SimpleLocationIndex> mainResults = new ArrayList();
            final ArrayList<SimpleLocationIndex> additionalResults = new ArrayList();

            boolean primaryHasLocation = false;
            boolean additionalHasLocation = false;
            for (final Index i : indices) {
                if (filter != null && !filter.test(i))
                    continue;

                boolean isPrimary = i.getSpi().isPrimary();
                if (isPrimary) {
                    mainResults.add(new SimpleLocationIndex(i));
                    if (i instanceof TrackingPointIndex)
                        primaryHasLocation = true;
                } else {
                    additionalResults.add(new SimpleLocationIndex(i));
                    if (i instanceof TrackingPointIndex)
                        additionalHasLocation = true;
                }
            }

            // Write main results
            if (!mainResults.isEmpty()) {
                final Path mainResultPath = root.resolve(RESULT_FILE_NAME);
                Files.deleteIfExists(mainResultPath);
                new CSVEncoder(
                        mainResultPath,
                        primaryHasLocation ? SimpleLocationIndex.class : SimpleYearIndex.class,
                        StandardCharsets.UTF_8
                ).encode(mainResults, true);
            }

            // Write additional results.
            if (!additionalResults.isEmpty()) {
                final Path additionalResultPath = root.resolve(ADDITIONAL_RESULT_FILE_NAME);
                Files.deleteIfExists(additionalResultPath);
                new CSVEncoder(
                        additionalResultPath,
                        additionalHasLocation ? SimpleLocationIndex.class : SimpleYearIndex.class,
                        StandardCharsets.UTF_8
                ).encode(additionalResults, true);
            }

            return this;
        }

        public void writeAdditionalValues() throws IOException {
            if (mappers == null || mappers.isEmpty())
                return;
            for (final AdditionalValueMapper mapper : mappers)
                if (mapper.writeValues(root, additional))
                    return;
        }
    }

    /**
     * Sort index by type, year, eventually tracking point, and finally by value.
     */
    private static class IndexComparator implements Comparator<Index> {

        @Override
        public int compare(Index o1, Index o2) {

            int comparison = o1.getSpi().compareTo(o2.getSpi());
            if (comparison == 0) {
                comparison = o1.getYear() - o2.getYear();
                if (comparison == 0) {
                    if (o1 instanceof TrackingPointIndex) {
                        if (o2 instanceof TrackingPointIndex) {
                            comparison = ((TrackingPointIndex) o1).getPoint().getName().compareTo(((TrackingPointIndex) o2).getPoint().getName());
                        } else {
                            comparison = 1;
                        }
                    } else if (o2 instanceof TrackingPointIndex) {
                        comparison = -1;
                    }
                }
            }

            return comparison == 0 ? Double.compare(o1.getValue().doubleValue(), o2.getValue().doubleValue()) : comparison;
        }
    }
}
