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

import com.vividsolutions.jts.geom.GeometryFactory;
import org.geotools.feature.AttributeType;
import org.geotools.feature.GeometryAttributeType;
import org.geotools.feature.IllegalAttributeException;
import org.opengis.filter.Filter;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import java.util.Arrays;


/**
 * This represents a Choice of AttributeTypes. That means, an Attribute of this
 * type may be one of any of this AttributeType's children. This attribute  is
 * not valid for Simple Features, and maps to the Choice construct in GML.
 * 
 * <p>
 * Another way to think about the ChoiceAttributeType is as a Union
 * construction from C - it can store a number of different types of value,
 * but it only stores the one value.  The parse and validate methods try out
 * each of the choices to  see if one of them might work, since all are valid.
 * The order that the child attributeTypes (the choices you can use) are
 * specified is important, because  some objects can parse and validate
 * against several types.  The first choice that returns true is the one that
 * will
 * </p>
 *
 * @author dzwiers
 * @author Chris Holmes, TOPP
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/main/src/main/java/org/geotools/feature/type/ChoiceAttributeType.java $
 */
public class ChoiceAttributeType implements AttributeType {
    private final boolean nill;
    private final int min;
    private final int max;
    private final String name;
    private final AttributeType[] children;
    private Filter restriction;

    /**
     * DOCUMENT ME!
     *
     * @param copy
     */
    public ChoiceAttributeType(ChoiceAttributeType copy) {
        nill = copy.isNillable();
        min = copy.getMinOccurs();
        max = copy.getMaxOccurs();
        name = copy.getName();
        this.children = copyChildren(copy.getAttributeTypes());
        restriction = copy.getRestriction();
    }

    // The field for 'Class type' should be added when GT has moved to java 1.5
    public ChoiceAttributeType(String name, int min, int max,
        AttributeType[] children, Filter restriction) {
        nill = calculateNillable(children);
        this.min = min;
        this.max = max;
        this.name = name;
        //ensure immutable.
        this.children = copyChildren(children);
        this.restriction = restriction;
    }

    public ChoiceAttributeType(String name, AttributeType[] children) {
        this(name, 1, 1, children, Filter.EXCLUDE);
    }

    public Filter getRestriction() {
        return restriction;
    }

    protected AttributeType[] copyChildren(AttributeType[] attributes) {
	int length = attributes.length;
	AttributeType[] returnArray = new AttributeType[length];
	System.arraycopy(attributes, 0, returnArray, 0, length);
	return returnArray;
    }


    /* (non-Javadoc)
     * @see org.geotools.feature.AttributeType#getName()
     */
    public final String getName() {
        return getLocalName();
    }
    
    /**
     * {@inheritDoc}
     */
    public String getLocalName() {
    	return name;
    }

    /**
     * Gets the class of the object.  For a choice this is fairly useless, as
     * it  just returns Object, since we can not tell more than that.
     *
     * @return currently always returns Object.class, since we can't tell more.
     *
     * @task REVISIT: Perhaps we should add a getTypes() method that returns an
     *       array of classes, that would represent the classes that you can
     *       choose from.
     * @task REVISIT: Would also be good if this could dynamically figure out
     *       the broadest class - like Number if the choices were Double and
     *       Integer.
     *
     * @see org.geotools.feature.AttributeType#getType()
     */
    public final Class getType() {
        return getBinding();
    }
    
    public Class getBinding() {
		//The field for 'Class type' should be added when GT has moved to java 1.5
        return Object.class;
    }

    /* (non-Javadoc)
     * @see org.geotools.feature.AttributeType#isNillable()
     */
    public boolean isNillable() {
        return nill;
    }

    public boolean calculateNillable(AttributeType[] children) {
        for (int i = 0, ii = children.length; i < ii; i++) {
            if (children[i].isNillable()) {
                return true;
            }
        }

        //none of the children can take a null, so no nulls are allowed.
        return false;
    }

