/*
 * Copyright 
 *   2005 axYus - www.axyus.com
 *   2005 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 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.importers;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Stack;
import java.util.Vector;

import org.apache.log4j.Logger;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import fr.gouv.finances.cp.utils.NullWriter;
import fr.gouv.finances.cp.utils.Pair;
import fr.gouv.finances.cp.utils.StringOutputStream;
import fr.gouv.finances.cp.utils.xml.NamespaceContextImpl;
import fr.gouv.finances.cp.xemelios.common.FileInfo;
//import fr.gouv.finances.cp.xemelios.common.config.CopyToModel;
import fr.gouv.finances.cp.xemelios.common.config.DocumentModel;
import fr.gouv.finances.cp.xemelios.common.config.EnteteModel;
import fr.gouv.finances.cp.xemelios.common.config.EtatModel;
import fr.gouv.finances.cp.xemelios.common.config.SpecialKeyModel;
import fr.gouv.finances.cp.xemelios.utils.XmlUtils;

/**
 * This class splits a file according to its "importable element"'s definition.
 * It also calculates special keys, budget and colletivites if it cans. 
 * This class is a spin-off from pre-3.0 DefaultImporteur.
 * 
 * @since 3.0
 * @author chm
 */
public class XmlSplitter extends DefaultHandler {
	private static Logger logger=Logger.getLogger(XmlSplitter.class);
	
    private static final Pair NULL_VALUE=new Pair("null-value","null-value");
    private byte[] LINE_SEP = System.getProperty("line.separator").getBytes();
    int elementDepth = 0;
    private DocumentModel dm = null;
    private OutputStream header = null, data = null, ref = null, footer = null, out = null;
    private Vector<File> fDatas = new Vector<File>();
    private int elementCount = 0, startIgnoredLevel = 0;
    private File tmpDir;
    private Stack<String> tagStack = new Stack<String>();
    private Stack<StringBuilder> charactersStack = new Stack<StringBuilder>();
    private boolean inRef = false;
    private StringOutputStream etatHeader = null;
    private EtatModel currentEtat;
    private String startIgnored;
    private FileInfo fileInfo;
    private Locator locator;
//    private Connection currentConnection;
    private Pair budget = null;
    private Pair collectivite = null;
    private Pair specialKey1=null, specialKey2=null, specialKey3=null;
    private String fileEncoding=null;
    private NamespaceContextImpl nsCtx;

    public XmlSplitter(OutputStream header, OutputStream ref, OutputStream footer, File tmpDir, DocumentModel dm, String fileEncoding) {
        super();
        this.header = header;
        this.data = null;
        this.ref = ref;
        this.footer = footer;
        this.tmpDir = tmpDir;
        this.dm = dm;
        this.fileInfo = new FileInfo();
        this.fileEncoding=fileEncoding;
        nsCtx = new NamespaceContextImpl();
        if(footer==null) logger.error("XmlSplitter is created with a null footer");
    }

    @Override
	public void setDocumentLocator(Locator locator) {
        this.locator = locator;
    }

    public Locator getLocator() {
        return locator;
    }

    public Vector<File> getDataFiles() {
        return fDatas;
    }

    @Override
	public void characters(char[] ch, int start, int length) throws SAXException {
        try {
            String s = new String(ch, start, length).trim();
            if (s.length() > 0) {
                if(out != null) {
                	//String s2 = StringEscapeUtils.escapeXml(s);
                    String s2 = s.replaceAll("&","&amp;").replaceAll("\"","&quot;").replaceAll("'","&apos;").replaceAll("<","&lt;").replaceAll(">","&gt;");
                    out.write(s2.getBytes(fileEncoding));
                }
                charactersStack.peek().append(new String(ch,start,length));
            }
        }
        catch (IOException ioEx) {
            throw new SAXException(ioEx);
        }
    }

