/*
 * Copyright
 *   2007 axYus - www.axyus.com
 *   2009 C.Marchand - christophe.marchand@axyus.com
 *
 * This file is part of XEMELIOS.
 *
 * XEMELIOS is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * XEMELIOS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with XEMELIOS; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package fr.gouv.finances.dgfip.xemelios.batch;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Properties;
import java.util.Timer;
import java.util.TimerTask;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import org.apache.log4j.Logger;
import org.apache.log4j.xml.DOMConfigurator;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

/**
 * This class plans batches, according to configuration file
 * @author chm
 */
public class BatchRunner {
    private static final Logger logger = Logger.getLogger(BatchRunner.class);
    public static final String DATE_FORMAT="HH:mm";
    private static final SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);
    private static final SimpleDateFormat debugSdf = new SimpleDateFormat("yyyy/MM/dd HH:mm");

    public static final int DEFAULT_STOP_PORT = 8006;
    public static final int DEFAULT_INFO_PORT = 8007;
    private File configFile;
    private ArrayList<MyTimer> timers;

    private static BatchRunner instance;
    private ArrayList<Batch> runningBatches;
    private BatchInformationListener infoListener = null;
    private boolean cancelled = false;

    public static void main(String args[]) {
        if (args.length == 0) {
            displaySyntax();
            throw new RuntimeException("invalid syntax");
        }
        File configFileName = new File(args[0]);
        if (!configFileName.exists()) {
            displaySyntax();
            throw new RuntimeException("config file not found: " + configFileName.getAbsolutePath());
        }
        if (args.length == 2 && "-stop".equals(args[1])) {
            new BatchRunner(configFileName, true);
        } else {
            new BatchRunner(configFileName);
        }
    }

    public BatchRunner(File configFile) {
        super();
        if(instance==null) {
            instance = this;
            runningBatches = new ArrayList<Batch>();
        }
        this.configFile = configFile;
        try {
            preInit();
        } catch(Throwable t) {
            return;
        }
        try {
            initialize();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public BatchRunner(File configFile, boolean stop) {
        super();
        if(instance==null) {
            instance = this;
            runningBatches = new ArrayList<Batch>();
        }
        this.configFile = configFile;
        try {
            preInit();
        } catch(Throwable t) {
            return;
        }
        if (!stop) {
            try {
                initialize();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        } else {
            try {
                DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
                domFactory.setNamespaceAware(true);
                DocumentBuilder domBuilder = domFactory.newDocumentBuilder();
                Document config = domBuilder.parse(configFile);
                XPathFactory xpf = XPathFactory.newInstance();
                XPath xp = xpf.newXPath();
                XPath xp2 = xpf.newXPath();
                String sStopPort = (String) xp.evaluate("/batches/@stop-listen-port", config, XPathConstants.STRING);
                int stopPort = DEFAULT_STOP_PORT;
                try {
                    stopPort = Integer.parseInt(sStopPort);
                } catch (NumberFormatException nfEx) {
                    throw new Exception("config file contains an invalid stop-port number");
                }
                stop(stopPort);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }

    @SuppressWarnings("unchecked")
    protected void initialize() throws Exception {
        DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
        domFactory.setNamespaceAware(true);
        DocumentBuilder domBuilder = domFactory.newDocumentBuilder();
        Document config = domBuilder.parse(configFile);
        XPathFactory xpf = new net.sf.saxon.xpath.XPathFactoryImpl();
        XPath xp = xpf.newXPath();
        XPath xp2 = xpf.newXPath();
        timers = new ArrayList<MyTimer>();
        String sStopPort = (String) xp.evaluate("/batches/@stop-listen-port", config, XPathConstants.STRING);
        int stopPort = DEFAULT_STOP_PORT;
        try {
            stopPort = Integer.parseInt(sStopPort);
        } catch (NumberFormatException nfEx) {
            throw new Exception("config file contains an invalid stop-port number");
        }
        String allowedStoppers = (String) xp.evaluate("/batches/@allowedStoppers", config, XPathConstants.STRING);
        try {
            startStopListener(stopPort,allowedStoppers);
        } catch(Exception ex) {
            // si on a pas pu démarrer le listener, on quitte
            for(Timer tm:timers) {
                tm.cancel();
                tm.purge();
            }
            return;
        }
        // on demarre le infoListener
        String sInfoPort = (String) xp.evaluate("/batches/@info-listen-port", config, XPathConstants.STRING);
        int infoPort = DEFAULT_INFO_PORT;
        try {
            infoPort = Integer.parseInt(sInfoPort);
        } catch (NumberFormatException nfEx) {
            Exception ex = new Exception("config file contains an invalid info-port number");
            logger.error("while reading info-port  number",ex);
        }
        try {
            startInfoListener(infoPort);
        } catch(Exception ex) {
            // si on a pas pu démarrer le listener, on logge
            logger.error("while starting info listener",ex);
        }

        Object ret = xp.evaluate("/batches/batch[not(exists(@periodicity))]", config, XPathConstants.NODESET);
        if (ret instanceof NodeList) {
        } else if (ret instanceof List) {
            for (Element batch : (List<Element>) ret) {
                String className = batch.getAttribute("class");
//                String periodicity = batch.getAttribute("periodicity");
//                String startTime = batch.getAttribute("start-time");
                Properties props = new Properties();
                List<Element> parameters = (List<Element>) xp2.evaluate("./parameter", batch, XPathConstants.NODESET);
                for (Element parameter : parameters) {
                    String pName = parameter.getAttribute("name");
                    String pValue = null;
                    if (parameter.hasAttribute("value")) {
                        pValue = parameter.getAttribute("value");
                    } else {
                        pValue = parameter.getTextContent();
                    }
                    if (pName != null && pValue != null) {
                        props.setProperty(pName, pValue);
                    }
                }
                List<Element> postProcessors = (List<Element>) xp2.evaluate("./post-processor", batch, XPathConstants.NODESET);
                ArrayList<String> postProcessorsClasses = new ArrayList<String>();
                for(Element el: postProcessors) {
                    postProcessorsClasses.add(el.getTextContent());
                }
                startNewBatch(className, /*periodicity, startTime,*/ props, postProcessorsClasses);
            }
        }


        ret = xp.evaluate("/batches/batch[@periodicity]", config, XPathConstants.NODESET);
        if (ret instanceof NodeList) {
        } else if (ret instanceof List) {
            for (Element batch : (List<Element>) ret) {
                String className = batch.getAttribute("class");
                String periodicity = batch.getAttribute("periodicity");
                String startTime = batch.getAttribute("start-time");
                Properties props = new Properties();
                List<Element> parameters = (List<Element>) xp2.evaluate("./parameter", batch, XPathConstants.NODESET);
                for (Element parameter : parameters) {
                    String pName = parameter.getAttribute("name");
                    String pValue = null;
                    if (parameter.hasAttribute("value")) {
                        pValue = parameter.getAttribute("value");
                    } else {
                        pValue = parameter.getTextContent();
                    }
                    if (pName != null && pValue != null) {
                        props.setProperty(pName, pValue);
                    }
                }
                List<Element> postProcessors = (List<Element>) xp2.evaluate("./post-processor", batch, XPathConstants.NODESET);
                ArrayList<String> postProcessorsClasses = new ArrayList<String>();
                for(Element el: postProcessors) {
                    postProcessorsClasses.add(el.getTextContent());
                }
                startNewBatch(className, periodicity, startTime, props, postProcessorsClasses);
            }
        }

    }

    protected void startNewBatch(String className, String periodicity, String startTime, Properties props, ArrayList<String> postProcessors) {
        if(cancelled) return;
        try {
            Class clazz = Class.forName(className);
            TimerBatchTask task = new TimerBatchTask(clazz, props, postProcessors);
            char unity = periodicity.charAt(periodicity.length() - 1);
            int qty = Integer.parseInt(periodicity.substring(0, periodicity.length() - 1));
            long period = 0;
            switch (unity) {
                case 's': {
                    period = qty * 1000;
                    break;
                }
                case 'm': {
                    period = qty * 1000 * 60;
                    break;
                }
                case 'h': {
                    period = qty * 1000 * 60 * 60;
                    break;
                }
                case 'd': {
                    period = qty * 1000 * 60 * 60 * 24;
                    break;
                }
                default:
                    throw new RuntimeException(periodicity + " is not a valid periodicity. Periodicity must be a quantity followed by 's', 'm', 'h' or 'd'");
            }
            if(startTime==null || startTime.length()==0) {
                try {
                    MyTimer timer = new MyTimer(className);
                    timers.add(timer);
                    timer.scheduleAtFixedRate(task, 1000, period);
                } catch(Exception ex) {
                    System.err.println("ERROR while starting "+task.batchClass.getName());
                }
            } else {
                Date d = sdf.parse(startTime);
                GregorianCalendar gc = new GregorianCalendar();
                GregorianCalendar thisMorning = new GregorianCalendar(gc.get(Calendar.YEAR),gc.get(Calendar.MONTH),gc.get(Calendar.DAY_OF_MONTH));
                Date start = new Date(thisMorning.getTimeInMillis()+d.getTime()+thisMorning.getTimeZone().getDSTSavings());
                MyTimer timer = new MyTimer(className);
                timers.add(timer);
                timer.scheduleAtFixedRate(task, start, period);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new RuntimeException(ex);
        }
    }
    protected void startNewBatch(String className, Properties props, ArrayList<String> postProcessors) {
        if(cancelled) return;
        try {
            Class clazz = Class.forName(className);
            Constructor<Batch> cstr = clazz.getConstructor(String[].class);
            Batch batch = cstr.newInstance(new Object[]{new String[]{}});
            batch.setProps(props);
            batch.setPostProcessors(postProcessors);
            batch.launchBatch();
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new RuntimeException(ex);
        }
    }

    public static void displaySyntax() {
        StringBuilder sb = new StringBuilder();
        sb.append("java ").append(BatchRunner.class.getName()).append(" <config-file> [-stop]\n\twhere <config-file> is the fully-qualified location of config file.");
        System.err.println(sb.toString());
    }

    public void stop(int port) throws Exception {
        logger.info("Stopping batch server");
        Socket so = new Socket("localhost", port);
        BufferedReader br = new BufferedReader(new InputStreamReader(so.getInputStream()));
        PrintWriter pw = new PrintWriter(so.getOutputStream(), true);
        String resp = br.readLine();
        if (resp.startsWith("+OK")) {
            pw.println("stop");
            resp = br.readLine();
        } else {
            logger.error("batch server has respond : " +resp);
        }
        so.close();
    }

    protected void preInit() throws Exception {
        // On initialise dans un premier temps les logs :
        File log4jConfiFile = getConfFile("log4j.conf.file", "log4j.xml");
        if (log4jConfiFile == null) {
            throw new Exception("Fichier de configuration de log4j introuvable");
        }
        DOMConfigurator.configure(log4jConfiFile.toString());
    }

    protected File getConfFile(String fileNameSystemProperty, String fileNameIfSystemPropertyNotFound) {
        return getConfFile(fileNameSystemProperty, fileNameIfSystemPropertyNotFound, fileNameIfSystemPropertyNotFound);
    }

    protected File getConfFile(String fileNameSystemProperty, String fileNameIfSystemPropertyNotFound, String errorDisplayName) {
        String confFileName = System.getProperty(fileNameSystemProperty);
        File confFile = null;
        if (confFileName == null) {
            confFileName = "conf" + File.separator + fileNameIfSystemPropertyNotFound;
            confFile = new File(confFileName);
            if (!confFile.exists()) {
                confFileName = fileNameIfSystemPropertyNotFound;
                confFile = new File(confFileName);
                if (!confFile.exists()) {
                    return null;
                }
            }
        } else {
            confFile = new File(confFileName);
            if (!confFile.exists()) {
                return null;
            }
        }
        return confFile;
    }

    protected void startInfoListener(int port) throws Exception {
        infoListener = new BatchInformationListener(this,port);
    }

    protected void startStopListener(int port, String allowedStoppers) throws Exception {
        StopServer stopServer = new StopServer(port, timers, allowedStoppers);
        try {
            stopServer.start();
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new RuntimeException("an other instance is already running.");
        }
    }

    public static File getBaseDir() {
        String runDir = System.getProperty("user.dir");
        File f = new File(runDir);
        // dans le cas d'un deploy batch
        if(f.getName().equals("bin")) return f.getParentFile();
        // dans le cas d'execution dans IDE
        else return f;
    }

    public static BatchRunner getInstance() { return instance; }
    public void registerBatch(Batch batch) {
        logger.info("registring batch "+batch.typeTraitementRefCode());
        runningBatches.add(batch);
    }
    public void unregisterBatch(Batch batch) {
        runningBatches.remove(batch);
        logger.info("unregistring batch "+batch.typeTraitementRefCode());
    }
    public String getInformations() {
        StringBuilder sb = new StringBuilder();
        for(Batch batch:runningBatches) {
            sb.append(batch.getInformations());
        }
        return sb.toString();
    }
    public class TimerBatchTask extends TimerTask {

        private Class batchClass;
        private Properties props;
        private List<String> postProcessors;

        public TimerBatchTask(Class clazz, Properties props, List<String> postProcessors) {
            super();
            this.batchClass = clazz;
            this.props = props;
            this.postProcessors=postProcessors;
        }

        @Override
        public void run() {
            try {
                @SuppressWarnings("unchecked")
                // TODO : vérifier que la précédente instance a finit
                Constructor<Batch> cstr = batchClass.getConstructor(String[].class);
                Batch batch = cstr.newInstance(new Object[]{new String[]{}});
                batch.setProps(props);
                batch.setPostProcessors(postProcessors);
                batch.launchBatch();
            } catch (Exception ex) {
                ex.printStackTrace();
                throw new RuntimeException(ex);
            }
        }
    }
}
