/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.app.xmlui.wing;

import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import org.apache.avalon.framework.parameters.Parameters;
import org.apache.cocoon.ProcessingException;
import org.apache.cocoon.caching.CacheableProcessingComponent;
import org.apache.cocoon.components.source.SourceUtil;
import org.apache.cocoon.environment.SourceResolver;
import org.apache.cocoon.transformation.AbstractTransformer;
import org.apache.cocoon.xml.dom.DOMStreamer;
import org.apache.excalibur.source.Source;
import org.apache.excalibur.source.SourceValidity;
import org.apache.excalibur.source.impl.validity.NOPValidity;
import org.dspace.app.xmlui.wing.element.Body;
import org.dspace.app.xmlui.wing.element.Meta;
import org.dspace.app.xmlui.wing.element.Options;
import org.dspace.app.xmlui.wing.element.PageMeta;
import org.dspace.app.xmlui.wing.element.UserMeta;
import org.dspace.app.xmlui.wing.element.WingDocument;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

/**
 * The Include class reads an DRI XML file from and merges it into the existing
 * document stream using the Wing framework.
 * 
 * If the file is not present at the source provided a blank document is merged, 
 * allowing the pipeline to continue excecution.  This is also logged as a warning.
 * 
 * @author Scott Phillips
 */

public class Include extends AbstractTransformer implements CacheableProcessingComponent
{

    /**
     * A data structure describing what elements are to be merged and upon what
     * key. The first map's key is the name of a mergeable element's name while
     * the value is a list of all attributes that the element must match on to
     * be considered the same element.
     */
    private static final Map<String, String[]> mergeableMap;

    /** Construct the mergeableMap from constant data */
    static
    {
        Map<String, String[]> buildMap = new HashMap<String, String[]>();

        buildMap.put(WingDocument.E_DOCUMENT, null);

        buildMap.put(Meta.E_META, null);
        buildMap.put(UserMeta.E_USER_META, null);
        buildMap.put(PageMeta.E_PAGE_META, null);
        buildMap.put("artifactmeta", null);
        buildMap.put("repositoryMeta", null);
        buildMap.put("community",
                new String[] { "repositoryIdentifier" });
        buildMap.put("collection",
                new String[] { "repositoryIdentifier" });

        buildMap.put(Body.E_BODY, null);

        buildMap.put(Options.E_OPTIONS, null);
        buildMap.put(org.dspace.app.xmlui.wing.element.List.E_LIST,
                new String[] { org.dspace.app.xmlui.wing.element.List.A_NAME });

        mergeableMap = buildMap;
    }

    /** The source document */
    private Document w3cDocument;

    /** Helper class to stream the w3c DOM into SAX events */
    private DOMStreamer streamer;

    /** Stack of our current location within the document */
    private Stack<Element> stack;
    
    /** The Cocoon source for the included XML document */
    private Source source;
    
    /** The src attribute to the cocoon source */
    private String src;

    /**
     * Read in the given src path into an internal DOM for later processing when
     * needed.
     * 
     * @param resolver
     *            Resolver for cocoon pipelines.
     * @param objectModel
     *            The pipelines's object model.
     * @param src
     *            The source parameter
     * @param parameters
     *            The transformer's parameters.
     */
    public void setup(SourceResolver resolver, Map objectModel, String src,
            Parameters parameters) throws ProcessingException, SAXException,
            IOException
    {
        this.src = src;
        this.source = resolver.resolveURI(src);
    }

    /**
     * Generate the unique key.
     * This key must be unique inside the space of this component.
     *
     * @return The generated key hashes the src
     */
    public Serializable getKey()
    {
        return this.src;
    }

    /**
     * Generate the validity object.
     *
     * @return The generated validity object or <code>null</code> if the
     *         component is currently not cacheable.
     */
    public SourceValidity getValidity()
    {
        if (source != null)
        {
        	if (source.exists())
        		// The file exists so return it's validity.
            {
                return source.getValidity();
            }
        	else
            {
        		// The file does not exist so we will just return always valid. This
        		// will have an nastly side effect that if a file is removed from a
        		// running system the cache will remain valid. However if the other
        		// option is to always invalidate the cache if the file is not present
        		// which is not desirable either.
                return NOPValidity.SHARED_INSTANCE;
            }
        }
        else
        {
            return null;
        }
    }
    
    
    /**
     * Receive notification of the beginning of a document.
     */
    public void startDocument() throws SAXException
    {
        try
        {
            w3cDocument = SourceUtil.toDOM(source);
        }
        catch (Exception e)
        {   
        	// since we were unable to parce an XML document from the source given we will
        	// simply log the error as a warning and create  a null stack
        	getLogger().warn("File to be included from " + source.toString() +" not found.");
        	
        	stack = null;
            super.startDocument();
            
            return;
        }
        	
        stack = new Stack<Element>();
        streamer = new DOMStreamer(contentHandler, lexicalHandler);
        super.startDocument();
    }

    /**
     * Receive notification of the end of a document.
     */
    public void endDocument() throws SAXException
    {
        stack = null;
        super.endDocument();
    }

