package org.codehaus.modello.plugin.stax;

/*
 * Copyright (c) 2004, Codehaus.org
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is furnished to do
 * so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

import javax.inject.Inject;
import javax.inject.Named;

import java.io.IOException;
import java.util.List;
import java.util.Map;

import org.codehaus.modello.ModelloException;
import org.codehaus.modello.model.Model;
import org.codehaus.modello.model.ModelAssociation;
import org.codehaus.modello.model.ModelClass;
import org.codehaus.modello.model.ModelDefault;
import org.codehaus.modello.model.ModelField;
import org.codehaus.modello.plugin.java.javasource.JClass;
import org.codehaus.modello.plugin.java.javasource.JConstructor;
import org.codehaus.modello.plugin.java.javasource.JField;
import org.codehaus.modello.plugin.java.javasource.JMethod;
import org.codehaus.modello.plugin.java.javasource.JParameter;
import org.codehaus.modello.plugin.java.javasource.JSourceCode;
import org.codehaus.modello.plugin.java.javasource.JSourceWriter;
import org.codehaus.modello.plugin.java.javasource.JType;
import org.codehaus.modello.plugin.java.metadata.JavaFieldMetadata;
import org.codehaus.modello.plugin.model.ModelClassMetadata;
import org.codehaus.modello.plugins.xml.metadata.XmlAssociationMetadata;
import org.codehaus.modello.plugins.xml.metadata.XmlFieldMetadata;
import org.codehaus.modello.plugins.xml.metadata.XmlModelMetadata;

/**
 * @author <a href="mailto:jason@modello.org">Jason van Zyl </a>
 * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse </a>
 */
@Named("stax-writer")
public class StaxWriterGenerator extends AbstractStaxGenerator {

    private boolean requiresDomSupport;

    @Inject
    private StaxSerializerGenerator serializerGenerator;

    public void generate(Model model, Map<String, Object> parameters) throws ModelloException {
        initialize(model, parameters);

        requiresDomSupport = false;

        try {
            generateStaxWriter();
        } catch (IOException ex) {
            throw new ModelloException("Exception while generating StAX Writer.", ex);
        }

        serializerGenerator.generate(model, parameters);
    }

