package org.geotools.feature.iso;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Logger;

import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.AttributeType;
import org.opengis.feature.type.ComplexType;
import org.opengis.feature.type.Name;

/**
 * Helper methods for dealing with Descriptor.
 * <p>
 * This methods opperate directly on the interfaces provided by geoapi, no
 * actual classes were harmed in the making of these utility methods.
 * </p>
 * 
 * @author Jody Garnett
 * @author Justin Deoliveira
 */
public class Descriptors {
    private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger(Descriptors.class.getPackage().getName());

    // /**
    // * Handle subtyping in a "sensible" manner.
    // * <p>
    // * We explored using the XMLSchema of extention and restriction, and have
    // * instead opted for the traditional Java lanaguage notion of an override.
    // * <p>
    // * The concept of an overrided allows both:
    // * <ul>
    // * <li>extention - completly new attribtues are tacked on the "end" of the
    // * list
    // * <li>restriction - attribute with the same qname are used to specify
    // * additional (or replace) information provided by the parent.
    // * </ol>
    // * Note - even <b>removal</b> ( a complicated (and silly) use of
    // * restriction in XMLSchema) is supported. To remove simply override an
    // * attribute mentioned by the parent with multiplicity 0:0.
    // * </p>
    // *
    // * @param parent
    // * @param subtype
    // * @return Descriptor resulting by extending the provided schema
    // (collisions
    // * on qname are treated as overrides)
    // */
    // public ComplexType subtype(ComplexType parent, ComplexType extend) {
    // /*
    // * if( schema instanceof AllDescriptor && subtype instanceof
    // * AllDescriptor ){ return subtype( (AllDescriptor) schema,
    // * (AllDescriptor) extend); } else if( schema instanceof
    // * ChoiceDescriptor && extend instanceof ChoiceDescriptor ){ return
    // * subtype( (ChoiceDescriptor) schema, (ChoiceDescriptor) extend); }
    // * else if( schema instanceof OrderedDescriptor && extend instanceof
    // * OrderedDescriptor ){ return subtype( (OrderedDescriptor) schema,
    // * (OrderedDescriptor) extend); } else { List<Descriptor> all = new
    // * ArrayList<Descriptor>(); all.add( schema ); all.add( extend );
    // * return factory.ordered( all, 1, 1 ); }
    // */
    // try {
    // return restriction(parent, extend);
    // }
    // catch(IllegalArgumentException structsDontMatch){
    // return extension(parent, extend);
    // }
    // }
    //
    // public ComplexType subtype(
    // ComplexType parent, Collection/*<AttributeDescriptor>*/ schema
    // ) {
    // try {
    // return restriction(parent, schema);
    // }
    // catch(IllegalArgumentException structsDontMatch){
    // return extension(parent,schema);
    // }
    // }

