/*
 * Copyright
 *   2007 axYus - www.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.exports;

import fr.gouv.finances.dgfip.xemelios.batch.PostProcessor;
import fr.gouv.finances.cp.xemelios.export.ExportUndertaker;
import fr.gouv.finances.cp.xemelios.export.Exporter;
import fr.gouv.finances.dgfip.xemelios.auth.XemeliosUser;
import fr.gouv.finances.dgfip.xemelios.batch.Batch;
import fr.gouv.finances.dgfip.xemelios.batch.BatchRunner;
import fr.gouv.finances.dgfip.xemelios.common.Constants;
import fr.gouv.finances.dgfip.xemelios.common.config.DocumentModel;
import fr.gouv.finances.dgfip.xemelios.common.config.DocumentsModel;
import fr.gouv.finances.dgfip.xemelios.common.config.ElementModel;
import fr.gouv.finances.dgfip.xemelios.common.config.Loader;
import fr.gouv.finances.dgfip.xemelios.data.DataConfigurationException;
import fr.gouv.finances.dgfip.xemelios.data.DataLayerManager;
import fr.gouv.finances.dgfip.xemelios.data.DataResultSet;
import fr.gouv.finances.dgfip.xemelios.data.impl.pool.PoolManager;
import fr.gouv.finances.dgfip.xemelios.export.ConfigModel;
import fr.gouv.finances.dgfip.xemelios.export.ExportJob;
import fr.gouv.finances.dgfip.xemelios.utils.FileUtils;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import org.apache.log4j.Logger;

/**
 *
 * @author Christophe Marchand <christophe.marchand@axyus.com>
 */
public class ExportRunner extends Batch {
    private static final Logger logger = Logger.getLogger(ExportRunner.class);
    
    public static final String PROP_PRIORITY_LEVEL = "priority.level";
    public static final String PROP_MAX_CONCURRENT_THREADS = "max.concurrent.threads";
    public static final String PROP_DOCUMENTS_DEF_DIR = "documents-def.dir";
    public static final String PROP_DATALAYER_NAME = "datalayer.name";
    public static final String PROP_UNDERTAKER = "exporter.class.name";
    
    private int priorityLevel = 1;
    private int maxConcurrentThreads = 1;

    private String exportTempDirectory = null;
    private String undertakerClassName;

    private static DocumentsModel docs;
    private static boolean initialized = false;

    private ThreadPool<ExportRunnerImpl> pool = null;

    public ExportRunner(String args[]) {
        super(args);
    }

    @Override
    protected void initialize() throws Exception {
        super.initialize();
        String sTmp = getProps().getProperty(PROP_MAX_CONCURRENT_THREADS);
        if(sTmp!=null && sTmp.length()>0) {
            try {
                maxConcurrentThreads = Integer.parseInt(sTmp);
            } catch(Throwable t) {}
        }
        try {
            String s = getProps().getProperty(PROP_PRIORITY_LEVEL);
            if(s!=null && s.length()>0) {
                priorityLevel = Integer.parseInt(s);
            }
        } catch(Throwable t) {}
        String dirs = getProps().getProperty("documents-def.dir");
        File baseDir = BatchRunner.getBaseDir();
        String canon = baseDir.getCanonicalPath().replace("\\", "/");
        String documentsDefDirs = dirs.replaceAll("%basedir%", canon);
        undertakerClassName = getProps().getProperty(PROP_UNDERTAKER);
        staticInit(documentsDefDirs);
    }
    protected static void staticInit(String path) throws Exception {
        if(initialized) return;
        docs = Loader.getDocumentsInfos(path);
        initDataLayer();
        initialized = true;
    }

