package be.iminds.ilabt.jfed.rspec.parser;

import be.iminds.ilabt.jfed.lowlevel.api.user_spec.UserSpec;
import be.iminds.ilabt.jfed.rspec.model.*;
import be.iminds.ilabt.jfed.rspec.parser.extensions.UserSpecWriter;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.RFC3339Util;
import javanet.staxutils.IndentingXMLEventWriter;
import org.slf4j.LoggerFactory;

import javax.xml.bind.*;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import java.io.StringWriter;
import java.util.*;
import java.util.concurrent.Callable;

import static be.iminds.ilabt.jfed.rspec.parser.RspecXmlConstants.*;

public class StaxRspecWriter implements Callable<String> {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(StaxRspecWriter.class);

    private final ModelRspec modelRspec;
    private final RspecFactory rspecFactory;

    private boolean useNitosENodeBHack;
    private boolean addSchemaLocation;
    private boolean addEmptySliverToNodeHack;
    //if set, allow links and nodes with these component managers
    private List<String> componentManagerFilter;

    public StaxRspecWriter(ModelRspec modelRspec) {
        this.modelRspec = modelRspec;
        this.rspecFactory = RspecFactoryFactory.getRspecFactoryInstance(modelRspec.getModelRspecType());

        this.useNitosENodeBHack = true;
        this.addSchemaLocation = true;
        this.componentManagerFilter = null;
        this.addEmptySliverToNodeHack = false;
    }

    private XMLEventFactory eventFactory;

    public void setUseNitosENodeBHack(boolean useNitosENodeBHack) {
        this.useNitosENodeBHack = useNitosENodeBHack;
    }

    public void setAddSchemaLocation(boolean addSchemaLocation) {
        this.addSchemaLocation = addSchemaLocation;
    }

    public void setAddEmptySliverToNodeHack(boolean addEmptySliverToNodeHack) {
        this.addEmptySliverToNodeHack = addEmptySliverToNodeHack;
    }

    public void setComponentManagerFilter(List<String> componentManagerFilter) {
        this.componentManagerFilter = componentManagerFilter;
    }


