package freenet.pluginmanager;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import freenet.config.SubConfig;
import freenet.crypt.AEADCryptBucket;
import freenet.node.FSParseException;
import freenet.node.Node;
import freenet.node.NodeInitException;
import freenet.node.ProgramDirectory;
import freenet.support.IllegalBase64Exception;
import freenet.support.SimpleFieldSet;
import freenet.support.api.Bucket;
import freenet.support.io.FileBucket;
import freenet.support.io.FileUtil;
import freenet.support.io.PaddedBucket;

public class PluginStores {
    
    final Node node;
    private final ProgramDirectory pluginStoresDir;
    
    public PluginStores(Node node, SubConfig installConfig) throws NodeInitException {
        this.node = node;
        pluginStoresDir = node.setupProgramDir(installConfig, "pluginStoresDir", "plugin-data", 
                "NodeClientCore.pluginStoresDir", "NodeClientCore.pluginStoresDir", null, null);
        File dir = pluginStoresDir.dir();
        if(!(dir.mkdirs() || (dir.exists() && dir.isDirectory() && dir.canRead() && dir.canWrite()))) {
            System.err.println("Unable to create folder for plugin data: "+pluginStoresDir.dir());
        }
    }

    private void writePluginStoreInner(String storeIdentifier, PluginStore pluginStore, boolean isEncrypted, boolean backup) throws IOException {
        Bucket bucket = makePluginStoreBucket(storeIdentifier, isEncrypted, backup);
        OutputStream os = bucket.getOutputStream();
        try {
            if(pluginStore != null) {
                pluginStore.exportStoreAsSFS().writeTo(os);
            }
        } finally {
            os.close();
        }
    }
    
    private File getPluginStoreFile(String storeIdentifier, boolean encrypted, boolean backup) {
        String filename = storeIdentifier;
        filename += ".data";
        if(backup)
            filename += ".bak";
        if(encrypted)
            filename += ".crypt";
        return pluginStoresDir.file(filename);
    }

    private Bucket makePluginStoreBucket(String storeIdentifier, boolean isEncrypted, boolean backup) 
    throws FileNotFoundException {
        File f = getPluginStoreFile(storeIdentifier, isEncrypted, backup);
        Bucket bucket = new FileBucket(f, false, true, false, false);
        if(isEncrypted) {
            byte[] key = node.getPluginStoreKey(storeIdentifier);
            if(key != null) {
                // We pad then encrypt, which is wasteful, but we have no way to persist the size.
                // Unfortunately AEADCryptBucket needs to know the real termination point.
                bucket = new AEADCryptBucket(bucket, key);
                bucket = new PaddedBucket(bucket);
            }
        }
        return bucket;
    }
    
    private Bucket findPluginStoreBucket(String storeIdentifier, boolean isEncrypted, boolean backup) 
    throws FileNotFoundException {
        File f = getPluginStoreFile(storeIdentifier, isEncrypted, backup);
        if(!f.exists()) return null;
        Bucket bucket = new FileBucket(f, false, false, false, false);
        if(isEncrypted) {
            byte[] key = node.getPluginStoreKey(storeIdentifier);
            if(key != null) {
                // We pad then encrypt, which is wasteful, but we have no way to persist the size.
                // Unfortunately AEADCryptBucket needs to know the real termination point.
                bucket = new AEADCryptBucket(bucket, key);
                bucket = new PaddedBucket(bucket, bucket.size());
            }
        }
        return bucket;
    }

    public PluginStore loadPluginStore(String storeIdentifier) {
        boolean isEncrypted = node.wantEncryptedDatabase();
        PluginStore store = loadPluginStore(storeIdentifier, isEncrypted, false);
        if(store != null) return store;
        store = loadPluginStore(storeIdentifier, isEncrypted, true);
        if(store != null) return store;
        isEncrypted = !isEncrypted;
        store = loadPluginStore(storeIdentifier, isEncrypted, false);
        if(store != null) return store;
        store = loadPluginStore(storeIdentifier, isEncrypted, true);
        return store;
    }
    
    private PluginStore loadPluginStore(String storeIdentifier, boolean isEncrypted, boolean backup) {
        Bucket bucket;
        try {
            bucket = findPluginStoreBucket(storeIdentifier, isEncrypted, backup);
            if(bucket == null) return null;
        } catch (FileNotFoundException e) {
            return null;
        }
        InputStream is = null;
        try {
            try {
                is = bucket.getInputStream();
                SimpleFieldSet fs = SimpleFieldSet.readFrom(is, false, false, true, true);
                return new PluginStore(fs);
            } finally {
                // Do NOT use Closer.close().
                // We use authenticated encryption, which will throw at close() time if the file is corrupt,
                // or has been modified while the node was offline etc.
                if(is != null) is.close();
            }
        } catch (IOException e) {
            // Hence, if close() throws, we DO need to catch it here.
            System.err.println("Unable to load plugin data for "+storeIdentifier+" : "+e);
            System.err.println("This could be caused by data corruption or bugs in Freenet.");
            // FIXME crypto - possible it's caused by attack while offline.
            return null;
        } catch (IllegalBase64Exception e) {
            // Hence, if close() throws, we DO need to catch it here.
            System.err.println("Unable to load plugin data for "+storeIdentifier+" : "+e);
            System.err.println("This could be caused by data corruption or bugs in Freenet.");
            // FIXME crypto - possible it's caused by attack while offline.
            return null;
        } catch (FSParseException e) {
            // Hence, if close() throws, we DO need to catch it here.
            System.err.println("Unable to load plugin data for "+storeIdentifier+" : "+e);
            System.err.println("This could be caused by data corruption or bugs in Freenet.");
            // FIXME crypto - possible it's caused by attack while offline.
            return null;
        }
    }

    public void writePluginStore(String storeIdentifier, PluginStore store) throws IOException {
        boolean isEncrypted = node.wantEncryptedDatabase();
        File backup = getPluginStoreFile(storeIdentifier, isEncrypted, true);
        File main = getPluginStoreFile(storeIdentifier, isEncrypted, false);
        if(backup.exists() && main.exists()) {
            FileUtil.secureDelete(backup);
        }
        if(main.exists()) {
            if(!main.renameTo(backup))
                System.err.println("Unable to rename "+main+" to "+backup+" when writing pluginstore for "+storeIdentifier);
        }
        writePluginStoreInner(storeIdentifier, store, isEncrypted, false);
        File f = getPluginStoreFile(storeIdentifier, !isEncrypted, true);
        if(f.exists()) {
            FileUtil.secureDelete(f);
        }
        f = getPluginStoreFile(storeIdentifier, !isEncrypted, false);
        if(f.exists()) {
            FileUtil.secureDelete(f);
        }
    }

}