    @Override
	public void endDocument() throws SAXException {
        out = null;
        try {
            header.flush();
            header.close();
            ref.flush();
            ref.close();
            footer.flush();
            footer.close();
        }
        catch (IOException ioEx) {
            throw new SAXException(ioEx);
        }
    }

    @Override
	public void endElement(String uri, String localName, String qName) throws SAXException {
        try {
        	if(tagStack.size()==1) {
        		// PATCH tout pourri a totof
        		out=footer;
        	}
            String tagName = XmlUtils.getShortTagName(uri, localName, qName);
            closeTag(uri, localName, qName);
            switch (tagStack.size()) {
                case 1: {
                    // fermeture du tag racine
                    if (!tagName.equals(dm.getBalise()))
                        throw new SAXException("Fermeture d'un element root inattendu");
                    break;
                }
                case 2: {
                    // fermeture d'un referentiel, d'un etat ou d'un ignore
                    if (inRef || (startIgnoredLevel == 2 && startIgnored.equals(tagName))) {
                        // ignore
                        if (out != ref) {
                            out.flush();
                            out.close();
                        }
                        // par defaut, le prochain c'est le footer
                        out = footer;
                        if(footer==null) logger.error("footer is null !");
                        startIgnoredLevel = 0;
                        if (inRef)
                            inRef = false;
                    } else if (currentEtat != null && currentEtat.getBalise().equals(tagName)) {
                        // fermeture de l'etat
                        if (data != null)
                            closeData(uri,localName,qName);
                        out = footer;
                        if(footer==null) logger.error("footer is null !");
                        currentEtat = null;
                        // AJOUT ICI
//                        if (currentConnection != null) {
//                            try {
//                                currentConnection.commit();
//                                currentConnection.close();
//                                currentConnection = null;
//                            }
//                            catch (SQLException sqlEx) {}
//                        }
                        // FIN AJOUT
                    } else if (dm.getEntetes().contains(tagName)) {
                        out.flush();
                        out = header;
                    }
                    break;
                }
                case 3: {
                    if (currentEtat != null && currentEtat.getEntetes().contains(tagName)) {
                        // fermeture d'un entete d'etat
                        out.flush(); // pour etre sur
                        out = new NullWriter(); // par defaut, il doit y avoir
                                                // des elements a venir.
                        // Mais si ce n'est pas le cas, on ne doit jamais fermer
                        // le tag d'etat (c'est fait dans closeData()
                    } else if (currentEtat != null && currentEtat.getImportableElement().getBalise().equals(tagName)) {
                        elementCount++;
                        if (elementCount == 1) {	// limit is always 1 now
                            closeData(uri,localName,qName);
                        }
                        out = new NullWriter(); // cf ci-dessus
                    } else if (currentEtat != null && startIgnoredLevel == 3 && startIgnored.equals(tagName)) {
                        out.close();
                        out = new NullWriter(); // cf vous savez ou
                    }
                    break;
                }
            }
            String path = XmlUtils.getPath(tagStack);
            String innerData = charactersStack.pop().toString();
            checkBudgetCollectivite(path,innerData);
            checkSpecialKey(path,innerData);
            tagStack.pop();
        }
        catch (IOException ioEx) {
            throw new SAXException(ioEx);
        }
    }

