/*
 * Copyright
 *   2007 axYus - www.axyus.com
 *   2007 JP.Tessier - jean-philippe.tessier@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 General Public License as published by
 * the Free Software Foundation; either version 2 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 General Public License for more details.
 *
 * You should have received a copy of the GNU 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.cp.xemelios.controls;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.Hashtable;
import java.util.Vector;

import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.validation.Schema;
import javax.xml.validation.Validator;

import org.apache.log4j.Logger;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import fr.gouv.finances.dgfip.utils.Pair;
import fr.gouv.finances.cp.utils.xml.marshal.XmlOutputter;
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.cp.xemelios.controls.models.ControlsModel;
import fr.gouv.finances.cp.xemelios.controls.models.DocumentControlModel;
import fr.gouv.finances.cp.xemelios.controls.models.ParamModel;
import fr.gouv.finances.cp.xemelios.controls.models.ParamModelTech;
import fr.gouv.finances.cp.xemelios.controls.tech.TECH01;
import fr.gouv.finances.cp.xemelios.controls.tech.TECH02;
import fr.gouv.finances.cp.xemelios.controls.tech.TECH03;
import fr.gouv.finances.cp.xemelios.ui.MainWindow;
import fr.gouv.finances.dgfip.utils.HostUtils;
import fr.gouv.finances.dgfip.xemelios.txt2xml.Txt2Xml;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import javax.xml.XMLConstants;
import javax.xml.validation.SchemaFactory;

public class MainControl implements Runnable {

    public static final int STATUS_NOT_CONTROLLED = -1;
    public static final int STATUS_NOT_XML = 1;
    public static final int STATUS_INVALID_ENCODING = 2;
    public static final int STATUS_NOT_SCHEMA_VALID = 3;
    public static final int STATUS_HAS_UNIT_CONTROL_ERRORS = 4;
    public static final int STATUS_CONTROL_NO_ERROR = 5;
    private final static Logger logger = Logger.getLogger(MainControl.class);
    private File fichierAController, rapport, inputXmlFile;
    private DocumentModel documentModel;
//    private DocumentsModel docsModel;
    private Hashtable<String, Object> hashParams = new Hashtable<String, Object>();
    private DocumentControlModel dcm;
    private Date datecontrole;
    private NamespaceContext nameSpaces;
    private SaxXmlWriter saxWriter = null;
    public static final String ID_DOCUMENT_RAPPORT = "DocumentRapport";
    private ControlsModel cm;
    private String IDrapport = "", domainesControles = "";
    private RapportWriter rapportWriter;
    private boolean hasTech01Error = false/*, hasTech02Error = false, hasTech03Error = false*/;
    private AbstractControlContentHandler cch = null;
    private File fichierEnrichi = null;
    private String fichierEnrichiLocation = null, rapportLocation = null;
    private ControlProgressListener cpl = null;
    private int status = STATUS_NOT_CONTROLLED;
    private ArrayList<Anomalie> anomaliesTechniques;
    private SAXParserFactory factorySAX = null;

    public MainControl(DocumentModel dm, DocumentsModel docsMod) {
        super();
        this.documentModel = dm;
//        this.docsModel = docsMod;
        cm = getControlsModel();
        dcm = cm.getDocumentById(dm.getId());
        nameSpaces = documentModel.getNamespaces();
        factorySAX = SAXParserFactory.newInstance();
        factorySAX.setNamespaceAware(true);
        anomaliesTechniques = new ArrayList<Anomalie>();
        saxWriter = new SaxXmlWriter();
    }

    public DocumentControlModel getDocumentControl() {
        return dcm;
    }

    protected boolean controlXmlSyntax(File inputFile, TECH01 tech01) {
        FileInputStream fisTech = null;
        try {
            fisTech = new FileInputStream(fichierAController);
            SAXParser parserXmlTech = factorySAX.newSAXParser();
            tech01.setDocModel(documentModel);
            parserXmlTech.parse(fisTech, tech01);
        } catch (FileNotFoundException e1) {
            e1.printStackTrace();
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (SAXException e) {
        } catch (IOException e) {
            e.printStackTrace();
        }
        domainesControles = tech01.getDomaines();
        anomaliesTechniques.addAll(tech01.getAnomalies());
        if (anomaliesTechniques.size() > 0) {
            status = STATUS_NOT_XML;
            return false;
        } else {
            return true;
        }
    }

    public void run() {
        if(getDocumentModel().getTextToXmlTransformer()!=null) {
            try {
                Class clazz = Class.forName(getDocumentModel().getTextToXmlTransformer());
                Txt2Xml transformer = (Txt2Xml) clazz.newInstance();
                if (transformer != null) {
                    inputXmlFile = transformer.transform(fichierAController);
                }
            } catch (Exception ex) {
                logger.error("while instancying textToXmlTransformer", ex);
            }
        }
        if(inputXmlFile==null) inputXmlFile = fichierAController;
        boolean technicalValidationFailed = false;
        TECH01 tech01 = new TECH01();
        TECH02 tech02 = new TECH02();
        TECH03 tech03 = new TECH03();
        tech03.setDocumentModel(documentModel);

        // on crée les éléments pour permettre l'écriture du rapport
        File tmpDir = new File(System.getProperty("java.io.tmpdir"), Constants.NOM_APP);
        if (!tmpDir.exists()) {
            tmpDir.mkdirs();
        }
        if (rapportLocation == null) {
            rapportLocation = new File(tmpDir, "rapport" + inputXmlFile.getName()).getAbsolutePath();
        }
        rapport = new File(rapportLocation);
        logger.debug("rapport: " + rapport.getAbsolutePath());
        datecontrole = new Date();

        Charset cs = Charset.forName("ISO-8859-1");
        OutputStreamWriter fos = null;
        try {
            fos = new OutputStreamWriter(new FileOutputStream(rapport), cs);
        } catch (Exception e1) {
            logger.error("while creating fos", e1);
        }
        rapportWriter = new RapportWriter(new XmlOutputter(fos, cs.name()));

        // on crée l'objet qui contient les controles
        cch = getControlContentHandler(documentModel, dcm, hashParams, MainWindow.instance, fichierAController.getName(), cpl);

        // on vérifie si le document est du XML
        hasTech01Error = !controlXmlSyntax(inputXmlFile, tech01);

        if (!hasTech01Error) {
            boolean shouldContinue = true;
            if (dcm.getControlTechById("TECH02") != null) {
                // on veut faire un controle d'encoding
                boolean isEncodingValid = tech02.encodingValid(dcm.getControlTechById("TECH02"), inputXmlFile);
                if (!isEncodingValid) {
                    Anomalie ano = new Anomalie(IdGenerator.nextId(),
                            "TECH02", "Contrôle de l'encoding du fichier.",
                            documentModel.getId(), "", "", "", "", "",
                            "Le fichier est encodé en " + tech02.getInputEncoding() + " alors qu'il devrait être encodé en " + tech02.getValidEncodings() + ".",
                            "Le fichier doit être encodé en " + tech02.getValidEncodings() + ".",
                            "BLOQUANT", "xpath namespacise",
                            new Hashtable<String, Object>());
                    anomaliesTechniques.add(ano);

                    // check if we should continue
                    shouldContinue = false;
                    ParamModelTech param = dcm.getControlTechById("TECH02").getParamTechById("failOnError");
                    if (param!=null && "false".equals(param.getValue())) {
                        shouldContinue = true;
                    } else {
                        technicalValidationFailed = true;
                    }
                    status = STATUS_INVALID_ENCODING;
                }
            }

            if (shouldContinue) {
                // now, check if we need a control against schema
                if (dcm.getControlTechById("TECH03") != null) {
                    // Contrôle de validité au schéma
                    SchemaFactory xmlsf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
                    File fileSchema = new File(documentModel.getBaseDirectory(), dcm.getControlTechById("TECH03").getParamTechById("schemaLocation").getValue());
                    Schema schema;
                    try {
                        schema = xmlsf.newSchema(fileSchema);
                        Validator validateur = schema.newValidator();
                        FileInputStream fis = new FileInputStream(inputXmlFile);
                        SAXSource saxSrc = new SAXSource(new InputSource(fis));
                        tech03.setDomainesControles(domainesControles);
                        validateur.setErrorHandler(tech03);
                        validateur.validate(saxSrc);
                        fis.close();
                    } catch (Exception ex) {
                        // an exception during schema loading
                        StringWriter sw = new StringWriter();
                        PrintWriter pw = new PrintWriter(sw, true);
                        ex.printStackTrace(pw);
                        Anomalie ano = new Anomalie(IdGenerator.nextId(),
                                "TECH03", "Contrôle de conformité as schéma du fichier.",
                                documentModel.getId(), "", "", "", "", "",
                                "Erreur inattendue lors du chargement du schéma:\n" + sw.toString(),
                                "Le schéma ne peut pas être chargé.",
                                "BLOQUANT", "xpath namespacise",
                                new Hashtable<String, Object>());
                        anomaliesTechniques.add(ano);
                    }
                    if (tech03.getAnomalies().size() > 0) {
                        // there is some errors...
                        anomaliesTechniques.addAll(tech03.getAnomalies());
                        shouldContinue = false;
                        // should we continue ?
                        ParamModelTech param = dcm.getControlTechById("TECH03").getParamTechById("failOnError");
                        if (param != null && "false".equals(param.getValue())) {
                            shouldContinue = true;
                        } else {
                            technicalValidationFailed = true;
                        }
                        status = STATUS_NOT_SCHEMA_VALID;
                    }
                }
            }
            // now, apply unit controls
            if (shouldContinue) {
                cch.setParameters(hashParams);
                cch.setControlsModel(dcm.getControls());
                try {
                    FileInputStream fis = new FileInputStream(inputXmlFile);
                    SAXParser parserXml = factorySAX.newSAXParser();
                    saxWriter.setNamespaceContext(nameSpaces);
                    if (fichierEnrichiLocation == null) {
                        fichierEnrichiLocation = new File(tmpDir, inputXmlFile.getName()).getAbsolutePath();
                    }
                    fichierEnrichi = new File(fichierEnrichiLocation);
                    DOMResult dr = new DOMResult();
                    saxWriter.setResult(dr);
                    cch.setSaxWriter(saxWriter);
                    cch.setNamespaceContext(nameSpaces);
                    cch.setFileName(fichierAController.getName());
                    parserXml.parse(fis, cch);

                    StreamResult sr = new StreamResult(fichierEnrichi);
                    TransformerFactory tf = net.sf.saxon.TransformerFactoryImpl.newInstance();
                    Transformer t = tf.newTransformer();
                    //                        t.setOutputProperty(OutputKeys.ENCODING, tech02.getInputEncoding().equals("") ? FileUtils.getFileEncoding(fichierAController) : tech02.getInputEncoding());
                    t.transform(new DOMSource(dr.getNode()), sr);
                    if (sr.getOutputStream() != null) {
                        sr.getOutputStream().flush();
                        sr.getOutputStream().close();
                    }
                    // ajout CHM
                    fis.close();
                    if(cch.getVTotalAnomalies().size()>0) {
                        anomaliesTechniques.addAll(cch.getVTotalAnomalies());
                        status = STATUS_HAS_UNIT_CONTROL_ERRORS;
                    } else {
                        status = STATUS_CONTROL_NO_ERROR;
                    }
                } catch (Exception ex) {
                    // an exception during schema loading
                    StringWriter sw = new StringWriter();
                    PrintWriter pw = new PrintWriter(sw, true);
                    ex.printStackTrace(pw);
                    Anomalie ano = new Anomalie(IdGenerator.nextId(),
                            "Contrôles métier", "Application des contrôles métier.",
                            documentModel.getId(), "", "", "", "", "",
                            "Erreur inattendue lors de l'application des contrôles métiers:\n" + sw.toString(),
                            ex.getMessage(),
                            "BLOQUANT", "xpath namespacise",
                            new Hashtable<String, Object>());
                    anomaliesTechniques.add(ano);
                }
            }
        }
        Pair pColl = cch.getCollectivite();
        if (pColl == null || pColl.key == null || pColl.libelle == null) {
            pColl = new Pair("00000000000000", "Collectivite de Contrôle");
        }
        Pair pBudg = cch.getBudget();
        if (pBudg == null || pBudg.key == null || pBudg.libelle == null) {
            pBudg = new Pair("00", "Budget de Contrôle");
        }
        try {
            writeRapport(pColl, pBudg, anomaliesTechniques, cch.MAX_ANOS);
        } catch (IOException ioEx) {
            // TODO
        }
        cpl.stopWaiting();
        if(technicalValidationFailed)         cpl.notifyTechniqueValidationFailed();

    }

    public Pair getCollectivite() throws IllegalStateException {
        if (status == STATUS_NOT_CONTROLLED) {
            throw new IllegalStateException("You must run control before calling getCollectivite()");
        }
        if (cch != null) {
            return cch.getCollectivite();
        } else {
            return null;
        }
    }

    public Pair getBudget() throws IllegalStateException {
        if (status == STATUS_NOT_CONTROLLED) {
            throw new IllegalStateException("You must run control before calling getBudget()");
        }
        if (cch != null) {
            return cch.getBudget();
        } else {
            return null;
        }
    }

    public Vector<Anomalie> getTotalAnomalies() throws IllegalStateException {
        if (status == STATUS_NOT_CONTROLLED) {
            throw new IllegalStateException("You must run control before calling getTotalAnomalies()");
        }
        return cch != null ? cch.getVTotalAnomalies() : new Vector<Anomalie>();
    }

    private ControlsModel getControlsModel() {
        ControlsModel ret = null;
        try {
            ControlParser cp = new ControlParser();
            cp.parse(new File(documentModel.getBaseDirectory(), documentModel.getControlConfigFile()));
            ControlsModel c = (ControlsModel) cp.getMarshallable();
            c.validate();
            ret = c;
        } catch (Exception ex) {
            logger.debug("Error in getControlsModel()!!!", ex);
        }
        return ret;
    }

    public void setFileToControl(File f) {
        this.fichierAController = f;
    }

    public File getFileToControl() {
        return fichierAController;
    }

    public void setParameters(Hashtable<String, Object> params) {
        this.hashParams = params;
    }

    private void writeRapport(Pair collec, Pair budg, ArrayList<Anomalie> vano, int maxanos) throws IOException {
        logger.debug("writing rapport: \n\tcollectivite = " + collec + "\n\tbuget=" + budg);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
        String msgAno = "";
        if (vano != null && vano.size() >= maxanos && !vano.get(0).getControleID().startsWith("TECH")) {
            msgAno = "true";
        } else {
            msgAno = "false";
        }

        IDrapport = datecontrole.getTime() + rapport.getName();
        // Entete du rapport
        Pair docid = new Pair("V", documentModel.getId());
        rapportWriter.writeElement("DocId", docid);
        rapportWriter.writeElement("NomFichier", new Pair("V", fichierAController.getName()), new Pair("fullPath", fichierAController.getAbsolutePath()), new Pair("host", HostUtils.getHostName()));
        //rapportWriter.writeElement("DateControle",new Pair("V",simpleDateFormat.format(new Date())));
        rapportWriter.writeElement("DateControle", new Pair("V", simpleDateFormat.format(datecontrole)));

        rapportWriter.openElement("Collectivite");
        rapportWriter.writeElement("Siret", new Pair("V", collec.key));
        rapportWriter.writeElement("Libelle", new Pair("V", collec.libelle));
        rapportWriter.closeElement("Collectivite");

        rapportWriter.openElement("Budget");
        rapportWriter.writeElement("Code", new Pair("V", budg.key));
        rapportWriter.writeElement("Libelle", new Pair("V", budg.libelle));
        rapportWriter.closeElement("Budget");

        rapportWriter.writeElement("TropAnos", new Pair("V", msgAno));
        rapportWriter.openElement("Domaines");
        String domaines[] = domainesControles.split(" / ");
        for (String domain : domaines) {
            rapportWriter.writeElement("Domaine", new Pair("V", domain));
        }
        rapportWriter.closeElement("Domaines");
        rapportWriter.openElement("Params");
        writeParameters();
        rapportWriter.closeElement("Params");

        // Corps du rapport
        rapportWriter.openElement("DonneesRapport");
        rapportWriter.openElement("Rapport");
        rapportWriter.writeElement("Id", new Pair("V", datecontrole.getTime() + rapport.getName()));
        if (vano != null) {
            Collections.sort(vano, Collections.reverseOrder());
            for (Anomalie anomalie : vano) {
                //writeAnomalie(anomalie);
                anomalie.marshall(rapportWriter);
            }
        }
        rapportWriter.closeElement("Rapport");
        rapportWriter.closeElement("DonneesRapport");
        if (cm.getMessage() != null) {
            rapportWriter.openElement("Message");
            rapportWriter.writeData(cm.getMessage().getHtmlContent());
            rapportWriter.closeElement("Message");
        }
        rapportWriter.endRapport();
        logger.debug("Le rapport se trouve dans : " + rapport.getParent());
    }

    @Deprecated
    private void writeAnomalie(Anomalie a) {
        rapportWriter.openElement("Anomalie");
        rapportWriter.writeElement("Id", new Pair("V", a.getIdAnomalie()));
        rapportWriter.writeElement("IdCtrl", new Pair("V", a.getControleID()));
        rapportWriter.writeElement("LibelleCtrl", new Pair("V", a.getControleLibelle()));
        rapportWriter.writeElement("EtatId", new Pair("V", a.getEtatID()));
        rapportWriter.writeElement("ElementImportable", new Pair("V", a.getelementImportable()));
        rapportWriter.writeElement("IdElementImportable", new Pair("V", a.getIdElementImportable()));
        rapportWriter.writeElement("ElementEnAnomalie", new Pair("V", a.getelementEnAnomalie()));
        rapportWriter.writeElement("LibelleLien", new Pair("V", a.getLibelleLien()));

        rapportWriter.openElement("Message");
        rapportWriter.writeData(a.getMessage().replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("&", "&amp;"));
        rapportWriter.closeElement("Message");

        rapportWriter.openElement("Regle");
        rapportWriter.writeData(a.getRegle().replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("&", "&amp;"));
        rapportWriter.closeElement("Regle");

        rapportWriter.writeElement("Severity", new Pair("V", a.getSeverity()));
        if (a.getXPath() != null) {
            rapportWriter.writeElement("XPath", new Pair("V", a.getXPath()));
        }
        if (a.getXslParams() != null && a.getXslParams().size() > 0) {
            for (String key : a.getXslParams().keySet()) {
                String value = a.getXslParams().get(key).toString();
                rapportWriter.openElement("XslParam");
                rapportWriter.writeElement("Key", new Pair("V", key));
                rapportWriter.writeElement("Value", new Pair("V", value));
                rapportWriter.closeElement("XslParam");
            }
        }
        rapportWriter.closeElement("Anomalie");
    }

    private String writeParameters() {
        StringBuffer params = new StringBuffer();
        for (ParamModel param : dcm.getParams()) {
            String paramValue = "";
            if (hashParams.get(param.getId()).getClass().equals(Boolean.class)) {
                if (hashParams.get(param.getId()).equals(true)) {
                    paramValue = "Oui";
                } else if (hashParams.get(param.getId()).equals(false)) {
                    paramValue = "Non";
                } else {
                    paramValue = "Nc";
                }
            } else if (hashParams.get(param.getId()).getClass().equals(BigDecimal.class)) {
                paramValue = hashParams.get(param.getId()).toString();
            } else if (hashParams.get(param.getId()).getClass().equals(String.class)) {
                paramValue = hashParams.get(param.getId()).toString();
            } else if (hashParams.get(param.getId()).getClass().equals(Integer.class)) {
                paramValue = hashParams.get(param.getId()).toString();
            } else if (hashParams.get(param.getId()) instanceof String[]) {
                if (((String[]) hashParams.get(param.getId())).length == 0) {
                    paramValue = "Non Renseigné";
                }
                for (int i = 0; i < ((String[]) hashParams.get(param.getId())).length; i++) {
                    paramValue += (i != 0 ? ", " : "") + ((String[]) hashParams.get(param.getId()))[i];
                }
            } else {
                paramValue = hashParams.get(param.getId()).getClass().getName();
            }
            rapportWriter.writeElement("Param", new Pair("Name", param.getName()), new Pair("Value", paramValue));
        }
        return params.toString();
    }

    public void writeControlConfigFile() throws IOException {
        File output = new File(documentModel.getBaseDirectory(), documentModel.getControlConfigFile());
        OutputStreamWriter os = new OutputStreamWriter(new FileOutputStream(output), "ISO-8859-1");
        XmlOutputter outputter = new XmlOutputter(os, "ISO-8859-1");
        cm.marshall(outputter);
        os.flush();
        os.close();
    }

    public int getControlStatus() {
        return status;
    }

    public DocumentModel getDocumentModel() {
        return documentModel;
    }

    public File getFichierEnrichi() throws IllegalStateException {
        if (status == STATUS_NOT_CONTROLLED) {
            throw new IllegalStateException("You must run control before calling getFichierEnrichi()");
        }
        return fichierEnrichi;
    }

    public File getRapport() throws IllegalStateException {
        if (status == STATUS_NOT_CONTROLLED) {
            throw new IllegalStateException("You must run control before calling getRapport()");
        }
        return rapport;
    }

    public String getIdRapport() {
        return IDrapport;
    }

    public void setControlProgressListener(ControlProgressListener cpl) {
        this.cpl = cpl;
    }

    public void setFichierEnrichiLocation(String absoluteFileName) throws IllegalStateException {
        if (status != STATUS_NOT_CONTROLLED) {
            throw new IllegalStateException("You must not change fichierEnrichi location after control");
        }
        fichierEnrichiLocation = absoluteFileName;
    }

    public void setRapportLocation(String absoluteFileName) throws IllegalStateException {
        if (status != STATUS_NOT_CONTROLLED) {
            throw new IllegalStateException("You must not change rapport location after control");
        }
        rapportLocation = absoluteFileName;
    }

    public AbstractControlContentHandler getControlContentHandler(DocumentModel dm, DocumentControlModel docCtrlMod, Hashtable<String, Object> parameters,
            MainWindow owner, String file, ControlProgressListener cpl) {
        return new DefaultControlContentHandler(dm, docCtrlMod, parameters, owner, file, cpl);
    }

    /**
     * Permet de renvoyer les informations clefs sur le rapport genere
     */
    public static class Triplet {

        public String iDrapport;
        public String codeCollectivite;
        public String codeBudget;

        public Triplet(String idRapport, String codColl, String codBud) {
            super();
            this.iDrapport = idRapport;
            this.codeCollectivite = codColl;
            this.codeBudget = codBud;
        }
    }
}
