/**
 * Redistribution and use of this software and associated documentation ("Software"), with or
 * without modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright statements and notices. Redistributions
 * must also contain a copy of this document.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
 * conditions and the following disclaimer in the documentation and/or other materials provided with
 * the distribution.
 *
 * 3. The name "Exolab" must not be used to endorse or promote products derived from this Software
 * without prior written permission of Intalio, Inc. For written permission, please contact
 * info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab" nor may "Exolab" appear in
 * their names without prior written permission of Intalio, Inc. Exolab is a registered trademark of
 * Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESSED OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INTALIO, INC. OR ITS
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
 * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Copyright 1999-2003 (C) Intalio Inc. All Rights Reserved.
 *
 * $Id$
 */

package org.exolab.castor.xml.schema;

import java.util.Enumeration;
import java.util.Iterator;
import java.util.Vector;

import org.exolab.castor.xml.ValidationException;
import org.exolab.castor.xml.validators.ValidationUtils;

/**
 * An XML Schema ElementDecl
 * 
 * @author <a href="mailto:kvisco@intalio.com">Keith Visco</a>
 * @version $Revision$ $Date: 2006-04-25 15:08:23 -0600 (Tue, 25 Apr 2006) $
 **/
public class ElementDecl extends Particle implements Referable {
  /** SerialVersionUID */
  private static final long serialVersionUID = -7804351635137964219L;

  // -------------------/
  // - Class Variables -/
  // -------------------/

  /**
   * Error message for a null argument
   **/
  private static String NULL_ARGUMENT =
      "A null argument was passed to the constructor of " + ElementDecl.class.getName();


  // --------------------/
  // - Member Variables -/
  // --------------------/

  /**
   * The block attribute for this element definition.
   **/
  private BlockList _block = null;

  /**
   * collection of Identity Constraints
   **/
  private final Vector<IdentityConstraint> _constraints = new Vector<>(3);

  /**
   * The default value for this element definition. Only useful for simpleContent.
   **/
  private String _default = null;

  /**
   * The name of a reference to a top-level element declaration
   **/
  private String _elementRefName = null;

  /**
   * The top-level element declaration this element reference points to
   **/
  private ElementDecl _referencedElement = null;

  /**
   * The final value for this element definition.
   **/
  private FinalList _final = null;

  /**
   * The fixed value for this element definition. Only used for simpleContent.
   **/
  private String _fixed = null;

  /**
   * The form type for this element definition. Specifies whether names should be qualified or
   * unqualified. Uses the default Form from the parent Schema if unspecified.
   **/
  private Form _form = null;

  /**
   * The unique ID for this element definition (optional).
   **/
  private String _id = null;

  /**
   * Flag indicating whether or not this Element declaration is abstract
   **/
  private boolean _isAbstract = false;

  /**
   * The element name
   **/
  private String _name = null;

  /**
   * Flag indicating whether or not the element value may be null.
   **/
  private boolean _nillable = false;

  /**
   * The parent for this ElementDecl
   **/
  private Structure _parent = null;

  /**
   * The parent schema that this element declaration belongs to
   **/
  private Schema _schema = null;

  /**
   * The substitutionGroup for this element definition.
   **/
  private String _substitutionGroup = null;

  /**
   * The XMLType for this element declaration
   **/
  private XMLType _xmlType = null;

  /**
   * Creates a new default element definition
   * 
   * @param schema the XML Schema to which this element declaration belongs <BR />
   *        This element definition will not be valid until a name has been set
   **/
  public ElementDecl(Schema schema) {
    this(schema, null);
  } // -- ElementDecl

  /**
   * Creates a new default element definition
   * 
   * @param schema the XML Schema to which this Element Declaration belongs
   * @param name the name of the Element being declared
   **/
  public ElementDecl(Schema schema, String name) {
    super();
    setName(name);
    if (schema == null) {
      String err = NULL_ARGUMENT + "; 'schema' must not be null.";
      throw new IllegalArgumentException(err);
    }
    setSchema(schema);
  } // -- ElementDecl

  /**
   * Adds the given IdentityConstraint to this element definition.
   *
   * @param constraint the IdentityConstraint to add.
   **/
  public void addIdentityConstraint(IdentityConstraint constraint) {
    if (constraint == null)
      return;
    _constraints.add(constraint);
  } // -- addIdentityConstraint

