/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2004-2006, GeoTools Project Managment Committee (PMC)
 *    
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.xml.handlers;

import java.net.URI;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.naming.OperationNotSupportedException;

import org.geotools.xml.DocumentFactory;
import org.geotools.xml.XMLElementHandler;
import org.geotools.xml.schema.All;
import org.geotools.xml.schema.Any;
import org.geotools.xml.schema.Choice;
import org.geotools.xml.schema.ComplexType;
import org.geotools.xml.schema.Element;
import org.geotools.xml.schema.ElementGrouping;
import org.geotools.xml.schema.ElementValue;
import org.geotools.xml.schema.Group;
import org.geotools.xml.schema.Sequence;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;


/**
 * <p>
 * This class is intended to handle parsing an xml element from an instance
 * document for elements who's type is both known and complex. This handler is
 * used within the XMLSAXHandler to handle sax events generated by the  SAX
 * parser.
 * </p>
 *
 * @author dzwiers www.refractions.net
 *
 * @see ComplexType
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/xml/src/main/java/org/geotools/xml/handlers/ComplexElementHandler.java $
 */
public class ComplexElementHandler extends XMLElementHandler {
    
    /** <code>serialVersionUID</code> field */
    private static final long serialVersionUID = ComplexElementHandler.class.hashCode();
    
    private ComplexType type; // saves casting all over
    private Element elem;
    private String text;
    private Attributes attr;
    private List elements;
    private Object value = null;
    private ElementHandlerFactory ehf;

    /**
     * Creates a new ComplexElementHandler object for Element  elem using
     * ElementHandlerFactory ehf.
     *
     * @param ehf ElementHandlerFactory
     * @param elem Element
     *
     * @throws SAXException
     */
    public ComplexElementHandler(ElementHandlerFactory ehf, Element elem)
        throws SAXException {
        this.ehf = ehf;

        if (elem == null) {
            logger.warning("ComplexType provided was null");
            throw new SAXException(new NullPointerException(
                    "ComplexType provided was null"));
        }

        this.elem = elem;

        try {
            type = (ComplexType) elem.getType();
        } catch (ClassCastException e) {
            logger.warning(e.toString());
            throw new SAXException(e);
        }
    }

    /**
     * @see org.geotools.xml.XMLElementHandler#getElement()
     */
    public Element getElement() {
        return elem;
    }

    /**
     * @see org.geotools.xml.XMLElementHandler#characters(java.lang.String)
     */
    public void characters(String text1) throws SAXException {
        if (type.isMixed()) {
            if (this.text != null) {
                this.text = this.text.concat(text1);
            } else {
                this.text = text1;
            }
        } else {
            if (!"".equals(text1.trim())) {
                if (type.getName() == null) {
                    throw new SAXException(
                        "This type may not have mixed content");
                }

                throw new SAXException("The " + type.getName()
                    + " type may not have mixed content");
            }
        }
    }

    /**
     * @param namespaceURI
     * @param localName
     * @param hints
     * @throws SAXException
     * @throws OperationNotSupportedException
     */
    public void endElement(URI namespaceURI, String localName, Map hints)
        throws OperationNotSupportedException, SAXException {
        text = (text == null) ? null : text.trim();
        
        if(hints == null){
            hints = new HashMap();
            hints.put(ElementHandlerFactory.KEY,ehf);
        }else{
            if(!hints.containsKey(ElementHandlerFactory.KEY))
                hints.put(ElementHandlerFactory.KEY,ehf);
        }

        if (elements == null) {
            if (type != null) {
                ElementValue[] vals;
				if(type.isMixed()){
					vals = new ElementValue[1];
                	vals[0] = new DefaultElementValue(null, text); // null is ok as
                			// this represents the mixed content
				}else{
					vals = new ElementValue[0];
				}
                value = type.getValue(elem, vals, attr, hints);
            } else {
                value = text;
            }

            return;
        }

        // validate the complex element ... throws an exception when it's been bad
        boolean validate = hints == null || !hints.containsKey(DocumentFactory.VALIDATION_HINT) ||
			hints.get(DocumentFactory.VALIDATION_HINT)==null || !(hints.get(DocumentFactory.VALIDATION_HINT) instanceof Boolean) ||
			((Boolean)hints.get(DocumentFactory.VALIDATION_HINT)).booleanValue();
        if(validate)
        	validateElementOrder();
        
        ElementValue[] vals = new ElementValue[elements.size()
            + (type.isMixed() ? 1 : 0)];

        for (int i = 0; i < elements.size(); i++) {
            XMLElementHandler xeh = (XMLElementHandler) elements.get(i);
            vals[i] = new DefaultElementValue(xeh.getElement(), xeh.getValue());
        }

        if (type.isMixed()) {
            vals[vals.length - 1] = new DefaultElementValue(null, text);

            // null is ok as this represents the mixed content
        }

        value = type.getValue(elem, vals, attr, hints);

        // clean memory
        attr = null;
        elements = null;
        text = null;
    }

