/*
 *    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.xsi;

import java.net.URI;

import org.geotools.xml.XSIElementHandler;
import org.geotools.xml.handlers.XMLTypeHelper;
import org.geotools.xml.schema.Element;
import org.geotools.xml.schema.ElementGrouping;
import org.geotools.xml.schema.Type;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;


/**
 * ElementTypeHandler purpose.
 * 
 * <p>
 * Represtents an 'element' declaration.
 * </p>
 *
 * @author dzwiers, Refractions Research, Inc. http://www.refractions.net
 * @author $Author:$ (last modification)
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/xml/src/main/java/org/geotools/xml/handlers/xsi/ElementTypeHandler.java $
 * @version $Id: ElementTypeHandler.java 22266 2006-10-19 11:30:55Z acuster $
 */
public class ElementTypeHandler extends ElementGroupingHandler {
    /** 'element' */
    public final static String LOCALNAME = "element";

    /** UNBOUNDED */
    private static int offset = 0;
    private String id;
    private String name;
    private String type;
    private String ref = null;
    private String defaulT;
    private String fixed;
    private String substitutionGroup;
    private int maxOccurs = 1;
    private int minOccurs = 1;
    private int finaL;
    private int block;
    private boolean form;
    private boolean abstracT;
    private boolean nillable;
    private Object child;

    // private List constraints;
    private int hashCodeOffset = getOffset();
    private DefaultElement cache = null;

    /*
     * helper for hashCode()
     */
    private static int getOffset() {
        return offset++;
    }

    /**
     * @see java.lang.Object#hashCode()
     */
    public int hashCode() {
        return (LOCALNAME.hashCode() * ((id == null) ? 1 : id.hashCode()) * ((ref == null)
        ? 1 : ref.hashCode()) * ((name == null) ? 1 : name.hashCode()))
        + hashCodeOffset;
    }

    /**
     * @see org.geotools.xml.XSIElementHandler#getHandler(java.lang.String,
     *      java.lang.String)
     */
    public XSIElementHandler getHandler(String namespaceURI, String localName)
        throws SAXException {
        if (SchemaHandler.namespaceURI.equalsIgnoreCase(namespaceURI)) {
            // child types
            //
            // simpleType
            if (SimpleTypeHandler.LOCALNAME.equalsIgnoreCase(localName)) {
                SimpleTypeHandler sth = new SimpleTypeHandler();

                if (child == null) {
                    child = sth;
                } else {
                    throw new SAXNotRecognizedException(
                        "Extension may only have one 'simpleType' or 'complexType' declaration.");
                }

                return sth;
            }

            // complexType
            if (ComplexTypeHandler.LOCALNAME.equalsIgnoreCase(localName)) {
                ComplexTypeHandler sth = new ComplexTypeHandler();

                if (child == null) {
                    child = sth;
                } else {
                    throw new SAXNotRecognizedException(
                        "Extension may only have one 'simpleType' or 'complexType' declaration.");
                }

                return sth;
            }

            // TODO add constraint checking
            //            // ref
            //            if (UniqueHandler.LOCALNAME.equalsIgnoreCase(localName)) {
            //                UniqueHandler sth = new UniqueHandler();
            //                constraints.add(sth);
            //
            //                return sth;
            //            }
            //
            //            // key
            //            if (KeyHandler.LOCALNAME.equalsIgnoreCase(localName)) {
            //                KeyHandler sth = new KeyHandler();
            //                constraints.add(sth);
            //
            //                return sth;
            //            }
            //
            //            // key
            //            if (KeyrefHandler.LOCALNAME.equalsIgnoreCase(localName)) {
            //                KeyrefHandler sth = new KeyrefHandler();
            //                constraints.add(sth);
            //
            //                return sth;
            //            }
        }

        return null;
    }