    @Override
    protected void doProcess() throws Exception {
        logger.debug("running export.runner for priority "+priorityLevel);
        Connection con = null;
        Statement st = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        ArrayList<ExportRunnerImpl> threads = new ArrayList<ExportRunnerImpl>();
        try {
            con = PoolManager.getInstance().getConnection();
            String sqlParam = "SELECT PARAM_VALUE FROM PARAMETERS WHERE PARAM_NAME='export.temp.directory'";
            st = con.createStatement();
            rs = st.executeQuery(sqlParam);
            if(rs.next()) {
                exportTempDirectory = rs.getString(1);
                rs.close();
            } else {
                logger.error("parameter 'export.temp.directory' not set !");
                throw new Exception("parameter 'export.temp.directory' not set !");
            }
            String sql = "SELECT EXPORT_ID, UTI_LOGIN FROM EXPORT_JOBS WHERE STATUS=1 AND PRIORITY=? AND CLEANED IS NULL";
            ps = con.prepareStatement(sql);
            ps.setInt(1,priorityLevel);
            rs = ps.executeQuery();
            while(rs.next()) {
                Long exportId = rs.getLong(1);
                String user = rs.getString(2);
                logger.debug("planning ("+exportId+","+user+") for priority "+priorityLevel+" - threads.size="+threads.size());
                ExportRunnerImpl impl = new ExportRunnerImpl(exportId, user,priorityLevel,exportTempDirectory,docs,undertakerClassName,getPostProcessors());
                logger.debug("ExportRunnerImpl created");
                threads.add(impl);
                logger.debug("thread "+exportId+","+user+"added, size is now "+threads.size());
            }
            logger.debug("creating pool for priority "+priorityLevel);
            pool = new ThreadPool<ExportRunnerImpl>(threads);
            logger.debug("starting pool for priority "+priorityLevel+" with "+maxConcurrentThreads+" max concurrent threads");
            pool.start(maxConcurrentThreads);
        } catch(SQLException sqlEx) {
            logger.error("doProcess()",sqlEx);
        } catch(Throwable t) {
            t.printStackTrace();
        } finally {
            if(con!=null) PoolManager.getInstance().releaseConnection(con);
        }
    }

    @Override
    public String getResumeTraitement() {
        return "export.runner.resume";
    }

    @Override
    public String typeTraitementRefCode() {
        return "export.runner<"+priorityLevel+">";
    }
    protected static void initDataLayer() throws DataConfigurationException {
        try {
            DataLayerManager.getImplementation();
        } catch(DataConfigurationException dcEx) {
            String availableLayers = System.getProperty(Constants.SYS_PROP_AVAILABLE_LAYERS,"fr.gouv.finances.cp.xemelios.data.impl.MySqlDataLayer");
            StringTokenizer tokenizer = new StringTokenizer(availableLayers, ",");
            while (tokenizer.hasMoreTokens()) {
                String className = tokenizer.nextToken();
                try {
                    Class.forName(className);
                } catch (ClassNotFoundException cnfEx) {
                    logger.error("instanciating data layer: ", cnfEx);
                }
            }
            DataLayerManager.setDataImpl("mysql");
        }
    }

    @Override
    public String getInformations() {
        StringBuilder sb = new StringBuilder();
        if(isStarted()) {
            sb.append(typeTraitementRefCode()).append(" - ").append(pool.getRunningThreadCount()).append(" running thread(s)\n");
            for(RunnableBatch rb: pool.getThreads()) {
                sb.append("\t").append(rb.getName()).append(": ").append(rb.isAlive()?"running":"idle").append("\n");
                if(rb.isAlive()) sb.append(rb.getInformations());
            }
        } else {
            sb.append(typeTraitementRefCode()).append(" - not started\n");
        }
        return sb.toString();
    }

    @Override
    protected String getBatchVersion() {
        return "1.0";
    }

    protected class ExportRunnerImpl extends RunnableBatch {
        private Long exportId;
        private String user;
        private int priority;
        private String exportDirectory;
        private DocumentsModel docs;
        private String undertakerClassName;
        private List<String> postProcessors;

        private ExportJob job = null;

        private File generatedFile;

        private PostProcessor pp;

        private boolean exportTerminated = false;

        public ExportRunnerImpl(Long exportId, String user, int priority, String exportDirectory, DocumentsModel docs, String undertakerClassName,List<String> postProcessors) {
            super();
            logger.debug("ExportRunnerImpl<init>("+exportId+","+user+")");
            this.exportId=exportId;
            this.user=user;
            this.priority=priority;
            this.exportDirectory=exportDirectory;
            this.docs=docs;
            this.undertakerClassName=undertakerClassName;
            this.postProcessors=postProcessors;
            setName("ExportRunnerImpl<"+exportId+","+user+">");

            switch(priority) {
                case 2: { setPriority(Thread.MIN_PRIORITY); break; }
                default: { setPriority(Thread.NORM_PRIORITY); break; }
            }
        }