    /*
     * This starts off the fun or checking element order for complex types
     * (note mixtures of All, Any, Choice, Element, Group, Sequence).
     *
     */
    private void validateElementOrder() throws SAXException {
        if ((elements == null) || (elements.size() == 0)) {
            // TODO ensure we have enough elements
            return;
        }

        int i = 0;
        int count =0;
        int[] i2 = new int[2];
        int cache = 0; // old pos.
        i2[1]=1;
        while(i<elements.size() && i2[1] == 1){
        	i2[0] = i;
        	i2[1] = 0;
            cache = i2[0];
            i2 = valid(type.getChild(), i);
            if( i2[1] == 0 && i == i2[0] ){
            	// done running
            	if (count < type.getChild().getMinOccurs()) {
                	StringBuffer buf = new StringBuffer();
                	buf.append("Too few elements for " );
                	buf.append( elem.getNamespace()+":"+elem.getName() );
                	buf.append( " (type = "+type.getName()+") " );
                	buf.append( ": " );
                	buf.append( count );
                	buf.append( " children, " );
                	buf.append( type.getChild().getMinOccurs() );
                	buf.append( " minOccurs" );        	  		
            		throw new SAXException( buf.toString() );
                }
            }else{
                if(cache == i2[0]){
                    // we have not progressed .. progress us
                    i = i2[0]+1;
                }else{
                    i = i2[0];
                }
            	count++;
            }
        }
        if(count > type.getChild().getMaxOccurs()){
        	StringBuffer buf = new StringBuffer();
        	buf.append("Too many elements for " );
        	buf.append( elem.getNamespace()+":"+elem.getName() );
        	buf.append( " (type = "+type.getName()+") " );
        	buf.append( ": " );
        	buf.append( count );
        	buf.append( " children, " );
        	buf.append( type.getChild().getMaxOccurs() );
        	buf.append( " maxOccurs" );        	        		
    		throw new SAXException( buf.toString() );
        }

        if (i != elements.size()) {
        	StringBuffer buf = new StringBuffer();
        	buf.append("Invalid Element ordering for " );
        	buf.append( elem.getNamespace()+":"+elem.getName() );
        	buf.append( " (type = "+type.getName()+") " );
        	buf.append( ". There were "+(elements.size()-i)+"elements which were unaccounted for" );      	  		
    		throw new SAXException( buf.toString() );
        }
    }

    /*
     * Generic validation method which simulates recursion, and avoids casting :)
     * The index is the starting index in the list of elements, for the particular
     * ElementGrouping. The last index matched is returned.
     */
    private int[] valid(ElementGrouping eg, int index) throws SAXException {
        if (eg == null) {
            return new int[]{index,1};
        }

        switch (eg.getGrouping()) {
        case ElementGrouping.SEQUENCE:
            int[] tmp = valid((Sequence) eg, index);
                        return tmp;

        case ElementGrouping.ALL:
            return valid((All) eg, index);

        case ElementGrouping.ANY:
            return valid((Any) eg, index);

        case ElementGrouping.CHOICE:
            return valid((Choice) eg, index);

        case ElementGrouping.GROUP:
            return valid((Group) eg, index);

        case ElementGrouping.ELEMENT:
            tmp = valid((Element) eg, index);
            return tmp;
        }

        return new int[]{index,1};
    }

    /*
     * Validates an All tag
     * @see valid(ElementGrouping)
     */
    private int[] valid(All all, int index) {
        Element[] elems = all.getElements();
        int[] r = new int[elems.length];

        for (int i = 0; i < r.length; i++)
            r[i] = 0;

        boolean c = true;
        int head = index;
        while (c) {
            c = false;

            for (int i = 0; i < elems.length; i++) {
                if (elems[i].getType().getName().equalsIgnoreCase(((XMLElementHandler) elements
                            .get(head)).getName())) {
                    r[i]++;
                    head++;
                    i = elems.length;
                    c = true;
                }
            }
        }

        for (int i = 0; i < r.length; i++) {
            if ((r[i] < elems[i].getMinOccurs())
                    || (r[i] > elems[i].getMaxOccurs())) {
                return new int[]{index,0};
            }
        }

        return new int[]{head,1};
    }

