package org.xbib.io.webdav.client;

import static org.xbib.io.webdav.api.DavConstants.NAMESPACE;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xbib.io.webdav.api.DavConstants;
import org.xbib.io.webdav.api.XMLizable;
import org.xbib.io.webdav.common.DavException;
import org.xbib.io.webdav.common.DavPropertyNameSet;
import org.xbib.io.webdav.common.DomUtil;
import org.xbib.io.webdav.common.ElementIterator;
import java.util.ArrayList;
import java.util.List;

/**
 * <code>UpdateInfo</code> encapsulates the request body of an UPDATE request.
 * RFC 3253 defines the request body as follows:
 * <pre>
 * &lt;!ELEMENT update ANY&gt;
 * ANY value: A sequence of elements with at most one DAV:label-name or
 * DAV:version element (but not both).
 * In addition at one DAV:prop element can be present.
 *
 * &lt;!ELEMENT version (href)&gt;
 * &lt;!ELEMENT label-name (#PCDATA)&gt; PCDATA value: string
 * prop: see RFC 2518, Section 12.11
 * </pre>
 * <p>
 * In order to reflect the complete range of version restoring and updating
 * of nodes defined by JSR170 the definition has been extended:
 * <pre>
 * &lt;!ELEMENT update ( (version | label-name | workspace ) , (prop)?, (removeExisting)? ) &gt;
 * &lt;!ELEMENT version (href+) &gt;
 * &lt;!ELEMENT label-name (#PCDATA) &gt;
 * &lt;!ELEMENT workspace (href) &gt;
 * &lt;!ELEMENT prop ANY &gt;
 * &lt;!ELEMENT removeExisting EMPTY &gt;
 * </pre>
 */
public class UpdateInfo implements XMLizable {

    /**
     * Xml element defining the top element in the UPDATE request body. RFC 3253
     * defines the following structure for the 'update' element.
     * <pre>
     * &lt;!ELEMENT update ANY&gt;
     * ANY value: A sequence of elements with at most one DAV:version element
     * and at most one DAV:prop element.
     * &lt;!ELEMENT version (href)&gt;
     * prop: see RFC 2518, Section 12.11
     * </pre>
     */
    private static final String XML_UPDATE = "update";

    private static final String XML_LABEL_NAME = "label-name";
    private static final String XML_VERSION = "version";
    private static final String XML_WORKSPACE = "workspace";


    public static final int UPDATE_BY_VERSION = 0;
    public static final int UPDATE_BY_LABEL = 1;
    public static final int UPDATE_BY_WORKSPACE = 2;

    private Element updateElement;

    private DavPropertyNameSet propertyNameSet = new DavPropertyNameSet();
    private String[] source;
    private int type;

    public UpdateInfo(String[] updateSource, int updateType, DavPropertyNameSet propertyNameSet) {
        if (updateSource == null || updateSource.length == 0) {
            throw new IllegalArgumentException("Version href array must not be null and have a minimal length of 1.");
        }
        if (updateType < UPDATE_BY_VERSION || updateType > UPDATE_BY_WORKSPACE) {
            throw new IllegalArgumentException("Illegal type of UpdateInfo.");
        }
        this.type = updateType;
        this.source = (updateType == UPDATE_BY_VERSION) ? updateSource : new String[]{updateSource[0]};
        if (propertyNameSet != null) {
            this.propertyNameSet = propertyNameSet;
        }
    }