    /**
     * Restriction only works on exact structure match.
     * <p>
     * This is the way XMLSchema handles it ...
     * </p>
     * 
     * @param schema
     * @param sub
     * @return
     */
    // @SuppressWarnings("unchecked")
    // public ComplexType restriction(ComplexType parent,ComplexType restrict) {
    //		
    // ComplexType type = null;
    //		
    // if (
    // parent instanceof ChoiceType && restrict instanceof ChoiceType
    // ) {
    //			
    // Set choices = (Set) restriction(
    // ((ChoiceType)parent).getAttributes(),
    // ((ChoiceType)restrict).getAttributes(),
    // new HashSet()
    // );
    //			
    // type = xmlFactory.createChoiceType(choices);
    // }
    // else if (parent instanceof SequenceType && restrict instanceof
    // SequenceType) {
    // List sequence = (List) restriction(
    // ((SequenceType)parent).getAttributes(),
    // ((SequenceType)restrict).getAttributes(),
    // new ArrayList()
    // );
    //			
    // type = xmlFactory.createSequenceType(sequence);
    // }
    // else if (
    // parent instanceof ComplexType && restrict instanceof ComplexType
    // ){
    // List elements = (List) restriction(
    // ((ComplexType)parent).getAttributes(),
    // ((ComplexType)restrict).getAttributes(),
    // new ArrayList()
    // );
    //			
    // ComplexType ct = (ComplexType) restrict;
    // type = xmlFactory.createType(
    // ct.getName(),elements,ct.isIdentified(),ct.isNillable().booleanValue(),
    // ct.getRestrictions(),ct,ct.isAbstract());
    //			
    // }
    // else {
    // throw new IllegalArgumentException("Cannot restrict provided schema");
    // }
    //		
    // return type;
    //		
    // }
    // public ComplexType restriction(ComplexType parent,
    // Collection/*<AttributeDescriptor>*/ schema) {
    // ComplexType type = null;
    //		
    // if (parent instanceof ChoiceType) {
    // Set choices = (Set) restriction(
    // ((ChoiceType)parent).getAttributes(),schema,new HashSet()
    // );
    // type = xmlFactory.createChoiceType(choices);
    // }
    // else if (parent instanceof SequenceType) {
    // List sequence = (List) restriction(
    // ((SequenceType)parent).getAttributes(), schema, new ArrayList()
    // );
    //			
    // type = xmlFactory.createSequenceType(sequence);
    // }
    // else if (parent instanceof ComplexType){
    // List elements = (List) restriction(
    // ((ComplexType)parent).getAttributes(),schema, new ArrayList()
    // );
    //			
    // //duplicate parent type with new schema
    // //JD: this is a bit of a hack, creating type manually (ie wihtout
    // // factory, because we have constructed the schema manually
    //		
    // ComplexType ct = (ComplexType) parent;
    // type = new ComplexTypeImpl(
    // ct.getName(),elements,ct.isIdentified(),
    // ct.isNillable().booleanValue(),ct.getRestrictions(),ct,
    // ct.isAbstract()
    // );
    // type = xmlFactory.createType(
    // ct.getName(),elements,ct.isIdentified(),ct.isNillable().booleanValue(),
    // ct.getRestrictions(),null,ct.isAbstract());
    // }
    // else {
    // throw new IllegalArgumentException("Cannot restrict provided schema");
    // }
    //		
    // return type;
    // }
    //	
    // public ComplexType extension(
    // ComplexType parent, Collection/*<AttributeDescriptor>*/ schema
    // ) {
    //		
    // //create a dummy type for the schema
    // ComplexType type = null;
    //		
    // if (parent instanceof ChoiceType) {
    //			
    // Set choices = new HashSet();
    // choices.addAll(((ChoiceType)parent).getAttributes());
    // choices.addAll(schema);
    //			
    // type = xmlFactory.createChoiceType(choices);
    // }
    // else if (parent instanceof SequenceType) {
    //			
    // List sequence = new ArrayList();
    //			
    // sequence.addAll(((SequenceType)parent).getAttributes());
    // sequence.addAll(schema);
    //			
    // type = xmlFactory.createSequenceType(sequence);
    // }
    // else if (parent instanceof ComplexType){
    // List elements = new ArrayList();
    // elements.addAll(((ComplexType)parent).getAttributes());
    // elements.addAll(schema);
    //			
    // //JD: fix this, passing in null here to avoid recalling this method
    // // this method needs to be factored out somewhere else
    // ComplexType ct = (ComplexType) parent;
    // type = xmlFactory.createType(
    // ct.getName(),elements,ct.isIdentified(),ct.isNillable().booleanValue(),
    // ct.getRestrictions(),null,ct.isAbstract());
    // }
    //		
    // return type;
    // }
    //	
    //	
    // /**
    // * Extending a schema.
    // * <p>
    // * Since we will be creating a new Descriptor we need the factory.
    // */
    // public ComplexType extension(ComplexType parent, ComplexType extend) {
    //		
    // ComplexType type = null;
    //		
    // if (parent instanceof ChoiceType && extend instanceof ChoiceType) {
    //			
    // Set choices = new HashSet();
    // choices.addAll(((ChoiceType)parent).getAttributes());
    // choices.addAll(((ChoiceType)extend).getAttributes());
    //			
    // type = xmlFactory.createChoiceType(choices);
    // }
    // else if (
    // parent instanceof SequenceType && extend instanceof SequenceType
    // ) {
    //			
    // List sequence = new ArrayList();
    //			
    // sequence.addAll(((SequenceType)parent).getAttributes());
    // sequence.addAll(((SequenceType)extend).getAttributes());
    //			
    // type = xmlFactory.createSequenceType(sequence);
    // }
    // else if (
    // parent instanceof ComplexType && extend instanceof ComplexType
    // ){
    // List elements = new ArrayList();
    // elements.addAll(((ComplexType)parent).getAttributes());
    // elements.addAll(((ComplexType)extend).getAttributes());
    //			
    // ComplexType ct = (ComplexType) extend;
    //			
    // //JD: this is a bit of a hack, creating type manually (ie wihtout
    // // factory, because we have constructed the schema manually
    // type = new ComplexTypeImpl(
    // ct.getName(),elements,ct.isIdentified(),
    // ct.isNillable().booleanValue(),ct.getRestrictions(),ct,
    // ct.isAbstract()
    // );
    //			
    // }
    //		
    // return type;
    //		
    // }
    /**
     * We can only restrict node if the restricftion is a subtype that used by
     * node.
     * 
     * @param node
     * @param restrict
     * @return restrict, iff restrict.getType() ISA node.getType()
     */
    AttributeDescriptor restrict(AttributeDescriptor node, AttributeDescriptor restrict) {

        if (node.type() == restrict.type()) {
            return restrict;
        }
        for (AttributeType/* <?> */type = (AttributeType) restrict.type(); type != null; type = type.getSuper()) {
            if (node.type().equals(type)) {
                return restrict;
            }
        }
        throw new IllegalArgumentException("Cannot restrict provided schema");
    }

