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

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import fr.cenra.rhomeo.api.data.TrackingPoint;
import fr.cenra.rhomeo.api.result.AdditionalValueMapper;
import fr.cenra.rhomeo.api.result.Index;
import fr.cenra.rhomeo.api.result.IndexSpi;
import fr.cenra.rhomeo.api.result.TrackingPointIndex;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.WorkflowStep;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.logging.Level;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 *
 * Special case : The results are immutable, so we just save data once, when the
 * manager is activated.
 *
 * @author Alexis Manin (Geomatys)
 */
@Component
public class ResultManager implements StepStateManager {

    private static final String RESULT_FILE = "results.json";
    private static final String ADDITIONAL_VALUE_DIR = "additional";

    @Autowired
    private Session session;

    @Autowired
    private StateManager manager;

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

    private Path target;

    private Future writeState;

    private ObjectMapper mapper;

    /**
     * Initialises Jackson serialisation feature in order to allow index reading
     * and writing.
     *
     * @param spis SPIs registered for index building.
     */
    @Autowired
    void init(final List<IndexSpi> spis) {
        final HashMap<String, IndexSpi> spiMap = new HashMap<>(spis.size());
        for (final IndexSpi spi : spis) {
            spiMap.put(spi.getName(), spi);
        }

        final SimpleModule mod = new SimpleModule("Index SPI");
        mod.addSerializer(new JsonSerializer<Index>() {
            @Override
            public Class<Index> handledType() {
                return Index.class;
            }

            @Override
            public void serialize(Index value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
                if (value.getSpi() == null)
                    return;

                jgen.writeStartObject();
                if (value instanceof TrackingPointIndex) {
                    jgen.writeStringField("location", ((TrackingPointIndex)value).getPoint().getName());
                }
                jgen.writeNumberField("year", value.getYear());
                jgen.writeStringField("spi", value.getSpi().getName());
                jgen.writeObjectField("value", value.getValue());
                jgen.writeEndObject();
            }
        });

        mod.addDeserializer(Index.class, new JsonDeserializer<Index>() {
            @Override
            public Index deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
                jp.nextToken();

                final String location;
                if ("location".equals(jp.getCurrentName())) {
                    location = jp.nextTextValue();
                    jp.nextToken();
                } else {
                    location = null;
                }

                final int year = jp.nextIntValue(0);
                jp.nextToken();

                final String strSpi = jp.nextTextValue();
                jp.nextToken();

                jp.nextToken();
                final Number value = jp.getNumberValue();
                jp.nextToken();

                IndexSpi spi = spiMap.get(strSpi);
                if (spi != null) {
                    Object entity = null;
                    if (location != null) {
                        for (final TrackingPoint tp : (List<TrackingPoint>) session.getDataset().getTrackingPoints()) {
                            if (tp.getYear() == year && location.equals(tp.getName())) {
                                entity = tp;
                                break;
                            }
                        }
                    } else {
                        entity = year;
                    }
                    return spi.createIndex(value, entity);
                }

                return null;
            }
        });

        mapper = new ObjectMapper();
        mapper.registerModule(mod);
    }

    @Override
    public void setBackupFolder(Path toWriteInto) {
        target = toWriteInto;
    }

    @Override
    public WorkflowStep getStep() {
        return WorkflowStep.RESULT;
    }

    @Override
    public void startSaving() {
        if (writeState != null && !writeState.isDone()) {
            writeState.cancel(true);
        }

        writeState = manager.threadPool.submit(this::writeResults);
    }

    @Override
    public void stopSaving() {
        // Nothing to do.
    }

    @Override
    public boolean restoreStep() {
        final Path file;
        if (target == null || !Files.isReadable(file = target.resolve(RESULT_FILE))) {
            return false;
        }

        try (final BufferedReader r = Files.newBufferedReader(file)) {
            Index[] read = mapper.readValue(r, Index[].class);
            session.setResults(new HashSet<>(Arrays.asList(read)));
        } catch (Exception e) {
            StateManager.LOGGER.log(Level.WARNING, "Cannot restore results.");
            return false;
        }

        if (avMappers != null && !avMappers.isEmpty()) {
            final Path avDir = target.resolve(ADDITIONAL_VALUE_DIR);
            try {
                if (Files.exists(avDir)) {
                    Set read;
                    for (final AdditionalValueMapper mapper : avMappers) {
                        read = mapper.readValues(avDir);
                        if (!read.isEmpty()) {
                            session.getAdditionalValues().addAll(read);
                            break;
                        }
                    }
                }
            } catch (Exception e) {
                StateManager.LOGGER.log(Level.WARNING, "Cannot restore additional results.");
            }
        }

        return true;
    }

    boolean writeResults() throws IOException {
        final Set<Index> results = session.getResults();
        if (results == null || results.isEmpty()) {
            return false;
        }

        final Path tmpFile = Files.createTempFile(target, "results", ".tmp");
        try (final BufferedWriter w = Files.newBufferedWriter(tmpFile, StandardCharsets.UTF_8)) {
            mapper.writeValue(w, results);
        }

        Files.move(tmpFile, target.resolve(RESULT_FILE), StandardCopyOption.REPLACE_EXISTING);

        if (avMappers != null && ! avMappers.isEmpty() && !session.getAdditionalValues().isEmpty()) {
            final Path tmpDir = Files.createTempDirectory(target, ADDITIONAL_VALUE_DIR);
            for (final AdditionalValueMapper avMapper : avMappers) {
                if (avMapper.writeValues(tmpDir, session.getAdditionalValues())) {
                    Files.move(tmpDir, target.resolve(ADDITIONAL_VALUE_DIR), StandardCopyOption.REPLACE_EXISTING);
                    break;
                }
            }
        }

        return true;
    }
}
