/*************************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.jackrabbit.vault.fs.impl.io;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;

import javax.jcr.ImportUUIDBehavior;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;

import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.vault.util.DocViewNode;
import org.apache.jackrabbit.vault.util.DocViewProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

/**
* {@code JcrSysViewTransformer} transforms a docview importer hierarchy to a jcr sysview one by translating the
 * vault specific docview nodes and properties into SAX events for the JCR sysview import content handler.
 *
 * @see Session#getImportContentHandler(String, int)
*/
public class JcrSysViewTransformer implements DocViewAdapter {

    /**
     * default logger
     */
    private static final Logger log = LoggerFactory.getLogger(JcrSysViewTransformer.class);

    /**
     * sysview handler for special content
     */
    private ContentHandler handler;

    /**
     * temporary recovery helper when 'rescuing' the child nodes
     */
    private ChildNodeStash recovery;

    private String rootName;

    private Node parent;

    private final String existingPath;

    private final Set<String> excludedNodeNames = new HashSet<String>();

    private long ignoreLevel = 0;

    public JcrSysViewTransformer(Node node) throws SAXException, RepositoryException {
        this(node, null);
    }

    JcrSysViewTransformer(Node node, String existingPath) throws RepositoryException, SAXException {
        Session session = node.getSession();
        parent = node;
        handler = session.getImportContentHandler(
                node.getPath(),
                existingPath != null
                        ? ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING
                        : ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING
        );
        // first define the current namespaces
        String[] prefixes = session.getNamespacePrefixes();
        handler.startDocument();
        for (String prefix: prefixes) {
            handler.startPrefixMapping(prefix, session.getNamespaceURI(prefix));
        }

        this.existingPath = existingPath;
        if (existingPath != null) {
            // check if there is an existing node with the name
            recovery = new ChildNodeStash(session).excludeName("rep:cache");
            recovery.stashChildren(existingPath);
        }
        excludeNode("rep:cache");
    }

    public List<String> close() throws SAXException {
        handler.endDocument();

        // get list of created paths
        List<String> paths = new ArrayList<String>();
        try {
            if (existingPath != null && parent.getSession().nodeExists(existingPath)) {
                addPaths(paths, parent.getSession().getNode(existingPath));
            } else if (rootName != null && parent.hasNode(rootName)) {
                addPaths(paths, parent.getNode(rootName));
            }
        } catch (RepositoryException e) {
            log.error("error while retrieving list of created nodes.");
        }

        // check for rescued child nodes
        if (recovery != null) {
            try {
                recovery.recoverChildren(existingPath);
            } catch (RepositoryException e) {
                log.error("Error while processing rescued child nodes");
            } finally {
                recovery = null;
            }
        }

        return paths;
    }

    private void addPaths(List<String> paths, Node node) throws RepositoryException {
        paths.add(node.getPath());
        NodeIterator iter = node.getNodes();
        while (iter.hasNext()) {
            addPaths(paths, iter.nextNode());
        }
    }

    public void startNode(DocViewNode ni) throws SAXException {
        if (ignoreLevel > 0) {
            DocViewSAXImporter.log.trace("ignoring child node of excluded node: {}", ni.name);
            ignoreLevel++;
            return;
        }
        if (excludedNodeNames.contains(ni.name)) {
            DocViewSAXImporter.log.trace("Ignoring excluded node {}", ni.name);
            ignoreLevel = 1;
            return;
        }

        DocViewSAXImporter.log.trace("Transforming element to sysview {}", ni.name);

        AttributesImpl attrs = new AttributesImpl();

        attrs.addAttribute(Name.NS_SV_URI, "name", "sv:name", "CDATA", ni.name);
        handler.startElement(Name.NS_SV_URI, "node", "sv:node", attrs);

        // add the properties
        for (DocViewProperty p: ni.props.values()) {
            if (p != null && p.values != null) {
                attrs = new AttributesImpl();
                attrs.addAttribute(Name.NS_SV_URI, "name", "sv:name", "CDATA", p.name);
                attrs.addAttribute(Name.NS_SV_URI, "type", "sv:type", "CDATA", PropertyType.nameFromValue(p.type));
                if (p.isMulti) {
                    attrs.addAttribute(Name.NS_SV_URI, "multiple", "sv:multiple", "CDATA", "true");
                }

                handler.startElement(Name.NS_SV_URI, "property", "sv:property", attrs);
                for (String v: p.values) {
                    handler.startElement(Name.NS_SV_URI, "value", "sv:value", DocViewSAXImporter.EMPTY_ATTRIBUTES);
                    handler.characters(v.toCharArray(), 0, v.length());
                    handler.endElement(Name.NS_SV_URI, "value", "sv:value");
                }
                handler.endElement(Name.NS_SV_URI, "property", "sv:property");
            }
        }

        if (rootName == null) {
            rootName = ni.name;
        }
    }

    public void endNode() throws SAXException {
        if (ignoreLevel > 0) {
            ignoreLevel--;
            return;
        }
        handler.endElement(Name.NS_SV_URI, "node", "sv:node");
    }

    public JcrSysViewTransformer excludeNode(String name) {
        excludedNodeNames.add(name);
        return this;
    }
}