/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2013, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This 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 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.patching.metadata;

import static org.jboss.as.controller.parsing.ParseUtils.readStringAttributeElement;
import static org.jboss.as.controller.parsing.ParseUtils.requireNoContent;
import static org.jboss.as.controller.parsing.ParseUtils.unexpectedAttribute;
import static org.jboss.as.controller.parsing.ParseUtils.unexpectedElement;
import static org.jboss.as.patching.HashUtils.bytesToHexString;
import static org.jboss.as.patching.HashUtils.hexStringToByteArray;
import static org.jboss.as.patching.IoUtils.NO_CONTENT;
import static org.jboss.as.patching.metadata.ModuleItem.MAIN_SLOT;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;

import org.jboss.as.controller.logging.ControllerLogger;
import org.jboss.as.patching.PatchingException;
import org.jboss.as.patching.installation.InstalledIdentity;
import org.jboss.as.patching.metadata.impl.PatchElementImpl;
import org.jboss.as.patching.metadata.impl.PatchElementProviderImpl;
import org.jboss.as.patching.metadata.impl.RequiresCallback;
import org.jboss.staxmapper.XMLExtendedStreamReader;
import org.jboss.staxmapper.XMLExtendedStreamWriter;

/**
 * @author Emanuel Muckenhuber
 */
class PatchXmlUtils implements XMLStreamConstants {

    private static final String PATH_DELIMITER = "/";

    enum Element {

        ADDED("added"),
        BUNDLES("bundles"),
        DESCRIPTION("description"),
        IDENTITY("identity"),
        REQUIRES("requires"),
        LINK("link"),
        MISC_FILES("misc-files"),
        MODULES("modules"),
        NO_UPGRADE("no-upgrade"),
        ONE_OFF("one-off"),
        PATCH("patch"),
        PATCH_ELEMENT("element"),
        REMOVED("removed"),
        UPDATED("updated"),
        UPGRADE("upgrade"),

        // default unknown element
        UNKNOWN(null),
        ;

        public final String name;
        Element(String name) {
            this.name = name;
        }

        static Map<String, Element> elements = new HashMap<String, Element>();
        static {
            for(Element element : Element.values()) {
                if(element != UNKNOWN) {
                    elements.put(element.name, element);
                }
            }
        }

        static Element forName(String name) {
            final Element element = elements.get(name);
            return element == null ? UNKNOWN : element;
        }

    }

    enum Attribute {

        ADD_ON("add-on"),
        DIRECTORY("directory"),
        EXISTING_PATH("existing-path"),
        HASH("hash"),
        ID("id"),
        IN_RUNTIME_USE("in-runtime-use"),
        NAME("name"),
        NEW_HASH("new-hash"),
        PATH("path"),
        CONDITION("condition"),
        SLOT("slot"),
        TO_VERSION("to-version"),
        URL("url"),
        VERSION("version"),


        // default unknown attribute
        UNKNOWN(null);

        private final String name;
        Attribute(String name) {
            this.name = name;
        }

        static Map<String, Attribute> attributes = new HashMap<String, Attribute>();
        static {
            for(Attribute attribute : Attribute.values()) {
                if(attribute != UNKNOWN) {
                    attributes.put(attribute.name, attribute);
                }
            }
        }

        static Attribute forName(String name) {
            final Attribute attribute = attributes.get(name);
            return attribute == null ? UNKNOWN : attribute;
        }
    }

