/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2002-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;

import java.net.URI;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.geotools.factory.CommonFactoryFinder;
import org.geotools.factory.Factory;
import org.geotools.factory.FactoryConfigurationError;
import org.geotools.factory.GeoTools;
import org.geotools.factory.Hints;

/**
 * A schema builder, because FeatureTypes are meant to be immutable, this
 * object is mutable.
 * 
 * <p>
 * The basic idea for usage is that you configure the builder to whatever state
 * is desired, setting properties and adding AttributeTypes. When the desired
 * state is acheived, the expected FeatureType can be retrieved by calling<br>
 * <code>getFeatureType()</code>
 * </p>
 * <p>
 * Repeated calls to getFeatureType will return the <i>same</i> FeatureType
 * given that no calls which modify the state of the factory are made.
 * </p>
 * 
 * <p>
 * Here's an example of how to use this: 
 * <code><pre> 
 * FeatureTypeBuilder build = FeatureTypeFactory.newInstance();  
 * build.addType(...);
 * build.setName(...);  
 * build.setNamespace(...);  
 * FeatureType type = build.getFeatureType(); 
 * </pre></code>
 * There are also a set of convenience methods for creation of FeatureTypes.
 * These are the various newFeatureType methods.
 * </p>
 * 
 * </p>
 *
 * @author Ian Schneider
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/main/src/main/java/org/geotools/feature/FeatureTypeBuilder.java $
 * @version $Id: FeatureTypeBuilder.java 25008 2007-04-04 02:17:19Z jgarnett $
 */
public abstract class FeatureTypeBuilder extends FeatureTypes implements Factory {
    /** The types that all features have. */
    private static Set builtInTypes = null;

    /** If the base types have been initialized */
    private static boolean initialized;

    /** The name to give the FeatureType to be created. */
    private String name;

    /** The namespace to give the FeatureType to be created. */
    private URI namespace;

    /** If something in the factory has changed. */
    private boolean dirty = true;

    /** The type created. */
    private FeatureType type = null;

    /** The current defaultGeometry of the FeatureType returned. */
    private GeometryAttributeType defaultGeometry = null;

    /** If the type is abstract. */
    private boolean abstractType = false;

    /** The types that this is derived from. */
    private java.util.Set superTypes;

    /**
     * Implementation hints - since this is a builder all
     * hints are passed onto the FeatureType.
     */
    Map hints;
    
    /**
     * An empty public constructor. Subclasses should not provide a
     * constructor.
     * @deprecated 
     */
    public FeatureTypeBuilder() {
    	this( Collections.EMPTY_MAP );
    }
    
    /**
     * An empty public constructor. Subclasses should not provide a
     * constructor.
     */
    public FeatureTypeBuilder( Map hints) {
    	this.hints = hints;
    }
    
    /**
     * Returns the implementation hints. The default implementation returns en empty map.
     * <p>
     * Since the building of a FeatureType involves the collaboration of may
     * Factory classes (that may be discovered over the course of the build process)
     * we are forced to indicate that *all* hints are used.
     * </p>
     * <p>
     * Strictly this is a Builder (not a factory) and has no need declair which
     * hints are used (as one can never *keep* this builder in a factory registery.
     * (It is stateful and cannot be used concurrently for example).
     */
    public Map getImplementationHints() {
        return hints;
    }    
    /**
     * Create a new FeatureTypeFactory with the given typeName.
     *
     * @param name The typeName of the feature to create.
     *
     * @return A new FeatureTypeFactory instance.
     *
     * @throws FactoryConfigurationError If there exists a configuration error.
     */
    public static FeatureTypeFactory newInstance(String typeName)
        throws FactoryConfigurationError {
        
        // warning not sure if CommonFactoryFinder is going to cache the instance or not?
        //
        Hints hints = GeoTools.getDefaultHints();
        if( hints == null ){
            hints = new Hints( Hints.FEATURE_TYPE_FACTORY_NAME, typeName );
        }
        else {
            hints.put( Hints.FEATURE_TYPE_FACTORY_NAME, typeName );
        }
        hints.put( Hints.FEATURE_TYPE_FACTORY_NAME, typeName );
        return CommonFactoryFinder.getFeatureTypeFactory( hints );
    }
    
