/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package network;

import backupAndRecovery.*;
import main.ConfigurationReader;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;
import java.util.Vector;
import org.apache.log4j.Logger;

/**
 * Purpouse of this class is to find computers willing to store our backup
 * files.
 *
 * This class multicasts request message containg basic information about
 * backup like its size and number of fragments. Responces are then collected
 * and stored within RepoHost container class.
 *
 * Polls sourced at random port.
 * 
 * @author Marcel Duris
 * @see backupAndRecovery.RepoHost
 */
public class RepositoryPoller {

    private ConfigurationReader cfg;
    private final int RCV_BUFFER_SIZE = 1000;
    private final int TIMEOUT = 5000;

    /**
     * Private container used to map response with host at which response originated
     */
    private class Response {

        private InetAddress source;
        private String message;

        public Response(InetAddress source, String message) {
            this.source = source;
            this.message = message;
        }

        public Response(InetAddress source, byte[] message) {
            this.source = source;
            this.message = new String(message).trim();
        }

        public InetAddress getSource() {
            return source;
        }

        public String getMessage() {
            return message;
        }
    }

    public RepositoryPoller() {
        cfg = ConfigurationReader.getCfg();
    }

    /**
     * Multicast request message containing size of one backup fragment in MB.
     * All computers capable of storing such fragment should respond with UDP
     * message containg Xml of their RepoHost. Computers must respond within
     * timeout as specified by RCV_TIMEOUT otherwise their response will be
     * ignored and discarded.
     *
     * @param meta meta information abou file to be backed up
     * @return array of RepoHost willing to participate
     */
    public RepoHost[] pollForNewBackup(BackupMetaContainer meta) {
        Logger.getLogger("qbackup").info("Polling for repository hosts.");
        byte[] request = ProtocolBackup.createRequest(meta);
        Vector<Response> responses = poll(request);

        Vector<RepoHost> hosts = new Vector<RepoHost>();
        for (int i = 0; i < responses.size(); i++) {
            try {
                RepoHost host = new RepoHost(new String(responses.get(i).getMessage()));
                hosts.add(host);
            } catch (IllegalArgumentException ex) {
                Logger.getLogger("qbackup").warn("Malformed response from: " + responses.get(i).getSource().getHostName());
            }
        }

        RepoHost[] result = new RepoHost[hosts.size()];
        return hosts.toArray(result);
    }

    /**
     * This method polls networked host for existing backup. Any hosts storing
     * any files related to requested backup respond with list of related files
     * they store.
     *
     * Result is hash map mapping filenames and list of addresses of hosts on
     * which this files are stored.
     *
     * @param meta metadata container of backup
     * @return Hashmap mapping filenames and addresses
     */
    public HashMap<String, Vector<InetAddress>> pollForFragments(BackupMetaContainer meta) {
        //navratova hodnota by mohlo byt napriklad volaco s parom nazov fragmentu, zoznam hostov, co to maju
        Logger.getLogger("qbackup").info("Polling for backup fragments.");
        byte[] request = ProtocolRecovery.buildRequest(meta);
        Vector<Response> responses = poll(request);

        //Parse responses
        HashMap<String, Vector<InetAddress>> result = new HashMap<String, Vector<InetAddress>>();
        for (Response response : responses) {
            InetAddress adr = response.getSource();
            String[] fragments = ProtocolRecovery.parseResponse(response.message).get(meta);
            if (fragments == null) {
                continue;
            }
            for (String fragment : fragments) {
                if (result.containsKey(fragment)) {
                    result.get(fragment).add(adr);
                } else {
                    Vector<InetAddress> val = new Vector<InetAddress>();
                    val.add(adr);
                    result.put(fragment, val);
                }
            }
        }

        return result;
    }