    /* (non-Javadoc)
     * @see org.geotools.feature.AttributeType#getMinOccurs()
     */
    public int getMinOccurs() {
        return min;
    }

    /* (non-Javadoc)
     * @see org.geotools.feature.AttributeType#getMaxOccurs()
     */
    public int getMaxOccurs() {
        return max;
    }

    /* (non-Javadoc)
     * @see org.geotools.feature.AttributeType#isGeometry()
     */
    public boolean isGeometry() {
        return false;
    }

    /**
     * Goes through the children, and searches for a parser that works. This
     * method  searches in the order in which the children are specified ...
     * please keep  this in mind when creating these objects if you care about
     * precedence.
     *
     * @param value The object to parse.
     *
     * @return The object parsed into the appropriate form for the Attribute.
     *
     * @throws IllegalArgumentException If the object could not be parsed by
     *         any of the child attribute Types.
     */
    public Object parse(Object value) throws IllegalArgumentException {
        for (int i = 0; i < children.length; i++) {
            try {
                return children[i].parse(value);
            } catch (IllegalArgumentException e) {
                // ignore ... try the next
            }
        }

        throw new IllegalArgumentException("Could not be parsed :(");
    }

    /**
     * Goes through the children, and searches for a validator that works. This
     * method  searches in the order in which the children are specified ...
     * please keep  this in mind when creating these objects if you care about
     * precedence.
     *
     * @param obj The object to validate.
     *
     * @throws IllegalArgumentException If none of the children can validate.
     */
    public void validate(Object obj) throws IllegalArgumentException {
        for (int i = 0; i < children.length; i++) {
            try {
                children[i].validate(obj);

                return; // validates
            } catch (IllegalArgumentException e) {
                // ignore ... try the next
            }
        }

        throw new IllegalArgumentException("Could not be validated :(");
    }

    /**
     * Goes through the children, and searches for a duplicator that works.
     * This method  searches in the order in which the children are specified
     * ... please keep  this in mind when creating these objects if you care
     * about precedence.
     *
     * @param src The object to be duplicated.
     *
     * @return A deep copy of the original object.
     *
     * @throws IllegalAttributeException For any attribute errors.
     * @throws IllegalArgumentException If the object could not be duplicated.
     */
    public Object duplicate(Object src) throws IllegalAttributeException {
        for (int i = 0; i < children.length; i++) {
            try {
                return children[i].duplicate(src);
            } catch (IllegalArgumentException e) {
                // ignore ... try the next
            }
        }

        throw new IllegalArgumentException("Could not be duplicated :(");
    }

    /**
     * Returns the default value for the first child which does not  throw an
     * exception, null otherwise.
     *
     * @return The default value of the first choice that does not throw an
     *         exception.
     */
    public Object createDefaultValue() {
        for (int i = 0; i < children.length; i++) {
            try {
                return children[i].createDefaultValue();
            } catch (IllegalArgumentException e) {
                // ignore ... try the next
            }
        }

        return null;
    }

    /**
     * This is only used twice in the whole geotools code base, and  one of
     * those is for a test, so we're removing it from the interface. If
     * getAttributeType does not have the AttributeType it will just return
     * null.  Gets the number of occurrences of this attribute.
     *
     * @param xPath XPath pointer to attribute type.
     *
     * @return Number of occurrences.
     */
    public boolean hasAttributeType(String xPath) {
        return getAttributeType(xPath) != null;
    }

    /**
     * Returns the number of attributes at the first 'level' of the schema.
     *
     * @return equivalent value to getAttributeTypes().length
     */
    public int getAttributeCount() {
        return children.length;
    }

    /**
     * Gets the attributeType at this xPath, if the specified attributeType
     * does not exist then null is returned.
     *
     * @param xPath XPath pointer to attribute type.
     *
     * @return True if attribute exists.
     */
    public AttributeType getAttributeType(String xPath) {
        AttributeType attType = null;
        int idx = find(xPath);

        if (idx >= 0) {
            attType = children[idx];
        }

        return attType;
    }