    /**
     * Import all of the AttributeTypes from the given FeatureType into this
     * factory.
     * 
     * <p>
     * If strict is true, non-uniquely named AttributeTypes will throw an
     * exception.
     * </p>
     * 
     * <p>
     * If strict is false, these will be silently ignored, but not added.
     * </p>
     * 
     * <p>
     * No other information is imported.
     * </p>
     *
     * @param type The FeatureType to import from.
     * @param strict Enforce namespace restrictions.
     *
     * @throws IllegalArgumentException If strict is true and there are naming
     *         problems.
     */
    public void importType(FeatureType type, boolean strict)
        throws IllegalArgumentException {
        for (int i = 0, ii = type.getAttributeCount(); i < ii; i++) {
            try {
                addType(type.getAttributeType(i));
            } catch (IllegalArgumentException iae) {
                if (strict) {
                    throw iae;
                }
            }
        }
    }

    /**
     * Set the super types of this factory. The types will be copied into a
     * Set.
     *
     * @param types A Collection of types.
     */
    public final void setSuperTypes(java.util.Collection types) {
        superTypes = new java.util.LinkedHashSet(types);
    }

    /**
     * Obtain the super types of this factory. Any user types will be appended
     * to the built in types of this factory.
     *
     * @return A Collection representing the super types of the FeatureType
     *         this factory will create.
     */
    public final java.util.Collection getSuperTypes() {
        Set supers = (superTypes == null) ? new HashSet() : superTypes;
        Set builtin = getBuiltinTypes();

        if (builtin != null) {
            supers.addAll(builtin);
        }

        return supers;
    }

    /**
     * A convienence method for importing AttributeTypes, simply calls<br>
     * <code> importType(type,false) </code>
     *
     * @param type The type to import.
     */
    public void importType(FeatureType type) {
        importType(type, false);
    }

    /**
     * Set the name of the FeatureType this factory will produce.
     *
     * @param name The new name. May be null.
     */
    public void setName(String name) {
        dirty |= isDifferent(name, this.name);
        this.name = name;
    }

    /**
     * Get the current configuration of the name of this factory.
     *
     * @return The current name. May be null.
     */
    public final String getName() {
        return name;
    }

    /**
     * Set the namespace of the FeatureType this factory will produce.
     *
     * @param namespace The new namespace. May be null.
     */
    public void setNamespace(URI namespace) {
        dirty |= isDifferent(namespace, this.namespace);
        this.namespace = namespace;
    }

    /**
     * Get the current configuration of the namespace of this factory.
     *
     * @return The current namespace. May be null.
     */
    public final URI getNamespace() {
        return namespace;
    }

    /**
     * Is this factory configured to be abstract?
     *
     * @return True if it is, false if it aint.
     */
    public final boolean isAbstract() {
        return abstractType;
    }

    /**
     * Configure this factory to produce an abstract type.
     *
     * @param a True or false.
     */
    public final void setAbstract(boolean a) {
        dirty = true;
        this.abstractType = a;
    }

    private boolean isDifferent(String s1, String s2) {
        if (s1 != null) {
            return !s1.equals(s2);
        }

        if (s2 != null) {
            return !s2.equals(s1);
        }

        return s1 != s2;
    }
    private boolean isDifferent(URI u1, URI u2) {
        if (u1 != null) {
            return !u1.equals(u2);
        }

        if (u2 != null) {
            return !u2.equals(u1);
        }

        return u1 != u2;
    }
    /**
     * Remove all the AttributeTypes in this factory.
     */
    public final void removeAll() {
        int cnt = getAttributeCount();

        for (int i = cnt; i > 0; i++) {
            removeType(i - 1);
        }
    }

    /**
     * Add an array of AttributeTypes to this factory.
     *
     * @param types The types or a null array.
     *
     * @throws NullPointerException If any of the types are null.
     * @throws IllegalArgumentException If there are naming problems.
     */
    public final void addTypes(AttributeType[] types)
        throws NullPointerException, IllegalArgumentException {
        if (types == null) {
            return;
        }

        for (int i = 0; i < types.length; i++) {
            addType(types[i]);
        }
    }

    /**
     * A the given AttributeType to this factory.
     *
     * @param type The type to add.
     *
     * @throws NullPointerException If the type is null.
     * @throws IllegalArgumentException If another type exists with the same
     *         name.
     */
    public final void addType(AttributeType type)
        throws NullPointerException, IllegalArgumentException {
        if (type == null) {
            throw new NullPointerException("type");
        }

        dirty = true;
        check(type);
        add(type);
    }

