/*
 * Copyright
 *   2007 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 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.cp.utils.xml.marshal;

import fr.gouv.finances.cp.utils.xml.marshal.escapers.NamespaceContextImpl;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.Stack;
import java.util.Vector;
import javax.xml.namespace.QName;
import org.apache.log4j.Logger;

/**
 * This class defines an xml writer.
 * <p>License : LGPL
 * @author: Christophe MARCHAND
 */
public class XmlOutputter {
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    private static final SimpleDateFormat dt_sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZZ");

    private static final Logger logger = Logger.getLogger(XmlOutputter.class);
    private static final String DEFAULT_ENCODING = "UTF-8";
    private String encoding;
    private OutputStreamWriter os;
    private static final String CRLF = System.getProperty("line.separator");
    private boolean writeStarted = false;
    private NamespaceContextImpl nsCtx;
    Charset cs = null;
    /**
     * Indent level. Incremented by <code>startTag(String)</code>, decremented by
     * <code>endTag(String)</code>.
     */
    private int indentLevel = 0;
    /**
     * Indent string.
     */
    public static final transient String INDENT_STR = " ";
    /**
     * A vector to store attributes.
     */
    private Vector<Attribute> attributes = null;
    /**
     * to store if attributes have been written or not.
     */
    private boolean attributesFlushed = false;
    /**
     * Indent string.
     */
    private String indentStr = INDENT_STR;
    /**
     * a stack to remember if top-element as child or not.
     */
    private Stack<MutableBoolean> stack = null;
    private CharacterEscaper escaper = null;
    private Stack<String> nsStack = null;

    class Attribute extends java.lang.Object {

        private String attName = null;
        private String value = null;
        private QName attQ = null;

        public Attribute(String attName, String value) {
            this.attName = attName;
            this.value = value;
        }

        public Attribute(QName attQ, String value) {
            this.attQ = attQ;
            this.value = value;
        }

        @Override
        public String toString() {
            StringBuffer sb = new StringBuffer();
            if (attName != null) {
                sb.append(" ").append(attName).append("=\"");
            } else {
                sb.append(" ");
                if (attQ.getPrefix() != null && attQ.getPrefix().length() > 0) {
                    sb.append(attQ.getPrefix()).append(":");
                }
                sb.append(attQ.getLocalPart()).append("=\"");
            }
            if (value != null) {
                sb.append(value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;"));
            }
            sb.append("\"");
            return sb.toString();
        }

        public String toString(CharacterEscaper escaper) {
            StringBuffer sb = new StringBuffer();
            if (attName != null) {
                sb.append(" ").append(attName).append("=\"");
            } else {
                sb.append(" ");
                if (attQ.getPrefix() != null && attQ.getPrefix().length() > 0) {
                    sb.append(attQ.getPrefix()).append(":");
                }
                sb.append(attQ.getLocalPart()).append("=\"");
            }
            if (value != null) {
                sb.append(escaper.escapeString(value));
            }
            sb.append("\"");
            return sb.toString();
        }
    }

    /**
     * Constructs a XmlOutputter object based on a <code>OutputStream</code>.
     * 
     * @param out the outputstream to write to
     * @param encoding The encoding of xml output
     * @see java.io.OutputStream
     */
    public XmlOutputter(OutputStreamWriter out, String encoding) {
        super();
        this.os = out;
        this.encoding = encoding;
        init();
    }

    /**
     * Constructs a XmlOutputter object based on a <code>OutputStream</code>, with the default encoding (UTF-8).
     * @param out the outputstream to write to
     */
    public XmlOutputter(OutputStreamWriter out) {
        this(out, DEFAULT_ENCODING);
    }

    /**
     * Writes a int attribute.
     * @param tag java.lang.String
     * @param value int
     */
    public void addAttribute(String attr, int value) {
        addAttribute(attr, Integer.toString(value));
    }

    /**
     * Writes a int attribute.
     * @param tag
     * @param value int
     */
    public void addAttribute(QName attr, int value) {
        addAttribute(attr, Integer.toString(value));
    }

    /**
     * Writes a long attribute.
     * @param tag java.lang.String
     * @param value long
     */
    public void addAttribute(String attr, long value) {
        addAttribute(attr, Long.toString(value));
    }

    /**
     * Writes a long attribute.
     * @param tag
     * @param value long
     */
    public void addAttribute(QName attr, long value) {
        addAttribute(attr, Long.toString(value));
    }

    /**
     * Writes an attribute.
     * @param tag java.lang.String
     * @param value java.lang.String
     */
    public void addAttribute(String attr, String value) {
        attributes.add(new Attribute(attr, value));
    }

