/**
 * 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 2004 (C) Intalio, Inc. All Rights Reserved.
 *
 * $Id$
 */

package org.exolab.castor.xml.schema.reader;

import org.exolab.castor.net.URILocation;
import org.exolab.castor.net.URIResolver;
import org.exolab.castor.xml.AttributeSet;
import org.exolab.castor.xml.Namespaces;
import org.exolab.castor.xml.XMLException;
import org.exolab.castor.xml.schema.Annotation;
import org.exolab.castor.xml.schema.AttributeGroup;
import org.exolab.castor.xml.schema.AttributeGroupDecl;
import org.exolab.castor.xml.schema.ComplexType;
import org.exolab.castor.xml.schema.Group;
import org.exolab.castor.xml.schema.ModelGroup;
import org.exolab.castor.xml.schema.RedefineSchema;
import org.exolab.castor.xml.schema.Schema;
import org.exolab.castor.xml.schema.SchemaContext;
import org.exolab.castor.xml.schema.SchemaException;
import org.exolab.castor.xml.schema.SchemaNames;
import org.exolab.castor.xml.schema.SimpleType;
import org.exolab.castor.xml.schema.XMLType;
import org.xml.sax.Locator;
import org.xml.sax.Parser;

/**
 * The purpose of this class is to read redefined elements in an XML schema. The following xml
 * schema structure can be redefined:
 * <ul>
 * <li>Complextypes</li>
 * <li>Simpletypes</li>
 * <li>AttributeGroup</li>
 * <li>Group</li>
 * </ul>
 * 
 * @author <a href="mailto:blandin@intalio.com">Arnaud Blandin</a>
 * @version $Revision$ $Date: 2006-04-25 15:08:23 -0600 (Tue, 25 Apr 2006) $
 **/
public class RedefineUnmarshaller extends ComponentReader {

  /**
   * The current ComponentReader used to read nested structures
   **/
  private ComponentReader _unmarshaller;

  /**
   * The current branch depth
   **/
  private int _depth = 0;

  /**
   * The parent XML schema
   */
  private Schema _schema;

  /**
   * The imported XML Schema
   */
  private Schema _importedSchema;

  private RedefineSchema _redefineSchema;

  /**
   * The XML Schema imported
   */

