package schedule;

import main.AppManager;
import main.ConfigurationReader;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import backupAndRecovery.BackupMetaContainer;
import java.util.Date;
import org.apache.log4j.Logger;

/**
 * This class provides basic means to manage configuration of scheduled actions.
 * It provides functions to read, write modify and load configuration into
 * scheduler.
 *
 * This class is implemented as singleton to avoid collisions and
 * inconsistencies in scheduling of operations.
 *
 * Time required by scheduler to ensure proper timing of tasks must be stored in
 * configuration in following format: <i>dom,dow,hrs,min</i> where dom is day
 * of month, dow is day of week, hrs is hour of execution and min is minute of
 * execution. Same restrictions as to TimeTask parameters apply
 *
 * @author Marcel Duris
 * @see schedule.TaskTime
 */
public class ScheduleConfigManager {

    public static final String BACKUP = "backup";
    public static final String DELETE = "delete";
    private static final String ROOT = "schedule";
    private static final String TK = "task";
    private static final String AC = "action";
    private static final String PM = "param";
    private static final String TM = "time";
    private static final String NA = "notAfter";
    private ArrayList<TaskContainer> cfgDetails;
    private static ScheduleConfigManager manager;

    private ScheduleConfigManager() {
        cfgDetails = new ArrayList<TaskContainer>();
    }

    /**
     * Returns instance of configuration manager.
     * 
     * @return Instance of ScheduleConfigManager
     */
    public static ScheduleConfigManager getScheduleConfigManager() {
        if (manager == null) {
            manager = new ScheduleConfigManager();
        }
        return manager;
    }

    /**
     * Reads configuration from default configuration file and loads it into
     * specified scheduler adding new tasks to those already scheluded. This
     * scheduler must be reloaded to allow new tasks to be scheduled.
     *
     * If default configuration file doesn't exist, nothing is read.
     * 
     * @param scheduler class capable of scheduling classes implementing ITask interface
     * @throws IllegalArgumentException when task in configuration is configured with illegal argument
     * @throws DocumentException when XML structure of configuration is incorrect
     */
    public void parseFile(Scheduler scheduler) throws IllegalArgumentException, DocumentException {
        parseFile(ConfigurationReader.getCfg().getSchedulerConfiguration(), scheduler);
    }

    /**
     * Similar to parseFile(Scheduler) function. Allows user to specify which
     * file should be read for configuration.
     * 
     * @param configuration file containing configuration
     * @param scheduler class capable of scheduling classes implementing ITask interface
     * @throws IllegalArgumentException when task in configuration is configured with illegal argument
     * @throws DocumentException when XML structure of configuration is incorrect
     */
    public void parseFile(File configuration, Scheduler scheduler) throws IllegalArgumentException, DocumentException {
        if (!configuration.exists() || !configuration.canRead()) {
            return;
        }
        SAXReader reader = new SAXReader();
        Document doc = reader.read(configuration);
        parseDocument(doc, scheduler);
    }

    /**
     * Parses Xml structure for configuration of scheduler in similar manner as
     * both parseFile functions except fact, that configuration is read from Xml
     * and not from file.
     * 
     * @param document DOM object containg xml configuration
     * @param scheduler class capable of scheduling classes implementing ITask interface
     * @throws IllegalArgumentException when task in configuration is configured with illegal argument
     * @throws DocumentException when XML structure of configuration is incorrect
     */
    public void parseDocument(Document document, Scheduler scheduler) throws IllegalArgumentException, DocumentException {
        cfgDetails = new ArrayList<TaskContainer>();
        Element root = document.getRootElement();
        for (Object oTask : root.elements()) {
            try {
                Element eTask = (Element) oTask;
                String taskName = eTask.elementTextTrim(AC);
                String parameters = eTask.elementTextTrim(PM);
                String time = eTask.elementTextTrim(TM);

                long notAfter = 0;
                if (eTask.element(NA) != null) {
                    notAfter = Long.parseLong(eTask.elementTextTrim(NA));
                    if (new Date().after(new Date(notAfter))) {
                        //if now is later than latest time for task, skip adding
                        continue;
                    }
                }

                cfgDetails.add(new TaskContainer(taskName, parameters, time, notAfter));

                ITask task = parseTask(taskName, parameters, time);
                if (scheduler != null) {
                    scheduler.addTask(task);
                }
            } catch (Exception ex) {
                Logger.getLogger("qbackup.scheduler").warn("Scheduler configuration element " +
                        ((Element) oTask).getName() + " malformed.");
                throw new IllegalArgumentException("Malformed configuration.");
            }
        }
        Logger.getLogger("qbackup.scheduler").info("Scheduler configuration loaded.");
    }