    Collection restriction(Collection schema, Collection restrict, Collection restriction) {

        if (schema.size() != restrict.size()) {
            throw new IllegalArgumentException(
                    "You must provide an exact structure match in order to implement restriction");
        }

        Iterator i = schema.iterator();
        Iterator j = restrict.iterator();
        while (i.hasNext() && j.hasNext()) {
            restriction
                    .add(restrict((AttributeDescriptor) i.next(), (AttributeDescriptor) j.next()));
        }
        return restriction;
    }

    /**
     * Locate type associated with provided name, or null if not found.
     * <p>
     * Namespaces are not taken in count, so if two properties share the same
     * local name, the first one that matches will be returned.
     * </p>
     * 
     * @param schema
     * @param name
     * @return
     */
    static public AttributeType type(Collection schema, Name name) {
        AttributeDescriptor node = node(schema, name);
        if (node != null)
            return (AttributeType) node.type();
        return null;
    }

    /**
     * Locate type associated with provided name, or null if not found.
     * <p>
     * Namespaces are not taken in count, so if two properties share the same
     * local name, the first one that matches will be returned.
     * </p>
     * 
     * @param schema
     * @param name
     * @return
     */
    static public AttributeType type(ComplexType schema, String name) {
        return type(schema, Types.typeName(name));
    }

    /**
     * Locate type associated with provided name, or null if not found.
     * 
     * @param schema
     * @param name
     * @return
     */
    static public AttributeType type(ComplexType schema, Name name) {
        AttributeDescriptor node = node(schema, name);
        if (node != null)
            return (AttributeType) node.type();
        return null;
    }

    /**
     * Finds the first node associated with the provided name disregarding
     * namespaces
     * 
     * @param schema
     * @param name
     * @return
     */
    static public AttributeDescriptor node(ComplexType schema, String name) {
        // return node(schema,new org.geotools.feature.Name(name));

        for (Iterator itr = list(schema).iterator(); itr.hasNext();) {
            AttributeDescriptor node = (AttributeDescriptor) itr.next();

            if (node.getName() == null) {
                // this may be due to old api usage style, where
                // only types had names
                LOGGER.warning("node has no name set, try to fix! " + node);
                if (node.type().getName().getLocalPart().equals(name)) {
                    return node;
                }
            } else {
                // this is the correct usage
                if (node.getName().getLocalPart().equals(name)) {
                    return node;
                }
            }

        }
        AttributeType superType = schema.getSuper();
        if (superType instanceof ComplexType) {
            return node((ComplexType) superType, name);
        }
        return null;
    }

    // static public List nodes(Attribute schema) {
    // List nodes = new ArrayList();
    //		
    // for (Iterator itr = list(schema).iterator(); itr.hasNext();) {
    // Descriptor child = (Descriptor)itr.next();
    // if (child instanceof AttributeDescriptor) {
    // AttributeDescriptor node = (AttributeDescriptor) child;
    // nodes.add(node);
    // }
    // }
    // return nodes;
    // }

    /**
     * Finds the node associated with the provided name.
     * 
     * @param schema
     * @param name
     * @return AttributeDescriptor assoicated with provided name, or null if not
     *         found.
     */
    static public AttributeDescriptor node(ComplexType schema, Name name) {
        return node(list(schema), name);
    }

    /**
     * Finds the node associated with the provided name.
     * 
     * @param schema
     * @param name
     * @return AttributeDescriptor assoicated with provided name, or null if not
     *         found.
     */
    static public AttributeDescriptor node(Collection schema, Name name) {
        for (Iterator itr = schema.iterator(); itr.hasNext();) {

            AttributeDescriptor node = (AttributeDescriptor) itr.next();

            Name nodeName = node.getName();
            if (nodeName == null) {
                // this may be due to old api usage style, where
                // only types had names
                LOGGER.warning("node has no name set, try to fix! " + node);
                Name name2 = node.type().getName();
                if (null == name.getNamespaceURI()) {
                    if (name.getLocalPart().equals(nodeName.getLocalPart())) {
                        return node;
                    }
                } else if (nodeName.getNamespaceURI().equals(name.getNamespaceURI())
                        && nodeName.getLocalPart().equals(name.getLocalPart())) {
                    return node;
                }
            } else {
                // this is the correct usage
                if (name.getNamespaceURI() != null) {
                    if (name.getLocalPart().equals(nodeName.getLocalPart())) {
                        return node;
                    }
                } else if (name.equals(nodeName)) {
                    return node;
                }
            }

        }
        return null;
    }

