/*
 * 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.importers;

import fr.gouv.finances.dgfip.utils.Pair;
import fr.gouv.finances.dgfip.xemelios.auth.XemeliosUser;
import fr.gouv.finances.dgfip.xemelios.common.FileInfo;
import fr.gouv.finances.dgfip.xemelios.common.PJRef;
import fr.gouv.finances.dgfip.xemelios.common.config.DocumentModel;
import fr.gouv.finances.dgfip.xemelios.common.config.DocumentsModel;
import fr.gouv.finances.dgfip.xemelios.data.DataConfigurationException;
import fr.gouv.finances.dgfip.xemelios.data.DataImpl;
import fr.gouv.finances.dgfip.xemelios.data.DataLayerManager;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

/**
 *
 * @author chm
 */
public class XemeliosArchiveImporter {
    private static final Logger logger = Logger.getLogger(XemeliosArchiveImporter.class);
    public static final transient String MANIFEST_FILENAME = "manifeste_XeMeLios.xml";
    private DocumentsModel documentsModel;
    private File archiveFirstFile;
    private Document manifest;
    Archive arch;
    private XemeliosUser user=null;
    private ImportServiceProvider isp;
    private FileInfo fileInfo = null;
    private ArrayList<File> filesToDrop, directoriesToDrop;
    public XemeliosArchiveImporter(DocumentsModel dms,File fToImport,ImportServiceProvider isp) {
        super();
        this.documentsModel=dms;
        this.archiveFirstFile=fToImport;
        this.isp = isp;
        fileInfo = new FileInfo();
        filesToDrop = new ArrayList<File>();
        directoriesToDrop = new ArrayList<File>();
    }
    public void setUser(XemeliosUser user) {
        this.user=user;
    }
    public XemeliosUser getUser() {
        if(user==null) {
            user = new XemeliosUser() {
                public String getId() {
                    return "authorized importer";
                }
                public String getDisplayName() {
                    return getId();
                }
                public boolean hasRole(String role) {
                    return true;
                }
                public boolean hasDocument(String document) {
                    return true;
                }

                public boolean hasCollectivite(String collectivite,DocumentModel dm) {
                    return true;
                }
            };
        }
        return user;
    }

    public FileInfo getFileInfo() {
        return fileInfo;
    }