    /**
     * Returns list of containers with configuration parameters of schedulable
     * tasks. Any tasks from any configuration file, that has been read from
     * configuration or added is in this list. Only exeptions are tasks, that
     * have been deleted by deleteTask(index) method.
     * 
     * @return ArrayList containing instances of ConfigDetailsContainer
     */
    public ArrayList<TaskContainer> getDetails() {
        return cfgDetails;
    }

    /**
     * Adds task into configuration.
     *
     * @param taskName name of task
     * @param param parameters to be passed to task at time of execution
     * @param dayOfMonth day of month to be passed to time iterator
     * @param dayOfWeek day of week to be passed to time iterator
     * @param hour hour to be passed to time iterator
     * @param minute minute to be passed to time iterator
     */
    public void addTask(String taskName, String param, int dayOfMonth, int dayOfWeek, int hour, int minute) {
        if (dayOfMonth != ITask.ANYTIME && dayOfWeek != ITask.ANYTIME) {
            throw new IllegalArgumentException("Day of week and day of month can not be both set.");
        }
        String taskTime = timeArgsToString(dayOfMonth, dayOfWeek, hour, minute);
        cfgDetails.add(new TaskContainer(taskName, param, taskTime));
    }

    public void addTask(String taskName, String param, int dayOfMonth, int dayOfWeek, int hour, int minute, Date notAfter) {
        if (dayOfMonth != ITask.ANYTIME && dayOfWeek != ITask.ANYTIME) {
            throw new IllegalArgumentException("Day of week and day of month can not be both set.");
        }
        String taskTime = timeArgsToString(dayOfMonth, dayOfWeek, hour, minute);
        cfgDetails.add(new TaskContainer(taskName, param, taskTime, notAfter.getTime()));
    }

    /**
     * Concatenates agruments used to determine time when action should run into
     * single string usable for storing of these parameters.
     *
     * @param dayOfMonth day of month
     * @param dayOfWeek day of week
     * @param hour hour
     * @param minute minute
     * @return single string representing passed arguments
     */
    private String timeArgsToString(int dayOfMonth, int dayOfWeek, int hour, int minute) {
        String taskTime = dayOfMonth + "," + dayOfWeek + "," + hour + "," + minute;
        return taskTime;
    }

    /**
     * Deletes task from configuration. Index of deleted task is same as in
     * list returned by getDetails() method.
     * 
     * @param index of task to be deleted from list of tasks
     */
    public void deleteTask(int index) {
        cfgDetails.remove(index);
    }

    /**
     * This method stops currently running scheduler and replaces configuration
     * it currently holds with configuration stored in manager. Scheduler is
     * then rescheduled so new configuration is used.
     *
     * @param scheduler Scheduler to be reconfigured
     */
    public void reconfigure(IScheduler scheduler) {
        scheduler.clear();
        for (TaskContainer taskConf : cfgDetails) {
            try {
                ITask task = parseTask(taskConf.getName(), taskConf.getParam(), taskConf.getTime());
                //ITask task = taskConf.getTask();
                scheduler.addTask(task);
            } catch (DocumentException ex) {
                Logger.getLogger("qbackup.scheduler").warn("Scheduler configuration malformed.");
            } catch (IllegalArgumentException ex) {
                Logger.getLogger("qbackup.scheduler").warn("Scheduler configuration malformed." + ex.getMessage());
            }
        }
        scheduler.reschedule();
    }

    /**
     * Saves current configuration into default configuration file belonging to
     * scheduler.
     * 
     * @throws FileNotFoundException if default configuration file cannot be created or altered.
     * @throws IOException if problem writing into configuration file.
     */
    public void saveConfig() throws FileNotFoundException, IOException {
        Document doc = DocumentHelper.createDocument();
        Element root = doc.addElement(ROOT);
        for (TaskContainer det : cfgDetails) {
            Element task = root.addElement(TK);
            task.addElement(AC).setText(det.getName());
            task.addElement(PM).setText(det.getParam());
            task.addElement(TM).setText(det.getTime());

            if (det.getNotAfter() != null) {
                task.addElement(NA).setText(det.getNotAfter());
            }
        }

        OutputStream out = new BufferedOutputStream(new FileOutputStream(ConfigurationReader.getCfg().getSchedulerConfiguration()));
        out.write(doc.asXML().getBytes());
        out.flush();
        out.close();
        Logger.getLogger("qbackup.scheduler").info("Scheduler configuration saved.");
    }