    /*
     * Validates an Any tag
     * @see valid(ElementGrouping)
     */
    private int[] valid(Any any, int index) {
        if (any.getNamespace().equals(((XMLElementHandler) elements.get(index)).getElement()
                                           .getType().getNamespace())) {
            return new int[]{index+1,1};
        }

        return new int[]{index,1};
    }

    /*
     * Validates an Choice tag
     * @see valid(ElementGrouping)
     */
    private int[] valid(Choice choice, int index) throws SAXException {
        ElementGrouping[] eg = choice.getChildren();

        if (eg == null) {
            return new int[]{index,1};
        }
        
        int i = 0; // choice child index;

        int end = index;
        int t = index;
        int count = 0;
        int t2[] = null;
        while(i<eg.length && end<elements.size()){
        	t2 = valid(eg[i], t);
        	if(t2[1] == 0 && t2[0] == t){// nothing, next
    			// move along
    			if(t2[0]>end && count>=eg[i].getMinOccurs() && count<=eg[i].getMaxOccurs())
    				end = t2[0];
    			count = 0;
    			i++;
    			t = index;
        	}else{
        		if(count==eg[i].getMaxOccurs()){
        			// move along
        			if(t2[0]>end && count>=eg[i].getMinOccurs())
        				end = t2[0];
        			count = 0;
        			i++;
        			t = index;
        		}else{
        			t = t2[0];
        			if(t == elements.size()){
        				end = t;
        			}
        			count++;
        		}
    		}
        }

        return new int[]{end,end==index?0:1};
    }

    /*
     * Validates an Group tag
     * @see valid(ElementGrouping)
     */
    private int[] valid(Group group, int index) throws SAXException {
        if (group.getChild() == null) {
            return new int[]{index,1};
        }

        return valid(group.getChild(), index);
    }

    /*
     * Validates an Element tag
     * @see valid(ElementGrouping)
     */
    private int[] valid(Element element, int index) {

    	// does this element equate to the index in the doc?

        int[] r = null;
        
        XMLElementHandler indexHandler = null;
        if(index<elements.size()){
            indexHandler = ((XMLElementHandler) elements.get(index));
        }else{
            // not found :)
            return new int[]{index,0};
        }
        
        if(r ==null && (indexHandler == null || indexHandler.getElement() == null))
        	return new int[]{index,0};
        
        if(r == null && indexHandler.getElement() == element)
        	r =  new int[]{index+1,1};
        
        if(r == null && element.getName()==null)
        	return new int[]{index,0};
        
        if(r == null && (element.getName()!=null && element.getName().equalsIgnoreCase(indexHandler.getName())))
        	r =  new int[]{index+1,1};
        if(r == null && element.getName()!=null){
        Element e = indexHandler.getElement();
        while(r == null && e != null){
        	if(element.getName().equalsIgnoreCase(e.getName())){
        		r =  new int[]{index+1,1};
        	}
        	e = e.getSubstitutionGroup();
        }
        }
        
        if(r == null){
            r = new int[]{index,0};
        }
        return r;
    }

    /*
     * Validates a Sequence tag
     * @see valid(ElementGrouping)
     */
    private int[] valid(Sequence seq, int index) throws SAXException {
        ElementGrouping[] eg = seq.getChildren();

        if (eg == null) {
            return new int[]{index,1};
        }

        int tIndex = index; // top of element matching list
        int t = 0; // top of child list
        
        int count = 0; // used for n-ary at a single spot
        int i2[] = new int[2];
        while(t<eg.length && tIndex<elements.size()){
        	i2 = valid(eg[t],tIndex); // new top element
        	if(i2[1]==1){ // they matched
        	    if(tIndex==i2[0]){
        	        // didn't more ahead ...
        	        t++; // force next spot
        	        count = 0; // reset
        	    }else{
        	        count ++;
        	        if(count<=eg[t].getMaxOccurs()){
        	            tIndex = i2[0]; // store index
        	        }else{
        	            // error, so redo
        	            if(eg[t].getMinOccurs()>count){
        	                // not good
//System.out.println("Seq Failed");
        	                return new int[]{index,0}; // not whole sequence
        	            }
        	            t++;
        	            count=0; // next defined type
        	        }
        	    }
        	}else{
                // didn't match
                
    			// move along and retest that spot
    			if(eg[t].getMinOccurs()>count){
    				// not good
//System.out.println("Seq Failed");
					return new int[]{index,0}; // not whole sequence
    			}
    			t++;
    			count=0; // next defined type
        	}
        }
//System.out.println("Seq index = "+tIndex+" Matched");
        return new int[]{tIndex,1};
    }