    protected static void writePatch(final XMLExtendedStreamWriter writer, final Patch patch) throws XMLStreamException {

        // id
        writer.writeAttribute(Attribute.ID.name, patch.getPatchId());

        // Description
        String description = patch.getDescription();
        if (description != null) {
            writer.writeStartElement(Element.DESCRIPTION.name);
            writer.writeCharacters(description);
            writer.writeEndElement(); // description
        }

        String link = patch.getLink();
        if (link != null) {
            writer.writeEmptyElement(Element.LINK.name);
            writer.writeAttribute(Attribute.URL.name, link);
        }

        // identity
        final Identity identity = patch.getIdentity();
        final Patch.PatchType type = identity.getPatchType();
        if (type == Patch.PatchType.CUMULATIVE) {
            writer.writeStartElement(Element.UPGRADE.name);
        } else {
            writer.writeStartElement(Element.NO_UPGRADE.name);
        }

        writer.writeAttribute(Attribute.NAME.name, identity.getName());
        writer.writeAttribute(Attribute.VERSION.name, identity.getVersion());

        // upgrade / no-upgrade
        if(type == Patch.PatchType.CUMULATIVE) {
            final Identity.IdentityUpgrade upgrade = identity.forType(Patch.PatchType.CUMULATIVE, Identity.IdentityUpgrade.class);
            writer.writeAttribute(Attribute.TO_VERSION.name, upgrade.getResultingVersion());
        }

        if(!identity.getRequires().isEmpty()) {
            writer.writeStartElement(Element.REQUIRES.name);
            for(String patchId : identity.getRequires()) {
                writer.writeStartElement(Element.PATCH.name);
                writer.writeAttribute(Attribute.ID.name, patchId);
                writer.writeEndElement(); // patch
            }
            writer.writeEndElement(); // includes
        }
        writer.writeEndElement(); // identity

        // elements
        final List<PatchElement> elements = patch.getElements();
        for(PatchElement element : elements) {

            writer.writeStartElement(Element.PATCH_ELEMENT.name);
            writer.writeAttribute(Attribute.ID.name, element.getId());

            if(element.getDescription() != null) {
                writer.writeStartElement(Element.DESCRIPTION.name);
                writer.writeCharacters(element.getDescription());
                writer.writeEndElement(); // description
            }

            // layer / add-on
            final PatchElementProvider provider = element.getProvider();
            assert provider != null;

            // identity
            final Patch.PatchType elementPatchType =  provider.getPatchType();
            if (elementPatchType == Patch.PatchType.CUMULATIVE) {
                writer.writeStartElement(Element.UPGRADE.name);
            } else {
                writer.writeStartElement(Element.NO_UPGRADE.name);
            }

            writer.writeAttribute(Attribute.NAME.name, provider.getName());
            if (provider.isAddOn()) {
                writer.writeAttribute(Attribute.ADD_ON.name, "true");
            }

            if(!provider.getRequires().isEmpty()) {
                writer.writeStartElement(Element.REQUIRES.name);
                for(String elementId : provider.getRequires()) {
                    writer.writeStartElement(Element.PATCH.name);
                    writer.writeAttribute(Attribute.ID.name, elementId);
                    writer.writeEndElement(); // element
                }
                writer.writeEndElement(); // includes
            }
            writer.writeEndElement(); // add-on / layer

            // Write the content modifications
            writeContentModifications(writer, element.getModifications());

            writer.writeEndElement(); // element
        }

        // Write the identity modifications directory for some tests
        writeContentModifications(writer, patch.getModifications());

    }