    /**
     * Remove the given type from this factory.
     *
     * @param type The type to remove.
     *
     * @throws NullPointerException If the type is null.
     */
    public final void removeType(AttributeType type)
        throws NullPointerException {
        if (type == null) {
            throw new NullPointerException("type");
        }

        dirty = true;

        AttributeType removed = remove(type);

        if (removed == defaultGeometry) {
            defaultGeometry = null;
        }
    }

    /**
     * Insert the given type at the index specified.
     *
     * @param idx The index to insert at.
     * @param type The AttributeType to insert.
     *
     * @throws NullPointerException If the type is null.
     * @throws IllegalArgumentException If the AttributeType is not allowed.
     * @throws ArrayIndexOutOfBoundsException If the index is out of range.
     */
    public final void addType(int idx, AttributeType type)
        throws NullPointerException, IllegalArgumentException, 
            ArrayIndexOutOfBoundsException {
        if (type == null) {
            throw new NullPointerException("type");
        }

        dirty = true;
        check(type);
        add(idx, type);
    }

    /**
     * Remove the AttributeType at the given index.
     *
     * @param idx The index to remove at.
     *
     * @throws ArrayIndexOutOfBoundsException If the index is out of bounds.
     */
    public final void removeType(int idx) throws ArrayIndexOutOfBoundsException {
        dirty = true;

        AttributeType removed = remove(idx);

        if (removed == defaultGeometry) {
            defaultGeometry = null;
        }
    }

    /**
     * Set the AttributeType at the given index. Overwrites the existing type.
     *
     * @param idx The index to use.
     * @param type The type to use.
     *
     * @throws IllegalArgumentException If the type is not good.
     * @throws NullPointerException if they type passed in is null
     * @throws ArrayIndexOutOfBoundsException if the index is out of bounds.
     */
    public final void setType(int idx, AttributeType type)
        throws IllegalArgumentException, NullPointerException, 
            ArrayIndexOutOfBoundsException {
        if (type == null) {
            throw new NullPointerException("type");
        }

        dirty = true;
        check(type);

        AttributeType removed = set(idx, type);

        if (removed == defaultGeometry) {
            defaultGeometry = null;
        }
    }

    /**
     * Swap the AttributeTypes at the given locations.
     *
     * @param idx1 The index of the first.
     * @param idx2 The index of the second.
     *
     * @throws ArrayIndexOutOfBoundsException if either index is not in the
     *         array bounds.
     */
    public final void swap(int idx1, int idx2)
        throws ArrayIndexOutOfBoundsException {
        // implementation note:
        // we must rely on the subclass implementation, which, hopefully does
        // not do any checking. If we used setType, there is a name overlap.
        AttributeType tmp = get(idx1);
        set(idx1, get(idx2));
        set(idx2, tmp);

        // must do this!
        dirty = true;
    }

    /**
     * Return the AttributeType currently used as the defaultGeometry property
     * for the FeatureType this factory will create.
     *
     * @return The AttributeType representing the defaultGeometry or null.
     */
    public final GeometryAttributeType getDefaultGeometry() {
        return defaultGeometry;
    }

    /**
     * Sets the defaultGeometry of this factory. If the defaultGeometry
     * AttributeType does not exist as an AttributeType within this factory,
     * it is added. This will overwrite the existing defaultGeometry, yet not
     * remove it from the existing AttributeTypes.
     *
     * @param defaultGeometry The AttributeType to use as the defaultGeometry.
     *        May be null.
     *
     * @throws IllegalArgumentException if the type is not a geometry.
     */
    public final void setDefaultGeometry(GeometryAttributeType defaultGeometry)
        throws IllegalArgumentException {
        // check if Geometry
        if ((defaultGeometry != null) && !defaultGeometry.isGeometry()) {
            String mess = "Attempted to set a non-geometry type as "
                + "defaultGeometry: ";
            throw new IllegalArgumentException(mess + defaultGeometry);
        }

        dirty = true; // do this!
        this.defaultGeometry = defaultGeometry;

        // if the defaultGeometry hasn't been added, add it!
        if ((defaultGeometry != null) && !contains(defaultGeometry)) {
            addType(defaultGeometry);
        }
    }

