/*
 * JBoss, Home of Professional Open Source
 * Copyright 2006, JBoss Inc., and others contributors as indicated
 * by the @authors tag. All rights reserved.
 * See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU Lesser General Public License, v. 2.1.
 * This program is distributed in the hope that it will be useful, but WITHOUT A
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public License,
 * v.2.1 along with this distribution; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA  02110-1301, USA.
 *
 * (C) 2005-2006, JBoss Inc.
 */
package org.jboss.tools.smooks.templating.model.xml;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
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 javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.eclipse.core.runtime.Assert;
import org.eclipse.emf.common.util.URI;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import org.jboss.tools.smooks.templating.model.ModelBuilder;
import org.jboss.tools.smooks.templating.model.ModelBuilderException;
import org.jboss.tools.smooks.templating.template.xml.XMLFreeMarkerTemplateBuilder;
import org.milyn.xml.DomUtils;

/**
 * XML Model Builder from an XML Sample.
 * <p/>
 * The generated model can then be used by the {@link XMLFreeMarkerTemplateBuilder}.
 *
 * @author <a href="mailto:tom.fennelly@jboss.com">tom.fennelly@jboss.com</a>
 */
public class XMLSampleModelBuilder extends ModelBuilder {
		
	private static DocumentBuilder docBuilder;
	private Document model;

	static {
		DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
		docBuilderFactory.setNamespaceAware(true);
		try {
			docBuilder = docBuilderFactory.newDocumentBuilder();
		} catch (ParserConfigurationException e) {
			throw new RuntimeException(Messages.XMLSampleModelBuilder_UnexpectedXMLException, e);
		}
	}

    public XMLSampleModelBuilder(URI xmlSampleURI) throws IOException, ModelBuilderException {
    	Assert.isNotNull(xmlSampleURI, "Null 'xmlSampleURI' arg in method call."); //$NON-NLS-1$
    	
    	File xmlSampleFile =  new File(xmlSampleURI.toFileString());
    	
    	if(!xmlSampleFile.exists()) {
    		throw new IOException("XML Sample '" + xmlSampleFile.getAbsolutePath() + "' not found."); //$NON-NLS-1$ //$NON-NLS-2$    		
    	} else if(!xmlSampleFile.isFile()) {
    		throw new IOException("XML Sample '" + xmlSampleFile.getAbsolutePath() + "' is not a normal file.  Might be a directory etc."); //$NON-NLS-1$ //$NON-NLS-2$    		
    	}
    	
    	try {
			model = docBuilder.parse(xmlSampleFile);
		} catch (SAXException e) {
			throw new ModelBuilderException("Error parsing XML Sample file.", e); //$NON-NLS-1$ 
		}
		
		Element documentElement = model.getDocumentElement();
		
        // The model has no metadata attached since it is based on only a sample, 
		// so mark it as not being a strict model...
        ModelBuilder.setStrictModel(model, false);
		
		trimNonModelNodes(documentElement);
		configureModelElementTypes(documentElement);
		configureModelElementCardinality(documentElement);
		registerNamepsaces(documentElement);
    }

	public static void trimNonModelNodes(Element element) {
		NodeList children = element.getChildNodes();
		List<Node> removeableChildren = new ArrayList<Node>();
		Set<QName> childElementSet = new HashSet<QName>();

		for(int i = 0; i < children.getLength(); i++) {
			Node child = children.item(i);
		
			if(child.getNodeType() == Node.ELEMENT_NODE) {
				Element childElement = (Element) child;
				QName childElementQName;

				if(childElement.getPrefix() != null) {
					childElementQName = new QName(childElement.getNamespaceURI(), childElement.getLocalName(), childElement.getPrefix());
				} else {
					childElementQName = new QName(childElement.getNamespaceURI(), childElement.getTagName());
				}
				
				if(!childElementSet.contains(childElementQName)) {
					childElementSet.add(childElementQName);
					trimNonModelNodes((Element) child);				
				} else {
					removeableChildren.add(child);
				}
			} else {
				removeableChildren.add(child);
			}
		}
		
		for(Node child : removeableChildren) {
			element.removeChild(child);
		}
	}

	private void configureModelElementTypes(Element element) {
		NodeList children = element.getChildNodes();
		int childCount = children.getLength();

		if(childCount > 0) {
			// Has child elements, therefore it's a "complex" element type...
			ModelBuilder.setElementType(element, ElementType.complex);
			
			for(int i = 0; i < childCount; i++) {
				Node child = children.item(i);			
	
				if(child.getNodeType() == Node.ELEMENT_NODE) {
					configureModelElementTypes((Element) child);
				} else {
					throw new IllegalStateException("The configureModelElementTypes method can only be called after the model has been trimed of non-model Nodes.  Call trimNonModelNodes() before calling configureModelElementTypes()."); //$NON-NLS-1$
				}
			}
		} else {
			// Has no child elements, therefore it's a "simple" element type...
			ModelBuilder.setElementType(element, ElementType.simple);
		}
	}

	private void configureModelElementCardinality(Element element) {
		NodeList children = element.getChildNodes();
		List<Node> removeableChildren = new ArrayList<Node>();
		int childCount = children.getLength();

		Map<String, Element> childElementByNames = new HashMap<String, Element>();
		for(int i = 0; i < childCount; i++) {
			Node child = children.item(i);			

			if(child.getNodeType() == Node.ELEMENT_NODE) {
				Element childElement = (Element) child;
				String elementName = DomUtils.getName(childElement) + ":" + childElement.getNamespaceURI(); // Yes, namespace can be null, but that's OK. //$NON-NLS-1$
				Element earlierOccurance = childElementByNames.get(elementName);

				// Mark every element as being optional and possibly being multiple...
				ModelBuilder.setMinMax(childElement, 0, -1);
				
				if(earlierOccurance != null) {
					// According to the sample XML, this element is definitely a 
					// collection item because it exists more than once, so lets mark it 
					// such that sub mappings on this element require this collection to be mapped beforehand...
					ModelBuilder.setEnforceCollectionSubMappingRules(earlierOccurance, true);
					// And remove the duplicates...
					removeableChildren.add(childElement);
				} else {
					// We've no way of knowing whether or not this element is a collection
					// item or not, so lets not enforce the collection sub mapping rules...
					ModelBuilder.setEnforceCollectionSubMappingRules(childElement, false);
				}
				
				configureModelElementCardinality(childElement);
			} else {
				throw new IllegalStateException("The configureModelElementTypes method can only be called after the model has been trimed of non-model Nodes.  Call trimNonModelNodes() before calling configureModelElementTypes()."); //$NON-NLS-1$
			}
		}
		
		for(Node child : removeableChildren) {
			element.removeChild(child);
		}
	}

	private void registerNamepsaces(Element element) {
		NamedNodeMap attributes = element.getAttributes();
		
		for(int i = 0; i < attributes.getLength(); i++) {
			registerNamespace(attributes.item(i));
		}
		
		NodeList children = element.getChildNodes();
		for(int i = 0; i < children.getLength(); i++) {
			Node child = children.item(i);			
			if(child.getNodeType() == Node.ELEMENT_NODE) {
				registerNamespace(child);
				registerNamepsaces((Element) child);
			}
		}
	}

	private void registerNamespace(Node node) {
		String nsPrefix = node.getPrefix();
		String nsURI = node.getNamespaceURI();
		
		if(nsPrefix != null && nsURI != null) {
			getNamespaces().setProperty(nsPrefix, nsURI);
		}
	}

	public Document buildModel() throws ModelBuilderException {
        return model;
    }
}