  /**
   * Returns the value of the 'block' attribute for this element
   *
   * @return the value of the block attribute.
   **/
  public BlockList getBlock() {
    return _block;
  } // -- getBlock

  /**
   * Returns the default value of this element definition.
   *
   * @return the default value of this element definition, or null if no default was specified.
   **/
  public String getDefaultValue() {
    if (isReference()) {
      ElementDecl elem = getReference();
      if (elem != null)
        return elem.getDefaultValue();
    }
    return _default;
  } // -- getDefaultValue

  /**
   * Returns the value of the 'final' attribute for this element definition.
   *
   * @return the FinalList for this element definition.
   **/
  public FinalList getFinal() {
    return _final;
  } // -- getFinal

  /**
   * Returns the fixed value of this element definition.
   *
   * @return the fixed value of this element definition, or null if no default was specified.
   */
  public String getFixedValue() {
    if (isReference()) {
      ElementDecl elem = getReference();
      if (elem != null)
        return elem.getFixedValue();
    }
    return _fixed;
  } // -- getFixedValue

  /**
   * Returns the Form for this element definition. The Form object species whether or not names are
   * qualified or unqualified in the scope of this element definition. If null, the Form should be
   * obtained from the parent Schema.
   *
   * @return the Form for this element definition, or null if not set.
   **/
  public Form getForm() {
    return _form;
  } // -- getForm

  /**
   * Returns the 'id' for this element definition.
   *
   * @return the 'id' for this element definition.
   **/
  public String getId() {
    return _id;
  } // -- getId

  /**
   * Returns an Enumeration of IdentityConstraint objects contained within this element definition.
   *
   * @return an Enumeration of IdentityConstraint objects contained within this element definition.
   **/
  public Enumeration<IdentityConstraint> getIdentityConstraints() {
    return _constraints.elements();
  } // -- getIdentityConstraints

  /**
   * Returns the name of this Element declaration. The name of the referenced element is returned if
   * the 'ref' attribute was used. The name returned will be an NCName (no namespace prefix will be
   * included with the name).
   *
   * @return the name of this element declaration
   */
  public String getName() {
    return getName(false);
  } // -- getName

  /**
   * Returns the name of this Element declaration. The name returned, if not null, will be an
   * NCName.
   *
   * @param ignoreRef if false the name of the referenced element (if specified) is returned,
   *        otherwise the localname (may be null).
   *
   * @return the name of this element declaration
   **/
  public String getName(boolean ignoreRef) {
    if (isReference() && ignoreRef == false) {
      String localName = _elementRefName;
      // -- check for namespace prefix
      int idx = localName.indexOf(':');
      if (idx > 0) {
        localName = localName.substring(idx + 1);
      }
      return localName;
    }
    return _name;
  } // -- getName

  /**
   * Returns the parent of this ElementDecl, this value may be null if no parent has been set.
   *
   * @return the parent Structure of this ElementDecl
   **/
  public Structure getParent() {
    return _parent;
  } // -- getParent

  /**
   * Returns the XMLType (ComplexType or SimpleType) of this ElementDecl.
   * 
   * @return the XMLType of this ElementDecl
   **/
  public XMLType getType() {

    XMLType result = null;

    if (isReference()) {
      ElementDecl element = getReference();
      if (element != null) {
        return element.getType();
      }
      return null;
    }



    if (_xmlType == null)
      return null;
    // 1--Anonymous types
    if (_xmlType.getName() == null)
      return _xmlType.getType();
    // --we look in the parent schema if we have redefinitions of types.
    result = _xmlType.getType();

    // -- the current XML schema might have a MasterSchema where all the
    // -- type definitions have a higher priority [this is useful when
    // -- resolving redefined types for instance].
    if (result != null) {
      Schema tempSchema = result.getSchema().getMasterSchema();
      if (tempSchema != null) {
        XMLType tempType = tempSchema.getType(result.getName());
        if (tempType != null) {
          result = tempType;
        }
      }
    }
    return result;
  } // -- getXMLType

  /**
   * Returns the ElementDecl that this element definition references. This will return null if this
   * element definition does not reference a different element definition.
   * 
   * @return the ElementDecl that this element definition references
   **/
  public ElementDecl getReference() {
    if (_referencedElement != null) {
      return _referencedElement;
    }

    ElementDecl result = null;
    if (_elementRefName != null) {
      result = _schema.getElementDecl(_elementRefName);
      if (result == null) {
        String err = "Unable to find element referenced :\" ";
        err += getName();
        err += "\"";
        throw new IllegalStateException(err);
      }
      _referencedElement = result;
    }
    return result;
  } // -- getReference