    /**
     * Get a FeatureType which reflects the state of this factory. Any
     * modifications to the state of the factory (adding, removing, or
     * reordering any AttributeTypes or changing any other properties -
     * isNillable,name,etc.), will cause the factory to "retool" itself. If
     * the factory has not changed since a call to this method, the return
     * value will be the same FeatureType which the previous method returned.
     * Otherwise, a new FeatureType will be created.
     *
     * @return The featureType reflecting the current factory state.
     *
     * @throws SchemaException if name is null or blank
     */
    public final FeatureType getFeatureType() throws SchemaException {
        // we're dirty, recreate the FeatureType
        if (dirty || (type == null)) {
            // no defaultGeometry assigned, search for one.
            if (defaultGeometry == null) {
                for (int i = 0, ii = getAttributeCount(); i < ii; i++) {
                    if (get(i) instanceof GeometryAttributeType) {
                        defaultGeometry = (GeometryAttributeType) get(i);
                        break;
                    }
                }
            }

            if ((name == null) || (name.trim().length() == 0)) {
                throw new SchemaException(
                    "Cannot create FeatureType with null or blank name");
            }

            type = createFeatureType();

            // oops, the subclass messed up...
            if (type == null) {
                throw new NullPointerException(getClass().getName()
                    + ".createFeatureType()");
            }

            if (isAbstract() && !type.isAbstract()) {
                throw new RuntimeException(
                    "FeatureTypeFactory poorly implemented, "
                    + "expected abstract type, received " + type);
            }

            // not dirty anymore.
            dirty = false;
        }

        return type;
    }

    /**
     * Returns a string representation of this factory.
     *
     * @return The string representing this factory.
     */
    public String toString() {
        String types = "";

        for (int i = 0, ii = getAttributeCount(); i < ii; i++) {
            types += get(i);

            if (i < ii) {
                types += " , ";
            }
        }

        return "FeatureTypeFactory(" + getClass().getName() + ") [ " + types
        + " ]";
    }

    /**
     * Check to see if this factory contains the given AttributeType. The
     * comparison is done by name.
     *
     * @param type The AttributeType to search for by name.
     *
     * @return <tt>true</tt> if a like-named AttributeType exists,
     *         <tt>false</tt> otherwise.
     */
    public final boolean contains(AttributeType type) {
        for (int i = 0, ii = getAttributeCount(); i < ii; i++) {
            if (get(i).getName().equals(type.getName())) {
                return true;
            }
        }

        return false;
    }

    /**
     * Checks to see if this factory already contains the type.
     *
     * @param type
     *
     * @throws IllegalArgumentException DOCUMENT ME!
     */
    protected void check(AttributeType type) {
        if (contains(type)) {
            throw new IllegalArgumentException("Duplicate AttributeTypes "
                + type);
        }
    }

    protected final Set getBuiltinTypes() {
        if ((builtInTypes == null) && !initialized) {
            builtInTypes = new HashSet();

            try {
                builtInTypes.add(FeatureTypes.newFeatureType(null, "Feature",
                        new URI("http://www.opengis.net/gml"), true));
                initialized = true;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

            addBaseTypes(builtInTypes);
        }

        return builtInTypes;
    }

    protected void addBaseTypes(Set types) {
        // base class hook
    }

    /**
     * DOCUMENT ME!
     *
     */
    protected abstract FeatureType createFeatureType()
        throws SchemaException;

    /**
     * DOCUMENT ME!
     *
     * @param type
     *
     * @throws IllegalArgumentException
     */
    protected abstract void add(AttributeType type)
        throws IllegalArgumentException;

    /**
     * DOCUMENT ME!
     *
     * @param type
     *
     */
    protected abstract AttributeType remove(AttributeType type);

    /**
     * DOCUMENT ME!
     *
     * @param idx
     * @param type
     *
     * @throws ArrayIndexOutOfBoundsException
     * @throws IllegalArgumentException
     */
    protected abstract void add(int idx, AttributeType type)
        throws ArrayIndexOutOfBoundsException, IllegalArgumentException;

    /**
     * DOCUMENT ME!
     *
     * @param idx
     *
     *
     * @throws ArrayIndexOutOfBoundsException
     */
    protected abstract AttributeType remove(int idx)
        throws ArrayIndexOutOfBoundsException;

    /**
     * DOCUMENT ME!
     *
     * @param idx
     *
     *
     * @throws ArrayIndexOutOfBoundsException
     */
    public abstract AttributeType get(int idx)
        throws ArrayIndexOutOfBoundsException;

    /**
     * DOCUMENT ME!
     *
     * @param idx
     * @param type
     *
     *
     * @throws ArrayIndexOutOfBoundsException
     * @throws IllegalArgumentException
     */
    protected abstract AttributeType set(int idx, AttributeType type)
        throws ArrayIndexOutOfBoundsException, IllegalArgumentException;

    /**
     * DOCUMENT ME!
     *
     */
    public abstract int getAttributeCount();

}
