package main;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import org.apache.log4j.Logger;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

/**
 * Singleton class containing configuration parameters of utility. If no
 * configuration file exists default values are used.
 *
 * Brief description of configuration file is provided in file 
 * configuration.dtd.
 *
 * @author Marcel Duris
 */
public class ConfigurationReader {

    /*
     * Names of elements in configuration file.
     */
    private final String ROOT = "configuration";
    private final String EAD = "address";
    private final String EPT = "port";
    private final String ERF = "repositoryFolder";
    private final String EMS = "maxSize";
    private final String EHT = "hostType";
    private final String ELI = "localInterface";
    private final String ELN = "interfaceName";
    private final String ELA = "localAddress";
    private final String EFC = "fragmentCount";
    private final String ECC = "checksumCount";
    //==Default Values==========================================================
    private final String DEF_ADDRESS = "239.255.255.1";
    private final int DEF_PORT = 39809;
    private final String DEF_FOLDER = ".";
    private final int DEF_SIZE = 10000;
    private final int DEF_FRAGS = 3;
    private final int DEF_CSUMS = 6;
    private final int DEF_MAX_BCPS = 4; /** default number of backups stored */
    private static final File DEF_CFG = new File("qbackup-config.xml");
    private final File DEF_SCHED_CFG = new File("qbackup-schedule.xml");
    //
    public final int UNKNOWN = 0;
    public final int NOTEBOOK_TYPE = 1;
    public final int DESKTOP_TYPE = 2;
    private final String[] TYPE_DESC = new String[]{"Unknown", "Notebook", "Desktop"};
    //
    private static ConfigurationReader configurationReader = null;
    private int maxSize; //max size in MB of repository folder
    private String folder; //folder in which backups will be stored
    private String address;
    private int port;
    private InetSocketAddress socket; //multicast listenning socket
    private InetAddress localhost;//unicast address of comuter
    private int mask; //length of subnet mask
    private int type; //type of host e.g. notebook/desktop/whatever
    private int fragments;
    private int checksums;
    private boolean usingDefs = true;
    private boolean warning;

    /**
     * Singleton private constructor.
     */
    private ConfigurationReader() {
    }

    private ConfigurationReader(File configFile) throws Exception {
        readConfig(configFile);
    }

    /**
     * Reads default configuration file. If file is invalid, or contains
     * malformed configuration, default configuration parameters are used.
     *
     * @return container with values read from default file or default values
     */
    public static ConfigurationReader readConfiguration() {
        return readConfiguration(DEF_CFG);
    }

    /**
     * Singleton factory method. If configuration has not been read yet,
     * new configuration reader is built. If it has been read previously
     * configuration is only updated.
     * 
     * @param configFile path to configuration file
     * @return Object representing configuration of application
     */
    public static ConfigurationReader readConfiguration(File configFile) {
        if (configurationReader == null) {
            configurationReader = new ConfigurationReader();
        }
        configurationReader.readConfig(configFile);

        return configurationReader;
    }

    /**
     * Returns object encapuslating configuration parameters. Configuration must
     * be initialized, i.e read, before this method is called. Prevents from
     * using default configuration file unintentionally.
     *
     * @return container with configuration parameters.
     */
    public static ConfigurationReader getCfg() {
        if (configurationReader != null) {
            return configurationReader;
        } else {
            throw new IllegalStateException("Configuration not read yet.");
        }
    }

    /**
     * Reads configuration from specified file. If configuration is invalid,
     * default values are used instead.
     *
     * @param configFile File containing configuration.
     */
    public void readConfig(File configFile) {
        warning = false;
        usingDefs = true;
        try {
            /* read configuration xml file */
            SAXReader reader = new SAXReader();
            Document propertyDocument = reader.read(configFile);

            Element root = propertyDocument.getRootElement();
            folder = root.element(ERF).getTextTrim();
            maxSize = Integer.parseInt(root.element(EMS).getTextTrim());
            type = Integer.parseInt(root.element(EHT).getTextTrim());

            /* read comunication multicast address and port */
            address = root.element(EAD).getTextTrim();
            port = Integer.parseInt(root.element(EPT).getTextTrim());
            socket = new InetSocketAddress(address, port);

            /* settings related to local interface */
            setLocalNet();//(root.element(ELI));

            /* fragment and checksum count settings */
            try {
                fragments = Integer.parseInt(root.element(EFC).getTextTrim());
                checksums = Integer.parseInt(root.element(ECC).getTextTrim());
            } catch (Exception ex) {
                fragments = DEF_FRAGS;
                checksums = DEF_CSUMS;
            }

            usingDefs = false;
        } catch (Exception e) {
            warning = true;
            Logger.getLogger("qbackup").warn("No configuration found, using default values");

            /* Default folder and size */
            folder = DEF_FOLDER;
            maxSize = DEF_SIZE;
            type = 0;
            /* Default address and port */
            socket = new InetSocketAddress(DEF_ADDRESS, DEF_PORT);
            try {
                /* Default local interface */
                setLocalNet();
            } catch (Exception ex) {
                System.err.println("Error setting interface.");
            }

            /* default fragment adn checksum count settings */
            fragments = DEF_FRAGS;
            checksums = DEF_CSUMS;
        }
    }