  /**
   * Returns the actual reference name of this AttributeDecl, or null if this AttributeDecl is not a
   * reference. The name returned, if not null, will be a QName, possibly containing the namespace
   * prefix.
   * 
   * @return the reference name
   */
  public String getReferenceName() {
    return _elementRefName;
  } // -- getReference

  /**
   * Returns the Id used to Refer to this Object
   *
   * @return the Id used to Refer to this Object
   * @see Referable
   **/
  public String getReferenceId() {
    if (_name != null)
      return "element:" + _name;
    return null;
  } // -- getReferenceId

  /**
   * Returns the XML Schema to which this element declaration belongs.
   * 
   * @return the XML Schema to which this element declaration belongs.
   **/
  public Schema getSchema() {
    return _schema;
  } // -- getSchema

  /**
   * Returns the substitutionGroup for this element declaration, or null if it's absent; if this
   * {@link ElementDecl} instance is a reference to a global element definition, return its
   * substitution group
   *
   * @return the substitutionGroup membership for this element declaration, or null if absent.
   **/
  public String getSubstitutionGroup() {
    if (isReference()) {
      return getReference().getSubstitutionGroup();
    }
    return _substitutionGroup;
  } // -- getSubstitutionGroup

  /**
   * Returns an enumeration of the elements that can be substitute to this element declaration.
   * 
   * @return an enumeration of the elements that can be substitute to this element declaration.
   */
  public Enumeration<ElementDecl> getSubstitutionGroupMembers() {
    Vector<ElementDecl> result = new Vector<>();
    Iterator<ElementDecl> enumeration = _schema.getElementDecls().iterator();
    while (enumeration.hasNext()) {
      ElementDecl temp = enumeration.next();
      String subName = temp.getSubstitutionGroup();
      if (subName != null) {
        // no namespace(s) or default namespace in use
        if (subName.equals(_name)) {
          result.add(temp);
        }
        // namespace(s) incl. prefix in use
        // TODO: find a better way of dealing with a namespace prefix
        else if (subName.endsWith(_name) && subName.indexOf(":") > 0) {
          result.add(temp);
        }
      }
    }
    return result.elements();
  }

  /**
   * Returns true if this element definition is abstract
   * 
   * @return true if this element definition is abstract
   **/
  public boolean isAbstract() {
    if (isReference()) {
      return _referencedElement.isAbstract();
    }
    return _isAbstract;
  } // -- isAbstract

  /**
   * Returns whether or not instances of this element definition may appear with no content.
   *
   * @return true if instances of this element definition may appear with no content, otherwise
   *         false.
   **/
  public boolean isNillable() {
    if (isReference()) {
      return _referencedElement.isNillable();
    }
    return _nillable;
  } // -- isNullable

  /**
   * Returns true if this element definition simply references another element Definition
   * 
   * @return true if this element definition is a reference
   **/
  public boolean isReference() {
    return (_elementRefName != null);
  } // -- isReference

  /**
   * Sets whether or not this element definition is abstract
   * 
   * @param isAbstract a boolean when true indicates that this element definition should be abstract
   **/
  public void setAbstract(boolean isAbstract) {
    _isAbstract = isAbstract;
  } // -- isAbstract

  /**
   * Returns true if this element has children (i.e if it holds attributes or elements).
   * 
   * @return true if this element has children (i.e if it holds attributes or elements).
   */
  public boolean hasChildren() {
    XMLType type = getType();
    if (type instanceof SimpleType)
      return false;

    if (type instanceof ComplexType) {
      // complexContent ->sure to have children
      if (((ComplexType) type).isComplexContent())
        return true;
      // else check for contentModel group
      else if (((ComplexType) type).getParticleCount() != 0)
        return true;
      // else check for attributes
      else {
        java.util.Enumeration temp = ((ComplexType) type).getAttributeDecls();
        return temp.hasMoreElements();
      }
    }

    return false;
  } // -- hasChildren

  /**
   * Removes the given IdentityConstraint from this element definition.
   *
   * @param constraint the IdentityConstraint to remove.
   * @return true if the IdentityConstraint was contained within this element defintion.
   **/
  public boolean removeIdentityConstraint(IdentityConstraint constraint) {
    if (constraint == null)
      return false;
    return _constraints.remove(constraint);
  } // -- removeIdentityConstraint