    /**
     * Writes an attribute.
     * @param tag
     * @param value java.lang.String
     */
    public void addAttribute(QName attr, String value) {
        attributes.add(new Attribute(attr, value));
    }

    /**
     * Writes a BigDecimal attribute.
     * @param tag java.lang.String
     * @param value java.math.BigDecimal
     */
    public void addAttribute(String attr, java.math.BigDecimal value) {
        addAttribute(attr, value.toString());
    }

    /**
     * Writes a BigDecimal attribute.
     * @param tag
     * @param value java.math.BigDecimal
     */
    public void addAttribute(QName attr, java.math.BigDecimal value) {
        addAttribute(attr, value.toString());
    }

    /**
     * Writes a boolean attribute.
     * @param tag java.lang.String
     * @param value boolean
     */
    public void addAttribute(String attr, boolean value) {
        addAttribute(attr, value ? "true" : "false");
    }

    /**
     * Writes a boolean attribute.
     * @param tag
     * @param value boolean
     */
    public void addAttribute(QName attr, boolean value) {
        addAttribute(attr, value ? "true" : "false");
    }
    /**
     * Writes a java.util.Date attribute, conforming to <tt>xs:date</tt>
     * @param attName
     * @param date
     */
    public void addAttribute(String attName, Date date) {
        addAttribute(attName,sdf.format(date));
    }
    /**
     * Writes a java.util.Date attribute, conforming to <tt>xs:date</tt>
     * @param attr
     * @param date
     */
    public void addAttribute(QName attr, Date date) {
        addAttribute(attr,sdf.format(date));
    }
    /**
     * Writes a java.util.Date attribute, conforming to <tt>xs:date</tt>
     * @param attName
     * @param date
     */
    public void addDateTimeAttribute(String attName, Date date) {
        addAttribute(attName,dt_sdf.format(date));
    }
    /**
     * Writes a java.util.Date attribute, conforming to <tt>xs:date</tt>
     * @param attr
     * @param date
     */
    public void addDateTimeAttribute(QName attr, Date date) {
        addAttribute(attr,dt_sdf.format(date));
    }

    /**
     * Adds character data (#CDATA) to encapsulated Writer.
     * @param cData java.lang.String
     */
    public void addCharacterData(String cData) {
        flushAttributes(false);
        try {
            os.write(escaper.escapeString(cData));
        } catch (Throwable t) {
        }
        if (cData.indexOf("\n") != (-1)) {
            (stack.peek()).setValue(true);
        }
    }

    public void addComment(String comment) {
        writeStarted = true;
        flushAttributes(false);
        try {
            os.write("<!-- ");
            os.write(comment);
            os.write(" -->");
        } catch (Throwable t) {
        }
    }

    /**
     * Close the stack-on-top element. If <code>tag</code> is different of stack-on-top,
     * throws an <code>InvalidParameterException</code>.
     * @param tag java.lang.String
     * @exception java.security.InvalidParameterException If tag name
     * doesn't match the previously opened one.
     */
    public void endTag(String tag) throws java.security.InvalidParameterException {
        if (attributesFlushed) {
            indentLevel--;
            MutableBoolean bool = (MutableBoolean) stack.pop();
            if (bool.booleanValue()) {
                try {
                    writeIndent();
                } catch (Throwable t) {
                }
            }
            try {
                os.write("</");
                os.write(tag);
                os.write(">");
            } catch (Throwable t) {
            }
            if (indentLevel == 0) {
                try {
                    os.write(CRLF);
                } catch (Throwable t) {
                }
            }
        } else {
            flushAttributes(true);
            indentLevel--;
        }
    }

    public void endTag(QName tag) throws java.security.InvalidParameterException {
        if (attributesFlushed) {
            indentLevel--;
            MutableBoolean bool = (MutableBoolean) stack.pop();
            if (bool.booleanValue()) {
                try {
                    writeIndent();
                } catch (Throwable t) {
                }
            }
            try {
                os.write("</");
                if (tag.getPrefix() != null && tag.getPrefix().length() > 0) {
                    os.write(tag.getPrefix());
                    os.write(":");
                }
                os.write(tag.getLocalPart());
                os.write(">");
            } catch (Throwable t) {
            }

            if (indentLevel == 0) {
                try {
                    os.write(CRLF);
                } catch (Throwable t) {
                }
            }

        } else {
            flushAttributes(true);
            indentLevel--;
        }
        String ns = tag.getPrefix().concat(":").concat(tag.getNamespaceURI());
        String pushed = nsStack.pop();
        if (ns.equals(pushed)) {
            nsCtx.removeMapping(tag.getPrefix());
        }
    }