    /**
     * Receive notification of the beginning of an element.
     * 
     * @param uri
     *            The Namespace URI, or the empty string if the element has no
     *            Namespace URI or if Namespace processing is not being
     *            performed.
     * @param localName
     *            The local name (without prefix), or the empty string if
     *            Namespace processing is not being performed.
     * @param qName
     *            The raw XML 1.0 name (with prefix), or the empty string if raw
     *            names are not available.
     * @param attributes
     *            The attributes attached to the element. If there are no
     *            attributes, it shall be an empty Attributes object.
     */
    public void startElement(String uri, String localName, String qName,
            Attributes attributes) throws SAXException
    {
        //getLogger().debug("startElement: " + localName);
        if(stack == null){
        	// do nothing fall thru to the call to super.startElement
        	// this means that the document to be read was not parsable
        	// or not found in startDocument()
        }
        else if (stack.size() == 0)
        {
            stack.push(w3cDocument.getDocumentElement());
        }
        else
        {
            Element peek = stack.peek();

            Element foundChild = null;
            for (Element child : getElementList(peek))
            {
                if (isEqual(child, uri, localName, qName, attributes))
                {
                    foundChild = child;
                }
            }

            if (foundChild != null)
            {
                peek.removeChild(foundChild);
            }

            stack.push(foundChild);
        }

        super.startElement(uri, localName, qName, attributes);
    }

    /**
     * Receive notification of the end of an element.
     * 
     * @param uri
     *            The Namespace URI, or the empty string if the element has no
     *            Namespace URI or if Namespace processing is not being
     *            performed.
     * @param localName
     *            The local name (without prefix), or the empty string if
     *            Namespace processing is not being performed.
     * @param qName
     *            The raw XML 1.0 name (with prefix), or the empty string if raw
     *            names are not available.
     */
    public void endElement(String uri, String localName, String qName)
            throws SAXException
    {
        //getLogger().debug("endElement: " + localName);
        
        
        // if the stack is null do nothing fall thru to the call to 
        // super.endElement
    	// this means that the document to be read was not parsable
    	// or not found in startDocument()
        if(stack!=null){
	        Element poped = stack.pop();
	
	        if (poped != null)
	        {
	            //getLogger().debug("startElement: streaming");
	
	            for (Node node : getNodeList(poped))
                {
	                streamer.stream(node);
                }
	        }
        }

        super.endElement(uri, localName, qName);
    }

    /**
     * Receive notification of character data.
     * 
     * @param c
     *            The characters from the XML document.
     * @param start
     *            The start position in the array.
     * @param len
     *            The number of characters to read from the array.
     */
    public void characters(char c[], int start, int len) throws SAXException
    {
        super.characters(c, start, len);
    }

    /**
     * Determine if the given SAX event is the same as the given w3c DOM
     * element. If so then return true, otherwise false.
     * 
     * @param child
     *            W3C DOM element to compare with the SAX event.
     * @param uri
     *            The namespace URI of the SAX event.
     * @param localName
     *            The localName of the SAX event.
     * @param qName
     *            The qualified name of the SAX event.
     * @param attributes
     *            The attributes of the SAX event.
     * @return if equal.
     */
    private boolean isEqual(Element child, String uri, String localName,
            String qName, Attributes attributes)
    {
        if (child == null)
        {
            return false;
        }

        if (uri != null && !uri.equals(child.getNamespaceURI()))
        {
            return false;
        }

        if (localName != null && !localName.equals(child.getLocalName()))
        {
            return false;
        }

        if (!mergeableMap.containsKey(localName))
        {
            return false;
        }

        String[] attributeIdentities = mergeableMap.get(localName);

        if (attributeIdentities != null)
        {
            for (String attributeIdentity : attributeIdentities)
            {
                String testIdentity = attributes.getValue(attributeIdentity);
                String childIdentity = child.getAttribute(attributeIdentity);

                if (childIdentity != null && childIdentity.equals(testIdentity))
                {
                    continue;
                }

                if (childIdentity == null && testIdentity == null)
                {
                    continue;
                }

                return false;
            }
        }

        return true;
    }

    /**
     * DOM Helper method - Get a list of all child elements.
     * 
     * @param element
     *            The parent element
     * @return a list of all child elements.
     */
    private static List<Element> getElementList(Element element)
    {
        if (element == null)
        {
            return new ArrayList<Element>();
        }

        NodeList nodeList = element.getChildNodes();

        List<Element> resultList = new ArrayList<Element>();
        for (int i = 0; i < nodeList.getLength(); i++)
        {
            if (nodeList.item(i).getNodeType() == Node.ELEMENT_NODE)
            {
                resultList.add((Element) nodeList.item(i));
            }
        }

        return resultList;
    }

    /**
     * DOM Helper method - Get a list of all child nodes.
     * 
     * @param element
     *            The parent element
     * @return a list of all child nodes.
     */
    private static List<Node> getNodeList(Element element)
    {
        if (element == null)
        {
            return new ArrayList<Node>();
        }

        NodeList nodeList = element.getChildNodes();

        List<Node> resultList = new ArrayList<Node>();
        for (int i = 0; i < nodeList.getLength(); i++)
        {
            resultList.add((Node) nodeList.item(i));
        }

        return resultList;
    }
    
    /**
     * Recycle
     */
    public void recycle()
    {
        this.w3cDocument = null;
        this.streamer = null;
        this.stack = null;
        this.source = null;
        
        super.recycle();
    }
}