  /**
   * Sets the value of the 'block' attribute for this element
   *
   * @param block the value of the block attribute for this element definition.
   **/
  public void setBlock(BlockList block) {
    _block = block;
  } // -- setBlock

  /**
   * Sets the value of the 'block' attribute for this element
   *
   * @param block the value of the block attribute for this element definition.
   **/
  public void setBlock(String block) {
    if (block == null)
      _block = null;
    else
      _block = new BlockList(block);
  } // -- setBlock

  /**
   * Sets the default value for this element definition.
   *
   * @param value the default value for this element definition.
   **/
  public void setDefaultValue(String value) {
    this._default = value;
  } // -- setDefaultValue

  /**
   * Sets the value of the 'final' attribute for this element definition.
   *
   * @param finalList the value of the final attribute for this element definition.
   **/
  public void setFinal(FinalList finalList) {
    _final = finalList;
  } // -- setFinal

  /**
   * Sets the value of the 'final' attribute for this element definition.
   *
   * @param finalValue the value of the final attribute for this element definition.
   **/
  public void setFinal(String finalValue) {
    if (finalValue == null)
      _final = null;
    else
      _final = new FinalList(finalValue);
  } // -- setFinal

  /**
   * Sets the fixed value for this element definition.
   *
   * @param value the fixed value for this element definition.
   **/
  public void setFixedValue(String value) {
    this._fixed = value;
  } // -- setDefaultValue

  /**
   * Sets the Form for this element definition. The Form object species whether or not names are
   * qualified or unqualified in the scope of this element definition. If null, the Form is to be
   * obtained from the parent Schema.
   *
   * @param form the Form type for this element definition.
   **/
  public void setForm(Form form) {
    _form = form;
  } // -- setForm

  /**
   * Sets the Id for this element definition.
   *
   * @param id the Id for this element definition.
   **/
  public void setId(String id) {
    _id = id;
  } // -- setId

  /**
   * Sets the name of the element that this Element definition defines.
   *
   * @param name the name of the defined element
   **/
  public void setName(String name) {

    if ((name == null) || (ValidationUtils.isNCName(name))) {
      _name = name;
    } else {
      String err = "error: '" + name + "' is not a valid NCName.";
      throw new IllegalArgumentException(err);
    }
  } // -- setName

  /**
   * Sets whether or not instances of this element definition may contain empty content
   *
   * @param nillable the flag when true indicates that instances of this element definition may
   *        appear with empty content
   **/
  public void setNillable(boolean nillable) {
    _nillable = nillable;
  } // -- setNillable

  /**
   * Sets the parent for this ElementDecl.
   *
   * @param parent the parent Structure for this ElementDecl
   **/
  protected void setParent(Structure parent) {
    if (parent != null) {
      switch (parent.getStructureType()) {
        case Structure.GROUP:
        case Structure.MODELGROUP:
        case Structure.SCHEMA:
          break;
        default:
          String error = "Invalid parent for element.";
          throw new IllegalArgumentException(error);
      }
    }
    _parent = parent;
  } // -- setParent

  /**
   * Sets the reference for this element definition
   * 
   * @param reference the Element definition that this definition references
   **/
  public void setReference(ElementDecl reference) {
    if (reference == null) {
      _elementRefName = null;
      _referencedElement = null;
    } else {
      if (reference.getSchema() == this.getSchema()) {
        _elementRefName = reference.getName();
        _referencedElement = reference;
      } else {
        String qName = reference.getName();
        String nsURI = reference.getSchema().getTargetNamespace();
        if (nsURI != null) {
          String prefix = getSchema().getNamespacePrefix(nsURI);
          if ((prefix != null) && (prefix.length() > 0))
            qName = prefix + ":" + qName;
        }
        _elementRefName = qName;
        _referencedElement = reference;
      }
    }
  } // -- setReference

  /**
   * Sets the name which this element declaration refers to
   * 
   * @param referenceName the name of the element definition that this definition references
   **/
  public void setReferenceName(String referenceName) {
    if ((referenceName == null) || (ValidationUtils.isQName(referenceName))) {
      _elementRefName = referenceName;
    } else {
      String err = "error: '" + referenceName + "' is not a valid QName.";
      throw new IllegalArgumentException(err);
    }
  } // -- setReference

