/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2011, 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.exoplatform.portal.mop.management.binding.xml;

import static org.gatein.common.xml.stax.navigator.Exceptions.*;
import static org.gatein.common.xml.stax.navigator.StaxNavUtils.createNavigator;
import static org.gatein.common.xml.stax.navigator.StaxNavUtils.parseContent;
import static org.gatein.common.xml.stax.writer.StaxWriterUtils.buildDefaultWriter;
import static org.gatein.common.xml.stax.writer.StaxWriterUtils.writeOptionalElement;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

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

import org.exoplatform.portal.config.model.I18NString;
import org.exoplatform.portal.config.model.LocalizedString;
import org.exoplatform.portal.config.model.NavigationFragment;
import org.exoplatform.portal.config.model.PageNavigation;
import org.exoplatform.portal.config.model.PageNode;
import org.exoplatform.portal.mop.Visibility;
import org.gatein.common.xml.stax.writer.StaxWriter;
import org.gatein.common.xml.stax.writer.WritableValueTypes;
import org.gatein.common.xml.stax.writer.builder.StaxWriterBuilder;
import org.gatein.management.api.binding.BindingException;
import org.gatein.management.api.binding.Marshaller;
import org.staxnav.StaxNavException;
import org.staxnav.StaxNavigator;
import org.staxnav.ValueType;

/**
 * @author <a href="mailto:nscavell@redhat.com">Nick Scavelli</a>
 * @version $Revision$
 */
public class NavigationMarshaller implements Marshaller<PageNavigation> {

    @Override
    public void marshal(PageNavigation navigation, OutputStream outputStream, boolean pretty) throws BindingException {
        try {
            StaxWriterBuilder builder = buildDefaultWriter(outputStream);
            if (!pretty) {
                builder.withFormatting(null);
            }

            StaxWriter<Element> writer = builder.build(Element.class);
            marshalNavigation(writer, navigation);
        } catch (StaxNavException e) {
            throw new BindingException(e);
        } catch (XMLStreamException e) {
            throw new BindingException(e);
        }
    }

    @Override
    public PageNavigation unmarshal(InputStream is) throws BindingException {
        try {
            StaxNavigator<Element> navigator = createNavigator(Element.class, Element.UNKNOWN, is);
            return unmarshalNavigation(navigator);
        } catch (StaxNavException e) {
            throw new BindingException(e);
        }
    }

    private void marshalNavigation(StaxWriter<Element> writer, PageNavigation navigation) throws XMLStreamException {
        writer.writeStartElement(Element.NODE_NAVIGATION);

        // Write gatein_objects xml namespace
        Utils.writeGateinObjectsNamespace(writer);

        // Priority
        writer.writeElement(Element.PRIORITY, WritableValueTypes.INTEGER, navigation.getPriority());

        // Page nodes
        ArrayList<NavigationFragment> fragments = navigation.getFragments();
        for (NavigationFragment fragment : fragments) {
            writer.writeStartElement(Element.PAGE_NODES);
            if (fragment.getParentURI() != null) {
                String parentUri = fragment.getParentURI();
                writeOptionalElement(writer, Element.PARENT_URI, parentUri);
            }

            Collection<PageNode> nodes = fragment.getNodes();
            if (nodes != null && !nodes.isEmpty()) {
                for (PageNode node : nodes) {
                    marshallNode(writer, node);
                }
            }
            writer.writeEndElement(); // End page-nodes
        }

        writer.writeEndElement(); // End node-navigation
    }

    public void marshallNode(StaxWriter<Element> writer, PageNode node) throws XMLStreamException {
        writer.writeStartElement(Element.NODE);
        writer.writeElement(Element.NAME, node.getName());

        if (node.getLabels() != null) {
            for (LocalizedString label : node.getLabels()) {
                if (label.getValue() == null)
                    continue;

                writer.writeStartElement(Element.LABEL);
                if (label.getLang() != null) {
                    String localeString = label.getLang().getLanguage();
                    if (localeString == null) {
                        throw new XMLStreamException("Language was null for locale " + label.getLang());
                    }
                    String country = label.getLang().getCountry();
                    if (country != null && country.length() > 0) {
                        localeString += "-" + country.toLowerCase();
                    }

                    writer.writeAttribute(new QName(XMLConstants.XML_NS_URI, "lang", XMLConstants.XML_NS_PREFIX), localeString);
                }
                writer.writeContent(label.getValue()).writeEndElement();
            }
        }

        writeOptionalElement(writer, Element.ICON, node.getIcon());

        writeOptionalElement(writer, Element.START_PUBLICATION_DATE, WritableValueTypes.DATE_TIME,
                node.getStartPublicationDate());
        writeOptionalElement(writer, Element.END_PUBLICATION_DATE, WritableValueTypes.DATE_TIME, node.getEndPublicationDate());

        String visibility = (node.getVisibility() == null) ? null : node.getVisibility().name();
        writeOptionalElement(writer, Element.VISIBILITY, visibility);
        writeOptionalElement(writer, Element.PAGE_REFERENCE, node.getPageReference());

        // Marshall children
        List<PageNode> children = node.getNodes();
        if (children != null && !children.isEmpty()) {
            for (PageNode child : children) {
                marshallNode(writer, child);
            }
        }

        writer.writeEndElement(); // End of node
    }

