/*
 * This file is part of Bisq.
 *
 * Bisq is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at
 * your option) any later version.
 *
 * Bisq is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Bisq. If not, see <http://www.gnu.org/licenses/>.
 */

package bisq.common.storage;

import bisq.common.app.DevEnv;
import bisq.common.config.Config;
import bisq.common.proto.persistable.PersistableEnvelope;
import bisq.common.proto.persistable.PersistenceProtoResolver;

import com.google.inject.Inject;

import javax.inject.Named;

import java.io.File;
import java.io.IOException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;

import static bisq.common.util.Preconditions.checkDir;
import static com.google.common.base.Preconditions.checkNotNull;

/**
 * That class handles the storage of a particular object to disk using Protobuffer.
 * <p></p>
 * For every data object we write a separate file to minimize the risk of corrupted files in case of inconsistency from newer versions.
 * In case of a corrupted file we backup the old file to a separate directory, so if it holds critical data it might be helpful for recovery.
 * <p></p>
 * We also backup at first read the file, so we have a valid file form the latest version in case a write operation corrupted the file.
 * <p></p>
 * The read operation is triggered just at object creation (startup) and is at the moment not executed on a background thread to avoid asynchronous behaviour.
 * As the data are small and it is just one read access the performance penalty is small and might be even worse to create and setup a thread for it.
 * <p></p>
 * The write operation used a background thread and supports a delayed write to avoid too many repeated write operations.
 */
public class Storage<T extends PersistableEnvelope> {
    private static final Logger log = LoggerFactory.getLogger(Storage.class);

    private final CorruptedDatabaseFilesHandler corruptedDatabaseFilesHandler;

    private final File dir;
    private FileManager<T> fileManager;
    private File storageFile;
    private T persistable;
    private String fileName;
    private int numMaxBackupFiles = 10;
    private final PersistenceProtoResolver persistenceProtoResolver;


    ///////////////////////////////////////////////////////////////////////////////////////////
    // Constructor
    ///////////////////////////////////////////////////////////////////////////////////////////

    @Inject
    public Storage(@Named(Config.STORAGE_DIR) File dir,
                   PersistenceProtoResolver persistenceProtoResolver,
                   CorruptedDatabaseFilesHandler corruptedDatabaseFilesHandler) {
        this.dir = checkDir(dir);
        this.persistenceProtoResolver = persistenceProtoResolver;
        this.corruptedDatabaseFilesHandler = corruptedDatabaseFilesHandler;
    }

    @Nullable
    public T initAndGetPersistedWithFileName(String fileName, long delay) {
        this.fileName = fileName;
        storageFile = new File(dir, fileName);
        fileManager = new FileManager<>(dir, storageFile, delay, persistenceProtoResolver);
        return getPersisted();
    }

    @Nullable
    public T initAndGetPersisted(T persistable, long delay) {
        return initAndGetPersisted(persistable, persistable.getClass().getSimpleName(), delay);
    }

    @Nullable
    public T initAndGetPersisted(T persistable, String fileName, long delay) {
        this.persistable = persistable;
        this.fileName = fileName;
        storageFile = new File(dir, fileName);
        fileManager = new FileManager<>(dir, storageFile, delay, persistenceProtoResolver);
        return getPersisted();
    }

    public void queueUpForSave() {
        queueUpForSave(persistable);
    }

    public void queueUpForSave(long delayInMilli) {
        queueUpForSave(persistable, delayInMilli);
    }

    public void setNumMaxBackupFiles(int numMaxBackupFiles) {
        this.numMaxBackupFiles = numMaxBackupFiles;
    }

    // Save delayed and on a background thread
    public void queueUpForSave(T persistable) {
        if (persistable != null) {
            checkNotNull(storageFile, "storageFile = null. Call setupFileStorage before using read/write.");

            fileManager.saveLater(persistable);
        } else {
            log.trace("queueUpForSave called but no persistable set");
        }
    }

    public void queueUpForSave(T persistable, long delayInMilli) {
        if (persistable != null) {
            checkNotNull(storageFile, "storageFile = null. Call setupFileStorage before using read/write.");

            fileManager.saveLater(persistable, delayInMilli);
        } else {
            log.trace("queueUpForSave called but no persistable set");
        }
    }

    public void remove(String fileName) {
        fileManager.removeFile(fileName);
    }


    ///////////////////////////////////////////////////////////////////////////////////////////
    // Private
    ///////////////////////////////////////////////////////////////////////////////////////////

    // We do the file read on the UI thread to avoid problems from multi threading.
    // Data are small and read is done only at startup, so it is no performance issue.
    @Nullable
    private T getPersisted() {
        if (storageFile.exists()) {
            long now = System.currentTimeMillis();
            try {
                T persistedObject = fileManager.read(storageFile);
                log.trace("Read {} completed in {}msec", storageFile, System.currentTimeMillis() - now);

                // If we did not get any exception we can be sure the data are consistent so we make a backup
                now = System.currentTimeMillis();
                fileManager.backupFile(fileName, numMaxBackupFiles);
                log.trace("Backup {} completed in {}msec", storageFile, System.currentTimeMillis() - now);

                return persistedObject;
            } catch (Throwable t) {
                log.error("We cannot read the persisted data. " +
                        "We make a backup and remove the inconsistent file. fileName=" + fileName);
                log.error(t.getMessage());
                try {
                    // We keep a backup which might be used for recovery
                    removeAndBackupFile(fileName);
                    DevEnv.logErrorAndThrowIfDevMode(t.toString());
                } catch (IOException e1) {
                    e1.printStackTrace();
                    log.error(e1.getMessage());
                    // We swallow Exception if backup fails
                }
                if (corruptedDatabaseFilesHandler != null)
                    corruptedDatabaseFilesHandler.onFileCorrupted(storageFile.getName());
            }
        }
        return null;
    }

    public void removeAndBackupFile(String fileName) throws IOException {
        fileManager.removeAndBackupFile(fileName);
    }
}

@@ -3284,32 +3284,152 @@
 Handler;%0A    %7D%0A%0A
+    @Nullable%0A    public T getPersisted(String fileName) %7B%0A        return getPersisted(new File(dir, fileName));%0A    %7D%0A%0A
     @Nullable%0A  
@@ -3695,32 +3695,43 @@
 rn getPersisted(
+storageFile
 );%0A    %7D%0A%0A    @N
@@ -4237,16 +4237,27 @@
 rsisted(
+storageFile
 );%0A    %7D
@@ -5852,16 +5852,32 @@
 rsisted(
+File storageFile
 ) %7B%0A    

Add new methods and refactor APIs. Needed later for new classes....