    /**
     * @see org.geotools.xml.XSIElementHandler#startElement(java.lang.String,
     *      java.lang.String, org.xml.sax.Attributes)
     */
    public void startElement(String namespaceURI, String localName,
        Attributes atts) throws SAXException {
        // abstract
        String abstracT1 = atts.getValue("", "abstracT");

        if (abstracT1 == null) {
            abstracT1 = atts.getValue(namespaceURI, "abstracT");
        }

        if ((abstracT1 == null) || "".equalsIgnoreCase(abstracT1)) {
            this.abstracT = false;
        } else {
            this.abstracT = Boolean.getBoolean(abstracT1);
        }

        // block
        String block1 = atts.getValue("", "block");

        if (block1 == null) {
            block1 = atts.getValue(namespaceURI, "block");
        }

        this.block = ComplexTypeHandler.findBlock(block1);

        // default
        defaulT = atts.getValue("", "default");

        if (defaulT == null) {
            defaulT = atts.getValue(namespaceURI, "default");
        }

        // final
        String finaL1 = atts.getValue("", "final");

        if (finaL1 == null) {
            finaL1 = atts.getValue(namespaceURI, "final");
        }

        this.finaL = ComplexTypeHandler.findFinal(finaL1);

        // fixed
        fixed = atts.getValue("", "fixed");

        if (fixed == null) {
            fixed = atts.getValue(namespaceURI, "fixed");
        }

        // form
        String form1 = atts.getValue("", "form");

        if (form1 == null) {
            form1 = atts.getValue(namespaceURI, "form");
        }

        this.form = "qualified".equalsIgnoreCase(form1);

        // id
        id = atts.getValue("", "id");

        if (id == null) {
            id = atts.getValue(namespaceURI, "id");
        }

        // maxOccurs
        String maxOccurs1 = atts.getValue("", "maxOccurs");

        if (maxOccurs1 == null) {
            maxOccurs1 = atts.getValue(namespaceURI, "maxOccurs");
        }

        if ((maxOccurs1 != null) && !"".equalsIgnoreCase(maxOccurs1)) {
            if ("unbounded".equalsIgnoreCase(maxOccurs1)) {
                this.maxOccurs = ElementGrouping.UNBOUNDED;
            } else {
                this.maxOccurs = Integer.parseInt(maxOccurs1);
            }
        } else {
            this.maxOccurs = 1;
        }

        // minOccurs
        String minOccurs1 = atts.getValue("", "minOccurs");

        if (minOccurs1 == null) {
            minOccurs1 = atts.getValue(namespaceURI, "minOccurs");
        }

        if ((minOccurs1 != null) && !"".equalsIgnoreCase(minOccurs1)) {
            this.minOccurs = Integer.parseInt(minOccurs1);
        } else {
            this.minOccurs = 1;
        }

        // name
        name = atts.getValue("", "name");

        if (name == null) {
            name = atts.getValue(namespaceURI, "name");
        }

        // nillable
        String nillable1 = atts.getValue("", "nillable");

        if (nillable1 == null) {
            nillable1 = atts.getValue(namespaceURI, "nillable");
        }

        if ((nillable1 == null) || "".equalsIgnoreCase(nillable1)) {
            this.nillable = false;
        } else {
            this.nillable = Boolean.valueOf(nillable1).booleanValue();

        }

        // ref
        ref = atts.getValue("", "ref");

        if (ref == null) {
            ref = atts.getValue(namespaceURI, "ref");
        }

        // substitutionGroup
        substitutionGroup = atts.getValue("", "substitutionGroup");

        if (substitutionGroup == null) {
            substitutionGroup = atts.getValue(namespaceURI, "substitutionGroup");
        }

        // type
        type = atts.getValue("", "type");

        if (type == null) {
            type = atts.getValue(namespaceURI, "type");
        }

        if ((ref != null) || "".equalsIgnoreCase(ref)) {
            if (!(((name == null) || "".equalsIgnoreCase(name))
                    && ((type == null) || "".equalsIgnoreCase(type)))) {
                throw new SAXException(
                    "Elements cannot have both a 'ref' and a 'name' + 'type' attribute");
            }

            name = type = ref;
        }
    }

    /**
     * @see org.geotools.xml.XSIElementHandler#getLocalName()
     */
    public String getLocalName() {
        return LOCALNAME;
    }

    /**
     * <p>
     * returns the element name
     * </p>
     *
     */
    public String getName() {
        return name;
    }

