package backupAndRecovery;

import main.RepositoryAdministrator;
import network.RepositoryPoller;
import main.ConfigurationReader;
import io.FileFragmentInputStream;
import reedSolomon.*;
import java.io.InputStream;
import java.net.UnknownHostException;
import io.Partlist;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Arrays;
import java.util.Date;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Vector;
import org.apache.log4j.Logger;

/** 
 * This class creates backups. It is responsible for every operation related
 * to succesful process of backing file up.
 *
 * @author Marcel Duris
 */
public class Backup {

    /**
     * This inner class provides way to iterate over networked peers. It is used
     * to write calculated checksums directly to network.
     */
    private class PeerOutputList implements Iterable<OutputStream> {

        /**
         * Inner iterator.
         */
        private class OutputIterator implements Iterator<OutputStream> {

            public boolean hasNext() {
                return pos < hosts.length;
            }

            /**
             * This method tries to find next host to which we can write our
             * checksum.
             *
             * @return output stream writing data over network to that host.
             */
            public OutputStream next() {
                OutputStream out = null;
                try {
                    Socket socket = new Socket();
                    socket.setSoTimeout(4000);
                    socket.connect(new InetSocketAddress(hosts[pos++ % hosts.length], ConfigurationReader.getCfg().getPort()));
                    out = new BufferedOutputStream(socket.getOutputStream());
                    out.write(("upload: " + partIterator.next() + "\n").getBytes());
                } catch (IOException ex) {
                    Logger.getLogger("qbackup.backup").error("Error connecting to peer. ");
                }
                return out;
            }

            public void remove() {
                throw new UnsupportedOperationException("Not supported yet.");
            }
        }
        private InetAddress[] hosts;
        private int pos;
        private Partlist parts;
        private Iterator<String> partIterator;

        /**
         * Constructor prepares class for iteration over list of hosts in
         * choosen order.
         * 
         * @param hosts array of IP addresses of hosts.
         * @param parts partlist to determine names of calculated files
         */
        public PeerOutputList(InetAddress[] hosts, Partlist parts) {
            this.parts = parts;
            this.hosts = hosts;
            partIterator = parts.iterator();
            pos = 0;
        }

        public Iterator<OutputStream> iterator() {
            return new OutputIterator();
        }
    }

    /** Implementation of rotation scheme to be used. */
    private IRotationScheme rotationScheme;

    /**
     * Basic constructor initializes backup engine.
     */
    public Backup() {
        rotationScheme = new RoundRobin();
    }

    /**
     * Creates backup of file identified by path. Method uses Reed-Solomon code
     * to calculate checksums and then upload them to networked hosts.
     * 
     * @param path path to file
     * @param n number of fragments into which file will be split
     * @param m number of checksums to be calculated
     * @throws IOException when desired file cannot be read
     * @throws IllegalArgumentException when some of arguments are invalid
     */
    public void createBackup(String path, int n, int m) throws IOException, IllegalArgumentException {
        //== check parameters ==================================================
        File file = new File(path);
        if (!file.canRead()) {
            throw new IOException("Unable to read file to be backed up;");
        }

        if (file.isDirectory()) {
            throw new IllegalArgumentException("Cannot backup folder.");
        }

        if (n < 2 || m < 2 || n + m > 65000) {
            throw new IllegalArgumentException("Invalid number of fragments or checksums.");
        }

        if (m <= n) {
            throw new IllegalArgumentException("Number of checksums must be larger than number of data fragments.");
        }

        //== get length of fragment ============================================
        long fs = (long) Math.ceil((double) file.length() / n);

        //== Generate backup name, limit to maximum length =====================
        String bcpName = file.getName();
        if (bcpName.length() > 10) {
            bcpName = bcpName.substring(0, 9);
        }
        bcpName += (new Date().getTime() / 1000);
        //== names of checksums, do not generate names for fragments ===========
        Partlist pl = new Partlist(bcpName, n, m, false);

        BackupMetaContainer meta = new BackupMetaContainer(file, bcpName, n, m);

        //== use rotation scheme to limit media usage ==========================
        rotationScheme.rotate(meta);

        //== create backups ====================================================
        if (checkMeta(meta)) {
            InetAddress[] hosts = getHostList(meta);
            if (hosts == null) {
                return;
            }

            PeerOutputList list = new PeerOutputList(hosts, pl);
            //== to ensure math is working and reuse existing proofs, fragments
            // are omitted, only checksums are used ============================
            //sendFragments(new FileFragmentInputStream(file, fs), list);
            ReedSolomonLocal.calculateChecksums(new FileFragmentInputStream(file, fs), list, m);
        }
    }