    protected static void writeContentModifications(final XMLExtendedStreamWriter writer, final Collection<ContentModification> modifications) throws XMLStreamException {
        // Sort by content and modification type
        final List<ContentModification> bundlesAdd  = new ArrayList<ContentModification>();
        final List<ContentModification> bundlesUpdate  = new ArrayList<ContentModification>();
        final List<ContentModification> bundlesRemove  = new ArrayList<ContentModification>();

        final List<ContentModification> miscAdd = new ArrayList<ContentModification>();
        final List<ContentModification> miscUpdate = new ArrayList<ContentModification>();
        final List<ContentModification> miscRemove = new ArrayList<ContentModification>();

        final List<ContentModification> modulesAdd = new ArrayList<ContentModification>();
        final List<ContentModification> modulesUpdate = new ArrayList<ContentModification>();
        final List<ContentModification> modulesRemove = new ArrayList<ContentModification>();

        for(final ContentModification mod : modifications) {
            final ModificationType modificationType = mod.getType();
            final ContentType contentType = mod.getItem().getContentType();
            switch (contentType) {
                case BUNDLE:
                    switch (modificationType) {
                        case ADD:
                            bundlesAdd.add(mod);
                            break;
                        case MODIFY:
                            bundlesUpdate.add(mod);
                            break;
                        case REMOVE:
                            bundlesRemove.add(mod);
                            break;
                    }
                    break;
                case MODULE:
                    switch (modificationType) {
                        case ADD:
                            modulesAdd.add(mod);
                            break;
                        case MODIFY:
                            modulesUpdate.add(mod);
                            break;
                        case REMOVE:
                            modulesRemove.add(mod);
                            break;
                    }
                    break;
                case MISC:
                    switch (modificationType) {
                        case ADD:
                            miscAdd.add(mod);
                            break;
                        case MODIFY:
                            miscUpdate.add(mod);
                            break;
                        case REMOVE:
                            miscRemove.add(mod);
                            break;
                    }
                    break;
            }
        }

        // Modules
        if (!modulesAdd.isEmpty() ||
                !modulesUpdate.isEmpty() ||
                !modulesRemove.isEmpty()) {
            writer.writeStartElement(Element.MODULES.name);
            writeSlottedItems(writer, Element.ADDED, modulesAdd);
            writeSlottedItems(writer, Element.UPDATED, modulesUpdate);
            writeSlottedItems(writer, Element.REMOVED, modulesRemove);
            writer.writeEndElement(); // modules
        }

        // Bundles
        if (!bundlesAdd.isEmpty() ||
                !bundlesUpdate.isEmpty() ||
                !bundlesRemove.isEmpty()) {
            writer.writeStartElement(Element.BUNDLES.name);
            writeSlottedItems(writer, Element.ADDED, bundlesAdd);
            writeSlottedItems(writer, Element.UPDATED, bundlesUpdate);
            writeSlottedItems(writer, Element.REMOVED, bundlesRemove);
            writer.writeEndElement(); // bundles
        }

        // Misc
        if (!miscAdd.isEmpty() ||
                !miscUpdate.isEmpty() ||
                !miscRemove.isEmpty()) {
            writer.writeStartElement(Element.MISC_FILES.name);
            writeMiscItems(writer, Element.ADDED, miscAdd);
            writeMiscItems(writer, Element.UPDATED, miscUpdate);
            writeMiscItems(writer, Element.REMOVED, miscRemove);
            writer.writeEndElement(); // misc-files
        }
    }

    protected void doReadElement(final XMLExtendedStreamReader reader, final PatchBuilder builder, InstalledIdentity originalIdentity) throws XMLStreamException {

        final PatchBuilder patch = builder;
        final int count = reader.getAttributeCount();
        for (int i = 0; i < count; i++) {
            final String value = reader.getAttributeValue(i);
            final Attribute attribute = Attribute.forName(reader.getAttributeLocalName(i));
            if(Attribute.ID == attribute) {
                patch.setPatchId(value);
            } else {
                throw unexpectedAttribute(reader, i);
            }
        }

        final Collection<ContentModification> modifications = patch.getModifications();
        while (reader.hasNext() && reader.nextTag() != END_ELEMENT) {
            final String localName = reader.getLocalName();
            final Element element = Element.forName(localName);
            switch (element) {
                case DESCRIPTION:
                    patch.setDescription(reader.getElementText());
                    break;
                case LINK:
                    final String link = readStringAttributeElement(reader, Attribute.URL.name);
                    builder.setLink(link);
                    break;
                case UPGRADE:
                    parseIdentity(reader, patch, Patch.PatchType.CUMULATIVE);
                    break;
                case NO_UPGRADE:
                    parseIdentity(reader, patch, Patch.PatchType.ONE_OFF);
                    break;
                case PATCH_ELEMENT:
                    parseElement(reader, patch);
                    break;
                case MODULES:
                    parseModules(reader, modifications);
                    break;
                case BUNDLES:
                    parseBundles(reader, modifications);
                    break;
                case MISC_FILES:
                    parseMiscFiles(reader, modifications);
                    break;
                default:
                    handleRootElement(localName, reader, patch, originalIdentity);
            }
        }
    }