  public RedefineUnmarshaller(final SchemaContext schemaContext, final Schema schema,
      final AttributeSet atts, final URIResolver uriResolver, final Locator locator,
      final SchemaUnmarshallerState state) throws XMLException {
    super(schemaContext);
    if (schema == null) {
      String err = SchemaNames.REDEFINE + " must be used with an existing parent XML Schema.";
      throw new SchemaException(err);
    }
    setURIResolver(uriResolver);

    URILocation uri = null;
    // -- Get schemaLocation
    String schemaLocation = atts.getValue(SchemaNames.SCHEMALOCATION_ATTR);
    _schema = schema;

    if (schemaLocation == null) {
      // -- <redefine/> or <redefine> <annotation>(*) </redefine>
      _redefineSchema = new RedefineSchema(schema);
      _schema.addRedefineSchema(_redefineSchema);
      return;
    }


    if (schemaLocation.indexOf("\\") != -1) {
      String err = "'" + schemaLocation + "' is not a valid URI as defined by IETF RFC 2396.";
      err += "The URI mustn't contain '\\'.";
      error(err);
    }

    uri = derive(locator, schemaLocation);
    if (uri != null) {
      schemaLocation = uri.getAbsoluteURI();
    }

    // -- Schema object to hold import schema
    boolean addSchema = false;
    _redefineSchema = schema.getRedefineSchema(schemaLocation);
    Schema importedSchema = null;

    boolean alreadyLoaded = false;

    // -- The schema is not yet loaded
    if (_redefineSchema == null) {
      if (uri instanceof SchemaLocation) {
        importedSchema = ((SchemaLocation) uri).getSchema();
        // -- set the main schema in order to handle
        // -- redefinition at runtime

        // importedSchema.addMainSchema(schema);

        _redefineSchema = new RedefineSchema(schema, importedSchema);
        schema.addRedefineSchema(_redefineSchema);
        alreadyLoaded = true;
      } else {
        importedSchema = new Schema();
        addSchema = true;
      }
    } else {
      // -- check schema location, if different, allow merge
      String tmpLocation = _redefineSchema.getOriginalSchema().getSchemaLocation();
      alreadyLoaded = schemaLocation.equals(tmpLocation);
    }

    state.markAsProcessed(schemaLocation, importedSchema);

    if (alreadyLoaded)
      return;

    // -- Parser Schema
    Parser parser = createParser("import");

    // -- Create Schema object and setup unmarshaller
    SchemaUnmarshaller schemaUnmarshaller = new SchemaUnmarshaller(getSchemaContext(), state);
    schemaUnmarshaller.setURIResolver(getURIResolver());
    schemaUnmarshaller.setSchema(importedSchema);

    // parse schema to be imported
    parseSchema(parser, schemaUnmarshaller, uri, schemaLocation, "import");

    // -- namespace checking
    String namespace = importedSchema.getTargetNamespace();
    if (namespace != null) {
      // -- Make sure targetNamespace is not the same as the
      // -- importing schema, see section 4.2.2 in the
      // -- XML Schema Recommendation
      if (!namespace.equals(schema.getTargetNamespace())) {
        String err =
            "The 'namespace' attribute in the <redefine> element must be the same of the targetNamespace of the global schema.\n"
                + namespace + " is different from:" + schema.getTargetNamespace();
        error(err);
      }
    } else {
      importedSchema.setTargetNamespace(schema.getTargetNamespace());
    }

    // -- set the main schema in order to handle
    // -- redefinition at runtime

    // importedSchema.addMainSchema(schema);

    _importedSchema = importedSchema;
    _redefineSchema = new RedefineSchema(schema, _importedSchema);
    // -- Add schema to list of redefine schemas (if not already present)
    if (addSchema) {
      importedSchema.setSchemaLocation(schemaLocation);
      _schema.addRedefineSchema(_redefineSchema);
    }
  }

  /**
   * Signals the start of an element with the given name.
   *
   * @param name the NCName of the element. It is an error if the name is a QName (ie. contains a
   *        prefix).
   * @param namespace the namespace of the element. This may be null. Note: A null namespace is not
   *        the same as the default namespace unless the default namespace is also null.
   * @param atts the AttributeSet containing the attributes associated with the element.
   * @param nsDecls the namespace declarations being declared for this element. This may be null.
   **/
  public void startElement(String name, String namespace, AttributeSet atts, Namespaces nsDecls)
      throws XMLException {

    // -- Do delegation if necessary
    if (_unmarshaller != null) {
      try {
        _unmarshaller.startElement(name, namespace, atts, nsDecls);
        _depth++;
        return;
      } catch (RuntimeException rtx) {
        error(rtx);
      }
    }

    if (name.equals(SchemaNames.ANNOTATION)) {
      _unmarshaller = new AnnotationUnmarshaller(getSchemaContext(), atts);
    } else if (name.equals(SchemaNames.ATTRIBUTE_GROUP)) {
      _unmarshaller = new AttributeGroupUnmarshaller(getSchemaContext(), _schema, atts);
    } else if (name.equals(SchemaNames.COMPLEX_TYPE)) {
      _unmarshaller = new ComplexTypeUnmarshaller(getSchemaContext(), _schema, atts);
    } else if (name.equals(SchemaNames.SIMPLE_TYPE)) {
      _unmarshaller = new SimpleTypeUnmarshaller(getSchemaContext(), _schema, atts);
    } else if (name.equals(SchemaNames.GROUP)) {
      _unmarshaller = new ModelGroupUnmarshaller(getSchemaContext(), _schema, atts);
    } else {
      String err = "<" + name + "> elements cannot be used in a redefine.";
      error(err);
    }

    _unmarshaller.setDocumentLocator(getDocumentLocator());

  }

