/**
 * 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 fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.api.process.ProcessContext;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.WorkflowStep;
import java.beans.IntrospectionException;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.logging.Level;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.MapChangeListener;
import javafx.collections.SetChangeListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 *
 * @author Alexis Manin (Geomatys)
 */
@Component
public class ProcessManager implements StepStateManager {

    private static final String INDICATOR_FILE = "indicators.txt";
    private static final String FILTER_FILE = "filter";
    private static final String USER_DATA_FILE = "userData";

    @Autowired
    private Session session;

    @Autowired
    private StateManager manager;

    private Path target;

    private ProcessContext ctx;

    private StateManager.Trigger indicatorTrigger;
    private StateManager.Trigger filterTrigger;
    private StateManager.Trigger userDataTrigger;

    private final SetChangeListener<Indicator> indicatorListener = this::indicatorsChanged;
    private final ChangeListener<Predicate> filterListener = this::filterChanged;
    private final MapChangeListener userDataListener = this::userDataChanged;

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

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

    @Override
    public void startSaving() {
        if (ctx != null)
            stopSaving();

        ctx = session.getProcessContext();

        // Submit backup tasks.
        filterTrigger = manager.prepareTask(this::writeFilter, "Process context filter");
        indicatorTrigger = manager.prepareTask(this::writeIndicators, "Process context indicators");
        userDataTrigger = manager.prepareTask(this::writeUserData, "Process context indicators");

        // Force writing process context in current state.
        filterTrigger.arm();
        indicatorTrigger.arm();
        userDataTrigger.arm();

        // listens on process context change.
        ctx.filterProperty().addListener(filterListener);
        ctx.getIndicators().addListener(indicatorListener);
        ctx.getDataCtx().getUserData().addListener(userDataListener);
    }

    @Override
    public void stopSaving() {
        if (ctx != null) {
            ctx.filterProperty().removeListener(filterListener);
            filterTrigger.close();
            filterTrigger = null;

            ctx.getIndicators().removeListener(indicatorListener);
            indicatorTrigger.close();
            indicatorTrigger = null;

            ctx.getDataCtx().getUserData().removeListener(userDataListener);
            userDataTrigger.close();
            userDataTrigger = null;

            ctx = null;
        }
    }

    @Override
    public boolean restoreStep() {
        final ProcessContext ctx = readProcessContext();
        if (ctx != null) {
            session.setProcessContext(ctx);
            return true;
        }

        return false;
    }

    ProcessContext readProcessContext() {
        // Cannot restore process context if no data has been prepared before hand.
        if (target == null || session.getDataContext() == null || session.getDataset() == null)
            return null;

        final Predicate filter;
        final Collection<Indicator> indics;
        final Map userData;
        try {
            filter = readFilter();
            indics = readIndicators();
            userData = readUserData();
        } catch (Exception e) {
            StateManager.LOGGER.log(Level.WARNING, "Cannot restore process context.", e);
            return null;
        }

        final boolean noIndic = indics == null || indics.isEmpty();
        final boolean noUserData = userData == null || userData.isEmpty();
        if (filter == null && noIndic && noUserData) {
            return null;
        }

        final ProcessContext context = new ProcessContext(session.getDataset(), session.getDataContext());
        if (filter != null) {
            context.setFilter(filter);
        }
        if (!noIndic) {
            context.getIndicators().addAll(indics);
        }
        if (!noUserData) {
            context.getDataCtx().getUserData().putAll(userData);
        }
        return context;
    }

    /**
     * @return Tracking points saved in backup file.
     */
    private Predicate readFilter() throws IOException, ClassNotFoundException {
        final Path file;
        if (target == null || !Files.isReadable((file = target.resolve(FILTER_FILE))))
            return null;

        try (final InputStream source = Files.newInputStream(file);
                final ObjectInputStream input = new ObjectInputStream(source)) {
            final Object read = input.readObject();
            if (read instanceof Predicate)
                return (Predicate) read;
        }

        return null;
    }

    private Collection<Indicator> readIndicators() throws IOException {
        final Path file;
        if (target == null || !Files.isReadable((file = target.resolve(INDICATOR_FILE))))
            return null;

        final HashSet<Indicator> indicators = new HashSet<>();
        try (final BufferedReader r = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
            r.lines().forEach(line -> {
                final Optional<Indicator> optIndicator = session.getIndicators().stream()
                        .filter(indicator -> line.equals(indicator.getName()))
                        .findFirst();
                optIndicator.ifPresent(indicators::add);
            });
        }

        return indicators;
    }

    private Map readUserData() throws IOException, ClassNotFoundException {
        final Path file;
        if (target == null || !Files.isReadable((file = target.resolve(USER_DATA_FILE))))
            return null;

        try (final InputStream source = Files.newInputStream(file);
                final ObjectInputStream input = new ObjectInputStream(source)) {
            final Object read = input.readObject();
            if (read instanceof Map)
                return (Map) read;
        }

        return null;
    }

    private boolean writeFilter() throws IOException, IntrospectionException {
        if (ctx.getFilter() == null)
            return false;

        final Path tmpFile = Files.createTempFile(target, FILTER_FILE, ".tmp");
        try (final OutputStream destination = Files.newOutputStream(tmpFile);
                final ObjectOutputStream output = new ObjectOutputStream(destination)) {
            output.writeObject(ctx.getFilter());
        }
        Files.move(tmpFile, target.resolve(FILTER_FILE), StandardCopyOption.REPLACE_EXISTING);
        return true;
    }

    private boolean writeIndicators() throws IOException {
        if (ctx.getIndicators().isEmpty())
            return false;

        final Path tmpFile = Files.createTempFile(target, INDICATOR_FILE, ".tmp");
        try (final BufferedWriter w = Files.newBufferedWriter(tmpFile, StandardCharsets.UTF_8)) {
            for (final Indicator i : ctx.getIndicators()) {
                w.write(i.getName());
                w.newLine();
            }
        }
        Files.move(tmpFile, target.resolve(INDICATOR_FILE), StandardCopyOption.REPLACE_EXISTING);
        return true;
    }

    private boolean writeUserData() throws IOException {
        // Defensive copy. Moreover, Serialization does not work with observable map.
        final Map<String, Object> userData = new HashMap(ctx.getDataCtx().getUserData());
        if (userData.isEmpty())
            return false;

        final Path tmpFile = Files.createTempFile(target, USER_DATA_FILE, ".tmp");
        try (final OutputStream destination = Files.newOutputStream(tmpFile);
                final ObjectOutputStream output = new ObjectOutputStream(destination)) {
            output.writeObject(userData);
        }

        Files.move(tmpFile, target.resolve(USER_DATA_FILE), StandardCopyOption.REPLACE_EXISTING);
        return true;
    }

    private void filterChanged(final ObservableValue<? extends Predicate> obs, Predicate oldValue, Predicate newValue) {
        filterTrigger.arm();
    }

    private void indicatorsChanged(final SetChangeListener.Change c) {
        indicatorTrigger.arm();
    }

    private void userDataChanged(final MapChangeListener.Change c) {
        userDataTrigger.arm();
    }
}
