/*
 * Copyright 2010 Philipp Erlacher
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package org.exolab.castor.xml;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.StringTokenizer;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.exolab.castor.mapping.FieldHandler;
import org.exolab.castor.mapping.MapItem;
import org.exolab.castor.xml.UnmarshalHandler.ArrayHandler;
import org.exolab.castor.xml.util.XMLFieldDescriptorImpl;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;

/**
 * A processor that assists {@link UnmarshalHandler} in dealing with the SAX 2
 * {@link ContentHandler#endElement(String, String, String)} callback method.
 * 
 * @author <a href=" mailto:philipp.erlacher AT gmail DOT com">Philipp Erlacher</a>
 * @since 1.3.2
 */
public class EndElementProcessor {

  /**
   * Standard logger to use.
   */
  private static final Log LOG = LogFactory.getLog(EndElementProcessor.class);

  /**
   * resource bundle
   */
  protected static ResourceBundle resourceBundle;

  /**
   * Callback {@link UnmarshalHandler} reference to set the actual state on this instance.
   */
  private final UnmarshalHandler _unmarshalHandler;

  static {
    resourceBundle = ResourceBundle.getBundle("UnmarshalHandlerMessages", Locale.getDefault());
  }

  /**
   * Creates an instance of this class, with a reference to the actual {@link UnmarshalHandler} for
   * which this processor deals with the SAX 2 endElement() callback method.
   * 
   * @param unmarshalHandler The {@link UnmarshalHandler} instance on which the results of
   *        processing the endElement method will be 'persisted'/set.
   */
  public EndElementProcessor(final UnmarshalHandler unmarshalHandler) {
    _unmarshalHandler = unmarshalHandler;
  }