    /**
     * Writes currently loaded config into default configuration file.
     *
     * @throws IOException if problems occur during writing into file
     */
    public void writeConfig() throws IOException {
        Document cfg = DocumentHelper.createDocument();
        Element root = cfg.addElement(ROOT);
        root.addElement(ERF).setText(folder);
        root.addElement(EMS).setText(Integer.toString(maxSize));
        root.addElement(EHT).setText(Integer.toString(type));
        root.addElement(EAD).setText(address);
        root.addElement(EPT).setText(Integer.toString(port));
        root.addElement(EFC).setText(Integer.toString(fragments));
        root.addElement(ECC).setText(Integer.toString(checksums));

        OutputStream out = new BufferedOutputStream(new FileOutputStream(DEF_CFG));
        out.write(cfg.asXML().getBytes());
        out.flush();
        out.close();
        Logger.getLogger("qbackup").info("Configuration changed");
    }

    /**
     * Auxiliary function used to set values related to local interface.
     */
    private void setLocalNet(/*Element localInterface*/) {
        Element localInterface = null;
        if (localInterface == null) {
            // use default settings if nothing is specified in config
            try {
                localhost = InetAddress.getLocalHost();
                NetworkInterface ni = NetworkInterface.getByInetAddress(localhost);
                mask = ni.getInterfaceAddresses().get(0).getNetworkPrefixLength();
            } catch (Exception ex) {
            }
        }/* else {
            // parse settings in config
            Element localName = localInterface.element(ELN);
            Element localAdd = localInterface.element(ELA);
            NetworkInterface ni;
            if (localAdd != null) {
                InetAddress adr = InetAddress.getByName(localAdd.getTextTrim());
                ni = NetworkInterface.getByInetAddress(adr);
                //warning: allways use first mask of interface -> inconsistent
                localhost = adr;
                mask = ni.getInterfaceAddresses().get(0).getNetworkPrefixLength();
            } else {
                ni = NetworkInterface.getByName(localName.getTextTrim());
                localhost = ni.getInterfaceAddresses().get(0).getAddress();
                mask = ni.getInterfaceAddresses().get(0).getNetworkPrefixLength();
            }
        }*/
    }

    /**
     * Returns maximum allowed size of repository folder in MB.
     *
     * @return maximum allowed size of repository folder in MB
     */
    public int getMaxSize() {
        return maxSize;
    }

    /**
     * Returns path to repository folder as specified in configuration.
     *
     * @return path to repository folder
     */
    public String getRepositoryFolderPath() {
        return folder;
    }

    /**
     * Returns object representation of repository folder.
     *
     * @return File object of repository folder
     */
    public File getFolder() {
        return new File(folder);
    }

    /**
     * Returns local port number used for pupouses of network communication.
     *
     * @return local port number
     */
    public int getPort() {
        return socket.getPort();
    }

    /**
     * Returns socket address of local aplication used for communication. Socket
     * address contains IP address and port number encapsulated in
     * InetSocketAddress object.
     *
     * @return socket address by local instance
     */
    public InetSocketAddress getSocketAddress() {
        return socket;
    }

    /**
     * Returns multicast address used to "broadcast" controll and supervisory
     * messages over network. Every client should listen on same address as
     * other clients send their messages to address they are configured to
     * listen to.
     *
     * @return InetAddress representing multicast controll address
     */
    public InetAddress getAddress() {
        return socket.getAddress();
    }

    /**
     * Returns unicast address of localhost, as in configuration user is allowed
     * to specify unicast address used for data transfer.
     *
     * @return address of this host
     */
    public InetAddress getLocalAddress() {
        return localhost;
    }

    /**
     * Returns number of bits used in subnet mask configured on local interface.
     *
     * @return length of subnet mask
     */
    public int getMaskLength() {
        return mask;
    }

    /**
     * Returns integer representing type of local host. Host type can be
     * unknown, notebook or desktop. Type of host is used for calculations of
     * reliability of local host as there is greater probility for notebooks
     * to disconnect from network.
     *
     * @return type of host
     * @see #getTypeDesc()
     */
    public int getType() {
        return type;
    }

    /**
     * Returns string descriptions of host types. Used in configuration menus.
     *
     * @return String[] containg descriptions
     */
    public String[] getTypeDesc() {
        return TYPE_DESC;
    }

    /**
     * Returns number of fragments file will be divided into.
     *
     * @return number of fragments
     */
    public int getFragmentCount() {
        return fragments;
    }