    /**
     * This method polls networked host for existing backup. Any hosts storing
     * any backup of requested filename respond with list of related files they
     * store.
     *
     * Result is hash map mapping filenames and list of addresses of hosts on
     * which this files are stored.
     *
     * @param filename name of file for which network peers are polled
     * @return Hashmap mapping filenames and addresses
     */
    public BackupMetaContainer[] pollForFilename(String filename) {
        Logger.getLogger("qbackup").info("Polling for backup fragments.");
        byte[] request = ProtocolRecovery.buildRequest(filename);
        Vector<Response> responses = poll(request);

        ArrayList<BackupMetaContainer> resMeta = new ArrayList<BackupMetaContainer>();
        for (Response response : responses) {
            Set<BackupMetaContainer> metas = ProtocolRecovery.parseResponse(response.message).keySet();
            if (metas == null) {
                continue;
            }
            for (BackupMetaContainer meta : metas) {
                if (!resMeta.contains(meta)) {
                    resMeta.add(meta);
                }
            }
        }

        BackupMetaContainer[] result = new BackupMetaContainer[resMeta.size()];
        result = resMeta.toArray(result);
        return result;
    }

    /**
     * Requests network peers to delete backup.
     *
     * @param meta metadata of backup to be deleted
     */
    public void delete(BackupMetaContainer meta) {
        byte[] request = ProtocolDelete.buildRequest(meta);
        poll(request);
    }

    /**
     * Auxiliary method used to poll peers. Multicast is used as underlying
     * technology.
     *
     * @param request byte array to be placed into request packet
     * @return vector of responces to be parsed
     */
    private Vector<Response> poll(byte[] request) {
        Vector<Response> responses = null;
        MulticastSocket s = null;
        try {
            s = new MulticastSocket();
            s.joinGroup(cfg.getAddress());
            s.setLoopbackMode(false);
            s.setSoTimeout(TIMEOUT);

            DatagramPacket p = new DatagramPacket(request, request.length, cfg.getSocketAddress());
            s.send(p);

            responses = getResponses(s);
        } catch (UnknownHostException e) {
            Logger.getLogger("qbackup").warn("Could not find address to poll.");
            return null;
        } catch (IOException e) {

            Logger.getLogger("qbackup").warn("Error joining group.");
            return null;
        } finally {
            try {
                s.leaveGroup(cfg.getAddress());
                s.close();
            } catch (Exception e) {
            }
            return responses;
        }
    }

    /**
     * Parses responses for request received on socket. Responces are stored
     * in Vector.
     *
     * @param s socket to listen on
     * @return Vector of received responses
     */
    private Vector<Response> getResponses(DatagramSocket s) {
        try {//set maximum time to wait for next response
            s.setSoTimeout(TIMEOUT);
        } catch (SocketException e) {
        }

        Vector<Response> responses = new Vector<Response>(); //RepoHost responses
        HashMap<InetAddress, ArrayList<byte[]>> map =
                new HashMap<InetAddress, ArrayList<byte[]>>();
        try {
            while (true) {
                DatagramPacket response = new DatagramPacket(new byte[RCV_BUFFER_SIZE], RCV_BUFFER_SIZE);
                s.receive(response);
                if (map.containsKey(response.getAddress())) {
                    map.get(response.getAddress()).add(response.getData());
                } else {
                    ArrayList<byte[]> data = new ArrayList<byte[]>();
                    data.add(response.getData());
                    map.put(response.getAddress(), data);
                }
                responses.add(new Response(response.getAddress(), response.getData()));
            }
        } catch (SocketTimeoutException ex) {
            return buildResponses(map);
            //return responses;
        } catch (IOException e) {
        }
        return null;
    }

    private Vector<Response> buildResponses(HashMap<InetAddress, ArrayList<byte[]>> map) {
        Vector<Response> result = new Vector<Response>();
        Set<InetAddress> addresses = map.keySet();
        for (InetAddress address : addresses) {
            ArrayList<byte[]> fragments = map.get(address);
            byte[][] responses = new FragmentManager().mergeFragments(fragments.toArray(new byte[1][]));
            for (byte[] response : responses) {
                result.add(new Response(address, response));
            }
        }
        return result;
    }
}