  public void compute(String name) throws org.xml.sax.SAXException {
    if (LOG.isTraceEnabled()) {
      String trace = MessageFormat.format(
          resourceBundle.getString("unmarshalHandler.log.trace.endElement"), new Object[] {name});
      LOG.trace(trace);
    }

    // -- If we are skipping elements that have appeared in the XML but for
    // -- which we have no mapping, decrease the ignore depth counter and
    // return
    if (_unmarshalHandler.getStrictElementHandler().skipEndElement()) {
      return;
    }

    // -- Do delagation if necessary
    if (_unmarshalHandler.getAnyNodeHandler().hasAnyUnmarshaller()) {
      _unmarshalHandler.getAnyNodeHandler().endElement(name);
      // we are back to the starting node
      if (_unmarshalHandler.getAnyNodeHandler().isStartingNode()) {
        _unmarshalHandler.setAnyNode(_unmarshalHandler.getAnyNodeHandler().getStartingNode());
      } else
        return;
    }

    if (_unmarshalHandler.getStateStack().isEmpty()) {
      String err = MessageFormat.format(
          resourceBundle.getString("unmarshalHandler.error.missing.startElement"),
          new Object[] {name});
      throw new SAXException(err);
    }

    // -- * Begin Namespace Handling
    // -- XXX Note: This code will change when we update the XML event API

    int idx = name.indexOf(':');
    if (idx >= 0) {
      name = name.substring(idx + 1);
    }
    // -- * End Namespace Handling

    UnmarshalState state = _unmarshalHandler.getStateStack().removeLastState();

    // -- make sure we have the correct closing tag
    XMLFieldDescriptor descriptor = state.getFieldDescriptor();

    if (!state.getElementName().equals(name)) {

      // maybe there is still a container to end
      if (descriptor.isContainer()) {
        _unmarshalHandler.getStateStack().pushState(state);
        // -- check for possible characters added to
        // -- the container's state that should
        // -- really belong to the parent state
        StringBuffer tmpBuffer = null;
        if (state.getBuffer() != null) {
          if (!UnmarshalHandler.isWhitespace(state.getBuffer())) {
            if (state.getClassDescriptor().getContentDescriptor() == null) {
              tmpBuffer = state.getBuffer();
              state.setBuffer(null);
            }
          }
        }
        // -- end container
        _unmarshalHandler.endElement(state.getElementName());

        if (tmpBuffer != null) {
          state = _unmarshalHandler.getStateStack().getLastState();
          if (state.getBuffer() == null)
            state.setBuffer(tmpBuffer);
          else
            state.getBuffer().append(tmpBuffer.toString());
        }
        _unmarshalHandler.endElement(name);
        return;
      }
      String err = MessageFormat.format(
          resourceBundle.getString("unmarshalHandler.error.different.endElement.expected"),
          new Object[] {state.getElementName(), name});
      throw new SAXException(err);
    }

    // -- clean up current Object
    Class<?> type = state.getType();

    if (type == null) {
      if (!state.isWrapper()) {
        // -- this message will only show up if debug
        // -- is turned on...how should we handle this case?
        // -- should it be a fatal error?
        String info = MessageFormat.format(
            resourceBundle.getString("unmarshalHandler.log.info.no.Descriptor.found"),
            new Object[] {state.getElementName()});
        LOG.info(info);
      }

      // -- handle possible location text content
      // -- TODO: cleanup location path support.
      // -- the following code needs to be improved as
      // -- for searching descriptors in this manner can
      // -- be slow
      StringBuffer tmpBuffer = null;
      if (state.getBuffer() != null) {
        if (!UnmarshalHandler.isWhitespace(state.getBuffer())) {
          tmpBuffer = state.getBuffer();
          state.setBuffer(null);
        }
      }
      if (tmpBuffer != null) {
        UnmarshalState targetState = state;
        String locPath = targetState.getElementName();
        while ((targetState = targetState.getParent()) != null) {
          if ((targetState.isWrapper()) || (targetState.getClassDescriptor() == null)) {
            locPath = targetState.getElementName() + "/" + locPath;
            continue;
          }

          XMLFieldDescriptor tmpDesc = targetState.getClassDescriptor().getContentDescriptor();
          if (tmpDesc != null && locPath.equals(tmpDesc.getLocationPath())) {
            if (targetState.getBuffer() == null)
              targetState.setBuffer(tmpBuffer);
            else
              targetState.getBuffer().append(tmpBuffer.toString());
          }
        }
      }

      // -- remove current namespace scoping
      _unmarshalHandler.getNamespaceHandling().removeCurrentNamespaceInstance();
      return;
    }

    // -- check for special cases
    boolean byteArray = false;
    if (type.isArray()) {
      byteArray = (type.getComponentType() == Byte.TYPE);
    }

    // -- If we don't have an instance object and the Class type
    // -- is not a primitive or a byte[] we must simply return
    if ((state.getObject() == null) && (!state.isPrimitiveOrImmutable())) {
      // -- remove current namespace scoping
      _unmarshalHandler.getNamespaceHandling().removeCurrentNamespaceInstance();
      return;
    }

    // / DEBUG System.out.println("end: " + name);

    if (state.isPrimitiveOrImmutable()) {

      String str = null;

      if (state.getBuffer() != null) {
        str = state.getBuffer().toString();
        state.getBuffer().setLength(0);
      }

      if (type == String.class && !((XMLFieldDescriptorImpl) descriptor).isDerivedFromXSList()) {
        if (str != null)
          state.setObject(str);
        else if (state.isNil()) {
          state.setObject(null);
        } else {
          state.setObject("");
        }
      }
      // -- special handling for byte[]
      else if (byteArray && !descriptor.isDerivedFromXSList()) {
        if (str == null)
          state.setObject(new byte[0]);
        else {
          state.setObject(_unmarshalHandler.decodeBinaryData(descriptor, str));
        }
      } else if (state.getConstructorArguments() != null) {
        state.setObject(
            _unmarshalHandler.createInstance(state.getType(), state.getConstructorArguments()));
      } else if (descriptor.isMultivalued() && descriptor.getSchemaType() != null
          && descriptor.getSchemaType().equals("list")
          && ((XMLFieldDescriptorImpl) descriptor).isDerivedFromXSList()) {
        StringTokenizer attrValueTokenizer = new StringTokenizer(str);
        List<Object> primitives = new ArrayList<Object>();
        while (attrValueTokenizer.hasMoreTokens()) {
          String tokenValue = attrValueTokenizer.nextToken();
          if (MarshalFramework.isPrimitive(descriptor.getFieldType())) {
            primitives.add(
                _unmarshalHandler.toPrimitiveObject(type, tokenValue, state.getFieldDescriptor()));
          } else {
            Class<?> valueType = descriptor.getFieldType();
            // -- handle base64/hexBinary
            if (valueType.isArray() && (valueType.getComponentType() == Byte.TYPE)) {
              primitives.add(_unmarshalHandler.decodeBinaryData(descriptor, tokenValue));
            }
          }

        }
        state.setObject(primitives);
      } else {
        if (state.isNil()) {
          state.setObject(null);
        } else {
          state.setObject(
              _unmarshalHandler.toPrimitiveObject(type, str, state.getFieldDescriptor()));
        }
      }
    } else if (ArrayHandler.class.isAssignableFrom(state.getType())) {
      state.setObject(((ArrayHandler) state.getObject()).getObject());
      state.setType(state.getObject().getClass());

    }

    // -- check for character content
    if ((state.getBuffer() != null) && (state.getBuffer().length() > 0)
        && (state.getClassDescriptor() != null)) {
      XMLFieldDescriptor cdesc = state.getClassDescriptor().getContentDescriptor();
      if (cdesc != null) {
        Object value = state.getBuffer().toString();
        if (MarshalFramework.isPrimitive(cdesc.getFieldType()))
          value = _unmarshalHandler.toPrimitiveObject(cdesc.getFieldType(), (String) value,
              state.getFieldDescriptor());
        else {
          Class<?> valueType = cdesc.getFieldType();
          // -- handle base64/hexBinary
          if (valueType.isArray() && (valueType.getComponentType() == Byte.TYPE)) {
            value = _unmarshalHandler.decodeBinaryData(descriptor, (String) value);
          }
        }

        try {
          FieldHandler handler = cdesc.getHandler();
          boolean addObject = true;
          if (_unmarshalHandler.isReuseObjects()) {
            // -- check to see if we need to
            // -- add the object or not
            Object tmp = handler.getValue(state.getObject());
            if (tmp != null) {
              // -- Do not add object if values
              // -- are equal
              addObject = (!tmp.equals(value));
            }
          }
          if (addObject)
            handler.setValue(state.getObject(), value);
        } catch (java.lang.IllegalStateException ise) {
          String err = MessageFormat.format(
              resourceBundle.getString("unmarshalHandler.error.unable.add.text"),
              new Object[] {descriptor.getXMLName(), ise.toString()});
          throw new SAXException(err, ise);
        }
      }
      // -- Handle references
      else if (descriptor.isReference()) {
        UnmarshalState pState = _unmarshalHandler.getStateStack().getLastState();
        _unmarshalHandler.processIDREF(state.getBuffer().toString(), descriptor,
            pState.getObject());
        _unmarshalHandler.getNamespaceHandling().removeCurrentNamespaceInstance();
        return;
      } else {
        // -- check for non-whitespace...and report error
        if (!UnmarshalHandler.isWhitespace(state.getBuffer())) {
          String err =
              MessageFormat.format(resourceBundle.getString("unmarshalHandler.error.illegal.text"),
                  new Object[] {name, state.getBuffer()});
          throw new SAXException(err);
        }
      }
    }

    // -- We're finished processing the object, so notify the
    // -- Listener (if any).
    Object stateObject = state.getObject();
    Object parentObject = (state.getParent() == null) ? null : state.getParent().getObject();
    _unmarshalHandler.getDelegateUnmarshalListener().unmarshalled(stateObject, parentObject);

    // -- if we are at root....just validate and we are done
    if (_unmarshalHandler.getStateStack().isEmpty()) {
      if (_unmarshalHandler.isValidating()) {
        ValidationException first = null;
        ValidationException last = null;

        // -- check unresolved references
        if (_unmarshalHandler.getResolveTable() != null
            && !_unmarshalHandler.getInternalContext().getLenientIdValidation()) {
          Enumeration enumeration = _unmarshalHandler.getResolveTable().keys();
          while (enumeration.hasMoreElements()) {
            Object ref = enumeration.nextElement();
            // if
            // (ref.toString().startsWith(MapItem.class.getName()))
            // continue;
            String msg = "unable to resolve reference: " + ref;
            if (first == null) {
              first = new ValidationException(msg);
              last = first;
            } else {
              last.setNext(new ValidationException(msg));
              last = last.getNext();
            }
          }
        }
        try {
          Validator validator = new Validator();
          ValidationContext context = new ValidationContext();
          context.setInternalContext(_unmarshalHandler.getInternalContext());
          validator.validate(state.getObject(), context);
          if (!_unmarshalHandler.getInternalContext().getLenientIdValidation()) {
            validator.checkUnresolvedIdrefs(context);
          }
          context.cleanup();
        } catch (ValidationException vEx) {
          if (first == null)
            first = vEx;
          else
            last.setNext(vEx);
        }
        if (first != null) {
          throw new SAXException(first);
        }
      }
      return;
    }

    // -- Add object to parent if necessary

    if (descriptor.isIncremental()) {
      // -- remove current namespace scoping
      _unmarshalHandler.getNamespaceHandling().removeCurrentNamespaceInstance();
      return; // -- already added
    }

    Object val = state.getObject();

    // --special code for AnyNode handling
    if (_unmarshalHandler.getAnyNode() != null) {
      val = _unmarshalHandler.getAnyNode();
      _unmarshalHandler.setAnyNode(null);
    }

    // -- save fieldState
    UnmarshalState fieldState = state;

    // -- have we seen this object before?
    boolean firstOccurance = false;

    // -- get target object
    state = _unmarshalHandler.getStateStack().getLastState();
    if (state.isWrapper()) {
      state = fieldState.getTargetState();
    }

    // -- check to see if we have already read in
    // -- an element of this type.
    // -- (Q: if we have a container, do we possibly need to
    // -- also check the container's multivalued status?)
    if (!descriptor.isMultivalued()) {

      if (state.isUsed(descriptor)) {

        String location = name;
        while (!_unmarshalHandler.getStateStack().isEmpty()) {
          UnmarshalState tmpState = _unmarshalHandler.getStateStack().removeLastState();
          if (!tmpState.isWrapper()) {
            if (tmpState.getFieldDescriptor().isContainer())
              continue;
          }
          location = state.getElementName() + "/" + location;
        }

        String err = MessageFormat.format(
            resourceBundle.getString("unmarshalHandler.error.element.occurs.more.than.once"),
            new Object[] {name, state.getType().getName(), location});

        ValidationException vx = new ValidationException(err);

        throw new SAXException(vx);
      }
      state.markAsUsed(descriptor);
      // -- if this is the identity then save id
      if (state.getClassDescriptor().getIdentity() == descriptor) {
        state.setKey(val);
      }
    } else {
      // -- check occurance of descriptor
      if (!state.isUsed(descriptor)) {
        firstOccurance = true;
      }

      // -- record usage of descriptor
      state.markAsUsed(descriptor);
    }

    try {
      FieldHandler handler = descriptor.getHandler();
      // check if the value is a QName that needs to
      // be resolved (ns:value -> {URI}value)
      String valueType = descriptor.getSchemaType();
      if ((valueType != null) && (valueType.equals(MarshalFramework.QNAME_NAME))) {
        val = _unmarshalHandler.getNamespaceHandling().resolveNamespace(val);
      }

      boolean addObject = true;
      if (_unmarshalHandler.isReuseObjects() && fieldState.isPrimitiveOrImmutable()) {
        // -- check to see if we need to
        // -- add the object or not
        Object tmp = handler.getValue(state.getObject());
        if (tmp != null) {
          // -- Do not add object if values
          // -- are equal
          addObject = (!tmp.equals(val));
        }
      }

      // -- special handling for mapped objects
      if (descriptor.isMapped()) {
        if (!(val instanceof MapItem)) {
          MapItem mapItem = new MapItem(fieldState.getKey(), val);
          val = mapItem;
        } else {
          // -- make sure value exists (could be a reference)
          MapItem mapItem = (MapItem) val;
          if (mapItem.getValue() == null) {
            // -- save for later...
            addObject = false;
            _unmarshalHandler.addReference(mapItem.toString(), state.getObject(), descriptor);
          }
        }
      }

      if (addObject) {
        // -- clear any collections if necessary
        if (firstOccurance && _unmarshalHandler.isClearCollections()) {
          handler.resetValue(state.getObject());
        }

        if (descriptor.isMultivalued() && descriptor.getSchemaType() != null
            && descriptor.getSchemaType().equals("list")
            && ((XMLFieldDescriptorImpl) descriptor).isDerivedFromXSList()) {
          List<Object> values = (List<Object>) val;
          for (Object value : values) {
            // -- finally set the value!!
            handler.setValue(state.getObject(), value);

            // If there is a parent for this object, pass along
            // a notification that we've finished adding a child
            _unmarshalHandler.getDelegateUnmarshalListener().fieldAdded(descriptor.getFieldName(),
                state.getObject(), fieldState.getObject());
          }
        } else {

          // -- finally set the value!!
          handler.setValue(state.getObject(), val);

          // If there is a parent for this object, pass along
          // a notification that we've finished adding a child
          _unmarshalHandler.getDelegateUnmarshalListener().fieldAdded(descriptor.getFieldName(),
              state.getObject(), fieldState.getObject());
        }
      }

    }
    /*
     * catch(java.lang.reflect.InvocationTargetException itx) {
     * 
     * Throwable toss = itx.getTargetException(); if (toss == null) toss = itx;
     * 
     * String err = "unable to add '" + name + "' to <"; err += state.descriptor.getXMLName(); err
     * += "> due to the following exception: " + toss; throw new SAXException(err); }
     */
    catch (Exception ex) {
      StringWriter sw = new StringWriter();
      PrintWriter pw = new PrintWriter(sw);
      ex.printStackTrace(pw);
      pw.flush();
      String err = MessageFormat.format(
          resourceBundle.getString("unmarshalHandler.error.unable.add.element"),
          new Object[] {name, state.getFieldDescriptor().getXMLName(), sw.toString()});
      throw new SAXException(err, ex);
    }

    // -- remove current namespace scoping
    _unmarshalHandler.getNamespaceHandling().removeCurrentNamespaceInstance();

    // remove additional (artifical aka container) state introduced for
    // single-valued (iow maxOccurs="1") choices.
    if (state.getFieldDescriptor().isContainer() && state.getClassDescriptor().isChoice()
        && !state.getFieldDescriptor().isMultivalued()) {
      _unmarshalHandler.endElement(state.getElementName());
    }

  }
}