    protected void handleRootElement(final String localName, final XMLExtendedStreamReader reader, final PatchBuilder builder, InstalledIdentity originalIdentity) throws XMLStreamException {
        throw unexpectedElement(reader);
    }

    static void parseElement(final XMLExtendedStreamReader reader, final PatchBuilder builder) throws XMLStreamException {

        String id = null;
        final int count = reader.getAttributeCount();
        for (int i = 0; i < count; i++) {
            final String value = reader.getAttributeValue(i);
            final Attribute attribute = Attribute.forName(reader.getAttributeLocalName(i));
            if (Attribute.ID == attribute) {
                id = value;
            } else {
                throw unexpectedAttribute(reader, i);
            }
        }

        final PatchElementImpl patchElement = new PatchElementImpl(id);
        try {
            builder.addElement(patchElement);
        } catch(IllegalStateException e) {
            throw new XMLStreamException(e);
        }
        final List<ContentModification> modifications = patchElement.getModifications();
        while (reader.hasNext() && reader.nextTag() != END_ELEMENT) {
            final Element element = Element.forName(reader.getLocalName());
            switch (element) {
                case DESCRIPTION:
                    patchElement.setDescription(reader.getElementText());
                    break;
                case UPGRADE:
                    parseElementProvider(reader, patchElement, Patch.PatchType.CUMULATIVE);
                    break;
                case NO_UPGRADE:
                    parseElementProvider(reader, patchElement, Patch.PatchType.ONE_OFF);
                    break;
                case MODULES:
                    parseModules(reader, modifications);
                    break;
                case BUNDLES:
                    parseBundles(reader, modifications);
                    break;
                case MISC_FILES:
                    parseMiscFiles(reader, modifications);
                    break;
                default:
                    throw unexpectedElement(reader);
            }
        }
    }

    static void parseElementProvider(final XMLExtendedStreamReader reader, PatchElementImpl patchElement, Patch.PatchType patchType) throws XMLStreamException {

        String name = null;
        boolean isAddOn = false;
        final int count = reader.getAttributeCount();
        for (int i = 0; i < count; i++) {
            final Attribute attribute = Attribute.forName(reader.getAttributeLocalName(i));
            if (Attribute.NAME == attribute) {
                name = reader.getAttributeValue(i);
            } else if (Attribute.ADD_ON == attribute) {
                isAddOn = Boolean.valueOf(reader.getAttributeValue(i));
            } else {
                throw unexpectedAttribute(reader, i);
            }
        }

        final PatchElementProviderImpl provider = new PatchElementProviderImpl(name, isAddOn);
        patchElement.setProvider(provider);
        switch (patchType) {
            case CUMULATIVE:
                provider.upgrade();
                break;
            case ONE_OFF:
                provider.oneOffPatch();
                break;
            default:
                throw new IllegalStateException();
        }

        int level = 0;
        while (reader.hasNext()) {
            if(reader.nextTag() == END_ELEMENT) {
                if(level == 0) {
                    break;
                } else {
                    --level;
                }
            }
            final Element element = Element.forName(reader.getLocalName());
            switch (element) {
                case REQUIRES:
                    break;
                case PATCH:
                    level = 1;
                    parseIncluded(reader, provider);
                    break;
                default:
                    throw unexpectedElement(reader);
            }
        }
    }