    private void generateStaxWriter() throws ModelloException, IOException {
        Model objectModel = getModel();

        String packageName =
                objectModel.getDefaultPackageName(isPackageWithVersion(), getGeneratedVersion()) + ".io.stax";

        String marshallerName = getFileName("StaxWriter");

        JSourceWriter sourceWriter = newJSourceWriter(packageName, marshallerName);

        JClass jClass = new JClass(packageName + '.' + marshallerName);
        initHeader(jClass);
        suppressAllWarnings(objectModel, jClass);

        jClass.addImport("java.io.IOException");
        jClass.addImport("java.io.OutputStream");
        jClass.addImport("java.io.Writer");
        jClass.addImport("java.io.StringWriter");
        jClass.addImport("java.text.DateFormat");
        jClass.addImport("java.util.Iterator");
        jClass.addImport("java.util.Locale");
        jClass.addImport("java.util.jar.Manifest");
        jClass.addImport("javax.xml.stream.*");

        addModelImports(jClass, null);

        jClass.addField(new JField(JType.INT, "curId"));
        jClass.addField(new JField(new JType("java.util.Map"), "idMap"));
        JConstructor constructor = new JConstructor(jClass);
        constructor.getSourceCode().add("idMap = new java.util.HashMap();");
        jClass.addConstructor(constructor);

        String root = objectModel.getRoot(getGeneratedVersion());

        ModelClass rootClass = objectModel.getClass(root, getGeneratedVersion());

        String rootElement = resolveTagName(rootClass);

        // ----------------------------------------------------------------------
        // Write the write( Writer, Model ) method which will do the unmarshalling.
        // ----------------------------------------------------------------------

        JMethod marshall = new JMethod("write");

        String rootElementParameterName = uncapitalise(root);
        marshall.addParameter(new JParameter(new JClass("Writer"), "writer"));
        marshall.addParameter(new JParameter(new JClass(root), rootElementParameterName));

        marshall.addException(new JClass("java.io.IOException"));
        marshall.addException(new JClass("XMLStreamException"));

        JSourceCode sc = marshall.getSourceCode();

        sc.add("XMLOutputFactory factory = XMLOutputFactory.newInstance();");

        // currently, only woodstox supports Windows line endings. It works with Java 6/RI and stax <= 1.1.1 as well
        // but we have no way to detect them
        sc.add("boolean supportWindowsLineEndings = false;");
        sc.add("if ( factory.isPropertySupported( \"com.ctc.wstx.outputEscapeCr\" ) )");
        sc.add("{");
        sc.indent();
        sc.add("factory.setProperty( \"com.ctc.wstx.outputEscapeCr\", Boolean.FALSE );");
        sc.add("supportWindowsLineEndings = true;");
        sc.unindent();
        sc.add("}");

        sc.add("if ( factory.isPropertySupported( \"org.codehaus.stax2.automaticEmptyElements\" ) )");
        sc.add("{");
        sc.addIndented("factory.setProperty( \"org.codehaus.stax2.automaticEmptyElements\", Boolean.FALSE );");
        sc.add("}");

        sc.add(
                "IndentingXMLStreamWriter serializer = new IndentingXMLStreamWriter( factory.createXMLStreamWriter( writer ) );");

        sc.add("if ( supportWindowsLineEndings )");
        sc.add("{");
        sc.addIndented("serializer.setNewLine( serializer.getLineSeparator() );");
        sc.add("}");

        sc.add("serializer.writeStartDocument( " + rootElementParameterName + ".getModelEncoding(), \"1.0\" );");

        sc.add("write" + root + "( " + rootElementParameterName + ", \"" + rootElement + "\", serializer );");

        sc.add("serializer.writeEndDocument();");

        jClass.addMethod(marshall);

        // ----------------------------------------------------------------------
        // Write the write( OutputStream, Model ) method which will do the unmarshalling.
        // ----------------------------------------------------------------------

        marshall = new JMethod("write");

        marshall.addParameter(new JParameter(new JClass("OutputStream"), "stream"));
        marshall.addParameter(new JParameter(new JClass(root), rootElementParameterName));

        marshall.addException(new JClass("java.io.IOException"));
        marshall.addException(new JClass("XMLStreamException"));

        sc = marshall.getSourceCode();

        sc.add("XMLOutputFactory factory = XMLOutputFactory.newInstance();");

        // currently, only woodstox supports Windows line endings. It works with Java 6/RI and stax <= 1.1.1 as well
        // but we have no way to detect them
        sc.add("boolean supportWindowsLineEndings = false;");
        sc.add("if ( factory.isPropertySupported( \"com.ctc.wstx.outputEscapeCr\" ) )");
        sc.add("{");
        sc.indent();
        sc.add("factory.setProperty( \"com.ctc.wstx.outputEscapeCr\", Boolean.FALSE );");
        sc.add("supportWindowsLineEndings = true;");
        sc.unindent();
        sc.add("}");

        sc.add("if ( factory.isPropertySupported( \"org.codehaus.stax2.automaticEmptyElements\" ) )");
        sc.add("{");
        sc.addIndented("factory.setProperty( \"org.codehaus.stax2.automaticEmptyElements\", Boolean.FALSE );");
        sc.add("}");

        sc.add(
                "IndentingXMLStreamWriter serializer = new IndentingXMLStreamWriter( factory.createXMLStreamWriter( stream, "
                        + rootElementParameterName + ".getModelEncoding() ) );");

        sc.add("if ( supportWindowsLineEndings )");
        sc.add("{");
        sc.addIndented("serializer.setNewLine( serializer.getLineSeparator() );");
        sc.add("}");

        sc.add("serializer.writeStartDocument( " + rootElementParameterName + ".getModelEncoding(), \"1.0\" );");

        sc.add("write" + root + "( " + rootElementParameterName + ", \"" + rootElement + "\", serializer );");

        sc.add("serializer.writeEndDocument();");

        jClass.addMethod(marshall);

        writeAllClasses(objectModel, jClass);

        if (requiresDomSupport) {
            createWriteDomMethod(jClass);
        }

        jClass.print(sourceWriter);

        sourceWriter.close();
    }

    private void writeAllClasses(Model objectModel, JClass jClass) throws ModelloException {
        for (ModelClass clazz : getClasses(objectModel)) {
            writeClass(clazz, jClass);
        }
    }