    @SuppressWarnings("unchecked")
    public Errors doImport() {
        Errors errors = checkArchive();
        if(errors.containsError()) {
            return errors;
        }
        File directory = archiveFirstFile.getParentFile();
        for(Volume volume:arch.volumes) {
            File volumeFile = new File(directory,volume.fileName);
            try {
                ZipFile zf = new ZipFile(volumeFile);
                ZipEntry ze = zf.getEntry(MANIFEST_FILENAME);
                DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
                DocumentBuilder builder = domFactory.newDocumentBuilder();
                manifest = builder.parse(zf.getInputStream(ze));
                XPathFactory xpf = new net.sf.saxon.xpath.XPathFactoryImpl();
                XPath xp = xpf.newXPath();
                Double fCount = (Double)xp.evaluate("count(//document[@volume="+volume.num+"] | //action)", manifest, XPathConstants.NUMBER);
//                Double fCount = (Double)xp.evaluate("count(//action)", manifest, XPathConstants.NUMBER);
                isp.displayProgress(fCount.intValue());
                Object o = xp.evaluate("//document[@volume="+volume.num+"] | //action", manifest, XPathConstants.NODESET);
                if(o instanceof NodeList) {
                    NodeList nl = (NodeList)o;
                    for(int i=0;i<nl.getLength();i++) {
                        Element document = (Element)nl.item(i);
                        doImportDocument(zf,document);
                    }
                } else if(o instanceof List) {
                    for(Element document:(List<Element>)o) {
                        if(document.getNodeName().equals("action")) {
                            doApplyAction(zf,document,xpf);
                        } else {
                            doImportDocument(zf,document);
                        }
                    }
                }

            } catch(Exception ex) {
                logger.error("",ex);
                if(ex instanceof ZipException) {
                    String msg = "Cette archive a probablement été renommée.\nSon nom d'orgine devait être \n\t"+volume.fileName+"\nElle ne peut être importée que si elle porte ce nom.";
                    boolean previousValue = isp.shouldDisplayFeedback();
                    errors.addError(Errors.SEVERITY_ERROR, msg);
                }
            } finally {
                cleanTempFiles();
                // TODO: fermer la ProgressDlg
            }
        }
//        logger.debug("endof doImport()");
        return errors;
    }
    protected void doImportDocument(ZipFile zf,Element document) throws IOException {
        String filePath = document.getAttribute("path");
        String type = document.getAttribute("type");
        String idColl = document.getAttribute("buIdCol");
        String libColl = document.getAttribute("buLibelle");
        String idBudg = document.getAttribute("buCode");
        String libBudg = "Budget Principal";
        String sTmp = document.getAttribute("libBudget");
        if(sTmp!=null && sTmp.length()>0) libBudg=sTmp;
        String codic = document.getAttribute("buCodic");
        String codColl = document.getAttribute("buCodeCol");
        String nomOrigine = document.getAttribute("nomOrigine");
        String idUnique = document.getAttribute("idUnique");
        String docId = null;
        if("CGE".equals(type)) {
            docId = "compteGestion";
        } else if("CGETAT".equals(type)) {
            docId = "compteGestionEtat";
        } else if("PES".equals(type)) {
            docId = "PES_Aller";
        } else if("PJ".equals(type)) {
            docId = null;
        } else if("ERTN".equals(type)) {
            docId = "ertn";
        } else if("EDMN".equals(type)) {
            docId = "edmn";
        } else {
            docId = type;
        }
        String skipProperty = null;
        if(document.getAttribute("skip-if-exists")!=null && document.getAttribute("skip-if-exists").length()>0)
            skipProperty = document.getAttribute("skip-if-exists");
        if(skipProperty!=null) {
            if("true".equals(System.getProperty(skipProperty))) {
                logger.info("skipping "+filePath+" because "+skipProperty+" exists.");
                return;
            }
        }
        String overwriteRule = EtatImporteur.OVERWRITE_RULE_DEFAULT;
        if(document.getAttribute("default-overwrite")!=null && document.getAttribute("default-overwrite").length()>0) {
            overwriteRule = document.getAttribute("default-overwrite");
        }
        Pair coll = new Pair(idColl,libColl);
        Pair budg = new Pair(idBudg,libBudg);
        if(docId!=null) {
            doImportXmlDocument(docId,zf,filePath,coll,budg, overwriteRule);
        } else {
            doImportPJ(zf,filePath,coll,nomOrigine, idUnique, overwriteRule);
        }
    }
    protected void doImportXmlDocument(String docId, ZipFile zf, String filePath,Pair collectivite, Pair budget, String overwriteRule) throws IOException {
        File source = extractFileFromZip(zf,filePath);
        DocumentModel dm = documentsModel.getDocumentById(docId);
        try {
            Class clazz = Class.forName(dm.getImportClass());
            Constructor cc = clazz.getConstructor(XemeliosUser.class);
            Object obj = cc.newInstance(getUser());
            if(!(obj instanceof EtatImporteur)) {
                throw new DataConfigurationException("Cette classe n'est pas un importeur.\nLe fichier de configuration qui vous a été livré est certainement invalide.\nVeuillez contacter votre fournisseur.");
            }
            EtatImporteur ei = (EtatImporteur)obj;
            ei.setImpSvcProvider(isp);
            ei.setOverwriteRule(overwriteRule);

            isp.setEtatImporter(ei);
            isp.setCollectivite(collectivite);
            isp.setBudget(budget);
            ei.setDocument(dm);
            File[] fichiers = new File[] { source };
            ei.setFilesToImport(fichiers);
            ei.run();
            fileInfo.merge(ei.getFileInfo());
        } catch(Exception ex) {
            logger.error("importer",ex);
        } finally {
            if(source.exists()) source.delete();
        }
    }
    protected void doImportPJ(ZipFile zf, String filePath, Pair collectivite, String nomOrigine, String idUnique, String overwriteRule) throws IOException {
        File f = extractFileFromZip(zf, filePath);
        try {
            // les PJ sont systématiquement écrasées
            DataImpl impl = DataLayerManager.getImplementation();
            PJRef pj = new PJRef();
            pj.setCollectivite(collectivite.key);
            pj.setFileName(nomOrigine);
            pj.setPjName(idUnique);
            pj.setUncompressedSize(f.length());
            pj.setTmpFileName(f.getAbsolutePath());
            impl.importPj(pj, getUser());
        } catch(Exception ex) {
            logger.error("doImportPJ(ZipFile,String,Pair,Pair,String,String)",ex);
        } finally {
            if(f.exists()) f.delete();
        }
    }
    protected void doApplyAction(ZipFile zf,Element action,XPathFactory xpf) throws XPathExpressionException, IOException, Exception {
        String className = action.getAttribute("class");
        Class clazz = Class.forName(className);
        Object instance = clazz.newInstance();
        if(!(instance instanceof AbstractImportPatcherImpl)) throw new Exception(className+" n'est pas un AbstractImportPatcherImpl");
        AbstractImportPatcherImpl patcher = (AbstractImportPatcherImpl)instance;
        XPath xp = xpf.newXPath();
        Object o = xp.evaluate("parameter", action, XPathConstants.NODESET);
        if(o instanceof NodeList) {
            // a faire un jour, si on fait plus du saxon
        } else if(o instanceof List) {
            for(Element param:(List<Element>)o) {
                String paramName = param.getAttribute("name");
                String type = param.getAttribute("type");
                if("java.io.File".equals(type)) {
                    String value = (String)xp.evaluate("./text()",param,XPathConstants.STRING);
                    ZipEntry ze = zf.getEntry(value);
                    File f = extractFileFromZip(zf,value);
                    if(f!=null) {
                        filesToDrop.add(f);
                        patcher.setParameter(paramName, f);
                    }
                } else if("java.lang.String".equals(type)) {
                    String value = param.getAttribute("value");
                    if(value==null) {
                        value = (String)xp.evaluate("./text()",param,XPathConstants.STRING);
                    }
                    patcher.setParameter(paramName,value);
                } else if("java.lang.Integer".equals(type)) {
                    String value = param.getAttribute("value");
                    if(value==null) {
                        value = (String)xp.evaluate("./text()",param,XPathConstants.STRING);
                    }
                    patcher.setParameter(paramName,Integer.valueOf(value));
                }
            }
            patcher.setImportServiceProvider(isp);
            FileInfo info = patcher.run();
            fileInfo.merge(info);
        }
    }
    protected File extractFileFromZip(ZipFile zf,String filePath) throws IOException {
        File tmpDir = new File(System.getProperty("java.io.tmpdir"));
        ZipEntry ze = zf.getEntry(filePath);
        File ret = new File(tmpDir,filePath);
        File parent = ret.getParentFile();
        parent.mkdirs();
        InputStream is = zf.getInputStream(ze);
        OutputStream fos = new FileOutputStream(ret);
        byte[] buffer = new byte[1024];
        int read = is.read(buffer);
        do {
            if(read>0) fos.write(buffer, 0, read);
            read = is.read(buffer);
        } while(read>0);
        fos.flush(); fos.close();
        filesToDrop.add(ret);
        while(!parent.equals(tmpDir)) {
            directoriesToDrop.add(parent);
            parent = parent.getParentFile();
        }
        return ret;
    }
    @SuppressWarnings("unchecked")
    public Errors checkArchive() {
        Errors errors = new Errors();
        if(!archiveFirstFile.exists()) {
            errors.addError(Errors.SEVERITY_ERROR,"Le fichier est introuvable");
        } else {
            try {
                ZipFile zf = new ZipFile(archiveFirstFile,ZipFile.OPEN_READ);
                ZipEntry ze = zf.getEntry(MANIFEST_FILENAME);
                if(ze==null) {
                    errors.addError(Errors.SEVERITY_ERROR, "Manifest ("+MANIFEST_FILENAME+") non trouvé dans l'archive");
                } else {
                    DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
                    DocumentBuilder builder = domFactory.newDocumentBuilder();
                    manifest = builder.parse(zf.getInputStream(ze));
                    XPathFactory xpf = XPathFactory.newInstance();
                    XPath xp = xpf.newXPath();
                    Object o = xp.evaluate("//volume", manifest, XPathConstants.NODESET);
                    TreeSet<Volume> volumes = new TreeSet<Volume>();
                    if(o instanceof NodeList) {
                        NodeList nl = (NodeList)o;
                        for(int i=0;i<nl.getLength();i++) {
                            Element volume = (Element)nl.item(i);
                            volumes.add(new Volume(volume));
                        }
                    } else if(o instanceof List) {
                        for(Element volume:(List<Element>)o) {
                            volumes.add(new Volume(volume));
                        }
                    }
                    int thisArchiveNum = 0;
                    for(Volume v:volumes) {
                        if(v.fileName.equals(archiveFirstFile.getName())) {
                            thisArchiveNum = v.num;
                            break;
                        }
                    }
                    arch = new Archive();
                    arch.setVolumes(volumes);
                    // on verifie si tous les volumes sont presents
                    File directory = archiveFirstFile.getParentFile();
                    for(Volume v: volumes) {
                        if(v.num!=1) {
                            File archive = new File(directory,v.fileName);
                            if(!archive.exists()) {
                                arch.removeVolume(v);
                                if(thisArchiveNum==1) {
                                    errors.addError(Errors.SEVERITY_WARNING, v.fileName+" introuvable");
                                }
                            }
                        }
                    }
                }
            } catch(ZipException zEx) {
                errors.addError(Errors.SEVERITY_ERROR,archiveFirstFile.getName()+" : archive zip corrompue.");
                logger.info("archive zip corrompue:",zEx);
            } catch(Exception ex) {
                errors.addError(Errors.SEVERITY_ERROR,ex.getMessage());
            }
        }
        return errors;
    }
    protected void cleanTempFiles() {
        for(File f:filesToDrop) {
            f.delete();
        }
        for(File f:directoriesToDrop) {
            f.delete();
        }
        filesToDrop = new ArrayList<File>();
        directoriesToDrop = new ArrayList<File>();
    }
    public static class Errors {
        public static final int SEVERITY_NONE = 0;
        public static final int SEVERITY_INFO = 1;
        public static final int SEVERITY_WARNING = 2;
        public static final int SEVERITY_ERROR = 3;
        
