// Copyright 2008-2012 severally by the contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package net.sf.practicalxml.converter;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.namespace.QName;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import net.sf.practicalxml.DomUtil;


/**
 *  Converts between an XML DOM and hierarchical Java collections, according
 *  to the following rules:
 *  <p>
 *  From DOM to Collection:
 *  <ul>
 *  <li> The top-level <code>Element</code> is transformed into <code>Map</code>.
 *  <li> The keys of this map will be the <em>local names</em> of the child
 *       elements, sans prefix.
 *  <li> Children that have text content will map to <code>String</code> values.
 *  <li> Children that have element content will map to <code>Map</code> values,
 *       and processed recursively.
 *  <li> Mixed content is not permitted; if an element contains mixed content,
 *       the text nodes will be discarded.
 *  <li> If multiple child elements have the same name, they will map to a
 *       <code>List</code>, which in turn contains either strings or maps.
 *  <li> Empty elements are added to the map with a value of <code>null</code>.
 *  <li> Order of keys is not preserved, but order of repeated elements is.
 *  </ul>
 *  <p>
 *  From  Collection to DOM:
 *  <ul>
 *  <li> A <code>Map</code> is converted to an element with children, where each
 *       key in the map becomes an <code>Element</code>.
 *  <li> <code>String</code> values are turned into text nodes under the key
 *       element.
 *  <li> <code>Map</code> values are turned into elements recursively.
 *  <li> <code>List</code> and <code>Set</code> values are turned into repeated
 *       elements with the same key (and may be in turn either strings or maps).
 *  </ul>
 *  <p>
 *  Each of the conversion functions allows the caller to specify a list of
 *  keys as a filter. If these keys are present, only the specified keys will
 *  be processed from the input.
 *
 *  @since 1.1.3
 */
public class CollectionConverter
{
    /**
     *  Creates a new DOM document from the passed map, without any namespace.
     *
     *   @param map         The source object. Its elements will be the children
     *                      of the document root.
     *   @param rootName    The local name given to the root element of the
     *                      generated document.
     *  @param  keyFilter   If present, the mappings will be limited to child
     *                      elements with the specified names.
     */
    public static Document convertToXml(
            Map<String,?> map, String rootName, String... keyFilter)
    {
        Element root = DomUtil.newDocument(rootName);
        appendElements(map, root, digestFilter(keyFilter));
        return root.getOwnerDocument();
    }


    /**
     *  Creates a new DOM document from the passed map, in which all elements
     *  are members of the specified namespace and will inherit the root's
     *  prefix (if any).
     *
     *   @param map         The source object. Its elements will be the children
     *                      of the document root.
     *   @param rootName    The qualified name given to the root element of the
     *                      generated document (this is a <code>QName</code> to
     *                      avoid ambiguous argument lists).
     *  @param  keyFilter   If present, the mappings will be limited to child
     *                      elements with the specified names.
     */
    public static Document convertToXml(
            Map<String,?> map, QName rootName, String... keyFilter)
    {
        Element root = DomUtil.newDocument(rootName);
        appendElements(map, root, digestFilter(keyFilter));
        return root.getOwnerDocument();
    }


    /**
     *  Converts a single XML element into a map.
     *
     *  @param  elem        The element.
     *  @param  keyFilter   If present, the mappings will be limited to child
     *                      elements with the specified names.
     */
    public static Map<String,?> convertToMap(Element elem, String... keyFilter)
    {
        return convertToMap(elem, digestFilter(keyFilter));
    }


    /**
     *  Converts a list of XML elements into a list of maps.
     *
     *  @param  elems       The elements.
     *  @param  keyFilter   If present, the mappings will be limited to child
     *                      elements with the specified names.
     */
    public static List<Map<String,?>> convertToMap(List<Element> elems, String... keyFilter)
    {
        List<Map<String,?>> result = new ArrayList<Map<String,?>>(elems.size());
        Set<String> filter = digestFilter(keyFilter);
        for (Element elem : elems)
            result.add(convertToMap(elem, filter));
        return result;
    }


//----------------------------------------------------------------------------
//  Internals
//----------------------------------------------------------------------------

    /**
     *  Converts the varargs filter into  set, null if no filter provided.
     */
    private static Set<String> digestFilter(String... keyFilter)
    {
        if (keyFilter.length == 0)
            return null;

        Set<String> filter = new HashSet<String>();
        for (String key : keyFilter)
            filter.add(key);
        return filter;
    }


    /**
     *  Common conversion code for a single element, with digested filter.
     *
     *  @param  elem        The element.
     *  @param  keyFilter   If not <code>null</code> contains the child element
     *                      names that will be put in the map.
     */
    public static Map<String,?> convertToMap(Element elem, Set<String> keyFilter)
    {
        Map<String,Object> result = new HashMap<String,Object>();
        for (Element child : DomUtil.getChildren(elem))
            appendChild(result, child, keyFilter);
        return result;
    }


    private static void appendChild(Map<String,Object> map, Element child, Set<String> keyFilter)
    {
        String key = DomUtil.getLocalName(child);
        if ((keyFilter != null) && !keyFilter.contains(key))
            return;

        Object value = getChildValue(child, keyFilter);

        if (!map.containsKey(key))
        {
            map.put(key, value);
            return;
        }

        Object current = map.get(key);
        if (current instanceof List)
        {
            List<Object> list = (List<Object>)current;
            list.add(value);
        }
        else
        {
            List<Object> list = new ArrayList<Object>();
            list.add(current);
            list.add(value);
            map.put(key, list);
        }
    }


    private static Object getChildValue(Element child, Set<String> keyFilter)
    {
        if (DomUtil.hasElementChildren(child))
            return convertToMap(child, keyFilter);
        else
            return DomUtil.getText(child);
    }


    private static void appendElements(Map<String,?> map, Element parent, Set<String> keyFilter)
    {
        for (Map.Entry<String,?> entry : map.entrySet())
        {
            String key = entry.getKey();
            if ((keyFilter != null) && !keyFilter.contains(key))
                continue;

            Object value = entry.getValue();
            appendElement(parent, key, value, keyFilter);
        }
    }



    private static void appendElement(Element parent, String key, Object value, Set<String> keyFilter)
    {
        if (value == null)
        {
            DomUtil.appendChildInheritNamespace(parent, key);
        }
        else if (value instanceof String)
        {
            Element child = DomUtil.appendChildInheritNamespace(parent, key);
            DomUtil.setText(child, (String)value);
        }
        else if (value instanceof Map)
        {
            Element child = DomUtil.appendChildInheritNamespace(parent, key);
            appendElements((Map<String,?>)value, child, keyFilter);
        }
        else if (value instanceof Collection)
        {
            for (Object obj : (Collection<?>)value)
                appendElement(parent, key, obj, keyFilter);
        }
        else if (value.getClass().isArray())
        {
            for (Object obj : (Object[])value)
                appendElement(parent, key, obj, keyFilter);
        }
    }
}