    private void writeClass(ModelClass modelClass, JClass jClass) throws ModelloException {
        String className = modelClass.getName();

        String uncapClassName = uncapitalise(className);

        JMethod marshall = new JMethod("write" + className);
        marshall.getModifiers().makePrivate();

        marshall.addParameter(new JParameter(new JClass(className), uncapClassName));
        marshall.addParameter(new JParameter(new JClass("String"), "tagName"));
        marshall.addParameter(new JParameter(new JClass("XMLStreamWriter"), "serializer"));

        marshall.addException(new JClass("java.io.IOException"));
        marshall.addException(new JClass("XMLStreamException"));

        JSourceCode sc = marshall.getSourceCode();

        sc.add("if ( " + uncapClassName + " != null )");

        sc.add("{");
        sc.indent();

        ModelClassMetadata classMetadata = (ModelClassMetadata) modelClass.getMetadata(ModelClassMetadata.ID);

        String namespace = null;
        XmlModelMetadata xmlModelMetadata =
                (XmlModelMetadata) modelClass.getModel().getMetadata(XmlModelMetadata.ID);

        // add namespace information for root element only
        if (classMetadata.isRootElement() && (xmlModelMetadata.getNamespace() != null)) {
            namespace = xmlModelMetadata.getNamespace(getGeneratedVersion());
            sc.add("serializer.setDefaultNamespace( \"" + namespace + "\" );");
        }

        sc.add("serializer.writeStartElement( tagName );");

        if (namespace != null) {
            sc.add("serializer.writeDefaultNamespace( \"" + namespace + "\" );");

            if (xmlModelMetadata.getSchemaLocation() != null) {
                String url = xmlModelMetadata.getSchemaLocation(getGeneratedVersion());

                sc.add("serializer.setPrefix( \"xsi\", \"http://www.w3.org/2001/XMLSchema-instance\" );");
                sc.add("serializer.writeNamespace( \"xsi\", \"http://www.w3.org/2001/XMLSchema-instance\" );");
                sc.add(
                        "serializer.writeAttribute( \"http://www.w3.org/2001/XMLSchema-instance\", \"schemaLocation\", \""
                                + namespace + " " + url + "\" );");
            }
        }

        if (isAssociationPartToClass(modelClass)) {
            if (modelClass.getIdentifierFields(getGeneratedVersion()).size() != 1) {
                writeIdMapCheck(sc, uncapClassName, "modello.id");
            }
        }

        ModelField contentField = null;

        String contentValue = null;

        List<ModelField> modelFields = getFieldsForXml(modelClass, getGeneratedVersion());

        // XML attributes
        for (ModelField field : modelFields) {
            XmlFieldMetadata xmlFieldMetadata = (XmlFieldMetadata) field.getMetadata(XmlFieldMetadata.ID);

            String fieldTagName = resolveTagName(field, xmlFieldMetadata);

            String type = field.getType();

            String value = getFieldValue(uncapClassName, field);

            if (xmlFieldMetadata.isContent()) {
                contentField = field;
                contentValue = value;
                continue;
            }

            if (xmlFieldMetadata.isAttribute()) {
                sc.add(getValueChecker(type, value, field));

                sc.add("{");
                sc.addIndented("serializer.writeAttribute( \"" + fieldTagName + "\", "
                        + getValue(field.getType(), value, xmlFieldMetadata) + " );");
                sc.add("}");
            }
        }

        if (contentField != null) {
            XmlFieldMetadata xmlFieldMetadata = (XmlFieldMetadata) contentField.getMetadata(XmlFieldMetadata.ID);
            sc.add("serializer.writeCharacters( " + getValue(contentField.getType(), contentValue, xmlFieldMetadata)
                    + " );");
        }

        // XML tags
        for (ModelField field : modelFields) {
            XmlFieldMetadata xmlFieldMetadata = (XmlFieldMetadata) field.getMetadata(XmlFieldMetadata.ID);

            if (xmlFieldMetadata.isContent()) {
                // skip field with type Content
                continue;
            }

            String fieldTagName = resolveTagName(field, xmlFieldMetadata);

            String type = field.getType();

            String value = getFieldValue(uncapClassName, field);

            if (xmlFieldMetadata.isAttribute()) {
                continue;
            }

            if (field instanceof ModelAssociation) {
                ModelAssociation association = (ModelAssociation) field;

                String associationName = association.getName();

                ModelField referenceIdentifierField = getReferenceIdentifierField(association);

                if (association.isOneMultiplicity()) {
                    sc.add(getValueChecker(type, value, association));
                    sc.add("{");
                    sc.indent();

                    if (referenceIdentifierField != null) {
                        // if xml.reference, then store as a reference instead

                        sc.add("serializer.writeStartElement( \"" + fieldTagName + "\" );");

                        writeElementAttribute(sc, referenceIdentifierField, value);

                        sc.add("serializer.writeEndElement();");
                    } else {
                        sc.add("write" + association.getTo() + "( (" + association.getTo() + ") " + value + ", \""
                                + fieldTagName + "\", serializer );");
                    }

                    sc.unindent();
                    sc.add("}");
                } else {
                    // MANY_MULTIPLICITY

                    XmlAssociationMetadata xmlAssociationMetadata =
                            (XmlAssociationMetadata) association.getAssociationMetadata(XmlAssociationMetadata.ID);

                    String valuesTagName = resolveTagName(fieldTagName, xmlAssociationMetadata);

                    type = association.getType();
                    String toType = association.getTo();

                    boolean wrappedItems = xmlAssociationMetadata.isWrappedItems();

                    if (ModelDefault.LIST.equals(type) || ModelDefault.SET.equals(type)) {
                        sc.add(getValueChecker(type, value, association));

                        sc.add("{");
                        sc.indent();

                        if (wrappedItems) {
                            sc.add("serializer.writeStartElement( " + "\"" + fieldTagName + "\" );");
                        }

                        sc.add("for ( Iterator iter = " + value + ".iterator(); iter.hasNext(); )");

                        sc.add("{");
                        sc.indent();

                        if (isClassInModel(association.getTo(), modelClass.getModel())) {
                            sc.add(toType + " o = (" + toType + ") iter.next();");

                            if (referenceIdentifierField != null) {
                                sc.add("serializer.writeStartElement( \"" + valuesTagName + "\" );");

                                writeElementAttribute(sc, referenceIdentifierField, "o");

                                sc.add("serializer.writeEndElement();");
                            } else {
                                sc.add("write" + toType + "( o, \"" + valuesTagName + "\", serializer );");
                            }
                        } else {
                            sc.add(toType + " " + singular(uncapitalise(field.getName())) + " = (" + toType
                                    + ") iter.next();");

                            sc.add("serializer.writeStartElement( " + "\"" + valuesTagName + "\" );");
                            sc.add("serializer.writeCharacters( " + singular(uncapitalise(field.getName())) + " );");
                            sc.add("serializer.writeEndElement();");
                        }

                        sc.unindent();
                        sc.add("}");

                        if (wrappedItems) {
                            sc.add("serializer.writeEndElement();");
                        }

                        sc.unindent();
                        sc.add("}");
                    } else {
                        // Map or Properties

                        sc.add(getValueChecker(type, value, field));

                        sc.add("{");
                        sc.indent();

                        if (wrappedItems) {
                            sc.add("serializer.writeStartElement( " + "\"" + fieldTagName + "\" );");
                        }

                        sc.add("for ( Iterator iter = " + value + ".keySet().iterator(); iter.hasNext(); )");

                        sc.add("{");
                        sc.indent();

                        sc.add("String key = (String) iter.next();");

                        sc.add("String value = (String) " + value + ".get( key );");

                        if (xmlAssociationMetadata.isMapExplode()) {
                            sc.add("serializer.writeStartElement( \"" + singular(associationName) + "\" );");
                            sc.add("serializer.writeStartElement( \"key\" );");
                            sc.add("serializer.writeCharacters( key );");
                            sc.add("serializer.writeEndElement();");
                            sc.add("serializer.writeStartElement( \"value\" );");
                            sc.add("serializer.writeCharacters( value );");
                            sc.add("serializer.writeEndElement();");
                            sc.add("serializer.writeEndElement();");
                        } else {
                            sc.add("serializer.writeStartElement( \"\" + key + \"\" );");
                            sc.add("serializer.writeCharacters( value );");
                            sc.add("serializer.writeEndElement();");
                        }

                        sc.unindent();
                        sc.add("}");

                        if (wrappedItems) {
                            sc.add("serializer.writeEndElement();");
                        }

                        sc.unindent();
                        sc.add("}");
                    }
                }
            } else {
                sc.add(getValueChecker(type, value, field));

                sc.add("{");
                sc.indent();

                if ("DOM".equals(field.getType())) {
                    sc.add("writeDom( (" + (domAsXpp3 ? "Xpp3Dom" : "org.w3c.dom.Element") + ") " + value
                            + ", serializer );");

                    requiresDomSupport = true;
                } else {
                    sc.add("serializer.writeStartElement( " + "\"" + fieldTagName + "\" );");
                    sc.add("serializer.writeCharacters( " + getValue(field.getType(), value, xmlFieldMetadata) + " );");
                    sc.add("serializer.writeEndElement();");
                }

                sc.unindent();
                sc.add("}");
            }
        }

        sc.add("serializer.writeEndElement();");

        sc.unindent();
        sc.add("}");

        jClass.addMethod(marshall);
    }