    /**
     * Create a new <code>UpdateInfo</code> object.
     *
     * @param updateElement
     * @throws DavException if the updateElement is <code>null</code>
     *                      or not a DAV:update element or if the element does not match the required
     *                      structure.
     */
    public UpdateInfo(Element updateElement) throws DavException {
        if (!DomUtil.matches(updateElement, XML_UPDATE, NAMESPACE)) {
            //LOG.log(Level.WARNING, "DAV:update element expected");
            throw new DavException(400);
        }

        boolean done = false;
        if (DomUtil.hasChildElement(updateElement, XML_VERSION, NAMESPACE)) {
            Element vEl = DomUtil.getChildElement(updateElement, XML_VERSION, NAMESPACE);
            ElementIterator hrefs = DomUtil.getChildren(vEl, DavConstants.XML_HREF, NAMESPACE);
            List<String> hrefList = new ArrayList<String>();
            while (hrefs.hasNext()) {
                hrefList.add(DomUtil.getText(hrefs.nextElement()));
            }
            source = hrefList.toArray(new String[hrefList.size()]);
            type = UPDATE_BY_VERSION;
            done = true;
        }

        // alternatively 'DAV:label-name' elements may be present.
        if (!done && DomUtil.hasChildElement(updateElement, XML_LABEL_NAME, NAMESPACE)) {
            source = new String[]{DomUtil.getChildText(updateElement, XML_LABEL_NAME, NAMESPACE)};
            type = UPDATE_BY_LABEL;
            done = true;
        }

        // last possibility: a DAV:workspace element
        if (!done) {
            Element wspElem = DomUtil.getChildElement(updateElement, XML_WORKSPACE, NAMESPACE);
            if (wspElem != null) {
                source = new String[]{DomUtil.getChildTextTrim(wspElem, DavConstants.XML_HREF, NAMESPACE)};
                type = UPDATE_BY_WORKSPACE;
            } else {
                //LOG.log(Level.WARNING, "DAV:update element must contain either DAV:version, DAV:label-name or DAV:workspace child element.");
                throw new DavException(400);
            }
        }

        // if property name set if present
        if (DomUtil.hasChildElement(updateElement, DavConstants.XML_PROP, NAMESPACE)) {
            Element propEl = DomUtil.getChildElement(updateElement, DavConstants.XML_PROP, NAMESPACE);
            propertyNameSet = new DavPropertyNameSet(propEl);
            updateElement.removeChild(propEl);
        } else {
            propertyNameSet = new DavPropertyNameSet();
        }
        this.updateElement = updateElement;
    }

    /**
     * @return
     */
    public String[] getVersionHref() {
        return (type == UPDATE_BY_VERSION) ? source : null;
    }

    /**
     * @return
     */
    public String[] getLabelName() {
        return (type == UPDATE_BY_LABEL) ? source : null;
    }

    /**
     * @return
     */
    public String getWorkspaceHref() {
        return (type == UPDATE_BY_WORKSPACE) ? source[0] : null;
    }

    /**
     * Returns a {@link DavPropertyNameSet}. If the DAV:update element contains
     * a DAV:prop child element the properties specified therein are included
     * in the set. Otherwise an empty set is returned.
     * <p>
     * <b>WARNING:</b> modifying the DavPropertyNameSet returned by this method does
     * not modify this <code>UpdateInfo</code>.
     *
     * @return set listing the properties specified in the DAV:prop element indicating
     * those properties that must be reported in the response body.
     */
    public DavPropertyNameSet getPropertyNameSet() {
        return propertyNameSet;
    }

    /**
     * @return
     */
    public Element getUpdateElement() {
        return updateElement;
    }

    /**
     * @param document
     * @see XMLizable#toXml(Document)
     */
    public Element toXml(Document document) {
        Element elem;
        if (updateElement != null) {
            elem = (Element) document.importNode(updateElement, true);
        } else {
            elem = createUpdateElement(source, type, document);
        }
        if (!propertyNameSet.isEmpty()) {
            elem.appendChild(propertyNameSet.toXml(document));
        }
        return elem;
    }

    /**
     * Factory method to create the basic structure of an <code>UpdateInfo</code>
     * object.
     *
     * @param updateSource
     * @param updateType
     * @param factory
     * @return
     */
    public static Element createUpdateElement(String[] updateSource, int updateType, Document factory) {
        if (updateSource == null || updateSource.length == 0) {
            throw new IllegalArgumentException("Update source must specific at least a single resource used to run the update.");
        }

        Element elem = DomUtil.createElement(factory, XML_UPDATE, NAMESPACE);
        switch (updateType) {
            case UPDATE_BY_VERSION:
                Element vE = DomUtil.addChildElement(elem, XML_VERSION, NAMESPACE);
                for (String source : updateSource) {
                    vE.appendChild(DomUtil.hrefToXml(source, factory));
                }
                break;
            case UPDATE_BY_LABEL:
                DomUtil.addChildElement(elem, XML_LABEL_NAME, NAMESPACE, updateSource[0]);
                break;
            case UPDATE_BY_WORKSPACE:
                Element wspEl = DomUtil.addChildElement(elem, XML_WORKSPACE, NAMESPACE, updateSource[0]);
                wspEl.appendChild(DomUtil.hrefToXml(updateSource[0], factory));
                break;
            // no default.
            default:
                throw new IllegalArgumentException("Invalid update type: " + updateType);
        }
        return elem;
    }
}