    private PageNavigation unmarshalNavigation(StaxNavigator<Element> navigator) throws StaxNavException {
        PageNavigation navigation = new PageNavigation();

        if (navigator.getName() == Element.NODE_NAVIGATION) {
            Element next = navigator.child();
            if (next == Element.PRIORITY) {
                Integer priority = parseContent(navigator, ValueType.INTEGER, PageNavigation.UNDEFINED_PRIORITY);
                navigation.setPriority(priority < 1 ? PageNavigation.UNDEFINED_PRIORITY : priority);
            } else if (next == Element.PAGE_NODES) {
                unmarshalPageNode(navigation, navigator, next);
                return navigation;
            }

            next = navigator.sibling();
            if (next == Element.PAGE_NODES) {
                unmarshalPageNode(navigation, navigator, next);
            } else if (next != null) {
                throw expectedElement(navigator, Element.PAGE_NODES);
            }

            return navigation;
        } else {
            throw unknownElement(navigator);
        }
    }

    private void unmarshalPageNode(PageNavigation navigation, StaxNavigator<Element> navigator, Element element) throws StaxNavException {
        for (StaxNavigator<Element> fork : navigator.fork(Element.PAGE_NODES)) {
            NavigationFragment fragment = new NavigationFragment();
            navigation.addFragment(fragment);

            element = fork.child();
            if (element == Element.PARENT_URI) {
                String parentUri = fork.getContent();
                if (parentUri == null) {
                    parentUri = "";
                } else if (parentUri.charAt(0) == '/') {
                    parentUri = parentUri.substring(1, parentUri.length());
                }
                fragment.setParentURI(parentUri);

                element = fork.sibling();
            }

            if (element == Element.NODE) {
                ArrayList<PageNode> nodes = new ArrayList<PageNode>();
                for (StaxNavigator<Element> nodeFork : fork.fork(Element.NODE)) {
                    nodes.add(unmarshalNode(nodeFork));
                }
                fragment.setNodes(nodes);
            } else if (element != null) {
                throw unknownElement(fork);
            }
        }
    }

    private PageNode unmarshalNode(StaxNavigator<Element> navigator) throws StaxNavException {
        PageNode node = new PageNode();
        I18NString labels = new I18NString();
        ArrayList<PageNode> children = new ArrayList<PageNode>();

        Element current = navigator.child();
        while (current != null) {
            switch (navigator.getName()) {
                case URI: // For backwards compatibility
                    current = navigator.sibling();
                    break;
                case NAME:
                    node.setName(navigator.getContent());
                    current = navigator.sibling();
                    break;
                case LABEL:
                    labels.add(Utils.parseLocalizedString(navigator));
                    current = navigator.sibling();
                    break;
                case ICON:
                    node.setIcon(navigator.getContent());
                    current = navigator.sibling();
                    break;
                case START_PUBLICATION_DATE:
                    node.setStartPublicationDate(navigator.parseContent(ValueType.DATE_TIME));
                    current = navigator.sibling();
                    break;
                case END_PUBLICATION_DATE:
                    node.setEndPublicationDate(navigator.parseContent(ValueType.DATE_TIME));
                    current = navigator.sibling();
                    break;
                case VISIBILITY:
                    node.setVisibility(navigator.parseContent(ValueType.get(Visibility.class)));
                    current = navigator.sibling();
                    break;
                case PAGE_REFERENCE:
                    node.setPageReference(navigator.getContent());
                    current = navigator.sibling();
                    break;
                case NODE:
                    PageNode child = unmarshalNode(navigator.fork());
                    children.add(child);
                    current = navigator.sibling();
                    break;
                case UNKNOWN:
                    throw unknownElement(navigator);
                default:
                    throw unexpectedElement(navigator);
            }
        }

        node.setLabels(labels);
        node.setChildren(children);

        return node;
    }
}