  /**
   * Sets the substitutionGroup for this element definition.
   *
   * @param substitutionGroup the substitutionGroup for this element definition.
   **/
  public void setSubstitutionGroup(String substitutionGroup) {
    _substitutionGroup = substitutionGroup;
  } // -- setSubstitutionGroup

  /**
   * Sets the XMLType for this Element declaration.
   * 
   * @param type the XMLType for this element declaration. <BR />
   *        <B>Note:</B> This method is mutually exclusive with #setTypeReference, if a reference
   *        has previously been set it will be ignored.
   **/
  public void setType(XMLType type) {
    // -- reset parent of current type
    if (_xmlType != null) {
      _xmlType.setParent(null);
    }
    if (type != null) {
      type.setParent(this);
    }

    _xmlType = type;
  } // -- setType


  /**
   * Sets the type of this element to be a reference.
   */
  public void setTypeReference(String name) {
    TypeReference reference = new TypeReference();
    reference.setName(name);
    reference.setSchema(_schema);
    setType(reference);
  }

  // -------------------------------/
  // - Implementation of Structure -/
  // -------------------------------/

  /**
   * Returns the type of this Schema Structure
   * 
   * @return the type of this Schema Structure
   **/
  public short getStructureType() {
    return Structure.ELEMENT;
  } // -- getStructureType

  /**
   * Checks the validity of this element definition.
   *
   * @throws ValidationException when this element definition is invalid.
   **/
  public void validate() throws ValidationException {

    // -- If this element merely references another element definition
    // -- just check that we can resolve the reference
    if (_elementRefName != null) {
      if (_schema.getElementDecl(_elementRefName) == null) {
        String err = "<element ref=\"" + _elementRefName + "\"> " + "is not resolvable.";
        throw new ValidationException(err);
      }
      return;
    }

    if (_name == null) {
      String err = "<element> is missing required 'name' or " + "'ref' attribute.";
      throw new ValidationException(err);
    }

    // --check that the particle information is not present on top level
    // --element

    // --do you really allow parent to be null???
    if (getParent() != null) {
      if (getParent().getStructureType() == Structure.SCHEMA) {
        if (isMinOccursSet()) {
          String err =
              "'minOccurs' declaration is prohibited on top level element '/" + getName() + "'.";
          throw new ValidationException(err);
        }
        if (isMaxOccursSet()) {
          String err =
              "'maxOccurs' declaration is prohibited on top level element '/" + getName() + "'.";
          throw new ValidationException(err);
        }
      }
    }

    // -- If type is anonymous, make sure the type is valid.
    // -- To prevent excess validation, we ONLY validate
    // -- if the type is anonymous, because otherwise
    // -- the Schema itself will validate the type.
    XMLType type = getType();
    if (type != null) {
      if (type.isComplexType()) {
        ComplexType complexType = (ComplexType) type;
        if (!complexType.isTopLevel()) {
          complexType.validate();
        }
      } else if (type.isSimpleType()) {
        SimpleType simpleType = (SimpleType) type;
        if (simpleType.getParent() != simpleType.getSchema()) {
          simpleType.validate();
        }
        // -- print warning message if ID, IDREF, IDREFS, NMTOKEN, NTOKENS are
        // -- used as element type
        int typeCode = simpleType.getTypeCode();
        switch (typeCode) {
          case SimpleTypesFactory.ID_TYPE:
          case SimpleTypesFactory.IDREF_TYPE:
          case SimpleTypesFactory.IDREFS_TYPE:
          case SimpleTypesFactory.NMTOKENS_TYPE:
          case SimpleTypesFactory.NMTOKEN_TYPE:
            String err = "Warning : For XML Compatibility " + simpleType.getName()
                + " should be used only as attributes\n.";
            // --Future versions will log the message
            System.out.println(err);
            break;
          default:
            break;

        }
      }
      // -- anyType
      else {
        // -- nothing to validate anyType is always valid.
      }

    }

  } // -- validate

  /**
   * Sets the XMl schema to where this element has been defined
   * 
   * @param schema The defining XML schema
   */
  private void setSchema(final Schema schema) {
    _schema = schema;
  }

  /**
   * Indicates whether a type is set for this element definiion.
   * 
   * @return True if a type is set.
   */
  public boolean hasXMLType() {
    return (_xmlType != null);
  }


} // -- Element