    @Override
    public String call() throws RspecParseException {
        try {
            //note: this requires woodstox, or namespaces get messed up. This is due to a strange bug related to attribute namespaces.
            // see for some info: http://stackoverflow.com/questions/3068901/xmloutputstream-repairing-namespaces-and-attributes-without-namespaces
            //                    not that the solution there does not work for XMLEventWriter

            XMLOutputFactory outputFactory = XMLOutputFactory.newInstance();
            outputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
            StringWriter sw = new StringWriter();
            XMLEventWriter innerEventWriter = outputFactory.createXMLEventWriter(sw);
            XMLEventWriter eventWriter = new IndentingXMLEventWriter(innerEventWriter);
            eventFactory = XMLEventFactory.newInstance();

            if (modelRspec.getOrigDefaultNamespace() != null) {
                eventWriter.setDefaultNamespace(modelRspec.getOrigDefaultNamespace());
                //LOG.trace("set default NS: \""+modelRspec.getOrigDefaultNamespace()+"\"");
                assert Objects.equals(NAMESPACE_RSPEC, modelRspec.getOrigDefaultNamespace());
            } else {
                eventWriter.setDefaultNamespace(NAMESPACE_RSPEC);
                //LOG.trace("set default NS: \""+NAMESPACE_RSPEC+"\"");
            }
            for (Map.Entry<String, String> e : modelRspec.getNameSpaceInfo().entrySet()) {
                if (Objects.equals(e.getValue(), NAMESPACE_RSPEC) &&
                        (modelRspec.getOrigDefaultNamespace() == null ||
                                (modelRspec.getOrigDefaultNamespace() != null && Objects.equals(modelRspec.getOrigDefaultNamespace(), NAMESPACE_RSPEC))
                        )) {
                    //don't set rspec namespace. It is the default namespace
                } else {
                    if (!Objects.equals(e.getValue(), NAMESPACE_XSI)) {
                        eventWriter.setPrefix(e.getKey(), e.getValue());
                        //LOG.trace("set NS: \""+e.getKey()+"\" -> \""+e.getValue()+"\"");
                    }
                }
            }
            eventWriter.add(eventFactory.createStartDocument());

            eventWriter.add(eventFactory.createStartElement(Q_RSPEC, null/*attributes*/, null/*namespaces*/));

            eventWriter.add(eventFactory.createNamespace(NAMESPACE_RSPEC));
            eventWriter.add(eventFactory.createAttribute(Q_ATT_RSPEC_TYPE, modelRspec.getType()));
            if (modelRspec.getExpiresString() != null)
                eventWriter.add(eventFactory.createAttribute(Q_ATT_RSPEC_EXPIRES, modelRspec.getExpiresString()));

            eventWriter.add(eventFactory.createAttribute(Q_ATT_RSPEC_GEN_BY, "jFed RSpec Editor"));
            eventWriter.add(eventFactory.createAttribute(Q_ATT_RSPEC_GEN_DATE, RFC3339Util.dateToRFC3339String(new Date(), false, false, true)));

            Map<String, String> nameSpacesInRspecTag = new HashMap<>();
            //add namespaces which we find pleasant to have defined already in the RSpec tag, whether we use them or not.
            nameSpacesInRspecTag.put(PREFIX_CLIENT, NAMESPACE_CLIENT);
            nameSpacesInRspecTag.put(PREFIX_JFED, NAMESPACE_JFED);
            nameSpacesInRspecTag.put(PREFIX_JFED_SSH_KEYS, NAMESPACE_JFED_SSH);
            nameSpacesInRspecTag.put(PREFIX_JFED_COMMAND, NAMESPACE_JFED_COMMAND);
            nameSpacesInRspecTag.put(PREFIX_DELAY, NAMESPACE_PROTOGENI_DELAY);
            nameSpacesInRspecTag.put(PREFIX_SHAREDVLAN, NAMESPACE_SHAREDLAN_EXT);
            nameSpacesInRspecTag.put(PREFIX_EMULAB, NAMESPACE_EMULAB_EXT);

            //only add GTS namespace if GeantTestbedTypes are present
            if (!modelRspec.getGeantTestbedTypes().isEmpty()) {
                nameSpacesInRspecTag.put(PREFIX_JFED_GTS, NAMESPACE_JFED_GTS);
            }

            //only add nitos reservation namespace if we have leases present.
            if (!modelRspec.getLeases().isEmpty() || !modelRspec.getChannels().isEmpty())
                nameSpacesInRspecTag.put(PREFIX_NITOS_BROKER, NAMESPACE_NITOS_BROKER);

            //merge with info from original RSpec. Make sure that the same namespace with a different prefix is handled correctly
            for (String origNs : modelRspec.getNameSpaceInfo().values()) {
                //short and simple way to remove all entries with the value from the map
                nameSpacesInRspecTag.values().remove(origNs);
            }
            nameSpacesInRspecTag.putAll(modelRspec.getNameSpaceInfo());

            for (Map.Entry<String, String> entry : nameSpacesInRspecTag.entrySet()) {
                String prefix = entry.getKey();
                String ns = entry.getValue();
                if (Objects.equals(ns, NAMESPACE_RSPEC) &&
                        (modelRspec.getOrigDefaultNamespace() == null ||
                                (modelRspec.getOrigDefaultNamespace() != null && Objects.equals(modelRspec.getOrigDefaultNamespace(), NAMESPACE_RSPEC))
                        )) {
                    //don't write rspec namespace. It is default namespace
                } else {
//                    eventWriter.setPrefix(e.getKey(), e.getValue());
                    eventWriter.add(eventFactory.createNamespace(prefix, ns));
                }
            }

            if (addSchemaLocation) {
                String schemaLocation =
                        "http://www.geni.net/resources/rspec/3 http://www.geni.net/resources/rspec/3/request.xsd ";
                if (modelRspec.getSchemaLocation() != null)
                    schemaLocation = modelRspec.getSchemaLocation();
                eventWriter.add(eventFactory.createAttribute("xsi", "http://www.w3.org/2001/XMLSchema-instance", "schemaLocation", schemaLocation));
            }

            for (Lease lease : modelRspec.getLeases()) {
                writeLease(eventWriter, lease);
            }

            for (Channel channel : modelRspec.getChannels()) {
                writeChannel(eventWriter, channel);
            }

            for (GeantTestbedType gtt : modelRspec.getGeantTestbedTypes()) {
                writeGeantTestbedType(eventWriter, gtt);
            }

            for (AddressPool addressPool : modelRspec.getAddressPools()) {
                writeAddressPool(eventWriter, addressPool);
            }

            for (ExecuteAnsiblePlaybook executeAnsiblePlaybook : modelRspec.getExecuteAnsiblePlaybooks()) {
                writeExecuteAnsibleCookbook(eventWriter, executeAnsiblePlaybook);
            }

            for (DistributeSshKeypair distributeSshKeypair : modelRspec.getDistributeSshKeypairs()) {
                writeDistributeSshKeypair(eventWriter, distributeSshKeypair);
            }

            for (RspecNode n : modelRspec.getNodes()) {
                if (componentManagerFilter == null)
                    writeNode(eventWriter, n);
                else {
                    if (n.getComponentManagerId() != null && componentManagerFilter.contains(n.getComponentManagerId().getValue()))
                        writeNode(eventWriter, n);
                }
            }
            for (RspecLink l : modelRspec.getLinks()) {
                if (componentManagerFilter == null)
                    writeLink(eventWriter, l);
                else {
                    for (GeniUrn urn : l.getComponentManagerUrns()) {
                        if (componentManagerFilter.contains(urn.getValue())) {
                            writeLink(eventWriter, l);
                            break; //write only once
                        }
                    }
                }
            }
            List<UserSpec> userSpecs = modelRspec.getSshKeys();
            if (userSpecs != null && !userSpecs.isEmpty()) {
                for (UserSpec userSpec : userSpecs) {
                    UserSpecWriter.writeUserSpec(eventFactory, eventWriter, userSpec);
                }
            }

            writeRoutableAddresses(eventWriter, modelRspec.getRoutableAddressesAvailable(), modelRspec.getRoutableAddressesConfigured());
            writeCollocateFactor(eventWriter, modelRspec.getMultiplexFactor());

            rspecFactory.writeExtraRspecXml(modelRspec, eventWriter);
            for (ExtraXml extra : modelRspec.getExtraXml())
                extra.writeEvents(eventWriter);

            eventWriter.add(eventFactory.createEndElement(Q_RSPEC, null/*namespaces*/));
            eventWriter.add(eventFactory.createEndDocument());
            eventWriter.close();

            return sw.getBuffer().toString();
        } catch (XMLStreamException e) {
            throw new RuntimeException("Problem writing RSpec", e);
        }
    }

    private void writeCollocateFactor(XMLEventWriter eventWriter, Integer multiplexFactor) throws XMLStreamException {
        if (multiplexFactor == null)
            return;

        eventWriter.add(eventFactory.createStartElement(Q_COLLOCATE_FACTOR, null, null));
        eventWriter.add(eventFactory.createAttribute(Q_ATT_COLLOCATE_FACTOR_COUNT, multiplexFactor.toString()));
        eventWriter.add(eventFactory.createEndElement(Q_COLLOCATE_FACTOR, null));
    }

