/*******************************************************************************
 * Copyright (c) 2016, 2017 Sebastian Stenzel and others.
 * All rights reserved.
 * This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
 *
 * Contributors:
 *     Sebastian Stenzel - initial API and implementation
 *******************************************************************************/
package org.cryptomator.common.settings;

import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.common.base.Suppliers;
import org.cryptomator.common.Environment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.stream.Stream;

@Singleton
public class SettingsProvider implements Supplier<Settings> {

	private static final ObjectMapper JSON = new ObjectMapper().setDefaultLeniency(true).registerModule(new JavaTimeModule());
	private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class);
	private static final long SAVE_DELAY_MS = 1000;

	private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>();
	private final Supplier<Settings> settings = Suppliers.memoize(this::load);
	private final Environment env;
	private final ScheduledExecutorService scheduler;

	@Inject
	public SettingsProvider(Environment env, ScheduledExecutorService scheduler) {
		this.env = env;
		this.scheduler = scheduler;
	}

	@Override
	public Settings get() {
		return settings.get();
	}

	private Settings load() {
		Settings settings = env.getSettingsPath() //
				.flatMap(this::tryLoad) //
				.findFirst() //
				.orElseGet(() -> Settings.create(this, env));
		return settings;
	}

	private Stream<Settings> tryLoad(Path path) {
		LOG.debug("Attempting to load settings from {}", path);
		try (InputStream in = Files.newInputStream(path, StandardOpenOption.READ)) {
			var json = JSON.reader().readValue(in, SettingsJson.class);
			LOG.info("Settings loaded from {}", path);
			var settings = new Settings(this, json);
			return Stream.of(settings);
		} catch (JacksonException e) {
			LOG.warn("Failed to parse json file {}", path, e);
			return Stream.empty();
		} catch (NoSuchFileException e) {
			return Stream.empty();
		} catch (IOException e) {
			LOG.warn("Failed to load json file {}", path, e);
			return Stream.empty();
		}
	}

	void saveNow(Settings settings) {
		try {
			scheduleSave(settings, 0L).get();
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
			LOG.error("Saving settings was interrupted.", e);
		} catch (ExecutionException e) {
			LOG.error("Unexpected exception while saving.", e);
		}
	}

	void scheduleSave(Settings settings) {
		scheduleSave(settings, SAVE_DELAY_MS);
	}

	private Future<?> scheduleSave(Settings settings, long delayMillis) {
		if (settings == null) {
			return CompletableFuture.completedFuture(null);
		}
		final Path settingsPath = env.getSettingsPath().findFirst().orElseThrow(); // always save to preferred (first) path
		Runnable saveCommand = () -> this.save(settings, settingsPath);
		ScheduledFuture<?> scheduledTask = scheduler.schedule(saveCommand, delayMillis, TimeUnit.MILLISECONDS);
		ScheduledFuture<?> previouslyScheduledTask = scheduledSaveCmd.getAndSet(scheduledTask);
		if (previouslyScheduledTask != null) {
			previouslyScheduledTask.cancel(false);
		}
		return scheduledTask;
	}

	private void save(Settings settings, Path settingsPath) {
		assert settings != null : "method should only be invoked by #scheduleSave, which checks for null";
		LOG.debug("Attempting to save settings to {}", settingsPath);
		try {
			Files.createDirectories(settingsPath.getParent());
			Path tmpPath = settingsPath.resolveSibling(settingsPath.getFileName().toString() + ".tmp");
			try (OutputStream out = Files.newOutputStream(tmpPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
				var jsonObj = settings.serialized();
				jsonObj.writtenByVersion = env.getAppVersionWithBuildNumber();
				JSON.writerWithDefaultPrettyPrinter().writeValue(out, jsonObj);
			}
			Files.move(tmpPath, settingsPath, StandardCopyOption.REPLACE_EXISTING);
			LOG.info("Settings saved to {}", settingsPath);
		} catch (IOException e) {
			LOG.error("Failed to save settings.", e);
		}
	}

}