    static void parseIdentity(final XMLExtendedStreamReader reader, final PatchBuilder builder, final Patch.PatchType patchType) throws XMLStreamException {

        String name = null;
        String version = null;
        String resultingVersion = null;
        final int count = reader.getAttributeCount();
        for (int i = 0; i < count; i++) {
            final Attribute attribute = Attribute.forName(reader.getAttributeLocalName(i));
            if (Attribute.VERSION == attribute) {
                version = reader.getAttributeValue(i);
            } else if (Attribute.TO_VERSION == attribute) {
                resultingVersion = reader.getAttributeValue(i);
            } else if (Attribute.NAME == attribute) {
                name = reader.getAttributeValue(i);
            } else {
                throw unexpectedAttribute(reader, i);
            }
        }

        final PatchIdentityBuilder identityBuilder;
        switch (patchType) {
            case CUMULATIVE:
                identityBuilder = builder.upgradeIdentity(name, version, resultingVersion);
                break;
            case ONE_OFF:
                identityBuilder = builder.oneOffPatchIdentity(name, version);
                break;
            default:
                throw new IllegalStateException();
        }

        int level = 0;
        while (reader.hasNext()) {
            if(reader.nextTag() == END_ELEMENT) {
                if(level == 0) {
                    break;
                } else {
                    --level;
                }
            }

            final Element element = Element.forName(reader.getLocalName());
            switch (element) {
                case REQUIRES:
                    break;
                case PATCH:
                    level = 1;
                    parseIncluded(reader, identityBuilder);
                    break;
                default:
                    throw unexpectedElement(reader);
            }
        }
    }

    static void parseIncluded(final XMLExtendedStreamReader reader, final RequiresCallback includes) throws XMLStreamException {

        final int count = reader.getAttributeCount();
        for (int i = 0; i < count; i++) {
            final String value = reader.getAttributeValue(i);
            final Attribute attribute = Attribute.forName(reader.getAttributeLocalName(i));
            if(Attribute.ID == attribute) {
                includes.require(value);
            } else {
                throw unexpectedAttribute(reader, i);
            }
        }
        requireNoContent(reader);
    }

    static void parseModules(final XMLExtendedStreamReader reader, final Collection<ContentModification> modifications) throws XMLStreamException {
        while (reader.hasNext() && reader.nextTag() != END_ELEMENT) {
            final Element element = Element.forName(reader.getLocalName());
            switch (element) {
                case ADDED:
                    modifications.add(parseModuleModification(reader, ModificationType.ADD));
                    break;
                case UPDATED:
                    modifications.add(parseModuleModification(reader, ModificationType.MODIFY));
                    break;
                case REMOVED:
                    modifications.add(parseModuleModification(reader, ModificationType.REMOVE));
                    break;
                default:
                    throw unexpectedElement(reader);
            }
        }
    }

    static void parseMiscFiles(final XMLExtendedStreamReader reader, final Collection<ContentModification> modifications) throws XMLStreamException {
        while (reader.hasNext() && reader.nextTag() != END_ELEMENT) {
            final Element element = Element.forName(reader.getLocalName());
            switch (element) {
                case ADDED:
                    modifications.add(parseMiscModification(reader, ModificationType.ADD));
                    break;
                case UPDATED:
                    modifications.add(parseMiscModification(reader, ModificationType.MODIFY));
                    break;
                case REMOVED:
                    modifications.add(parseMiscModification(reader, ModificationType.REMOVE));
                    break;
                default:
                    throw unexpectedElement(reader);
            }
        }
    }

    static void parseBundles(final XMLExtendedStreamReader reader, final Collection<ContentModification> modifications) throws XMLStreamException {
        while (reader.hasNext() && reader.nextTag() != END_ELEMENT) {
            final Element element = Element.forName(reader.getLocalName());
            switch (element) {
                case ADDED:
                    modifications.add(parseBundleModification(reader, ModificationType.ADD));
                    break;
                case UPDATED:
                    modifications.add(parseBundleModification(reader, ModificationType.MODIFY));
                    break;
                case REMOVED:
                    modifications.add(parseBundleModification(reader, ModificationType.REMOVE));
                    break;
                default:
                    throw unexpectedElement(reader);
            }
        }
    }

    static ContentModification parseBundleModification(final XMLExtendedStreamReader reader, final ModificationType type) throws XMLStreamException {
        return parseSlottedItem(reader, type, ContentType.BUNDLE);
    }