    /**
     * Finds the node associated with the provided type.
     * <p>
     * Note a type may be included in more then one node, in which case this
     * will only find the first one.
     * </p>
     * 
     * @param schema
     * @param type
     * @return AttributeDescriptor assoicated with provided name, or null if not
     *         found.
     */
    static public AttributeDescriptor node(ComplexType schema, AttributeType type) {
        for (Iterator itr = list(schema).iterator(); itr.hasNext();) {
            AttributeDescriptor node = (AttributeDescriptor) itr.next();
            if (node.type() == type) {
                return node;
            }
        }
        return null;
    }

    /**
     * List of nodes matching AttributeType.
     * 
     * @param schema
     * @param type
     * @return List of nodes for the provided type, or empty.
     */
    static public List/* <AttributeDescriptor> */nodes(ComplexType schema, AttributeType type) {
        List/* <AttributeDescriptor> */nodes = new ArrayList/* <AttributeDescriptor> */();
        for (Iterator itr = list(schema).iterator(); itr.hasNext();) {
            AttributeDescriptor node = (AttributeDescriptor) itr.next();
            if (node.type().equals(type)) {
                nodes.add(node);
            }
        }
        return nodes;
    }

    /**
     * List of types described by this schema.
     * <p>
     * On the cases where order matters, the returned list preserves the order
     * of descriptors declared in <code>schema</code>
     * </p>
     * 
     * @param type
     * @return List of nodes for the provided type, or empty.
     */
    static public List/* <AttributeType> */types(AttributeType type) {
        List/* <AttributeType> */types = new ArrayList/* <AttributeType> */();
        for (Iterator itr = list(type).iterator(); itr.hasNext();) {
            AttributeDescriptor node = (AttributeDescriptor) itr.next();
            types.add(node.type());
        }
        return types;
    }

    /**
     * True if there may be more then one AttributeType in the schema.
     * <p>
     * This may happen if:
     * <ul>
     * <li>The AttributeType is referenced by more then one node.
     * <li>The node referencing the type has multiplicy greater then 1
     * </ul>
     * 
     * @param schema
     * @param type
     * @return
     */
    public static boolean multiple(ComplexType schema, AttributeType type) {
        // return maxOccurs( schema, type ) != 1;
        return maxOccurs(schema, type) > 1;
    }

    public static int maxOccurs(ComplexType schema, AttributeType type) {
        List/* <AttributeDescriptor> */nodes = nodes(schema, type);
        if (nodes.isEmpty())
            return 0;

        int max = 0;
        for (Iterator itr = nodes.iterator(); itr.hasNext();) {
            AttributeDescriptor node = (AttributeDescriptor) itr.next();
            if (max == Integer.MAX_VALUE) {
                return Integer.MAX_VALUE;
            }
            max += node.getMaxOccurs();
        }
        return max;
    }

    /**
     * Returns the list of descriptors defined in the provided schema,
     * preserving declaration order when relevant.
     * 
     * @param schema
     * @return
     */
    // @SuppressWarnings("unchecked")
    static public List/* <? extends Descriptor> */list(AttributeType type) {

        ArrayList list = new ArrayList();

        if (type instanceof ComplexType) {
            list = new ArrayList(((ComplexType) type).attributes());
        }

        return list;

        // if (schema instanceof OrderedDescriptor) {
        // return ((OrderedDescriptor) schema).sequence();
        // } else if (schema instanceof AllDescriptor) {
        // return new ArrayList/*<AttributeDescriptor>*/(((AllDescriptor)
        // schema)
        // .all());
        // } else if (schema instanceof ChoiceDescriptor) {
        // return new ArrayList/*<Descriptor>*/(((ChoiceDescriptor) schema)
        // .options());
        // }
        //		
        // return Collections.EMPTY_LIST;

    }

    /**
     * Determines if a collection of attribute descriptors is "simple".
     * 
     * @param schema
     *            Collection of attribute descriptors.
     * 
     * @return True if schema is simple, otherwise false.
     */
    public static boolean isSimple(Collection/* <AttributeDescriptor> */schema) {
        for (Iterator itr = schema.iterator(); itr.hasNext();) {
            AttributeDescriptor d = (AttributeDescriptor) itr.next();
            if (d.getMinOccurs() != 1 || d.getMaxOccurs() != 1) {
                return false;
            }
            if (d.type() instanceof ComplexType) {
                return false;
            }
        }

        return true;
    }
}