    /**
     * @see org.geotools.xml.XSIHandlers.ElementGroupingHandler#compress(org.geotools.xml.XSIHandlers.SchemaHandler)
     */
    protected ElementGrouping compress(SchemaHandler parent)
        throws SAXException {

        synchronized(this){
            if (cache != null)
            	return cache;
            cache = new DefaultElement();
        }

        cache.id = id;
        cache.name = name;
        cache.namespace = parent.getTargetNamespace();
        cache.defaulT = defaulT;
        cache.fixed = fixed;
        cache.block = block;
        cache.finaL = finaL;
        cache.abstracT = abstracT;
        cache.form = form;
        cache.nillable = nillable;
        cache.minOccurs = minOccurs;
        cache.maxOccurs = maxOccurs;

        if (substitutionGroup != null) {
            cache.substitutionGroup = parent.lookUpElement(substitutionGroup);
        }

        if (child == null) {
            cache.type = parent.lookUpType(type);
        } else if (child instanceof SimpleTypeHandler) {
            cache.type = ((SimpleTypeHandler) child).compress(parent);
        } else {
            cache.type = ((ComplexTypeHandler) child).compress(parent);
        }

        if (ref != null) {
            Element e = parent.lookUpElement(ref);

            if (e == null) {
                throw new SAXException("Element '" + ref
                    + "' was referenced and not found");
            }

            cache.name = e.getName();
            cache.type = e.getType();

            if ((defaulT == null) || "".equalsIgnoreCase(defaulT)) {
                cache.defaulT = e.getDefault();
            }

            if ((fixed == null) || "".equalsIgnoreCase(fixed)) {
                cache.fixed = e.getFixed();
            }

            if (block == 0) {
                cache.block = e.getBlock();
            }

            if (finaL == 0) {
                cache.finaL = e.getFinal();
            }

            cache.minOccurs = (minOccurs == 1) ? e.getMinOccurs() : minOccurs;
            cache.maxOccurs = (maxOccurs == 1) ? e.getMaxOccurs() : maxOccurs;

            if (substitutionGroup != null) {
                cache.substitutionGroup = e.getSubstitutionGroup();
            }
        }

        //  TODO add constraint checking

        return cache;
    }

    /**
     * @see org.geotools.xml.XSIElementHandler#getHandlerType()
     */
    public int getHandlerType() {
        return DEFAULT;
    }

    /**
     * @see org.geotools.xml.XSIElementHandler#endElement(java.lang.String,
     *      java.lang.String)
     */
    public void endElement(String namespaceURI, String localName){
        // do nothing
    }

    /**
     * <p>
     * Default implementation of an element
     * </p>
     *
     * @author dzwiers
     */
    private static class DefaultElement implements Element {
        int block;
        int finaL;
        boolean abstracT;
        boolean form;
        boolean nillable;
        int maxOccurs;
        int minOccurs;
        String name;
        String id;
        String defaulT;
        String fixed;
        URI namespace;
        Element substitutionGroup;
        Type type;

        /**
         * @see org.geotools.xml.xsi.ElementGrouping#findChildElement(java.lang.String)
         */
        public Element findChildElement(String name1) {
            if (this.name != null) {
                if (this.name.equalsIgnoreCase(name1)) {
                    return this;
                }
            }

            return (type == null) ? null : type.findChildElement(name1);
        }

        /**
         * @see org.geotools.xml.xsi.Element#isAbstract()
         */
        public boolean isAbstract() {
            return abstracT;
        }

        /**
         * @see org.geotools.xml.xsi.Element#getBlock()
         */
        public int getBlock() {
            return block;
        }

        /**
         * @see org.geotools.xml.xsi.Element#getDefault()
         */
        public String getDefault() {
            return defaulT;
        }

        /**
         * @see org.geotools.xml.xsi.Element#getFinal()
         */
        public int getFinal() {
            return finaL;
        }

        /**
         * @see org.geotools.xml.xsi.Element#getFixed()
         */
        public String getFixed() {
            return fixed;
        }

        /**
         * @see org.geotools.xml.xsi.Element#isForm()
         */
        public boolean isForm() {
            return form;
        }

        /**
         * @see org.geotools.xml.xsi.ElementGrouping#getMaxOccurs()
         */
        public int getMaxOccurs() {
            return maxOccurs;
        }

        /**
         * @see org.geotools.xml.xsi.ElementGrouping#getMinOccurs()
         */
        public int getMinOccurs() {
            return minOccurs;
        }

        /**
         * @see org.geotools.xml.xsi.Element#getName()
         */
        public String getName() {
            return name;
        }

        /**
         * @see org.geotools.xml.xsi.Element#isNillable()
         */
        public boolean isNillable() {
            return nillable;
        }

        /**
         * @see org.geotools.xml.xsi.Element#getSubstitutionGroup()
         */
        public Element getSubstitutionGroup() {
            return substitutionGroup;
        }

        /**
         * @see org.geotools.xml.xsi.Element#getType()
         */
        public Type getType() {
            return type;
        }

        public int getGrouping() {
            return ELEMENT;
        }

        /**
         * @see org.geotools.xml.xsi.Element#getId()
         */
        public String getId() {
            return id;
        }

        /**
         * @see org.geotools.xml.schema.Element#getNamespace()
         */
        public URI getNamespace() {
            return namespace;
        }

		public Element findChildElement(String localName, URI namespaceURI) {
			 if (this.name != null) {
	                if (this.name.equalsIgnoreCase(localName) && getNamespace().equals(namespaceURI)) {
	                    return this;
	                }
	            }

	            return (type == null) ? null : XMLTypeHelper.findChildElement(type, localName, namespaceURI);
		}
    }
}