    static ContentModification parseModuleModification(final XMLExtendedStreamReader reader, final ModificationType type) throws XMLStreamException {
        return parseSlottedItem(reader, type, ContentType.MODULE);
    }

    static ContentModification parseSlottedItem(final XMLExtendedStreamReader reader, ModificationType modificationType, ContentType contentType) throws XMLStreamException {

        String moduleName = null;
        String slot = "main";
        byte[] hash = NO_CONTENT;
        byte[] targetHash = NO_CONTENT;

        final int count = reader.getAttributeCount();
        for (int i = 0; i < count; i++) {
            final Attribute attribute = Attribute.forName(reader.getAttributeLocalName(i));
            switch (attribute) {
                case NAME:
                    moduleName = reader.getAttributeValue(i);
                    break;
                case SLOT:
                    slot = reader.getAttributeValue(i);
                    break;
                case HASH:
                    if (modificationType == ModificationType.REMOVE) {
                        targetHash = hexStringToByteArray(reader.getAttributeValue(i));
                    } else {
                        hash = hexStringToByteArray(reader.getAttributeValue(i));
                    }
                    break;
                case NEW_HASH:
                    if (modificationType == ModificationType.REMOVE) {
                        hash = hexStringToByteArray(reader.getAttributeValue(i));
                    } else {
                        targetHash = hexStringToByteArray(reader.getAttributeValue(i));
                    }
                    break;
                default:
                    throw unexpectedAttribute(reader, i);
            }
        }
        requireNoContent(reader);

        final ModuleItem item = contentType == ContentType.MODULE ? new ModuleItem(moduleName, slot, hash) : new BundleItem(moduleName, slot, hash);
        return new ContentModification(item, targetHash, modificationType);
    }

    static ContentModification parseMiscModification(final XMLExtendedStreamReader reader, ModificationType type) throws XMLStreamException {

        String path = null;
        byte[] hash = NO_CONTENT;
        boolean directory = false;
        boolean affectsRuntime = false;
        byte[] targetHash = NO_CONTENT;
        ModificationCondition condition = null;

        final int count = reader.getAttributeCount();
        for (int i = 0; i < count; i++) {
            final Attribute attribute = Attribute.forName(reader.getAttributeLocalName(i));
            switch (attribute) {
                case DIRECTORY:
                    directory = Boolean.parseBoolean(reader.getAttributeValue(i));
                    break;
                case PATH:
                    path = reader.getAttributeValue(i);
                    break;
                case HASH:
                    if (type == ModificationType.REMOVE) {
                        targetHash = hexStringToByteArray(reader.getAttributeValue(i));
                    } else {
                        hash = hexStringToByteArray(reader.getAttributeValue(i));
                    }
                    break;
                case NEW_HASH:
                    if (type == ModificationType.REMOVE) {
                        hash = hexStringToByteArray(reader.getAttributeValue(i));
                    } else {
                        targetHash = hexStringToByteArray(reader.getAttributeValue(i));
                    }
                    break;
                case IN_RUNTIME_USE:
                    affectsRuntime = Boolean.parseBoolean(reader.getAttributeValue(i));
                    break;
                case CONDITION:
                    try {
                        condition = ModificationCondition.Factory.fromString(reader.getAttributeValue(i));
                    } catch (PatchingException e) {
                        throw ControllerLogger.ROOT_LOGGER.invalidAttributeValue(reader.getAttributeValue(i), new QName(
                                attribute.name), reader.getLocation());
                    }
                    break;
                default:
                    throw unexpectedAttribute(reader, i);
            }
        }
        requireNoContent(reader);

        final MiscContentItem item = createMiscItem(path, hash, directory, affectsRuntime);
        return new ContentModification(item, targetHash, type, condition);
    }

    private static MiscContentItem createMiscItem(String path, byte[] hash, boolean directory, boolean affectsRuntime) {
        final String[] s = path.split(PATH_DELIMITER);
        final int length = s.length;
        final String name = s[length - 1];
        final String[] itemPath = Arrays.copyOf(s, length - 1);
        return new MiscContentItem(name, itemPath, hash, directory, affectsRuntime);
    }