    /**
     * Used to check meta information and determine desirability of new backup.
     *
     * @param meta information about backup going to be crated.
     * @return true if and only if new backup is desirable
     */
    private boolean checkMeta(BackupMetaContainer meta) {
        return true;
    }

    /**
     * Writes meta information to repository.
     * 
     * @param meta information to be written
     */
    private void writeMeta(BackupMetaContainer meta) {
        try {

            OutputStream out =
                    new BufferedOutputStream(
                    RepositoryAdministrator.getOutputStream(meta.getBackupName() + ".xml"));

            out.write(meta.getXmlString().getBytes());
            out.flush();
            out.close();
        } catch (IOException ex) {
            Logger.getLogger("qbackup.backup").error("Error writing metafile.");
        }
    }

    /**
     * Polls network for list of host capable of storing our new backup. Hosts
     * are sorted in order from most desirable to least desirable host.
     * 
     * @param meta information about backup
     * @return list of IP addresses of hosts
     */
    private InetAddress[] getHostList(BackupMetaContainer meta) {
        RepositoryPoller poll = new RepositoryPoller();
        RepoHost[] candidates = poll.pollForNewBackup(meta);

        if (candidates == null || candidates.length == 0) {
            Logger.getLogger("qbackup.backup").warn("No peers found.");
            return null;
        }

        InetAddress[] hosts = chooseHosts(candidates.length, candidates);
        return hosts;
    }

    /**
     * Sorts found hosts in order from most desirable to the least desirable.
     * Also location of host in network is taken into account, so dispersion
     * of backup over network is maximized.
     *
     * @param count number of hosts to be choosen.
     * @param candidates list of found hosts.
     * @return array of addresses of host in correct order
     */
    private InetAddress[] chooseHosts(int count, RepoHost[] candidates) {
        Logger.getLogger("qbackup.backup").info("Sorting peers...");
        RepoHost[][] hbs = divideBySubnet(candidates);
        int cc = 0;
        int lvl = 0;
        //int i = 0;
        InetAddress[] ch = new InetAddress[count];
        while (cc < count) {
            RepoHost[] aux = new RepoHost[hbs.length];
            int m = 0;
            for (int i = 0; i < aux.length; i++) {
                if (lvl < hbs[i].length) {
                    aux[i] = hbs[i][lvl];
                    m++;
                }
            }

            /* nie velmi pekne, ale ide to */
            Arrays.sort(aux, new RepoHostComparator());

            int n = Math.min(m, count - cc);
            for (int i = 0; i < n; i++) {
                ch[cc] = aux[i].getAddress();
                cc++;
            }
            lvl++;
        }
        Logger.getLogger("qbackup.backup").info("Peers sorted.");
        return ch;
    }

    /**
     * Divides hosts by their subnet address. Result is matrix of hosts
     * which rows represent hosts in same subnet.
     * 
     * @param hosts array of hosts
     * @return two dimensional array of hosts divided by subnet
     */
    private RepoHost[][] divideBySubnet(RepoHost[] hosts) {
        Logger.getLogger("qbackup.backup").info("Sorting by network address.");
        Hashtable<InetAddress, Integer> kv =
                new Hashtable<InetAddress, Integer>(50, (float) 0.75);
        Vector<Vector<RepoHost>> ssbns = new Vector<Vector<RepoHost>>();
        for (int i = 0; i < hosts.length; i++) {
            InetAddress subnet = getSubnet(hosts[i]);
            if (kv.containsKey(subnet)) {
                Vector<RepoHost> sortedHosts = ssbns.get(kv.get(subnet));
                int position = getPosition(sortedHosts, hosts[i]);
                sortedHosts.add(position, hosts[i]);
            } else {
                Vector<RepoHost> nsbn = new Vector<RepoHost>();
                nsbn.add(hosts[i]);
                ssbns.add(nsbn);
                int position = ssbns.size() - 1;
                kv.put(subnet, position);
            }
        }

        return vvector2array(ssbns);
    }

