package backupAndRecovery;

import main.RepositoryAdministrator;
import network.RepositoryPoller;
import main.ConfigurationReader;
import io.Partlist;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Vector;
import org.apache.log4j.Logger;
import reedSolomon.ReedSolomonLocal;

/**
 * Purpose of this class is to perfrom recovery of stored backup. This class
 * comunicates over network to find and recover requested backup.
 *
 * @author Marcel Duris
 */
public class Recovery {

    /**
     * Main method of class, performs procedures to succesfully recover backup.
     *
     * @param meta metadata container of backup to be restored
     * @param dest file into which recovered file should be saved
     */
    public void recover(BackupMetaContainer meta, File dest) {
        Logger.getLogger("qbackup.recovery").info("Recovering file " + meta.getFilename());
        RepositoryPoller poll = new RepositoryPoller();
        HashMap<String, Vector<InetAddress>> files = poll.pollForFragments(meta);

        String bcpName = meta.getBackupName();
        int n = meta.getFragmentCount();
        int m = meta.getChecksumCount();
        Partlist parts = new Partlist(bcpName, n, m);

        downloadFragments(files, parts);
        Iterable<InputStream> ins = RepositoryAdministrator.getInputStreamsForBackup(meta);
        Iterable<OutputStream> outs = RepositoryAdministrator.getOutputStreamsForBackup(meta);

        try {
            ReedSolomonLocal.recover(ins, outs, n, m);
            concat(meta, parts, dest);
        } catch (IOException ex) {
            Logger.getLogger("qbackup.recovery").error("Could not recover. " + ex.getMessage());
        } catch (Exception ex) {
            Logger.getLogger("qbackup.recovery").error("Could not recover. " + ex.getMessage());
        }

        Logger.getLogger("qbackup.recovery").info("Recovery finished.");
    }

    /**
     * Downloads accessible file fragments and checksums. Passed table maps 
     * all fragments belonging to backup onto list of addresses, where these 
     * files are stored. Second passed argument is used to determine name of 
     * these files.
     *
     * @param partHostMap map of filenames and host addresses
     * @param parts partlist iterating over filenames
     * @return
     */
    private boolean downloadFragments(HashMap<String, Vector<InetAddress>> partHostMap, Partlist parts) {
        Logger.getLogger("qbackup.recovery").info("Downloading Checksums.");
        Iterator<String> iterator = parts.iterator();
        int count = 0;
        while (count < parts.getDataCount() && iterator.hasNext()) {
            String partName = iterator.next();
            if (partHostMap.containsKey(partName)) {
                Vector<InetAddress> adrs = partHostMap.get(partName);
                if (!containsLocalhost(adrs)) { //if we should download from ourselves, than we already have fragment.
                    downloadSingleFragment(partName, adrs);
                }
                count++;
            }
        }
        Logger.getLogger("qbackup.recovery").info("Checksums downloaded.");
        return count >= parts.getDataCount();
    }

    /**
     * This method downloads only single fragment from first accessible host
     * from list. If file could not be downloaded, false is returned. If file
     * was downloaded successfully, this method returns true.
     * 
     * @param filename name of file to download
     * @param addresses Vector of addresses of hosts storing file
     * @return
     */
    private boolean downloadSingleFragment(String filename, Vector<InetAddress> addresses) {
        int port = ConfigurationReader.getCfg().getPort();
        Socket socket = null;
        InputStream in = null;
        OutputStream out = null;
        OutputStream lout = null; //local output stream
        int adrptr = 0; //address pointer

        try {
            while (true) {
                try {
                    socket = new Socket(addresses.get(adrptr), port);
                    break;
                } catch (IOException ex) {
                    adrptr++;
                    if (adrptr > addresses.size()) {
                        return false;
                    }
                }
            }
            in = socket.getInputStream();
            out = socket.getOutputStream();

            out.write(("download: " + filename + "\n").getBytes());
            out.flush();

            lout = RepositoryAdministrator.getOutputStream(filename);
            copyStream(in, lout);
            return true;

        } catch (IOException ex) {
            //Logger.getLogger(Recovery.class.getName()).log(Level.SEVERE, null, ex);
            return false;
        } finally {
            if (socket == null) {
                return false;
            }
            try {
                in.close();
            } catch (IOException ex) {
                //ex.printStackTrace();
                System.err.println("Error closing network input.");
            }
            try {
                out.close();
            } catch (IOException ex) {
                //ex.printStackTrace();
                System.err.println("Error closing file output.");
            }
            try {
                lout.flush();
                lout.close();
            } catch (IOException ex) {
                //ex.printStackTrace();
                System.err.println("Error closing file output.");
            }
            try {
                socket.close();
            } catch (IOException ex) {
            }
        }
    }