    protected static void writeAppliesToVersions(XMLExtendedStreamWriter writer, List<String> appliesTo) throws XMLStreamException {
        for (String version : appliesTo) {
//            writer.writeStartElement(Element.APPLIES_TO_VERSION.name);
            writer.writeCharacters(version);
            writer.writeEndElement();
        }
    }

    protected static void writeSlottedItems(final XMLExtendedStreamWriter writer, final Element element, final List<ContentModification> modifications) throws XMLStreamException {
        for(final ContentModification modification : modifications) {
            writeSlottedItem(writer, element, modification);
        }
    }

    protected static void writeSlottedItem(final XMLExtendedStreamWriter writer, Element element, ContentModification modification) throws XMLStreamException {

        writer.writeEmptyElement(element.name);

        final ModuleItem item = (ModuleItem) modification.getItem();
        final ModificationType type = modification.getType();

        writer.writeAttribute(Attribute.NAME.name, item.getName());
        if (!MAIN_SLOT.equals(item.getSlot())) {
            writer.writeAttribute(Attribute.SLOT.name, item.getSlot());
        }
        byte[] hash = item.getContentHash();
        if (hash.length > 0 && type != ModificationType.REMOVE) {
            writer.writeAttribute(Attribute.HASH.name,  bytesToHexString(hash));
        }
        if(type == ModificationType.REMOVE) {
            final byte[] existingHash = modification.getTargetHash();
            if (existingHash.length > 0) {
                writer.writeAttribute(Attribute.HASH.name, bytesToHexString(existingHash));
            }
        } else if(type == ModificationType.MODIFY) {
            final byte[] existingHash = modification.getTargetHash();
            if (existingHash.length > 0) {
                writer.writeAttribute(Attribute.NEW_HASH.name, bytesToHexString(existingHash));
            }
        }
    }

    protected static void writeMiscItems(final XMLExtendedStreamWriter writer, final Element element, final List<ContentModification> modifications) throws XMLStreamException {
        for(final ContentModification modification : modifications) {
            writeMiscItem(writer, element, modification);
        }
    }

    protected static void writeMiscItem(final XMLExtendedStreamWriter writer, final Element element, final ContentModification modification) throws XMLStreamException {

        writer.writeEmptyElement(element.name);

        final MiscContentItem item = (MiscContentItem) modification.getItem();
        final ModificationType type = modification.getType();

        final StringBuilder path = new StringBuilder();
        for(final String p : item.getPath()) {
            path.append(p).append(PATH_DELIMITER);
        }
        path.append(item.getName());

        writer.writeAttribute(Attribute.PATH.name, path.toString());
        if (item.isDirectory()) {
            writer.writeAttribute(Attribute.DIRECTORY.name, "true");
        }

        final ModificationCondition condition = modification.getCondition();
        if(condition != null) {
            writer.writeAttribute(Attribute.CONDITION.name, condition.toString());
        }

        if(type == ModificationType.REMOVE) {
            final byte[] existingHash = modification.getTargetHash();
            if (existingHash.length > 0) {
                writer.writeAttribute(Attribute.HASH.name, bytesToHexString(existingHash));
            }
            if(item.isAffectsRuntime()) {
                writer.writeAttribute(Attribute.IN_RUNTIME_USE.name, "true");
            }
        } else {
            byte[] hash = item.getContentHash();
            if(hash.length > 0) {
                writer.writeAttribute(Attribute.HASH.name, bytesToHexString(hash));
            }

            if(type == ModificationType.MODIFY) {
                writer.writeAttribute(Attribute.NEW_HASH.name, bytesToHexString(modification.getTargetHash()));
                if (item.isAffectsRuntime()) {
                    writer.writeAttribute(Attribute.IN_RUNTIME_USE.name, "true");
                }
            }
        }
    }



}