        @Override
        public void run() {
            logger.debug("running ExportRunnerImpl for "+user+"/"+exportId);
            Connection con = null;
            File temp = null;
            try {
                job = DataLayerManager.getImplementation().getExportJob(exportId,user);
                con = PoolManager.getInstance().getConnection();
                String sql = "UPDATE EXPORT_JOBS SET STATUS="+ExportJob.STATUS_RUNNING+", RUN_DATE=NOW() WHERE EXPORT_ID=? AND UTI_LOGIN=?";
                PreparedStatement ps = con.prepareStatement(sql);
                ps.setLong(1, exportId);
                ps.setString(2,user);
                ps.executeUpdate();
                PoolManager.getInstance().releaseConnection(con); con=null;
                DataResultSet drs = job.getDataResultSet();
                File dir = new File(exportDirectory);
                temp = File.createTempFile("exp-", null, dir);
                temp.delete();
                temp.mkdir();
                String fileName = null;
                if(job.getFileName().toLowerCase().endsWith(".xls")) fileName = job.getFileName();
                else fileName = job.getFileName().concat(".xls");
                generatedFile = new File(temp,fileName);
                DocumentModel dm = docs.getDocumentById(job.getDocumentId());
                User user2 = new User(user);    // job.getOwner()
logger.debug("getting ConfigModel");
                ConfigModel cm = DataLayerManager.getImplementation().getConfigExport(job.getDocumentId(), job.getEtatId(), job.getExportConfigId().toString(), user2);
logger.debug("creating MyExporter");
                MyExporter exporter = new MyExporter(generatedFile, drs, dm, job.getEtatId(), dm.getEtatById(job.getEtatId()).getElementById(job.getElementId()), cm, this, undertakerClassName);
logger.debug("running myExporter");
                exporter.runImmediatly();
            } catch(Exception ex) {
                logger.error("run()",ex);
                try {
                    con = PoolManager.getInstance().getConnection();
                    String sql = "UPDATE EXPORT_JOBS SET STATUS="+ExportJob.STATUS_ERROR+", ERROR_MESSAGE=? WHERE EXPORT_ID=? AND UTI_LOGIN=?";
                    PreparedStatement ps = con.prepareStatement(sql);
                    ps.setString(1,ex.getMessage());
                    ps.setLong(2, exportId);
                    ps.setString(3, user);
                    ps.executeUpdate();
                } catch(SQLException sqlEx) {}
                if(temp!=null) {
                    if(temp.exists()) {
                        if(temp.isDirectory()) {
                            try {
                                FileUtils.dropRecursiveDirectory(temp);
                            } catch(Exception ex2) {}
                        } else {
                            temp.delete();
                        }
                    }
                }
            } finally {
                if(con!=null) PoolManager.getInstance().releaseConnection(con);
                // c'est fait par le MyExporter
                // getNotifier().notifyThreadStop(this);
            }
        }
        @SuppressWarnings("unchecked")
        public void notifyTerminated() {
            exportTerminated = true;
            Connection con = null;
            try {
                con = PoolManager.getInstance().getConnection();
                PreparedStatement ps = con.prepareStatement("UPDATE EXPORT_JOBS SET GENERATED_FILE_NAME=? WHERE EXPORT_ID=? AND UTI_LOGIN=?");
                ps.setString(1, generatedFile.getAbsolutePath());
                ps.setLong(2, exportId);
                ps.setString(3,user);
                ps.executeUpdate();
                PoolManager.getInstance().releaseConnection(con); con=null;
                job.setGeneratedFileName(generatedFile.getAbsolutePath());

                // post processors
                for(String postProcessor:postProcessors) {
                    pp = null;
                    try {
                        Class clazz = Class.forName(postProcessor);
                        Object o = clazz.newInstance();
                        pp = (PostProcessor)o;
                    } catch(Exception ex) {
                        logger.error("instanciating post processor",ex);
                    }
                    if(pp!=null) {
                        pp.setConfigData(job);
                        pp.postProcess();
                    }
                }

                con = PoolManager.getInstance().getConnection();
                ps=con.prepareStatement("UPDATE EXPORT_JOBS SET STATUS="+ExportJob.STATUS_TERMINATED+" WHERE EXPORT_ID=? AND UTI_LOGIN=?");
                ps.setLong(1, exportId);
                ps.setString(2, user);
                ps.executeUpdate();

            } catch(Exception ex) {
                logger.error("notifyTerminated()",ex);
                try {
                    String sql = "UPDATE EXPORT_JOBS SET STATUS="+ExportJob.STATUS_ERROR+", ERROR_MESSAGE=? WHERE EXPORT_ID=? AND UTI_LOGIN=?";
                    if(con==null) con = PoolManager.getInstance().getConnection();
                    PreparedStatement ps = con.prepareStatement(sql);
                    ps.setString(1,ex.getMessage());
                    ps.setLong(2, exportId);
                    ps.setString(3, user);
                    ps.executeUpdate();
                } catch(SQLException sqlEx) {}
                // cleanning
                File generated = new File(job.getGeneratedFileName());
                if(generated.exists()) {
                    try {
                        FileUtils.dropRecursiveDirectory(generated.getParentFile());
                    } catch(Throwable t) {}
                }
            } finally {
                if(con!=null) {
                    PoolManager.getInstance().releaseConnection(con);
                    con=null;
                }
            }
            getNotifier().notifyThreadStop(this);
        }