    private void writeElementAttribute(JSourceCode sc, ModelField referenceIdentifierField, String value) {
        if (referenceIdentifierField instanceof DummyIdModelField) {
            writeIdMapCheck(sc, value, referenceIdentifierField.getName());
        } else {
            String v = getValue(
                    referenceIdentifierField.getType(),
                    getFieldValue(value, referenceIdentifierField),
                    (XmlFieldMetadata) referenceIdentifierField.getMetadata(XmlFieldMetadata.ID));
            sc.add("serializer.writeAttribute( \"" + referenceIdentifierField.getName() + "\", " + v + " );");
        }
    }

    private static void writeIdMapCheck(JSourceCode sc, String value, String attributeName) {
        sc.add("if ( !idMap.containsKey( " + value + " ) )");
        sc.add("{");
        sc.indent();

        sc.add("++curId;");
        sc.add("String id = String.valueOf( curId );");
        sc.add("idMap.put( " + value + ", id );");
        sc.add("serializer.writeAttribute( \"" + attributeName + "\", id );");

        sc.unindent();
        sc.add("}");
        sc.add("else");
        sc.add("{");
        sc.addIndented("serializer.writeAttribute( \"" + attributeName + "\", (String) idMap.get( " + value + " ) );");
        sc.add("}");
    }