    @Override
	public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        try {
            String tagName = XmlUtils.getShortTagName(uri, localName, qName);
//            logger.debug("<"+tagName);
            switch (tagStack.size()) {
                case 0: {
                    // si ce n'est pas le tag du doc que l'on importe, bye
                    if (!tagName.equals(dm.getBalise()))
                        throw new SAXException("Le fichier importé n'est pas un " + dm.getTitre());
                    // sinon, on ecrit dans l'entete du doc
                    out = header;
//                    logger.debug(tagName+"\tdocument tag");
                    break;
                }
                case 1: {
                    if (dm.getEntetes().contains(tagName)) {
                        // c'est un tag d'entete de document
                        out = header;
//                        EnteteModel em = dm.getMap().get(tagName);
//                        if (em != null && em.getCopies().size() > 0) {
//                            StringBuffer buff = new StringBuffer();
//                            buff.append(new String(getXmlText(uri, localName, qName, attributes))).append(new String(getCloseTag(uri, localName, qName)));
//                            em.setData(buff.toString());
//                        }
//                        logger.debug(tagName+"\tdocument entete");
                    } else if (dm.getReferentiel().equals(tagName)) {
                        // c'est le referentiel
                        out = ref;
                        inRef = true;
//                        logger.debug(tagName+"\tdocument referentiel");
                    } else {
                        // demarre-t-on un nouvel etat ?
                        EtatModel em = dm.getEtatByTagName(tagName);
                        if (em != null) {
                            // oui !
                            // inEtat = true;
                            currentEtat = em;
                            etatHeader = new StringOutputStream();
                            out = etatHeader;
//                            logger.debug(tagName+"\tetat tag"+em.getId());
                        } else {
                            // non, on ignore
                            out = new NullWriter();
                            startIgnored = tagName;
                            startIgnoredLevel = tagStack.size() + 1;
//                            logger.debug(tagName+"\tignored etat");
                        }
                    }
                    break;
                }
                case 2: {
                    if (currentEtat != null && currentEtat.getEntetes().contains(tagName)) {
                        out = etatHeader;
//                        logger.debug(tagName+"\tetat entete");
                    } else if (currentEtat != null && currentEtat.getImportableElement().getBalise().equals(tagName)) {
                        // on commence un nouvel element
                        if (data == null) startData();
                        out = data;
                        fileInfo.incElement(currentEtat.getTitre());
//                        logger.debug(tagName+"\timportable element");
                    } else if (currentEtat != null) {
                        // element d'un etat que l'on doit ignorer
                        out = new NullWriter();
                        startIgnored = tagName;
                        startIgnoredLevel = tagStack.size() + 1;
//                        logger.debug(tagName+"\tignored element");
                    }
                    break;
                }
            }
            tagStack.push(tagName);
            charactersStack.push(new StringBuilder());
            
            String path = XmlUtils.getPath(tagStack);
            for(int i=0;i<attributes.getLength();i++) {
                String attrName = attributes.getLocalName(i);
                String value=attributes.getValue(i);
                String fullPath = path+"/@"+attrName;
                checkBudgetCollectivite(fullPath,value);
                checkSpecialKey(fullPath,value);
                
            }
            
            writeTag(uri, localName, qName, attributes);
// commente suite depreciation copy-to dans entete. Utiliser les XSL d'import pour faire ça
//            if (tagStack.size() == 3 && currentEtat != null && currentEtat.getImportableElement().getBalise().equals(tagName)) {
//                Hashtable<String, EnteteModel> map = dm.getMap();
//                for (Enumeration<String> enumer = map.keys(); enumer.hasMoreElements();) {
//                    String enteteName = enumer.nextElement();
//                    EnteteModel em = map.get(enteteName);
//                    if (em != null && em.getCopies().size() > 0) {
//                        for (CopyToModel ctm : em.getCopies()) {
//                            if (ctm.getElementRef().equals(currentEtat.getImportableElement().getId())) {
//                                out.write(LINE_SEP);
//                                out.write(em.getData().getBytes(fileEncoding));
//                            }
//                        }
//                    }
//                }
//            }
        }
        catch (IOException ioEx) {
            throw new SAXException(ioEx);
        }
    }

    private void closeData(String uri, String localName, String qName) throws IOException {
        data.write("</".getBytes());
        String prefix = null;
        if(uri!=null && uri.length()>0) {
        	prefix = nsCtx.getPrefix(uri);
        	if(prefix!=null && prefix.length()>0) prefix = prefix+":";
        	else prefix="";
        } else prefix = "";
        data.write(prefix.getBytes(fileEncoding));
        data.write(currentEtat.getBalise().getBytes(fileEncoding));
//        data.write(qName.getBytes(fileEncoding));
        data.write(">".getBytes());
        data.flush();
        data.close();
        data = null;
    }

    private OutputStream startData() throws IOException {
        File f = File.createTempFile("data-", "", tmpDir);
        data = new FileOutputStream(f);
        elementCount = 0;
        fDatas.add(f);
        try { data.write(etatHeader.getBytes(fileEncoding)); } catch(UnsupportedEncodingException ueEx) { /* TODO */ }
        return data;
    }

    private void writeTag(String uri, String localName, String qName, Attributes attributes) throws IOException {
        out.write(LINE_SEP);
        out.write(getXmlText(uri, localName, qName, attributes));
    }


    private void closeTag(String uri, String localName, String qName) throws IOException {
        if(out==null) {
            logger.error("trying to close tag "+qName+" with a null output");
        }
        out.write(getCloseTag(uri, localName, qName));
    }

    private byte[] getCloseTag(String uri, String localName, String qName) throws UnsupportedEncodingException {
//    	logger.debug("closing tag "+qName);
        StringBuffer buff = new StringBuffer();
//        buff.append("</").append(XmlUtils.getShortTagName(uri, localName, qName)).append(">");
        buff.append("</").append(qName).append(">");
        return buff.toString().getBytes(fileEncoding);
    }

    private byte[] getXmlText(String uri, String localName, String qName, Attributes attributes) throws UnsupportedEncodingException {
//logger.debug(qName);
        StringBuffer ret = new StringBuffer();
//        String tagName = XmlUtils.getShortTagName(uri, localName, qName);
        ret.append("<").append(qName);
        for (int i = 0; i < attributes.getLength(); i++) {
            String attName = attributes.getLocalName(i);
            String attVal = attributes.getValue(i);
            String nsUri = attributes.getURI(i);
            String prefix = null;
            if(nsUri!=null && nsUri.length()>0) {
                prefix = nsCtx.getPrefix(nsUri);
                if(prefix!=null && prefix.length()>0) prefix = prefix+":";
                else prefix="";
            } else prefix="";
            ret.append(" ").append(prefix).append(attName).append("=\"").append(escapeXml(attVal)).append("\"");
        }
        for(NamespaceContextImpl.NsMapping map:nsCtx.getMappings()) {
            if(!map.isWritten()) {
                ret.append(" xmlns");
                if(map.getPrefix().length()>0) ret.append(":").append(map.getPrefix());
                ret.append("=\"").append(map.getUri()).append("\"");
                map.setWritten();
            }
        }
        ret.append(">");
        return ret.toString().getBytes(fileEncoding);
    }

    protected String getEndPath(int level) {
        String ret = "";
        if (level > 0) {
            Stack<String> tmp = new Stack<String>();
            for (int i = 0; i < level; i++) {
                String tag = tagStack.pop();
                tmp.push(tag);
                ret = tag + "/" + ret;
            }
            ret = ret.substring(0, ret.length() - 1);
            while (!tmp.isEmpty())
                tagStack.push(tmp.pop());
        }
        return ret;
    }


    /**
     * Return informations on splitted file
     * @return
     */
    public FileInfo getFileInfo() {
        return fileInfo;
    }
    private void checkBudgetCollectivite(String path,String value) {
        if(dm.getBudgetPath()!=null) {
            if(dm.getBudgetPath().getCodePath()!=null) {
                if(path.equals(dm.getBudgetPath().getCodePath().getPath())) {
                    if(budget==null) budget = new Pair();
                    budget.key=value;
//                    return;
                }
            }
            if(dm.getBudgetPath().getLibellePath()!=null) {
                if(path.equals(dm.getBudgetPath().getLibellePath().getPath())) {
                    if(budget==null) budget = new Pair();
                    budget.libelle=value;
//                    return;
                }
            }
        }
        if(dm.getCollectivitePath()!=null) {
            if(dm.getCollectivitePath().getCodePath()!=null) {
                if(path.equals(dm.getCollectivitePath().getCodePath().getPath())) {
                    if(collectivite==null) collectivite = new Pair();
                    collectivite.key=value;
//                    return;
                }
            }
            if(dm.getCollectivitePath().getLibellePath()!=null) {
                if(path.equals(dm.getCollectivitePath().getLibellePath().getPath())) {
                    if(collectivite==null) collectivite = new Pair();
                    collectivite.libelle=value;
//                    return;
                }
            }
        }
    }
    private void checkSpecialKey(String path,String value) {
        Vector<SpecialKeyModel> spDefs = dm.getSpecialKeys(); 
        if(specialKey1==null && spDefs!=null && spDefs.size()>0) {
            SpecialKeyModel skm = null;
            for(int i=0;i<spDefs.size();i++) {
                SpecialKeyModel o = spDefs.get(i);
                if(o.getPos()==1) {
                    skm=o;
                    break;
                }
            }
            if(skm==null) {
                // it can not have anymore special-key-1
                specialKey1=NULL_VALUE;
            }
            if(skm!=null && path.equals(skm.getPath())) {
                String key = skm.transformValue(value);
                String lib = skm.getDescriptionOfValue(key);
                if(lib==null) lib = key;
                specialKey1 = new Pair(key,lib);
            }
        }
        if(specialKey2==null && spDefs!=null && spDefs.size()>0) {
            SpecialKeyModel skm = null;
            for(int i=0;i<spDefs.size();i++) {
                SpecialKeyModel o = spDefs.get(i);
                if(o.getPos()==2) {
                    skm=o;
                    break;
                }
            }
            if(skm==null) {
                // it can not have anymore special-key-1
                specialKey2=NULL_VALUE;
            }
            if(skm!=null && path.equals(skm.getPath())) {
                String key = skm.transformValue(value);
                String lib = skm.getDescriptionOfValue(key);
                if(lib==null) lib = key;
                specialKey2 = new Pair(key,lib); 
            }
        }
        if(specialKey3==null && spDefs!=null && spDefs.size()>0) {
            SpecialKeyModel skm = null;
            for(int i=0;i<spDefs.size();i++) {
                SpecialKeyModel o = spDefs.get(i);
                if(o.getPos()==3) {
                    skm=o;
                    break;
                }
            }
            if(skm==null) {
                // it can not have anymore special-key-1
                specialKey3=NULL_VALUE;
            }
            if(skm!=null && path.equals(skm.getPath())) {
                String key = skm.transformValue(value);
                String lib = skm.getDescriptionOfValue(key);
                if(lib==null) lib = key;
                specialKey3 = new Pair(key,lib);
            }
        }
    }
    /**
     * Returns the budget the splitted file concerns
     * @return
     */
    public Pair getBudget() {
        return budget;
    }
    /**
     * Returns the collectivite the splitted file concerns
     * @return
     */
    public Pair getCollectivite() {
        return collectivite;
    }
    /**
     * Returns the special key1 found in this file.
     * @return
     */
    public Pair getSpecialKey1() {
        return specialKey1==NULL_VALUE?null:specialKey1;
    }
    /**
     * Returns the special key2 found in this file
     * @return
     */
    public Pair getSpecialKey2() {
        return specialKey2==NULL_VALUE?null:specialKey2;
    }
    /**
     * Returns the special key3 found in this file
     * @return
     */
    public Pair getSpecialKey3() {
        return specialKey3==NULL_VALUE?null:specialKey3;
    }
    
    public static String escapeXml(String s) {
        return s.replaceAll("&","&amp;").replaceAll("\"","&quot;").replaceAll("'","&apos;").replaceAll("<","&lt;").replaceAll(">","&gt;");
    }

    public void endPrefixMapping(String prefix) throws SAXException {
        super.endPrefixMapping(prefix);
        nsCtx.removeMapping(prefix);
    }

    public void startPrefixMapping(String prefix, String uri) throws SAXException {
        super.startPrefixMapping(prefix, uri);
        nsCtx.addMapping(prefix,uri);
    }
}