    /**
     * Reads input stream and writes it to desired output file specified by
     * argument.
     *
     * @param input stream to read
     * @param outputFile file to write to
     */
    private void copyStream(InputStream input, OutputStream output) {
        InputStream in = new BufferedInputStream(input);
        OutputStream out = new BufferedOutputStream(output);

        int n = 0;
        byte[] buff = new byte[1024];
        try {
            while ((n = in.read(buff)) > 0) {
                out.write(buff, 0, n);
            }
            out.flush();
        } catch (IOException ex) {
            Logger.getLogger("qbackup.recovery").error(ex.getMessage());
        }
    }

    /**
     * Used to determine whether list of hosts contains localhost. In that case
     * download of file is not necessary.
     * 
     * @param adrs vector of addresses
     * @return true if and only if localhost is present in vector
     */
    private boolean containsLocalhost(Vector<InetAddress> adrs) {
        InetAddress local = ConfigurationReader.getCfg().getLocalAddress();
        for (InetAddress adr : adrs) {
            if (adr.equals(local)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Merges recovered fragments of original file into resulting recovered
     * file.
     *
     * @param meta metadata about file
     * @param parts partlist used to determine filenames
     * @param dest destination file
     * @return true if file was recovered succesfully
     */
    private boolean concat(BackupMetaContainer meta, Partlist parts, File dest) {
        OutputStream out = null;
        //File dest = new File(destFolder, meta.getFilename());
        long bc = 0; //bytes copied
        try {
            out = new BufferedOutputStream(new FileOutputStream(dest));
            Iterator<String> iterator = parts.iterator();
            for (int i = 0; i < meta.getFragmentCount(); i++) {
                try {
                    bc += appendFile(out, iterator.next(), meta.getSize() - bc);
                } catch (IOException ex) {
                    Logger.getLogger("qbackup.recovery").error("Could not assemble file. " + ex.getMessage());
                    return false;
                }
            }
        } catch (FileNotFoundException ex) {
            Logger.getLogger("qbackup.recovery").error("Could not assemble file. " + ex.getMessage());
        } finally {
            try {
                if (out == null) {//avoid null pointer exception
                    return false;
                }
                out.flush();
                out.close();
            } catch (IOException ex) {
                Logger.getLogger("qbackup.recovery").error("Could not assemble file. " + ex.getMessage());
            }
        }

        return true;
    }

    /**
     * Reads file fragment from repository and appends it to output stream.
     *
     * @param out output stream into which file should be copied
     * @param fragmentName name of file in repository
     * @param remaining number of bytes to copy
     * @return number of bytes copied
     * @throws FileNotFoundException if file could not be found in repository
     * @throws IOException if problem occurs during copy
     */
    private long appendFile(OutputStream out, String fragmentName, long remaining) throws FileNotFoundException, IOException {
        InputStream in = RepositoryAdministrator.getInputStream(fragmentName);

        int r = 0;
        long count = 0;
        byte[] buff = new byte[1024];
        while ((r = in.read(buff)) > 0) {
            int toWrite = (int) Math.min(r, remaining - count);//cast is safe, lower value is always int
            out.write(buff, 0, toWrite);
            count += r;
        }
        in.close();
        return count;
    }
}