    /**
     * writes attributes to writer. Adds <code>/&gt;</code> if <code>close</code>,
     * and just <code>&gt;</code> otherwise.
     * @param close boolean
     */
    private void flushAttributes(boolean close) {
        if (!attributesFlushed) {
            for (Attribute attr : attributes) {
                try {
                    os.write(attr.toString(escaper));
                } catch (Throwable t) {
                }
            }
            attributesFlushed = true;
            try {
                if (close) {
                    os.write("/>");
                } else {
                    os.write(">");
                }
            } catch (Throwable t) {
            }
        }
    }

    public void addCData(String cData) {
        flushAttributes(false);
        try{
            os.write("<![CDATA[");
            os.write(cData);
            os.write("]]>");
        } catch(IOException ex) {
            // TODO, maybe
        }
    }

    /**
     * Class initialization method. Called by all constructors.
     * If you override this method, do not forget to add <code>super.init()</code>
     * in your code.
     */
    private void init() throws RuntimeException {
        String osEncoding = fmtEncoding(os.getEncoding());
        if(!osEncoding.equals(fmtEncoding(encoding))) {
            logger.debug("compare "+osEncoding+" to "+fmtEncoding(encoding));
            throw new RuntimeException("Can not create an " + encoding + "-XmlOutputter on a " + os.getEncoding() + "-OutputStreamWritter");
        }
        attributes = new Vector<Attribute>();
        stack = new Stack<MutableBoolean>();
        writeStarted = true;
        escaper = CharacterEscaperFactory.getCharacterEscaper(encoding);
        nsCtx = new NamespaceContextImpl();
        nsStack = new Stack<String>();
        try {
            os.write("<?xml version=\"1.0\" encoding=\"");
            os.write(encoding);
            os.write("\"?");
        } catch (Throwable t) {
        }
    }

    private static String fmtEncoding(String encoding) {
        StringBuilder sb = new StringBuilder();
        for(char c:encoding.toCharArray()) {
            if('A'<=c && c<='Z') sb.append(c);
            if('0'<=c && c<='9') sb.append(c);
            if('a'<=c && c<='z') sb.append(Character.toUpperCase(c));
        }
        return sb.toString();
    }

    /**
     * An easy way to define an custom indent string.
     * @param s java.lang.String
     */
    public void setIndentStr(String s) {
        indentStr = s;
    }

    /**
     * Opens a new Tag.
     * @param tag java.lang.String
     */
    public void startTag(String tag) {
        flushAttributes(false);
        attributesFlushed = false;
        attributes.removeAllElements();
        try {
            writeIndent();
            os.write("<");
            os.write(tag);
            indentLevel++;
            if (!stack.isEmpty()) {
                ((MutableBoolean) stack.peek()).setValue(true);
            }
        } catch (Throwable t) {
        }
        stack.push(new MutableBoolean(false));
    }

    public void startTag(QName tag) {
        flushAttributes(false);
        attributesFlushed = false;
        attributes.removeAllElements();
        try {
            writeIndent();
            os.write("<");
            if (tag.getPrefix() != null && tag.getPrefix().length() > 0) {
                os.write(tag.getPrefix());
                os.write(":");
            }
            os.write(tag.getLocalPart());
            Iterator it = nsCtx.getPrefixes(tag.getNamespaceURI());
            if (it == null) {
                os.write(" xmlns");
                if (tag.getPrefix() != null && tag.getPrefix().length() > 0) {
                    os.write(":");
                    os.write(tag.getPrefix());
                }
                os.write("=\"");
                os.write(tag.getNamespaceURI());
                os.write("\"");
                nsCtx.addMapping(tag.getPrefix(), tag.getNamespaceURI());
                nsStack.push(tag.getPrefix().concat(":").concat(tag.getNamespaceURI()));
            } else {
                nsStack.push(null);
            }
            indentLevel++;
            if (!stack.isEmpty()) {
                ((MutableBoolean) stack.peek()).setValue(true);
            }
        } catch (Throwable t) {
        }
        stack.push(new MutableBoolean(false));
    }

    /**
     * Writes the needed indentation.
     */
    public void writeIndent() throws IOException {
        os.write(CRLF);
        for (int i = 0; i < indentLevel; i++) {
            os.write(indentStr);
        }
    }

    public void close() throws IOException {
        os.flush();
        os.close();
    }

    public OutputStreamWriter getWriter() {
        return os;
    }
}