        private List<Error> errors;

        private int maxSeverity = SEVERITY_NONE;
        private Errors() {
            super();
            errors = new ArrayList<Error>();
        }
        public void addError(int severity,String message) {
            if(severity>maxSeverity) maxSeverity = severity;
            errors.add(new Error(severity,message));
        }
        public boolean containsError() { return maxSeverity == SEVERITY_ERROR; }
        public List<Error> getErrors() { return errors; }

    }
    public static class Error {
        private int severity = Errors.SEVERITY_NONE;
        private String message;
        public Error(int severity, String message) {
            this.severity=severity;
            this.message=message;
        }
        public String getSeverityMessage() {
            switch(severity) {
                case Errors.SEVERITY_NONE: return "";
                case Errors.SEVERITY_INFO: return "Information";
                case Errors.SEVERITY_WARNING: return "Warning";
                case Errors.SEVERITY_ERROR: return "Erreur";
            }
            return null;
        }
        public int getSeverity() { return severity; }
        public String getMessage() { return message; }
    }
    private class Volume implements Comparable {
        String fileName;
        int num = 0;
        public Volume(Element el) {
            fileName = el.getAttribute("fichier");
            String str = el.getAttribute("num");
            try {
                num = Integer.parseInt(str);
            } catch(NumberFormatException nfEx) { }
        }

        public int compareTo(Object o) {
            if(o instanceof Volume) {
                return num - ((Volume)o).num;
            }
            return 1;
        }
    }
    private class Archive {
        TreeSet<Volume> volumes;
        public Archive() {
            super();
            volumes = new TreeSet<Volume>();
        }
        public void setVolumes(Set<Volume> vols) {
            volumes.addAll(vols);
        }
        public void removeVolume(Volume vol) {
            volumes.remove(vol);
        }
    }
}