    /**
     * Returns number of checksums that will be calculated.
     *
     * @return number of checksums
     */
    public int getChecksumCount() {
        return checksums;
    }

    public File getSchedulerConfiguration() {
        return DEF_SCHED_CFG;
    }

    /**
     * Used to determine if default configuration values had to be loaded
     * instead of those stored in configuration file.
     *
     * @return true if and only if configuration file could not be  parsed
     */
    public boolean usingDefaults() {
        return usingDefs;
    }

    /**
     * Returns object representation of file in which configuration reader
     * expects configuration to be stored.
     * 
     * @return default File that is read for configuration values
     */
    public File getDefaultConfigFile() {
        return DEF_CFG;
    }

    public int getMaxBackupCount() {
        return DEF_MAX_BCPS;
    }

    /**
     * Sets maximum allowed size of repository folder. Minimum allowed size to
     * be used is 100MB. Meant for reading values from configuration dialog.
     *
     * @param size value in MB which repository cannot exceed
     */
    public void setMaxSize(String size) {
        int mb;
        try {
            mb = Integer.parseInt(size);
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Repository folder size must be number.");
        }
        if (mb > 100) {
            maxSize = mb;
        } else {
            throw new IllegalArgumentException("Repository folder size limit invalid or too small.");
        }
    }

    /**
     * Sets path to repository folder.  Meant for reading values from
     * configuration dialog.
     *
     * @param folder path to repository folder.
     */
    public void setRepositoryFolderPath(String folder) {
        File f = new File(folder);
        if (f.exists() && f.canRead() && f.canWrite()) {
            this.folder = folder;
        } else {
            throw new IllegalArgumentException("Invalid repository folder.");
        }
    }

    /**
     * Sets port number to be used for networking purpouses. Only range of
     * dynamic port numbers i.e from 49152 to 65534 can be used.  Meant for
     * reading values from configuration dialog.
     * 
     * @param portString
     */
    public void setPort(String portString) {
        int portNumber;
        try {
            portNumber = Integer.parseInt(portString);
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Port must be number.");
        }
        if (portNumber > 9151 && portNumber < 65535) {
            this.port = portNumber;
        } else {
            throw new IllegalArgumentException("Port number not in dynamic range.");
        }
    }

    /**
     * Sets multicast address using which other hosts will be contacted. Must be
     * multicast.  Meant for reading values from configuration dialog.
     *
     * @param address String of IP address
     */
    public void setAddress(String address) {
        InetAddress adr = null;
        try {
            adr = InetAddress.getByName(address);
        } catch (Exception ex) {
            throw new IllegalArgumentException("Invalid management address.");
        }
        if (adr.isMulticastAddress()) {
            this.address = address;
        } else {
            throw new IllegalArgumentException("Management address must be multicast.");
        }
    }

    /**
     * Sets type of local host. Valid values are public constants of this class.
     * Meant for reading values from configuration dialog.
     *
     * @param typeNumber
     */
    public void setType(int typeNumber) {
        if (typeNumber > -1 && typeNumber < TYPE_DESC.length) {
            this.type = typeNumber;
        } else {
            throw new IllegalArgumentException("Invalid host type.");
        }
    }

    /**
     * Sets number of fragments used for checksum calculations during backing
     * up. Meant for reading values from configuration dialog. Backup and
     * Recovery classes are not required to use this value.
     *
     * @param fragments string representation of number of fragments
     * @see backupAndRecovery.Backup
     * @see backupAndRecovery.Recovery
     */
    public void setFragments(String fragments) {
        int fcount;
        try {
            fcount = Integer.parseInt(fragments);
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Fragment count must be number.");
        }
        if (fcount > 1 && fcount < 30000) {
            this.fragments = fcount;
        } else {
            throw new IllegalArgumentException("Invalid fragment count.");
        }
    }

    /**
     * Sets number of checksums to be calculated during process of backing up.
     * Meant for reading values from configuration dialog. Backup and
     * Recovery classes are not required to use this value.
     *
     * @param checksums string representation of number of checksums
     * @see backupAndRecovery.Backup
     * @see backupAndRecovery.Recovery
     */
    public void setChecksums(String checksums) {
        int ccount;
        try {
            ccount = Integer.parseInt(checksums);
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Checksum count must be number.");
        }
        if (ccount > 1 && ccount < 30000) {
            this.checksums = ccount;
        } else {
            throw new IllegalArgumentException("Invalid checksum count.");
        }
    }

    /**
     * If fallback to default configuration parameters was necessary as result
     * of invalid configuration file, returns true. If reading of configuration
     * was succesfull and no defaults were neccessary, return false.
     *
     * @return true iff configuration file is malformed
     */
    public boolean configTrouble() {
        return warning;
    }
}
