/**
 * 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.core.RhomeoCore;
import fr.cenra.rhomeo.core.RhomeoRuntimeException;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.WorkflowStep;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ObservableValue;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.nio.IOUtilities;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

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

    static final Logger LOGGER = Logging.getLogger("fr.cenra.rhomeo.core.state");

    @Autowired
    private Session session;

    final ExecutorService threadPool;

    private final ReadOnlyObjectWrapper<Path> folder;

    private final Map<WorkflowStep, StepStateManager> stepManagers;

    StateManager() throws IOException {
        Files.createDirectories(RhomeoCore.RESTORATION_FOLDER);
        threadPool = Executors.newCachedThreadPool();

        // Register state savers.
        this.stepManagers = new HashMap<>(WorkflowStep.values().length);

        folder = new ReadOnlyObjectWrapper<>(this, "state folder");
        folder.addListener(this::pathChanged);
    }

    @PostConstruct
    private void init() {
        session.workflowStepProperty().addListener(this::stepChanged);
    }

    @Autowired
    private void initManagers(final Collection<StepStateManager> stepManagers) {
        for (final StepStateManager ssm : stepManagers) {
            this.stepManagers.put(ssm.getStep(), ssm);
        }
    }

    @PreDestroy
    private void shutdown() {
        threadPool.shutdown();
        try {
            threadPool.awaitTermination(10, TimeUnit.SECONDS);
        } catch (InterruptedException ex) {
            threadPool.shutdownNow();
        }
    }

    /**
     * Try to restore applicative state from file-system backup.
     *
     * /!\ You MUST NOT call this method once user has started seizure. Once in
     * edition mode, the manager performs backups as daemon, so it cannot restore
     * previous data.
     *
     * @return True if a backup have been found and loaded. False otherwise.
     * @throws IllegalStateException If a backup is running.
     * @throws IOException If an error happens while analysing backup files.
     */
    public synchronized boolean restoreState() throws IOException {
        if (session.getWorkflowStep() != null) {
            throw new IllegalStateException("Cannot restore dataset while backup is running !");
        }

        final List<Path> dirs;
        if (folder.get() == null) {
            dirs = Files.list(RhomeoCore.RESTORATION_FOLDER)
                    .filter(input -> {
                        if (!Files.isDirectory(input)) {
                            return false;
                        }

                        try {
                            return Long.parseLong(input.getFileName().toString()) > 0;
                        } catch (NumberFormatException e) {
                            return false;
                        }
                    }).collect(Collectors.toList());

            dirs.sort((o1, o2) -> Long.compare(Long.parseLong(o1.getFileName().toString()), Long.parseLong(o2.getFileName().toString())));
        } else {
            dirs = Collections.singletonList(folder.get());
        }

        int stepCount = 0;
        final Iterator<Path> it = dirs.iterator();
        while (it.hasNext() && stepCount < 1) {
            folder.set(it.next());
            while (restoreStep(WorkflowStep.values()[stepCount])) {
                session.requestWorkflowStep(WorkflowStep.values()[stepCount++]);
            }
        }

        /*
         * We remove last scanned folder from the previously built list, because
         * we'll use it as backup folder. For the remaining ones, we'll delete
         * them. They've got no reason to remain if they don't serve our backup
         * purpose.
         */
        if (!dirs.isEmpty()) {
            it.remove();
            for (final Path p : dirs) {
                submitDeletion(p);
            }
        }

        return stepCount > 0;
    }

    private synchronized void resetContext() {
        try {
            final Path newPath = RhomeoCore.RESTORATION_FOLDER.resolve(Long.toString(System.currentTimeMillis()));
            Files.createDirectories(newPath);
            folder.set(newPath);
        } catch (IOException ex) {
            LOGGER.log(Level.SEVERE, "Impossible to set a new restoration context.", ex);
            folder.set(null);
        }
    }

    public ReadOnlyObjectProperty<Path> folderProperty() {
        return folder.getReadOnlyProperty();
    }

    public Path getFolder() {
        return folder.get();
    }

    /*
     * STATE OPERATIONS
     */

    private boolean restoreStep(WorkflowStep target) {
        final StepStateManager ssm = stepManagers.get(target);
        if (ssm != null) {
            return ssm.restoreStep();
        }

        return false;
    }

    private void startSaving(final WorkflowStep target) {
        final StepStateManager ssm = stepManagers.get(target);
        if (ssm != null) {
            ssm.startSaving();
        }
    }

    private void stopSaving(final WorkflowStep target) {
        final StepStateManager ssm = stepManagers.get(target);
        if (ssm != null) {
            ssm.stopSaving();
        }
    }

    /*
     * PROPERTY LISTENERS
     */

    private void stepChanged(final ObservableValue<? extends WorkflowStep> obs, WorkflowStep oldStep, WorkflowStep newStep) {
        if (oldStep != null) {
            stopSaving(oldStep);
        }

        if (newStep != null) {
            if (folder.get() == null) {
                resetContext(); // No folder parameterized yet. Create a new one.
            }
            startSaving(newStep);
        } else {
            resetContext(); // Seizure properly stopped. We clear backups.
        }
    }

    /**
     * When changing backup folder, we notify each step manager with a new sub-directory.
     * Once done, we delete the old backup.
     * @param obs
     * @param oldPath
     * @param newPath
     */
    private void pathChanged(final ObservableValue<? extends Path> obs, final Path oldPath, final Path newPath) {
        try {
            if (newPath != null) {
                for (final Map.Entry<WorkflowStep, StepStateManager> entry : stepManagers.entrySet()) {
                    final Path subDir = newPath.resolve(entry.getKey().name());
                    try {
                        Files.createDirectories(subDir);
                    } catch (IOException ex) {
                        throw new RhomeoRuntimeException("Cannot create a backup directory", ex);
                    }
                    entry.getValue().setBackupFolder(subDir);
                }
            } else {
                for (final StepStateManager ssm : stepManagers.values()) {
                    ssm.setBackupFolder(null);
                }
            }
        } finally {
            if (oldPath != null) {
                submitDeletion(oldPath);
            }
        }
    }

    private void submitDeletion(final Path dir) {
        threadPool.submit(() -> {
            Thread.currentThread().setName("State deletion");
            try {
                IOUtilities.deleteRecursively(dir);
            } catch (IOException ex) {
                LOGGER.log(Level.WARNING, "Cannot remove directory ".concat(dir.toString()), ex);
            }
        });
    }

    /**
     * Setup a trigger which controls the call to a given task. The aim is to
     * provide a simple way to launch a process with the following constraints :
     * - One unique instance of the process must be running at any time.
     * - Whatever the number of time the process is queried between two calls,
     * it will be triggered for execution only once.
     * @param task The task to put as triggerable process.
     * @return A trigger to allow calling input process anytime needed.
     */
    Trigger prepareTask(final Callable task, final String name) {
        return new Trigger(task, name);
    }

    /**
     * A trigger which controls the call to a given task. The aim is to provide
     * a simple way to launch a process with the following constraints :
     * - One unique instance of the process must be running at any time.
     * - Whatever the number of time the process is queried between two calls,
     * it will be triggered for execution only once.
     */
    class Trigger implements AutoCloseable {

        final AtomicBoolean flag;
        final Future taskState;

        private Trigger(final Callable task) {
            this(task, null);
        }

        private Trigger(final Callable task, final String name) {
            flag = new AtomicBoolean(false);
            taskState = threadPool.submit(() -> {
                final Thread thread = Thread.currentThread();
                if (name != null && !name.isEmpty())
                    thread.setName(name);
                while (!thread.isInterrupted()) {
                    if (flag.getAndSet(false)) {
                        try {
                            task.call();
                        } catch (Exception e) {
                            LOGGER.log(Level.WARNING, "A state management task has failed.", e);
                        }
                    } else
                        synchronized (flag) {
                            flag.wait();
                        }
                }
                return true;
            });
        }

        /**
         * Ask for a call of the associated process.
         */
        public void arm() {
            flag.set(true);
            synchronized (flag) {
                flag.notify();
            }
        }

        /**
         * Definitely cancel associated process.
         */
        public void close() {
            taskState.cancel(true);
        }
    }
}