    /**
     * 
     * TODO summary sentence for startElement ...
     * 
     * @see org.geotools.xml.XMLElementHandler#startElement(java.net.URI, java.lang.String, org.xml.sax.Attributes)
     * @param namespaceURI
     * @param localName
     * @param attr1
     */
    public void startElement(URI namespaceURI, String localName, Attributes attr1) {
        this.attr = new AttributesImpl(attr1);
    }

    /**
     * 
     * TODO summary sentence for getHandler ...
     * 
     * @see org.geotools.xml.XMLElementHandler#getHandler(java.net.URI, java.lang.String, java.util.Map)
     * @param namespaceURI
     * @param localName
     * @param hints
     * @return XMLElementHandler
     * @throws SAXException
     */
    public XMLElementHandler getHandler(URI namespaceURI, String localName,
        Map hints) throws SAXException {
        if (elements == null) {
            elements = new LinkedList();
        }

        logger.finest("Starting search for element handler " + localName
            + " :: " + namespaceURI);

        Element e = XMLTypeHelper.findChildElement(type, localName, namespaceURI);
        if (e != null && namespaceURI.equals(e.getNamespace())){
            XMLElementHandler r = ehf.createElementHandler(e);

            if (type.cache(r.getElement(), hints)) {
                elements.add(r);
            }

            return r;
        }

        logger.finest("Checking the document schemas");

        XMLElementHandler r = ehf.createElementHandler(namespaceURI, localName);

        if (r != null) {
            if (type.cache(r.getElement(), hints)) {
                elements.add(r);
            }

            return r;
        }

        // validation?
        if(hints != null && hints.containsKey(DocumentFactory.VALIDATION_HINT)){
            Boolean valid = (Boolean)hints.get(DocumentFactory.VALIDATION_HINT);
            if(valid != null && !valid.booleanValue()){
                return new IgnoreHandler();
            }
        }
        
        throw new SAXException("Could not find element handler for "
            + namespaceURI + " : " + localName + " as a child of "
            + type.getName() + ".");
    }

    /**
     * 
     * TODO summary sentence for getValue ...
     * 
     * @see org.geotools.xml.XMLElementHandler#getValue()
     * @return Object
     */
    public Object getValue() {
        // endElement sets the value
        return value;
    }

    /**
     * @see org.geotools.xml.XMLElementHandler#getName()
     */
    public String getName() {
        return elem.getName();
    }
    
    /**
     * Remove the given XMLElementHandler from the Child-List
     * @param handler
     */
    public void removeElement(XMLElementHandler handler){
		if (elements != null){
			elements.remove(handler);
		}
	}
    
    /**
     * returns the Type of the Elementhandler
	 * @return type
	 */
	public ComplexType getType() {
		return type;
	}

    /**
     * <p>
     * Default Implementation used to pass values to type instances
     * </p>
     *
     * @author dzwiers
     *
     * @see ElementValue
     */
    private static class DefaultElementValue implements ElementValue {
        Element t;
        Object value;

        /**
         * Stores the two values for use within the specified type
         *
         * @param t Element
         * @param o String
         */
        public DefaultElementValue(Element t, Object o) {
            this.t = t;
            value = o;
        }

        /**
         * 
         * TODO summary sentence for getElement ...
         * 
         * @see org.geotools.xml.schema.ElementValue#getElement()
         * @return Element
         */
        public Element getElement() {
            return t;
        }

        /**
         * 
         * TODO summary sentence for getValue ...
         * 
         * @see org.geotools.xml.schema.ElementValue#getValue()
         * @return Object
         */
        public Object getValue() {
            return value;
        }
        /* (non-Javadoc)
		 * @see java.lang.Object#toString()
		 */
		public String toString() {
			StringBuffer buf = new StringBuffer();
			if( t != null && t.toString() != null ){
				buf.append( t.toString() );				
			}
			else {
				buf.append( getClass().getName() );
			}
			buf.append("[");
			buf.append( value );
			buf.append("]");
			return buf.toString();
		}
    }
}