  /**
   * Signals to end of the element with the given name.
   *
   * @param name the NCName of the element. It is an error if the name is a QName (ie. contains a
   *        prefix).
   * @param namespace the namespace of the element.
   **/
  public void endElement(String name, String namespace) throws XMLException {

    // -- Do delegation if necessary
    if ((_unmarshaller != null) && (_depth > 0)) {
      _unmarshaller.endElement(name, namespace);
      --_depth;
      return;
    }

    // -- use internal JVM String
    name = name.intern();

    // -- check for name mismatches
    if ((_unmarshaller != null)) {
      if (!name.equals(_unmarshaller.elementName())) {
        String err = "error: missing end element for ";
        err += _unmarshaller.elementName();
        error(err);
      }
    } else {
      String err = "error: missing start element for " + name;
      throw new SchemaException(err);
    }

    // -- call unmarshaller.finish() to perform any necessary cleanup
    _unmarshaller.finish();

    if (name.equals(SchemaNames.ANNOTATION)) {
      _redefineSchema.addAnnotation((Annotation) _unmarshaller.getObject());
    } else if (name.equals(SchemaNames.ATTRIBUTE_GROUP)) {
      endElementForAttributeGroup();
    } else if (name.equals(SchemaNames.COMPLEX_TYPE)) {
      endElementForComplexType();
    } else if (name.equals(SchemaNames.SIMPLE_TYPE)) {
      endElementForSimpleType();
    } else if (name.equals(SchemaNames.GROUP)) {
      endElementForGroup();
    } else {
      String err =
          "In a <redefine>, only complexTypes|simpleTypes|groups or attributeGroups can be redefined.";
      error(err);
    }

    _unmarshaller = null;
  }

  private void endElementForGroup() throws XMLException, SchemaException {
    addErrorIfNoSchemaLocationDefinedUponRedefine();

    ModelGroup group = (((ModelGroupUnmarshaller) _unmarshaller).getGroup());

    String structureName = group.getName();
    addErrorIfRedefinedTypeDoesNotHaveAName(structureName, "group");

    // 1-- the group must exist in the imported schema
    Group original = _importedSchema.getModelGroup(structureName);
    if (original == null) {
      String err =
          "When redefining a group, the group must be present in the imported XML schema.\n"
              + "Group: " + structureName + " is not defined in XML Schema:"
              + _importedSchema.getSchemaLocation();
      error(err);
    }

    // -- code needs to be added to check the Particle Valid (Restriction) constraint
    // --TBD

    group.setRedefined();
    _redefineSchema.addGroup(group);
  }

  private void endElementForSimpleType() throws XMLException, SchemaException {
    addErrorIfNoSchemaLocationDefinedUponRedefine();

    SimpleType simpleType = ((SimpleTypeUnmarshaller) _unmarshaller).getSimpleType();
    
    String structureName = simpleType.getName();
    addErrorIfRedefinedTypeDoesNotHaveAName(structureName, "simpleType");

    // 1-- the simpleType must exist in the imported schema
    SimpleType original =
        _importedSchema.getSimpleType(structureName, _schema.getTargetNamespace());
    if (original == null) {
      String err =
          "When redefining a simpleType, the simpleType must be present in the imported XML schema.\n"
              + "SimpleType: " + structureName + " is not defined in XML Schema:"
              + _importedSchema.getSchemaLocation();
      error(err);
    }

    // 2-- the base type must be itself
    XMLType baseType = simpleType.getBaseType();
    // --just check the names since a top level complexType can only be defined once.
    if (!baseType.getName().equals(structureName)) {
      String err =
          "When redefining a simpleType, the simpleType must use itself as the base type definition.\n"
              + "SimpleType: " + structureName + " uses:" + baseType.getName()
              + " as its base type.";
      error(err);
    }

    simpleType.setRedefined();
    _redefineSchema.addSimpleType(simpleType);
    getResolver().addResolvable(simpleType.getReferenceId(), simpleType);
  }