    /**
     * This method parses configuration parameters of single task in
     * configuration. Type of task to be executed, parameters and execution time
     * are determined and checked so correct instance of ITask can be returned.
     * 
     * @param taskName name of task from configuration
     * @param parameters String representation of parameters according to type of task
     * @param time day of month, day of week, hour and minute separated by commas
     * @return instance of ITask matching passed arguments
     * 
     * @throws DocumentException if configuration parameters, i.e value of parameters argument, is not in valid format
     * @throws IllegalArgumentException if time or task name is in invalid format
     * @see ITask
     */
    public ITask parseTask(String taskName, String parameters, String time) throws DocumentException, IllegalArgumentException {
        ITask task = null;

        int dom = 0;
        int dow = 0;
        int hod = 0;
        int min = 0;
        String times[] = time.split(",");
        if (times.length != 4) {
            throw new IllegalArgumentException("Invalid time format.");
        }
        try {
            dom = Integer.parseInt(times[0]);
            dow = Integer.parseInt(times[1]);
            hod = Integer.parseInt(times[2]);
            min = Integer.parseInt(times[3]);
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Invalid time format.");
        }

        if (taskName.compareToIgnoreCase(BACKUP) == 0) {
            task = parseBackup(parameters, dom, dow, hod, min);
        } else if (taskName.compareToIgnoreCase(DELETE) == 0) {
            task = parseDelete(parameters, dom, dow, hod, min);
        } else {
            throw new IllegalArgumentException("Unknown task name: " + taskName);
        }

        return task;
    }

    /**
     * Creates instance of task corresponding to configuration of backup task.
     * 
     * @param parameters path to file to be backed up
     * @param dom day of month
     * @param dow day of week
     * @param hod hour
     * @param min minute
     * @return schedulable ITask that will create backup of file
     */
    private ITask parseBackup(String parameters, int dom, int dow, int hod, int min) {

        final String path = parameters;
        ConfigurationReader cfg = ConfigurationReader.getCfg();
        final int n = cfg.getFragmentCount();
        final int m = cfg.getChecksumCount();

        return new Task(new Runnable() {

            public void run() {
                AppManager.backupFile(path, n, m);
            }
        }, dom, dow, hod, min);
    }

    /**
     * Creates instance of ITask that will delete existing from repository.
     * 
     * @param parameters Xml containg meta information abou backup
     * @param dom day of month
     * @param dow day of week
     * @param hod hour
     * @param min minute
     * @return schedulable ITask that will delete backup of file when run
     * @throws DocumentException if parameters argument does not contain valid backup meta information
     */
    private Task parseDelete(String parameters, int dom, int dow, int hod, int min) throws DocumentException {
        parameters = parameters.replaceAll("&lt;", "<").replaceAll("&gt;", ">");
        final BackupMetaContainer meta = new BackupMetaContainer(DocumentHelper.parseText(parameters));
        return new Task(new Runnable() {

            public void run() {
                AppManager.delete(meta);
            }
        }, dom, dow, hod, min);
    }

    /**
     * Converts parameters of various tasks into human readable format.
     *
     * @param name name of action
     * @param param parameters to parse
     * @return human readable parameters
     */
    public static String paramToHuman(String name, String param) {
        if (name.equals(DELETE)) {
            param = param.replaceAll("&lt;", "<").replaceAll("&gt;", ">");
            try {
                BackupMetaContainer meta = new BackupMetaContainer(DocumentHelper.parseText(param));
                String result = meta.getBackupName() + " created at " + meta.getBackupTime();
                return result;
            } catch (DocumentException ex) {
            }
        }
        return param;
    }

    /**
     * Converts concatenated string representation used to store execution time
     * into human readable format.
     *
     * @param time String containing concatenated time parameters
     * @return String containg human readable description of time
     */
    public static String timeToHuman(String time) {
        String times[] = time.split(",");
        int dom = Integer.parseInt(times[0]);
        int dow = Integer.parseInt(times[1]);
        int hod = Integer.parseInt(times[2]);
        int min = Integer.parseInt(times[3]);

        String result;
        String[] days = new String[]{"ANY", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
        if (dom == ITask.ANYTIME) {
            if (dow == ITask.ANYTIME) {
                result = "Every day";
            } else {
                result = "Every " + days[dow];
            }
        } else {
            result = "Each month on day " + dom;
        }

        if (hod == ITask.ANYTIME) {
            result += ", every hour at minute " + min;
        } else {
            result += " at " + hod + ":" + min;
        }

        return result;
    }
}
