// 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.bean;

import java.util.EnumSet;

import org.w3c.dom.Element;

import net.sf.practicalxml.DomUtil;
import net.sf.practicalxml.converter.ConversionException;
import net.sf.practicalxml.converter.ConversionConstants;
import net.sf.practicalxml.converter.internal.ConversionUtils;
import net.sf.practicalxml.converter.internal.TypeUtils;


/**
 *  Packaging class used for XML output appenders.
 */
public abstract class Bean2XmlAppenders
{
    /**
     *  An <code>Appender</code> appends children to a single node of the output
     *  tree. One instance is created for each non-leaf node; different object
     *  types use different appender classes, and the driver is responsible for
     *  picking the appropriate appender.
     *  <p>
     *  This base class maintains options and other state, and provides common
     *  services to subclasses. Subclasses implement the {@link #appendValue}
     *  and {@link #appendContainer} methods, which are called by the driver.
     */
    public static abstract class Appender
    {
        private EnumSet<Bean2XmlOptions> _options;
        private Appender _parent;
        private Object _nodeObject;

        /**
         *  Constructor called by (@link DirectAppender). This contains explicit
         *  defaults that are inherited by child appenders.
         *
         *  @param  options     Options controlling the conversion.
         *  @param  rootObject  The root object being converted.
         */
        protected Appender(EnumSet<Bean2XmlOptions> options, Object rootObject)
        {
            _options = options;
            _nodeObject = rootObject;
        }

        /**
         *  Constructor called by all other subclasses. Will inherit settings from
         *  parent.
         *
         *  @param  parent      The parent appender. May not be <code>null</code>.
         *  @param  nodeObject  The object "owned" by this appender.
         */
        protected Appender(Appender parent, Object nodeObject)
        {
            this(parent._options, nodeObject);
            _parent = parent;
        }

        /**
         *  Hides the mechanism that we used to manage options.
         */
        protected boolean isOptionSet(Bean2XmlOptions option)
        {
            return _options.contains(option);
        }

        /**
         *  Determines whether the passed object is a circular reference.
         */
        protected boolean isCircularReference(Object obj)
        {
            return (obj == null)                ? false
                 : (obj instanceof String)      ? false
                 : (obj instanceof Enum)        ? false
                 : obj.getClass().isPrimitive() ? false
                 : (obj == _nodeObject)         ? true
                 : (_parent != null)            ? _parent.isCircularReference(obj)
                 : false;
        }

        /**
         *  Returns <code>true</code> to indicate whether the appender should not
         *  output the passed value. Also tests for circular references.
         */
        protected boolean shouldSkip(Object value)
        {
            if (value == null)
            {
                return ! isOptionSet(Bean2XmlOptions.NULL_AS_EMPTY)
                    && ! isOptionSet(Bean2XmlOptions.NULL_AS_XSI_NIL);
            }

            if (isCircularReference(value))
            {
                if (isOptionSet(Bean2XmlOptions.SKIP_CIRCULAR_REFERENCES))
                    return true;
                else
                    throw new ConversionException("circular reference: " + value);
            }

            return false;
        }

        /**
         *  Optionally sets a type attribute on the generated element.
         */
        protected void setType(Element elem, Class<?> klass)
        {
            if (isOptionSet(Bean2XmlOptions.USE_TYPE_ATTR))
                TypeUtils.setType(elem, klass);
        }

        /**
         *  Sets the value of the generated element. Manages optional null
         *  handling.
         */
        protected void setValue(Element elem, String value)
        {
            if (value != null)
                DomUtil.setText(elem, value);
            else if (isOptionSet(Bean2XmlOptions.NULL_AS_EMPTY))
                DomUtil.setText(elem, "");
            else if (isOptionSet(Bean2XmlOptions.NULL_AS_XSI_NIL))
                ConversionUtils.setXsiNil(elem, true);
        }

        /**
         *  Appends a value element to the current element. Value elements have
         *  associated text, but no other children.
         *
         *  @param name     Name to be associated with the node.
         *  @param klass    Java class for this node. May (depending on options)
         *                  be stored in the <code>type</code> attribute.
         *  @param value    The node's value. May be <code>null</code>, in
         *                  which case the appender decides whether or not
         *                  to actually append the node.
         *
         *  @return The appended element. This is a convenience for subclasses,
         *          which may want to set additional attributes after their
         *          super has done the work of appending the element.
         *  @throws ConversionException if unable to append the node.
         */
        public abstract Element appendValue(String name, Class<?> klass, String value);