    private void writeChannel(XMLEventWriter eventWriter, Channel channel) throws XMLStreamException {
        eventWriter.add(eventFactory.createStartElement(Q_CHANNEL, null, null));

        if (channel.getComponentId() != null) {
            eventWriter.add(eventFactory.createAttribute(Q_ATT_COM_ID, channel.getComponentId().toString()));
        }
        if (channel.getComponentManagerId() != null) {
            eventWriter.add(eventFactory.createAttribute(Q_ATT_COM_MAN_ID, channel.getComponentManagerId().toString()));
        }
        if (channel.getComponentName() != null) {
            eventWriter.add(eventFactory.createAttribute(Q_ATT_COM_NAME, channel.getComponentName()));
        }
        if (channel.getFrequency() != null) {
            eventWriter.add(eventFactory.createAttribute(Q_ATT_FREQUENCY, channel.getFrequency()));
        }

        if (channel.getEditorX() != -1.0 && channel.getEditorY() != -1.0) {
            eventWriter.add(eventFactory.createAttribute(Q_ATT_JFED_LOCATION_X, Double.toString(channel.getEditorX())));
            eventWriter.add(eventFactory.createAttribute(Q_ATT_JFED_LOCATION_Y, Double.toString(channel.getEditorY())));
        }

        for (String leaseIdRef : channel.getLeaseIdRefs()) {
            eventWriter.add(eventFactory.createStartElement(Q_LEASE_REF, null /*attributes */, null /*namespaces */));
            eventWriter.add(eventFactory.createAttribute(Q_ATT_ID_REF, leaseIdRef));
            eventWriter.add(eventFactory.createEndElement(Q_LEASE_REF, null /*namespaces*/));
        }

        for (ExtraXml extraXml : channel.getExtraXml())
            extraXml.writeEvents(eventWriter);

        eventWriter.add(eventFactory.createEndElement(Q_CHANNEL, null));
    }