    /**
     * Returns subnet address of host
     *
     * @param host container storing infomation about host
     * @return array of bytes representing subnet address of host
     */
    private InetAddress getSubnet(RepoHost host) {

        byte[] address = host.getAddress().getAddress();
        byte[] mask = host.getSubnetMask();

        byte[] subnetB = new byte[address.length];
        for (int j = 0; j <
                address.length; j++) {
            subnetB[j] = (byte) ((int) address[j] & (int) mask[j]);
        }

        InetAddress subnet = null;
        try {
            subnet = InetAddress.getByAddress(subnetB);
        } catch (UnknownHostException ex) {
        }
        return subnet;
    }

    /**
     * Auxiliary function for bubble sort, returns number of correct position
     * of host in Vector of hosts.
     * 
     * @param sorted sorted list of peers into which we want to add new host
     * @param host being added
     * @return list including new host
     */
    private int getPosition(Vector<RepoHost> sorted, RepoHost host) {
        int position = 0;
        while (position < sorted.size() && sorted.get(position).getWeight() > host.getWeight()) {
            position++;
        }

        return position;
    }

    /**
     * Auxiliary, converts from one representation of list into another.
     * @param vv
     * @return
     */
    private RepoHost[][] vvector2array(Vector<Vector<RepoHost>> vv) {
        RepoHost[][] a = new RepoHost[vv.size()][];
        for (int i = 0; i <
                vv.size(); i++) {
            Vector<RepoHost> sbnt = vv.get(i);
            a[i] = new RepoHost[sbnt.size()];
            sbnt.copyInto(a[i]);
        }

        return a;
    }

    /**
     * Sends original file fragments over network to storing hosts.
     * 
     * @param fin Fragmenting input stream
     * @param pout iterator over network hosts
     * @see PeerOutputList
     * @see io.FileFragmentInputStream
     */
    private void sendFragments(FileFragmentInputStream fin, PeerOutputList pout) {
        Logger.getLogger("qbackup.backup").info("Sending file fragments...");
        Iterator<OutputStream> iout = pout.iterator();
        for (InputStream in : fin) {
            OutputStream out = null;
            try {
                //System.out.println("Next fragment.");
                out = iout.next();
                int r = 0;
                while ((r = in.read()) > -1) {
                    out.write(r);
                }
            } catch (IOException ex) {
                Logger.getLogger("qbackup.backup").error("Error sending fragment.");
            } finally {
                try {
                    out.flush();
                    out.close();
                } catch (IOException e) {
                }
                Logger.getLogger("qbackup.backup").info("Finished sending fragments.");
            }
        }
    }

    /*public File prepareDir(File directory) {
    try {
    //File out = File.createTempFile("qbackup", null);
    File out = new File("D:/ahoj.zip");
    ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(out));
    File[] files = directory.listFiles();
    for (File file : files) {
    addFile(file, zip);
    }
    zip.flush();
    zip.close();

    return out;
    } catch (IOException ex) {
    Logger.getLogger(Backup.class.getName()).log(Level.SEVERE, null, ex);
    }
    return null;
    }

    private void addFile(File file, ZipOutputStream zip) throws IOException {
    if (file.isDirectory()) {
    File[] files = file.listFiles();
    for (File entry : files) {
    System.out.println(entry.toString());
    addFile(entry, zip);
    }
    } else {
    ZipEntry entry = new ZipEntry(file.getPath());
    zip.putNextEntry(entry);
    BufferedInputStream in = new BufferedInputStream(new FileInputStream(file));
    byte[] buff = new byte[10000];
    int r;
    while ((r = in.read(buff)) > 0) {
    zip.write(buff, 0, r);
    }
    }
    }*/
}