  private void endElementForComplexType() throws XMLException, SchemaException {
    addErrorIfNoSchemaLocationDefinedUponRedefine();
    ComplexType complexType = ((ComplexTypeUnmarshaller) _unmarshaller).getComplexType();
    
    // -- Checks that the complexType exists in the imported schema
    String structureName = complexType.getName();
    addErrorIfRedefinedTypeDoesNotHaveAName(structureName, "complexType");

    // 1-- the complexType must exist in the imported schema
    ComplexType original = _importedSchema.getComplexType(structureName);
    if (original == null) {
      String err =
          "When redefining a complexType, the complexType must be present in the imported XML schema.\n"
              + "ComplexType: " + structureName + " is not defined in XML Schema:"
              + _importedSchema.getSchemaLocation();
      error(err);
    }

    // 2-- the base type must be itself
    XMLType baseType = complexType.getBaseType();
    // --just check the names since a top level complexType can only be defined once.
    if (baseType == null || !baseType.getName().equals(structureName)) {
      String err =
          "When redefining a complexType, the complexType must use itself as the base type definition.\n"
              + "ComplexType: " + structureName + " uses:" + baseType + " as its base type.";
      error(err);
    }

    complexType.setRedefined();
    _redefineSchema.addComplexType(complexType);
    getResolver().addResolvable(complexType.getReferenceId(), complexType);
  }

  private void endElementForAttributeGroup() throws XMLException, SchemaException {
    addErrorIfNoSchemaLocationDefinedUponRedefine();

    AttributeGroupDecl group =
        (AttributeGroupDecl) (((AttributeGroupUnmarshaller) _unmarshaller).getAttributeGroup());

    String structureName = group.getName();
    addErrorIfRedefinedTypeDoesNotHaveAName(structureName, "attribute group");

    // 1-- the attributeGroup must exist in the imported schema
    AttributeGroup original = _importedSchema.getAttributeGroup(structureName);
    if (original == null) {
      String err =
          "When redefining an AttributeGroup, the AttributeGroup must be present in the imported XML schema.\n"
              + "AttributeGroup: " + structureName + " is not defined in XML Schema:"
              + _importedSchema.getSchemaLocation();
      error(err);
    }

    // -- TODO: add code to check the Derivation Valid (Restriction, Complex) constraint.
    group.setRedefined();
    _redefineSchema.addAttributeGroup(group);
  }

  public void characters(char[] ch, int start, int length) throws XMLException {
    // -- Do delagation if necessary
    if (_unmarshaller != null) {
      _unmarshaller.characters(ch, start, length);
    }
  }

  /**
   * Checks that the redefined schema has a 'schemaLocation' attribute set.
   * @throws XMLException
   */
  private void addErrorIfNoSchemaLocationDefinedUponRedefine() throws XMLException {
    if (_redefineSchema.getSchemaLocation() == "") {
      String err =
          "In a <redefine>, only annotations can be defined when no -schemaLocation- is specified.";
      error(err);
    }
  }
  
  /**
   * Checks that the given type exists in the imported schema; if not, add an error
   * @param structureName Name of the structure to check
   * @param typeName Name of enclosing type
   * @throws XMLException 
   */
  private void addErrorIfRedefinedTypeDoesNotHaveAName(String structureName, String typeName) throws XMLException {
    // -- Checks that the given type exists in the imported schema
    if (structureName == null) {
      String err = "When redefining a " + typeName + ", the " + typeName + " must have a name.\n";
      error(err);
    }
  }

  /**
   * Sets the name of the element that this UnknownUnmarshaller handles
   **/
  public String elementName() {
    return SchemaNames.REDEFINE;
  }

  /**
   * Returns the Object created by this ComponentReader
   * 
   * @return the Object created by this ComponentReader
   **/
  public Object getObject() {
    return _redefineSchema;
  }

}