    private void writeRoutableAddresses(XMLEventWriter eventWriter, Integer available, Integer configured) throws XMLStreamException {
        if (available == null && configured == null)
            return;

        eventWriter.add(eventFactory.createStartElement(Q_NODE_ROUTABLEADRESSES, null, null));

        if (available != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_ROUTABLEADRESSES_AVAILABLE, available + ""));
        if (configured != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_ROUTABLEADRESSES_CONFIGURED, configured + ""));

        eventWriter.add(eventFactory.createEndElement(Q_NODE_ROUTABLEADRESSES, null));
    }

    private void writeGeantTestbedType(XMLEventWriter eventWriter, GeantTestbedType gtt) throws XMLStreamException {
        eventWriter.add(eventFactory.createStartElement(Q_GEANT_TESTBED_TYPE, null, null));

        if (gtt.getName() != null) {
            eventWriter.add(eventFactory.createAttribute(Q_ATT_GTT_NAME, gtt.getName()));
        }

        if (gtt.getEditorX() != -1.0 && gtt.getEditorY() != -1.0) {
            eventWriter.add(eventFactory.createAttribute(Q_ATT_JFED_LOCATION_X, Double.toString(gtt.getEditorX())));
            eventWriter.add(eventFactory.createAttribute(Q_ATT_JFED_LOCATION_Y, Double.toString(gtt.getEditorY())));
        }

        if (gtt.getDsl() != null) {
            eventWriter.add(eventFactory.createStartElement(Q_GTS_DSL, null, null));
            eventWriter.add(eventFactory.createCData(gtt.getDsl()));
            eventWriter.add(eventFactory.createEndElement(Q_GTS_DSL, null));
        }

//        if (gtt.getPortBindings() != null) {
//            for (Map.Entry<String, GeantTestbedType.RspecInterfaceBinding> gtsToRspecBinding : gtt.getPortBindings().entrySet()) {
//
//                eventWriter.add(eventFactory.createStartElement(Q_GTS_PORT_BINDING, null, null));
//                eventWriter.add(eventFactory.createAttribute(Q_ATT_GTS_PORT, gtsToRspecBinding.getKey()));
//
//                eventWriter.add(eventFactory.createAttribute(Q_ATT_RSPEC_NODE, gtsToRspecBinding.getValue().getNode()));
//                eventWriter.add(eventFactory.createAttribute(Q_ATT_RSPEC_INTERFACE, gtsToRspecBinding.getValue().getInterface()));
//
//                eventWriter.add(eventFactory.createEndElement(Q_GTS_PORT_BINDING, null)) ;
//            }
//        }

        eventWriter.add(eventFactory.createEndElement(Q_GEANT_TESTBED_TYPE, null));
    }

    private void writeAddressPool(XMLEventWriter eventWriter, AddressPool addressPool) throws XMLStreamException {
        eventWriter.add(eventFactory.createStartElement(Q_ROUTABLE_POOL, null, null));
        if (addressPool.getClientId() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_CLIENT_ID, addressPool.getClientId()));
        if (addressPool.getComponentManagerId() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_COM_MAN_ID, addressPool.getComponentManagerId().toString()));
        if (addressPool.getCount() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_COUNT, addressPool.getCount().toString()));
        if (addressPool.getType() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_ROUTABLE_POOL_TYPE, addressPool.getType()));
        if (addressPool.getEditorX() != -1.0)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_JFED_LOCATION_X, Double.toString(addressPool.getEditorX())));
        if (addressPool.getEditorY() != -1.0)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_JFED_LOCATION_Y, Double.toString(addressPool.getEditorY())));

        for (IPv4 iPv4 : addressPool.getIPv4s()) {
            writeIPv4(eventWriter, iPv4);
        }

        eventWriter.add(eventFactory.createEndElement(Q_ROUTABLE_POOL, null));
    }

    private void writeExecuteAnsibleCookbook(XMLEventWriter eventWriter, ExecuteAnsiblePlaybook executeAnsiblePlaybook) throws XMLStreamException {
        eventWriter.add(eventFactory.createStartElement(Q_JFED_EXECUTE_ANSIBLE_PLAYBOOK, null, null));

        if (executeAnsiblePlaybook.getSource() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_SOURCE, executeAnsiblePlaybook.getSource()));
        if (executeAnsiblePlaybook.getOutputFilename() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_OUTPUT_FILE, executeAnsiblePlaybook.getOutputFilename()));

        eventWriter.add(eventFactory.createEndElement(Q_JFED_EXECUTE_ANSIBLE_PLAYBOOK, null));
    }

    private void writeDistributeSshKeypair(XMLEventWriter eventWriter, DistributeSshKeypair distributeSshKeypair) throws XMLStreamException {
        eventWriter.add(eventFactory.createStartElement(Q_JFED_DISTRIBUTE_SSH_KEYPAIR, null, null));

        if (distributeSshKeypair.getLocation() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_LOCATION, distributeSshKeypair.getLocation()));
        if (distributeSshKeypair.getUser() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_USER, distributeSshKeypair.getUser().toString()));

        eventWriter.add(eventFactory.createEndElement(Q_JFED_DISTRIBUTE_SSH_KEYPAIR, null));
    }

    private void writeIPv4(XMLEventWriter eventWriter, IPv4 iPv4) throws XMLStreamException {
        eventWriter.add(eventFactory.createStartElement(Q_IPV4, null, null));
        if (iPv4.getAddress() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_IPV4_ADDRESS, iPv4.getAddress()));
        if (iPv4.getNetmask() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_IPV4_NETMASK, iPv4.getNetmask()));
        eventWriter.add(eventFactory.createEndElement(Q_IPV4, null));
    }

    public void writeLease(XMLEventWriter eventWriter, Lease lease) throws XMLStreamException {
        eventWriter.add(eventFactory.createStartElement(Q_LEASE, null /* attributes*/, null /* namespaces */));

        if (lease.getLeaseId() != null) {         //only appears in manifests and advertisements
            eventWriter.add(eventFactory.createAttribute(Q_ATT_ID, lease.getLeaseId()));
        }

        if (lease.getClientId() != null) {
            eventWriter.add(eventFactory.createAttribute(Q_ATT_CLIENT_ID, lease.getClientId()));
        }

        if (lease.getValidFromString() != null) {
            eventWriter.add(eventFactory.createAttribute(Q_ATT_VALID_FROM, lease.getValidFromString()));
        }

        if (lease.getValidUntilString() != null) {
            eventWriter.add(eventFactory.createAttribute(Q_ATT_VALID_UNTIL, lease.getValidUntilString()));
        }

        eventWriter.add(eventFactory.createEndElement(Q_LEASE, null /*namespaces*/));

    }

    /**
     * HACK FEDIBBTDEV-2591:
     * detect if the node is a flex:e_node_b
     * */
    private static boolean isFlexENodeB(RspecNode node) {
        if (node.getHardwareTypes() == null || node.getHardwareTypes().isEmpty())
            return false;
        for (HardwareType hardwareType : node.getHardwareTypes()) {
            if (hardwareType.getName().equals(HACK_FIXED_HARDWARETYPE_E_NODE_B))
                return true;
        }
        return false;
    }

    public void writeNode(XMLEventWriter eventWriter, RspecNode node) throws XMLStreamException {
        boolean isFlexENodeB = useNitosENodeBHack && isFlexENodeB(node);

        if (isFlexENodeB) {
            eventWriter.add(eventFactory.createStartElement(Q_NITOS_E_NODE_B, null/*attributes*/, null/*namespaces*/));
        } else {
            eventWriter.add(eventFactory.createStartElement(Q_NODE, null/*attributes*/, null/*namespaces*/));
        }

        if (node.getClientId() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_CLIENT_ID, node.getClientId()));

        if (node.getExclusive() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_NODE_EXCLUSIVE,
                    node.getExclusive() ? "true" : "false"));
        if (node.getComponentManagerId() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_COM_MAN_ID, node.getComponentManagerId().toString()));
        if (node.getComponentId() != null) {
            GeniUrn comId = node.getComponentId();
            if (isFlexENodeB) {
                comId = GeniUrn.createGeniUrnFromEncodedParts(
                        comId.getEncodedTopLevelAuthority(),
                        "e_node_b",
                        comId.getEncodedResourceName()
                );
            }
            eventWriter.add(eventFactory.createAttribute(Q_ATT_COM_ID, comId.toString()));
        }
        if (node.getComponentName() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_COM_NAME, node.getComponentName()));
        if (node.getSliverId() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_SLIVER_ID, node.getSliverId().getValue()));

        if (addEmptySliverToNodeHack) {
            boolean mustAdd = true;
            for (ExtraXml extraXml : node.getExtraXml())
                if (extraXml.getStartElementName() != null &&
                        Objects.equals(extraXml.getStartElementName().getLocalPart(), "sliver")) {
                    mustAdd = false;
                    break;
                }
            if (mustAdd) {
                //completely illegal according to NAMESPACE_RSPEC, but this is a hack needed for PLE.
                eventWriter.add(eventFactory.createStartElement(new QName(NAMESPACE_RSPEC, "sliver"), null/*attributes*/, null/*namespaces*/));
                eventWriter.add(eventFactory.createEndElement(new QName(NAMESPACE_RSPEC, "sliver"), null/*namespaces*/));
            }
        }

        for (SliverType sliverType : node.getSliverTypes()) {
            eventWriter.add(eventFactory.createStartElement(Q_NODE_SLIVERTYPE, null/*attributes*/, null/*namespaces*/));
            if (sliverType.getName() != null)
                eventWriter.add(eventFactory.createAttribute(Q_ATT_NODE_SLIVERTYPE_NAME, sliverType.getName()));
            for (DiskImage diskImage : sliverType.getDiskImages())
                if (diskImage == null || (diskImage.getName() == null && diskImage.getUrl() == null)) {
                    LOG.warn("List of diskImages contained DiskImage without name or url: " + diskImage + " (will be skipped)");
                } else {
                    eventWriter.add(eventFactory.createStartElement(Q_NODE_SLIVERTYPE_DISKIMAGE, null/*attributes*/, null/*namespaces*/));
                    if (diskImage.getName() != null)
                        eventWriter.add(eventFactory.createAttribute(Q_ATT_DISKIMAGE_NAME, diskImage.getName()));
                    if (diskImage.getUrl() != null)
                        eventWriter.add(eventFactory.createAttribute(Q_ATT_DISKIMAGE_URL, diskImage.getUrl()));
                    if (diskImage.getOs() != null)
                        eventWriter.add(eventFactory.createAttribute(Q_ATT_DISKIMAGE_OS, diskImage.getOs()));
                    if (diskImage.getVersion() != null)
                        eventWriter.add(eventFactory.createAttribute(Q_ATT_DISKIMAGE_VERSION, diskImage.getVersion()));
                    if (diskImage.getDescription() != null)
                        eventWriter.add(eventFactory.createAttribute(Q_ATT_DISKIMAGE_DESCRIPTION, diskImage.getDescription()));
                    if (diskImage.isDefault() != null)
                        eventWriter.add(eventFactory.createAttribute(Q_ATT_DISKIMAGE_DEFAULT,
                                diskImage.isDefault() ? "true" : "false"));
                    eventWriter.add(eventFactory.createEndElement(Q_NODE_SLIVERTYPE_DISKIMAGE, null/*namespaces*/));
                }
            if (sliverType.hasXen()) {
                eventWriter.add(eventFactory.createStartElement(Q_NODE_SLIVERTYPE_XEN, null/*attributes*/, null/*namespaces*/));
                if (sliverType.getXenCores() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_XEN_CORES, sliverType.getXenCores() + ""));
                if (sliverType.getXenRam() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_XEN_RAM, sliverType.getXenRam() + ""));
                if (sliverType.getXenDisk() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_XEN_DISK, sliverType.getXenDisk() + ""));
                eventWriter.add(eventFactory.createEndElement(Q_NODE_SLIVERTYPE_XEN, null/*namespaces*/));
            }
            for (ExtraXml extraXml : sliverType.getExtraXml())
                extraXml.writeEvents(eventWriter);
            eventWriter.add(eventFactory.createEndElement(Q_NODE_SLIVERTYPE, null/*namespaces*/));
        }
        if (node.getAvailable() != null) {
            eventWriter.add(eventFactory.createStartElement(Q_NODE_AVAILABLE, null/*attributes*/, null/*namespaces*/));
            eventWriter.add(eventFactory.createAttribute(Q_ATT_NODE_AVAILABLE_NOW,
                    node.getAvailable() ? "true" : "false"));
            eventWriter.add(eventFactory.createEndElement(Q_NODE_AVAILABLE, null/*namespaces*/));
        }
        if (!isFlexENodeB) {
            for (HardwareType hardwareType : node.getHardwareTypes()) {
                eventWriter.add(eventFactory.createStartElement(Q_NODE_HARDWARETYPE, null/*attributes*/, null/*namespaces*/));
                eventWriter.add(eventFactory.createAttribute(Q_ATT_NODE_HARDWARETYPE_NAME, hardwareType.getName()));
                if (hardwareType.hasTypeSlots()) {
                    eventWriter.add(eventFactory.createStartElement(Q_NODE_HARDWARETYPE_NODETYPE, null/*attributes*/, null/*namespaces*/));
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_NODE_HARDWARETYPE_NODETYPE_TYPESLOTS, hardwareType.getTypeSlotsString()));
                    eventWriter.add(eventFactory.createEndElement(Q_NODE_HARDWARETYPE_NODETYPE, null/*namespaces*/));
                }
                eventWriter.add(eventFactory.createEndElement(Q_NODE_HARDWARETYPE, null/*namespaces*/));
            }
        }
        if (node.getRoutableControlIp()) {
            eventWriter.add(eventFactory.createStartElement(Q_NODE_ROUTABLECONTROLIP, null/*attributes*/, null/*namespaces*/));
            //no attributes.
            eventWriter.add(eventFactory.createEndElement(Q_NODE_ROUTABLECONTROLIP, null/*namespaces*/));
        }

        if (!node.getLoginServices().isEmpty()
                || !node.getProxyServices().isEmpty()
                || !node.getInstallServices().isEmpty()
                || !node.getExecuteServices().isEmpty()
                || !node.getExtraServicesXml().isEmpty()
                || node.hasEmptyTag(RspecNode.EmptyTag.SERVICES)) {
            eventWriter.add(eventFactory.createStartElement(Q_NODE_SERVICES, null/*attributes*/, null/*namespaces*/));

            for (LoginService s : node.getLoginServices()) {
                eventWriter.add(eventFactory.createStartElement(Q_NODE_SERVICES_LOGIN, null/*attributes*/, null/*namespaces*/));
                if (s.getAuthentication() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_NODE_SERVICES_LOGIN_AUTH, s.getAuthentication()));
                if (s.getHostname() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_NODE_SERVICES_LOGIN_HOSTNAME, s.getHostname()));
                if (s.getPort() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_NODE_SERVICES_LOGIN_PORT, s.getPort()));
                if (s.getUsername() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_NODE_SERVICES_LOGIN_USERNAME, s.getUsername()));
                eventWriter.add(eventFactory.createEndElement(Q_NODE_SERVICES_LOGIN, null/*namespaces*/));
            }
            for (ProxyService s : node.getProxyServices()) {
                eventWriter.add(eventFactory.createStartElement(Q_PROXY_PROXY, null, null));

                if (s.getProxy() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_PROXY, s.getProxy()));
                if (s.getFor() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_FOR, s.getFor()));
                if (s.getHostKey() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_HOSTKEY, s.getHostKey()));

                eventWriter.add(eventFactory.createEndElement(Q_PROXY_PROXY, null));
            }
            for (ExecuteService s : node.getExecuteServices()) {
                eventWriter.add(eventFactory.createStartElement(Q_NODE_SERVICES_EXECUTE, null/*attributes*/, null/*namespaces*/));
                if (s.getShell() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_NODE_SERVICES_EXECUTE_SHELL, s.getShell()));
                if (s.getCommand() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_NODE_SERVICES_EXECUTE_COMMAND, s.getCommand()));
                if (s.getFinishedFile() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_JFED_EXECUTE_FINISHED_FLAG, s.getFinishedFile()));
                eventWriter.add(eventFactory.createEndElement(Q_NODE_SERVICES_EXECUTE, null/*namespaces*/));
            }
            for (InstallService s : node.getInstallServices()) {
                eventWriter.add(eventFactory.createStartElement(Q_NODE_SERVICES_INSTALL, null/*attributes*/, null/*namespaces*/));
                if (s.getInstallPath() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_NODE_SERVICES_INSTALL_PATH, s.getInstallPath()));
                if (s.getUrl() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_NODE_SERVICES_INSTALL_URL, s.getUrl()));
                eventWriter.add(eventFactory.createEndElement(Q_NODE_SERVICES_INSTALL, null/*namespaces*/));
            }
            for (AnsibleService s : node.getAnsibleServices()) {
                eventWriter.add(eventFactory.createStartElement(Q_JFED_ANSIBLE_SERVICE, null, null));
                if (s.getPlaybookCommand() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_PLAYBOOK_COMMAND, s.getPlaybookCommand()));
                if (s.getGalaxyCommand() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_GALAXY_COMMAND, s.getGalaxyCommand()));
                if (s.getLogfile() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_LOGFILE, s.getLogfile()));
                if (s.getInstallRequirements() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_INSTALL_REQUIREMENTS, s.getInstallRequirements()));
                if (s.getExecutePlaybook() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_EXECUTE_PLAYBOOK, s.getExecutePlaybook()));
                if(s.getInventory() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_INVENTORY, s.getInventory()));
                if (s.getDebug() != null)
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_DEBUG, s.getDebug().toString()));

                eventWriter.add(eventFactory.createEndElement(Q_JFED_ANSIBLE_SERVICE, null));

            }
            for (ExtraXml extra : node.getExtraServicesXml())
                extra.writeEvents(eventWriter);

            eventWriter.add(eventFactory.createEndElement(Q_NODE_SERVICES, null/*namespaces*/));
        }

        if (node.getLocation() != null) {
            eventWriter.add(eventFactory.createStartElement(Q_NODE_LOCATION, null/*attributes*/, null/*namespaces*/));
            NodeLocation loc = node.getLocation();
            if (loc.getCountry() != null)
                eventWriter.add(eventFactory.createAttribute(Q_ATT_NODE_LOCATION_COUNTRY, loc.getCountry()));
            if (loc.getLatitude() != null)
                eventWriter.add(eventFactory.createAttribute(Q_ATT_NODE_LOCATION_LAT, loc.getLatitude()));
            if (loc.getLongitude() != null)
                eventWriter.add(eventFactory.createAttribute(Q_ATT_NODE_LOCATION_LONG, loc.getLongitude()));
            eventWriter.add(eventFactory.createEndElement(Q_NODE_LOCATION, null/*namespaces*/));
        }

        if (node.getEditorX() >= 0.0 || node.getEditorY() >= 0.0) {
            eventWriter.add(eventFactory.createStartElement(Q_JFED_LOCATION, null/*attributes*/, null/*namespaces*/));
            if (node.getEditorX() >= 0.0)
                eventWriter.add(eventFactory.createAttribute(Q_ATT_JFED_LOCATION_X, node.getEditorX() + ""));
            if (node.getEditorY() >= 0.0)
                eventWriter.add(eventFactory.createAttribute(Q_ATT_JFED_LOCATION_Y, node.getEditorY() + ""));
            eventWriter.add(eventFactory.createEndElement(Q_JFED_LOCATION, null/*namespaces*/));
        }

        for (RspecInterface iface : node.getInterfaces()) {
            writeIface(eventWriter, iface);
        }

        for (String leaseIdRef : node.getLeaseIdRefs()) {
            eventWriter.add(eventFactory.createStartElement(Q_LEASE_REF, null /*attributes */, null /*namespaces */));
            eventWriter.add(eventFactory.createAttribute(Q_ATT_ID_REF, leaseIdRef));
            eventWriter.add(eventFactory.createEndElement(Q_LEASE_REF, null /*namespaces*/));
        }

        List<String> ansibleGroups = node.getAnsibleGroups();
        if(ansibleGroups != null && !ansibleGroups.isEmpty()){
            for (String ansibleGroup : ansibleGroups) {
                eventWriter.add(eventFactory.createStartElement(Q_JFED_ANSIBLE_GROUP, null, null));
                eventWriter.add(eventFactory.createAttribute(Q_ATT_ANSIBLE_GROUP_NAME, ansibleGroup));
                eventWriter.add(eventFactory.createEndElement(Q_JFED_ANSIBLE_GROUP, null));
            }
        }

        rspecFactory.writeExtraNodeXml(node, eventWriter);

        for (ExtraXml extra : node.getExtraXml())
            extra.writeEvents(eventWriter);
        if (isFlexENodeB)
            eventWriter.add(eventFactory.createEndElement(Q_NITOS_E_NODE_B, null/*namespaces*/));
        else
            eventWriter.add(eventFactory.createEndElement(Q_NODE, null/*namespaces*/));
    }

    private void writeIface(XMLEventWriter eventWriter, RspecInterface iface) throws XMLStreamException {
        eventWriter.add(eventFactory.createStartElement(Q_INTERFACE, null/*attributes*/, null/*namespaces*/));
        if (iface.getClientId() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_CLIENT_ID, iface.getClientId()));

        if (iface.getSliverId() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_SLIVER_ID, iface.getSliverId().getValue()));
        if (iface.getComponentId() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_COM_ID, iface.getComponentId()));

        if (iface.getComponentName() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_COM_NAME, iface.getComponentName()));
        if (iface.getRole() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_IFACE_ROLE, iface.getRole()));
        if (iface.getPublicIpv4() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_IFACE_PUBIP4, iface.getPublicIpv4()));

        if (iface.getMacAddress() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_INTERFACE_MAC, iface.getMacAddress()));

        for (RspecInterface.IpAddress ip : iface.getIpAddresses()) {
            eventWriter.add(eventFactory.createStartElement(Q_INTERFACE_IP, null/*attributes*/, null/*namespaces*/));
            eventWriter.add(eventFactory.createAttribute(Q_ATT_INTERFACE_IP_ADDRESS, ip.getAddress()));
            if (ip.getNetmask() != null)
                eventWriter.add(eventFactory.createAttribute(Q_ATT_INTERFACE_IP_NETMASK, ip.getNetmask()));
            if (ip.getType() != null)
                eventWriter.add(eventFactory.createAttribute(Q_ATT_INTERFACE_IP_TYPE, ip.getType()));
            eventWriter.add(eventFactory.createEndElement(Q_INTERFACE_IP, null/*namespaces*/));
        }

        for (ExtraXml extra : iface.getExtraXml())
            extra.writeEvents(eventWriter);
        eventWriter.add(eventFactory.createEndElement(Q_INTERFACE, null/*namespaces*/));
    }

    private void writeIfaceRef(XMLEventWriter eventWriter, RspecInterface iface) throws XMLStreamException {
        eventWriter.add(eventFactory.createStartElement(Q_INTERFACE_REF, null/*attributes*/, null/*namespaces*/));
        if (iface.getClientId() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_CLIENT_ID, iface.getClientId()));
        if (iface.getSliverId() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_SLIVER_ID, iface.getSliverId().getValue()));
        if (iface.getComponentId() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_COM_ID, iface.getComponentId()));
        if (iface.getFelixVlan() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_FELIX_VLAN, Integer.toString(iface.getFelixVlan())));
        for (ExtraXml extra : iface.getRefExtraXml())
            extra.writeEvents(eventWriter);
        eventWriter.add(eventFactory.createEndElement(Q_INTERFACE_REF, null/*namespaces*/));
    }


    private void writeLink(XMLEventWriter eventWriter, RspecLink link) throws XMLStreamException {
        eventWriter.add(eventFactory.createStartElement(Q_LINK, null/*attributes*/, null/*namespaces*/));
        if (link.getClientId() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_CLIENT_ID, link.getClientId()));
        if (link.getSliverId() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_SLIVER_ID, link.getSliverId().getValue()));
        if (link.getVlanTag() != null)
            eventWriter.add(eventFactory.createAttribute(Q_ATT_LINK_VLANTAG, link.getVlanTag().toString()));

        for (GeniUrn urn : link.getComponentManagerUrns()) {
            eventWriter.add(eventFactory.createStartElement(Q_LINK_COM_MAN, null/*attributes*/, null/*namespaces*/));
            eventWriter.add(eventFactory.createAttribute(Q_ATT_LINK_COM_MAN_NAME, urn.getValue()));
            eventWriter.add(eventFactory.createEndElement(Q_LINK_COM_MAN, null/*namespaces*/));
        }

        for (RspecInterface iface : link.getInterfaces()) {
            writeIfaceRef(eventWriter, iface);
        }

        for (String linkType : link.getLinkTypes()) {
            eventWriter.add(eventFactory.createStartElement(Q_LINK_TYPE, null/*attributes*/, null/*namespaces*/));
            eventWriter.add(eventFactory.createAttribute(Q_ATT_LINK_TYPE_NAME, linkType));
            eventWriter.add(eventFactory.createEndElement(Q_LINK_TYPE, null/*namespaces*/));
        }

        if (link.getSharedLan() != null) {
            eventWriter.add(eventFactory.createStartElement(Q_LINK_SHAREDLAN_LINKSHAREDVLAN, null/*attributes*/, null/*namespaces*/));
            eventWriter.add(eventFactory.createAttribute(Q_LINK_SHAREDLAN_ATT_NAME, link.getSharedLan()));
            eventWriter.add(eventFactory.createEndElement(Q_LINK_SHAREDLAN_LINKSHAREDVLAN, null/*namespaces*/));
        }

        if (link.hasVlanTaggingTag()) {
            eventWriter.add(eventFactory.createStartElement(Q_LINK_VLAN_TAGGING, null/*attributes*/, null/*namespaces*/));
            eventWriter.add(eventFactory.createAttribute(Q_LINK_VLAN_TAGGING_ENABLED_ATT_NAME,
                    link.isVlanTaggingEnabled() ? "true" : "false"));
            eventWriter.add(eventFactory.createEndElement(Q_LINK_VLAN_TAGGING, null/*namespaces*/));
        }
        if (link.hasNoMacLearningTag()) {
            eventWriter.add(eventFactory.createStartElement(Q_LINK_LINKATTRIBUTE, null/*attributes*/, null/*namespaces*/));
            eventWriter.add(eventFactory.createAttribute(Q_LINK_LINKATTRIBUTE_KEY, Q_LINK_LINKATTRIBUTE_KEY_NO_MAC_LEARNING));
            eventWriter.add(eventFactory.createAttribute(Q_LINK_LINKATTRIBUTE_VALUE, link.getNoMacLeaningValueString())); //reconstruct values like "yep"
            //don't overwrite values like "yep"        link.isNoMacLearning() ? "true" : "false"));
            eventWriter.add(eventFactory.createEndElement(Q_LINK_LINKATTRIBUTE, null/*namespaces*/));
        }
        if (link.hasBestEffortTag()) {
            eventWriter.add(eventFactory.createStartElement(Q_LINK_BEST_EFFORT, null/*attributes*/, null/*namespaces*/));
            eventWriter.add(eventFactory.createAttribute(Q_LINK_BEST_EFFORT_ENABLED_ATT_NAME,
                    link.isBestEffortEnabled() ? "true" : "false"));
            eventWriter.add(eventFactory.createEndElement(Q_LINK_BEST_EFFORT, null/*namespaces*/));
        }
        if (link.hasAutoShareLan()) {
            for (String sharedLanName : link.getAutoShareLanNames()) {
                eventWriter.add(eventFactory.createStartElement(Q_JFED_LINK_AUTOSHARELAN, null/*attributes*/, null/*namespaces*/));
                eventWriter.add(eventFactory.createAttribute(Q_ATT_JFED_LINK_AUTOSHARELAN_NAME, sharedLanName));
                eventWriter.add(eventFactory.createEndElement(Q_JFED_LINK_AUTOSHARELAN, null/*namespaces*/));
            }
        }
        if (link.hasLinkMultiplexingTag()) {
            eventWriter.add(eventFactory.createStartElement(Q_LINK_LINK_MULTIPLEXING, null/*attributes*/, null/*namespaces*/));
            eventWriter.add(eventFactory.createAttribute(Q_LINK_LINK_MULTIPLEXING_ENABLED_ATT_NAME,
                    link.isLinkMultiplexingEnabled() ? "true" : "false"));
            eventWriter.add(eventFactory.createEndElement(Q_LINK_LINK_MULTIPLEXING, null/*namespaces*/));
        }

        if (link.hasInterswitchDisallowTag()) {
            eventWriter.add(eventFactory.createStartElement(Q_LINK_INTERSWITCH, null/*attributes*/, null/*namespaces*/));
            eventWriter.add(eventFactory.createAttribute(Q_LINK_INTERSWITCH_ALLOW,
                    link.isInterswitchDisallowed() ? "false" : "true"));
            eventWriter.add(eventFactory.createEndElement(Q_LINK_INTERSWITCH, null/*namespaces*/));
        }


        for (LinkSetting linkSetting : link.getLinkSettings())
            if (linkSetting.isActive() || linkSetting.isShownIfNotActive()) {
                eventWriter.add(eventFactory.createStartElement(Q_LINK_PROPERTY, null/*attributes*/, null/*namespaces*/));
                eventWriter.add(eventFactory.createAttribute(Q_ATT_LINK_PROPERTY_SOURCE, linkSetting.getFromIface().getClientId()));
                eventWriter.add(eventFactory.createAttribute(Q_ATT_LINK_PROPERTY_DEST, linkSetting.getToIface().getClientId()));
                if (linkSetting.isCapacitySet())
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_LINK_PROPERTY_CAPACITY, linkSetting.getCapacity_Kbps() + ""));
                if (linkSetting.isLatencySet())
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_LINK_PROPERTY_LATENCY, linkSetting.getLatency_ms() + ""));
                if (linkSetting.isPacketLossSet())
                    eventWriter.add(eventFactory.createAttribute(Q_ATT_LINK_PROPERTY_LOSS, linkSetting.getPacketLoss() + ""));
                eventWriter.add(eventFactory.createEndElement(Q_LINK_PROPERTY, null/*namespaces*/));
            }

        rspecFactory.writeExtraLinkXml(link, eventWriter);
        for (ExtraXml extra : link.getExtraXml())
            extra.writeEvents(eventWriter);
        eventWriter.add(eventFactory.createEndElement(Q_LINK, null/*namespaces*/));
    }


    @SuppressWarnings("unchecked")
    public static void writeWithJaxb(XMLEventWriter eventWriter, QName name, Object o, Class c) {
        JAXBElement je = new JAXBElement(name, c, o);

        try {
            JAXBContext jc = JAXBContext.newInstance(c);
            Marshaller marshaller = jc.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
            marshaller.marshal(je, eventWriter);
        } catch (JAXBException e) {
            throw new RuntimeException("error writing " + c.getName() + "", e);
        }
    }
}