        /**
         *  Appends a container element to the current element. Container
         *  elements have other elements as children, and may have a type,
         *  but do not have an associated value.
         */
        public abstract Element appendContainer(String name, Class<?> klass);
    }


    /**
     *  An appender that sets values directly on the passed element. Used for
     *  the conversion root element.
     */
    public static class DirectAppender
    extends Appender
    {
        private Element _elem;

        public DirectAppender(EnumSet<Bean2XmlOptions> options, Element elem, Object rootObj)
        {
            super(options, rootObj);
            _elem = elem;
        }

        @Override
        public Element appendValue(String name, Class<?> klass, String value)
        {
            if (!shouldSkip(value))
            {
                setType(_elem, klass);
                setValue(_elem, value);
            }
            return _elem;
        }

        @Override
        public Element appendContainer(String name, Class<?> klass)
        {
            setType(_elem, klass);
            return _elem;
        }
    }


    /**
     *  Basic appender, which appends new elements to a parent element.
     */
    public static class BasicAppender
    extends Appender
    {
        private Element _elem;

        public BasicAppender(Appender parent, Element elem, Object nodeObject)
        {
            super(parent, nodeObject);
            _elem = elem;
        }

        @Override
        public Element appendValue(String name, Class<?> klass, String value)
        {
            if (shouldSkip(value))
                return null;

            try
            {
                Element child = DomUtil.appendChildInheritNamespace(_elem, name);
                setType(child, klass);
                setValue(child, value);
                return child;
            }
            catch (Exception ee)
            {
                throw new ConversionException("unable to append child: " + name, _elem, ee);
            }
        }

        @Override
        public Element appendContainer(String name, Class<?> klass)
        {
            Element child = DomUtil.appendChildInheritNamespace(_elem, name);
            setType(child, klass);
            return child;
        }
    }


    /**
     *  Appender for children of an indexed/iterated item (array, list, or set).
     *  Each element will have an incremented <code>index</code> attribute that
     *  indicates the position of the element within the iteration.
     */
    public static class IndexedAppender
    extends BasicAppender
    {
        private int _index = 0;

        public IndexedAppender(Appender parent, Element node, Object nodeObject)
        {
            super(parent, node, nodeObject);
        }


        @Override
        public Element appendValue(String name, Class<?> klass, String value)
        {
            Element child = super.appendValue(name, klass, value);
            if ((child != null) && isOptionSet(Bean2XmlOptions.USE_INDEX_ATTR))
                ConversionUtils.setAttribute(
                        child,
                        ConversionConstants.AT_ARRAY_INDEX,
                        String.valueOf(_index++));
            return child;
        }
    }


    /**
     *  Appender for children of a <code>Map</code>. Depending on options,
     *  will either create children named after the key, or a generic "data"
     *  child with the key as an attribute.
     */
    public static class MapAppender
    extends BasicAppender
    {
        public MapAppender(Appender parent, Element node, Object nodeObject)
        {
            super(parent, node, nodeObject);
        }


        @Override
        public Element appendValue(String name, Class<?> klass, String value)
        {
            Element child = super.appendValue(determineName(name), klass, value);
            setMapKeyIfNeeded(child, name);
            return child;
        }


        @Override
        public Element appendContainer(String name, Class<?> klass)
        {
            Element child = super.appendContainer(determineName(name), klass);
            setMapKeyIfNeeded(child, name);
            return child;
        }


        private String determineName(String origName)
        {
            return isOptionSet(Bean2XmlOptions.MAP_KEYS_AS_ELEMENT_NAME)
                 ? origName
                 : ConversionConstants.EL_COLLECTION_ITEM;
        }

        private void setMapKeyIfNeeded(Element child, String origName)
        {
            if ((child != null) && !isOptionSet(Bean2XmlOptions.MAP_KEYS_AS_ELEMENT_NAME))
                ConversionUtils.setAttribute(child, ConversionConstants.AT_MAP_KEY, origName);
        }
    }
}