    private String getFieldValue(String uncapClassName, ModelField field) {
        JavaFieldMetadata javaFieldMetadata = (JavaFieldMetadata) field.getMetadata(JavaFieldMetadata.ID);

        return uncapClassName + "." + getPrefix(javaFieldMetadata) + capitalise(field.getName()) + "()";
    }

    private void createWriteDomMethod(JClass jClass) {
        if (domAsXpp3) {
            jClass.addImport("org.codehaus.plexus.util.xml.Xpp3Dom");
        }
        String type = domAsXpp3 ? "Xpp3Dom" : "org.w3c.dom.Element";
        JMethod method = new JMethod("writeDom");
        method.getModifiers().makePrivate();

        method.addParameter(new JParameter(new JType(type), "dom"));
        method.addParameter(new JParameter(new JType("XMLStreamWriter"), "serializer"));

        method.addException(new JClass("XMLStreamException"));

        JSourceCode sc = method.getSourceCode();

        // start element
        sc.add("serializer.writeStartElement( dom.get" + (domAsXpp3 ? "Name" : "TagName") + "() );");

        // attributes
        if (domAsXpp3) {
            sc.add("String[] attributeNames = dom.getAttributeNames();");
            sc.add("for ( int i = 0; i < attributeNames.length; i++ )");
            sc.add("{");

            sc.indent();
            sc.add("String attributeName = attributeNames[i];");
            sc.add("serializer.writeAttribute( attributeName, dom.getAttribute( attributeName ) );");
            sc.unindent();

            sc.add("}");
        } else {
            sc.add("org.w3c.dom.NamedNodeMap attributes = dom.getAttributes();");
            sc.add("for ( int i = 0; i < attributes.getLength(); i++ )");
            sc.add("{");

            sc.indent();
            sc.add("org.w3c.dom.Node attribute = attributes.item( i );");
            sc.add("serializer.writeAttribute( attribute.getNodeName(), attribute.getNodeValue() );");
            sc.unindent();

            sc.add("}");
        }

        // child nodes & text
        if (domAsXpp3) {
            sc.add("Xpp3Dom[] children = dom.getChildren();");
            sc.add("for ( int i = 0; i < children.length; i++ )");
            sc.add("{");
            sc.addIndented("writeDom( children[i], serializer );");
            sc.add("}");

            sc.add("String value = dom.getValue();");
            sc.add("if ( value != null )");
            sc.add("{");
            sc.addIndented("serializer.writeCharacters( value );");
            sc.add("}");
        } else {
            sc.add("org.w3c.dom.NodeList children = dom.getChildNodes();");
            sc.add("for ( int i = 0; i < children.getLength(); i++ )");
            sc.add("{");
            sc.indent();
            sc.add("org.w3c.dom.Node node = children.item( i );");
            sc.add("if ( node instanceof org.w3c.dom.Element)");
            sc.add("{");
            sc.addIndented("writeDom( (org.w3c.dom.Element) children.item( i ), serializer );");
            sc.add("}");
            sc.add("else");
            sc.add("{");
            sc.addIndented("serializer.writeCharacters( node.getTextContent() );");
            sc.add("}");
            sc.unindent();
            sc.add("}");
        }

        sc.add("serializer.writeEndElement();");

        jClass.addMethod(method);
    }
}