    /**
     * Find the position of a given AttributeType.
     *
     * @param type The type to search for.
     *
     * @return -1 if not found, a zero-based index if found.
     */
    public int find(AttributeType type) {
        if (type == null) {
            return -1;
        }

        int idx = find(type.getName());

        if ((idx < 0) || !children[idx].equals(type)) {
            idx = -1;
        }

        return idx;
    }

    /**
     * Find the position of an AttributeType which matches the given String.
     *
     * @param attName the name to look for
     *
     * @return -1 if not found, zero-based index otherwise
     */
    public int find(String attName) {
        int i = 0;

        while ((i < children.length) && !attName.equals(children[i].getName()))
            i++;

        return (i == children.length) ? (-1) : i;
    }

    /**
     * Gets the attributeType at the specified index.
     *
     * @param position the position of the attribute to check.
     *
     * @return The attribute type at the specified position.
     */
    public AttributeType getAttributeType(int position) {
        return children[position];
    }

    public AttributeType[] getAttributeTypes() {
        return (AttributeType[]) children.clone();
    }

    public boolean equals(Object other) {
        if (other == null) {
            return false;
        }

        if (!(other instanceof ChoiceAttributeType)) {
            return false;
        }

        ChoiceAttributeType att = (ChoiceAttributeType) other;

        if (name == null) {
            if (att.getName() != null) {
                return false;
            }
        } else if (!name.equals(att.getName())) {
            return false;
        }

        //hmmm... This makes the assumption that the order of the choices
        //matters - not sure if that's true.  Though the order does matter a
        //a bit for our parse method, so this is probably right, since two
        //with different orders could have diff. behaviors for that method.
        if (!Arrays.equals(children, att.getAttributeTypes())) {
            return false;
        }

        return true;
    }

    /**
     * Override of hashCode.
     *
     * @return hashCode for this object.
     */
    public int hashCode() {
        int hash = name.hashCode();

        for (int i = 0, ii = children.length; i < ii; i++) {
            hash ^= children[i].hashCode();
        }

        return hash;
    }

    /**
     * Gets a representation of this object as a string.
     *
     * @return A representation of this object as a string
     */
    public String toString() {
        String details = "name=" + name;
        details += ((" , nillable=" + nill) + ", min=" + min + ", max=" + max);
        details += (", choices: " + Arrays.asList(children));

        return "ChoiceAttributeType [" + details + "]";
    }

    /**
     * A special class that is made so a Choice can serve as the Default
     * Geometry in a FeatureType, by implementing GeometryAttributeType. It
     * must be a choice between other GeometryAttributeTypes.
     *
     * @author Chris Holmes, TOPP
     *
     * @task TODO: Need to write code to check that all the geometry attributes
     *       are in the same crs.  Right now we just blindly assume they are
     *       and return the first.
     */
    public static final class Geometric extends ChoiceAttributeType
        implements GeometryAttributeType {
        public Geometric(Geometric copy) {
            super(copy);
        }

        // The field for 'Class type' should be added when GT has moved to java 1.5
        public Geometric(String name, int min, int max,
            GeometryAttributeType[] children, Filter restriction) {
            super(name, min, max, children, restriction);
        }

        public Geometric(String name, GeometryAttributeType[] children) {
            super(name, children);
        }

        public CoordinateReferenceSystem getCoordinateSystem() {
            //Hack - this is not guaranteed to be right, since right now we
            //don't check in the constructors that all crses are the same.
            GeometryAttributeType first = (GeometryAttributeType) getAttributeType(0);

            return first.getCoordinateSystem();
        }

        public GeometryFactory getGeometryFactory() {
            //Hack - this is not guaranteed to be right, since right now we
            //don't check in the constructors that all crses are the same.
            GeometryAttributeType first = (GeometryAttributeType) getAttributeType(0);

            return first.getGeometryFactory();
        }

        /* (non-Javadoc)
         * @see org.geotools.feature.AttributeType#isGeometry()
         */
        public boolean isGeometry() {
            return true;
        }
    }
}
