package network;

import main.RepositoryAdministrator;
import backupAndRecovery.*;
import main.ConfigurationReader;
import backupAndRecovery.RepoHost;
import java.io.Closeable;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.SocketTimeoutException;
import org.apache.log4j.Logger;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;

/**
 * This class is responsible for listenning to other hosts requests and
 * responding to them.
 * 
 * @author Marcel Duris
 */
public class ManagementServer extends Thread implements Closeable {

    private ConfigurationReader cfg;
    private MulticastSocket msocket;
    private InetSocketAddress group;
    private final int BUFF_SIZE = 2000;
    private boolean running = true;

    /**
     * Default constructor starts listening on multicast socket in separate
     * thread.
     *
     * @throws IOException if problem opening management multicast socket
     */
    public ManagementServer() throws IOException {
        Logger.getLogger("qbackup").info("Starting management server.");
        cfg = ConfigurationReader.getCfg();
        group = cfg.getSocketAddress();
        try {
            msocket = new MulticastSocket(group.getPort());
            msocket.setSoTimeout(0);
            msocket.joinGroup(cfg.getAddress());
        } catch (IOException ex) {
            //no socket or unable to join
            Logger.getLogger("qbackup").error("Unable to start management server. " + ex.getMessage());
            close();
            throw new IOException("Unable to start management server.");
        }
        start();
    }

    /**
     * Leaves multicast group and closes sockets. Prepares this class for
     * shutdown.
     */
    public void close() {
        Logger.getLogger("qbackup").info("Closing management socket.");
        running = false;
        try {
            msocket.leaveGroup(group.getAddress());
            msocket.close();
        } catch (IOException ex) {
        }
    }

    @Override
    public void run() {
        listen();
    }

    /**
     * Listens for requests and starts respond actions.
     */
    private void listen() {
        Logger.getLogger("qbackup").info("Listening for control messages.");
        DatagramPacket p;
        while (running) {
            try {
                p = new DatagramPacket(new byte[BUFF_SIZE], BUFF_SIZE);
                msocket.receive(p);

                serveMessage(p);
            } catch (SocketTimeoutException ex) {//TODO: nieco chytre? asi nie
            } catch (Exception ex) {
                Logger.getLogger("qbackup").warn("Error processing management message.");
            }
        }
    }

    /**
     * Performs all necessary operations after receiving a multicast message.
     * 
     * @param packet datagram packet storing received message
     */
    public void serveMessage(DatagramPacket packet) {
        try {
            Logger.getLogger("qbackup").info("Processing message.");
            String message = new String(packet.getData(), "UTF-8").trim();

            //SAXReader reader = new SAXReader();
            Document msg = DocumentHelper.parseText(new String(message));//reader.read(new StringReader(message));
            Element root = msg.getRootElement();

            if (root == null) {
                return;
            }

            String rootName = root.getName();
            byte[][] responses = null;

            if (rootName.compareToIgnoreCase("backupRequest") == 0) {
                responses = getBackupResponse(msg, packet.getAddress());
            }

            if (rootName.compareToIgnoreCase("fragmentPoll") == 0) {
                responses = getFragmentResponse(msg);
            }

            if (rootName.compareToIgnoreCase("delete") == 0) {
                responses = getDeleteResponse(msg);
            }

            if (responses != null) {
                for (byte[] response : responses) {
                    FragmentManager fm = new FragmentManager();
                    byte[][] fragments = fm.createFragments(response);
                    for (byte[] fragment : fragments) {
                        DatagramPacket p =
                                new DatagramPacket(fragment, fragment.length, packet.getSocketAddress());
                        msocket.send(p);
                    }
                }
            }
        } catch (DocumentException ex) {
            Logger.getLogger("qbackup").warn("Received malformed message from " +
                    packet.getAddress().getHostAddress());
        } catch (IOException ex) {
            Logger.getLogger("qbackup").warn("Error sending response to " +
                    packet.getAddress().getHostAddress());
        }
    }

    /**
     * Purpouse of this function is to properly respond to request for
     * repository hosts. Hosts capable of storing file fragments of size
     * stated in request should respond with parameters used to contact them.
     * This host also prepares itself for reception of data.
     *
     * @param msg Request XML message
     * @return array of bytes that should be send as response in udp datagram
     * @see BackupRequest.dtd
     * @see RepoHostResponse.dtd
     */
    private byte[][] getBackupResponse(Document msg, InetAddress sender) {
        try {
            InetAddress address = cfg.getLocalAddress();
            NetworkInterface networkInterface =
                    NetworkInterface.getByInetAddress(address);
            int maskLenght = networkInterface.getInterfaceAddresses().get(0).getNetworkPrefixLength();
            int repoSize = cfg.getMaxSize();
            //send port number on which this host will listen for data
            int port = cfg.getPort();
            int repoUsed = getRepoUsed();
            int backupsStored = RepositoryAdministrator.getBackupCount();
            int type = cfg.getType();

            RepoHost host = new RepoHost(address, maskLenght, port,
                    repoSize, repoUsed, backupsStored, type);

            BackupMetaContainer meta = ProtocolBackup.parseRequest(msg);

            RepositoryAdministrator.addBackup(meta);

            return new byte[][]{host.getXml().getBytes()};
        } catch (Exception ex) {
            return null;
        }
    }

    /**
     * Creates response on request created to list existing backups. This method
     * determines type of request which can be either request for known backup
     * or request for backups of certain file.
     * 
     * @param msg Xml Document of request
     * @return array of responses to be send to requestor
     */
    private byte[][] getFragmentResponse(Document msg) {
        BackupMetaContainer[] metas = null;
        if (ProtocolRecovery.getRequestType(msg) == ProtocolRecovery.FULL_REQUEST) {
            metas = new BackupMetaContainer[]{ProtocolRecovery.getMeta(msg)};
            //String[] fragments = RepositoryAdministrator.listFragments(FragmentRequest.getMeta(msg));
        }

        if (ProtocolRecovery.getRequestType(msg) == ProtocolRecovery.FILENAME_REQUEST) {
            metas = RepositoryAdministrator.listBackups(ProtocolRecovery.getFilename(msg));
        }

        if (ProtocolRecovery.getRequestType(msg) == ProtocolRecovery.UNKNOWN) {
            return null;
        }

        byte[][] result = new byte[metas.length][];
        for (int i = 0; i < metas.length; i++) {
            String[] files = RepositoryAdministrator.listFragments(metas[i]);
            result[i] = ProtocolRecovery.buildResponse(metas[i], files);
        }

        return result;
    }

    /**
     * Proceses Delete request, empty response is generated. Still may be
     * improved.
     *
     * @param msg Xml Document of request
     * @return null
     */
    private byte[][] getDeleteResponse(Document msg) {
        BackupMetaContainer meta = ProtocolDelete.getMeta(msg);
        RepositoryAdministrator.deleteBackup(meta);
        return null;
    }

    /**
     * Returns size of used portion of repository in MB.
     * @return used repository in MB
     */
    public int getRepoUsed() {
        //round to MB, use SI not 1024
        long sizeMB = (RepositoryAdministrator.getRepositoryUsed() + 500000) / 1000000;
        return (int) sizeMB;
    }
}