        @Override
        public String getInformations() {
            StringBuilder sb = new StringBuilder();
            sb.append("\t").append(getName()).append(":");
            if(isAlive()) {
                sb.append("export running.");
                if(exportTerminated) {
                    sb.append(" Main export terminated.");
                    if(pp!=null) {
                        sb.append(" Running post-processor ").append(pp.getClass().getName()).append(".");
                    }
                } else {
                    sb.append(" Main export running.");
                }
            } else {
                if(isStarted()) {
                    sb.append("export fully terminated.");
                } else {
                    sb.append("idle");
                }
            }
            sb.append("\n");
            return sb.toString();
        }
    }

    private class User implements XemeliosUser {
        private String userId;
        public User(String userId) {
            super();
            this.userId = userId;
        }
        public String getId() {
            return userId;
        }
        public String getDisplayName() {
            return userId;
        }
        public boolean hasRole(String role) {
            return true;
        }
        public boolean hasDocument(String document) {
            return true;
        }
        public boolean hasCollectivite(String collectivite, DocumentModel dm) {
            return true;
        }
    }

    private class MyExporter extends Exporter {
        private ExportRunnerImpl impl;
        private String undertakerClassName;

        public MyExporter(File f, DataResultSet drs, DocumentModel dm, String etatId, ElementModel em, ConfigModel cf, ExportRunnerImpl notifier, String undertakerClassName) {
            super(f,drs,dm,etatId,em,cf);
            this.undertakerClassName=undertakerClassName;
            if(this.undertakerClassName==null || this.undertakerClassName.length()==0) {
                this.undertakerClassName = "fr.gouv.finances.cp.xemelios.export.JxlExporter";
            }
            this.impl=notifier;
        }

        public void runImmediatly() throws Exception {
logger.debug("runImmediatly");
            doExport();
            cleanAfterRun();
logger.debug("notifyStop");
            impl.notifyTerminated();
        }





        @Override
        protected void done() {
logger.debug("MyExporter.done()");
            super.done();
            impl.notifyTerminated();
        }

        @Override
        protected ExportUndertaker getUndertaker() {
            try {
                Class undertakerClass = Class.forName(undertakerClassName);
                Class[] exporterArgsClass = new Class[]{Exporter.class};
                Object[] exporterArgs = new Object[]{this};
                Constructor constructor = undertakerClass.getConstructor(exporterArgsClass);
                ExportUndertaker undertaker = (ExportUndertaker) constructor.newInstance(exporterArgs);

                return undertaker;
            } catch (ClassNotFoundException e) {
                logger.error("",e);
                return null;
            } catch (NoSuchMethodException e) {
                logger.error("",e);
                return null;
            } catch (InstantiationException e) {
                logger.error("",e);
                return null;
            } catch (IllegalAccessException e) {
                logger.error("",e);
                return null;
            } catch (IllegalArgumentException e) {
                logger.error("",e);
                return null;
            } catch (InvocationTargetException e) {
                logger.error("",e);
                return null;
            }
        }

    }
}
