/**
 * Copyright (c) 2007-2012 IBM Corporation, CEA, and others.
 * All rights reserved.   This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 *
 * Contributors:
 *   IBM - Initial API and implementation
 *   Kenn Hussey (CEA) - 351783
 */
package org.eclipse.emf.ecore.xmi.impl;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.impl.EPackageRegistryImpl;
import org.eclipse.emf.ecore.resource.ContentHandler;
import org.eclipse.emf.ecore.resource.impl.ContentHandlerImpl;
import org.eclipse.emf.ecore.util.BasicExtendedMetaData;
import org.eclipse.emf.ecore.util.ExtendedMetaData;
import org.eclipse.emf.ecore.util.FeatureMap;
import org.eclipse.emf.ecore.xmi.XMIResource;
import org.eclipse.emf.ecore.xmi.XMLDefaultHandler;
import org.eclipse.emf.ecore.xmi.XMLLoad;
import org.eclipse.emf.ecore.xmi.XMLOptions;
import org.eclipse.emf.ecore.xmi.XMLParserPool;
import org.eclipse.emf.ecore.xmi.XMLResource;
import org.eclipse.emf.ecore.xml.type.AnyType;
import org.eclipse.emf.ecore.xml.type.XMLTypeDocumentRoot;
import org.eclipse.emf.ecore.xml.type.XMLTypePackage;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

/**
 * A {@link ContentHandler} content handler implementation for dealing with XML content.
 */
public class XMLContentHandlerImpl extends ContentHandlerImpl
{
  /**
   * Returns whether the given name and namespace represents a specialize XMI root element tag.
   * @param name the element name to consider.
   * @param namespace the element namespace to consider.
   * @return whether the given name and namespace represents a specialize XMI root element tag.
   */
  public static boolean isXMINameAndNamespace(String name, String namespace)
  {
    return XMIResource.XMI_TAG_NAME.equals(name) && isXMINamespace(namespace);
  }

  /**
   * Returns whether the given namespace represents an XMI namespace.
   * @param namespace the element namespace to consider.
   * @return whether the given namespace represents an XMI namespace.
   */
  public static boolean isXMINamespace(String namespace)
  {
    return
      namespace != null &&
        (namespace.startsWith(XMIResource.XMI_2_4_NAMESPACE_PREFIX) ||
           namespace.equals(XMIResource.XMI_2_1_URI) ||
           namespace.equals(XMIResource.XMI_URI));
  }

  private static final XMLParserPool XML_PARSER_POOL = new XMLParserPoolImpl(1, true);
  private static final Map<Object, Object> DEFAULT_SAVE_OPTIONS;
  private static final Map<Object, Object> DEFAULT_LOAD_OPTIONS;
  static
  {
    Map<Object, Object> defaultLoadOptions = new HashMap<Object, Object>();
    Map<Object, Object> defaultSaveOptions = new HashMap<Object, Object>();

    defaultLoadOptions.put(XMLResource.OPTION_USE_ENCODED_ATTRIBUTE_STYLE, Boolean.TRUE);
    defaultSaveOptions.put(XMLResource.OPTION_USE_ENCODED_ATTRIBUTE_STYLE, Boolean.TRUE);

    ExtendedMetaData extendedMetaData = new BasicExtendedMetaData(new EPackageRegistryImpl());
    defaultLoadOptions.put(XMLResource.OPTION_EXTENDED_META_DATA, extendedMetaData);
    defaultSaveOptions.put(XMLResource.OPTION_EXTENDED_META_DATA, extendedMetaData);

    XMLOptions xmlOptions = new XMLOptionsImpl();
    xmlOptions.setProcessAnyXML(true);
    defaultLoadOptions.put(XMLResource.OPTION_XML_OPTIONS, xmlOptions);

    defaultLoadOptions.put(XMLResource.OPTION_USE_PARSER_POOL, XML_PARSER_POOL);

    DEFAULT_LOAD_OPTIONS = defaultLoadOptions;
    DEFAULT_SAVE_OPTIONS = defaultSaveOptions;
  }

  /**
   * Creates a specialized XML resource for consuming just the root elements
   * as well as the first nested element,
   * if the root element is an {@link #isXMINameAndNamespace(String, String) XMI root element}.
   * @return an XML Resource for the root element and possibly the first nested element.
   */
  protected XMLResource createXMLResource()
  {
    return
      new XMLResourceImpl()
      {
        {
          defaultLoadOptions = DEFAULT_LOAD_OPTIONS;
          defaultSaveOptions = DEFAULT_SAVE_OPTIONS;
        }

        @Override
        protected XMLLoad createXMLLoad()
        {
          return
            new XMLLoadImpl(createXMLHelper())
            {
              @Override
              public XMLDefaultHandler createDefaultHandler()
              {
                return
                  new SAXXMLHandler(resource, helper, options)
                  {
                    @Override
                    public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException
                    {
                      super.startElement(uri, localName, name, attributes);
                      int depth = elements.size();
                      if (depth == 1 && !isXMINameAndNamespace(localName, uri) ||
                            depth == 2 && !isXMINamespace(uri))
                      {
                        endElement(uri, localName, name);
                        if (depth == 2)
                        {
                          endElement(uri, localName, name);
                        }
                        endDocument();
                        throw new RuntimeException();
                      }
                    }

                    @Override
                    protected EPackage handleMissingPackage(String uriString)
                    {
                      EPackage result = super.handleMissingPackage(uriString);
                      return
                        result == XMLTypePackage.eINSTANCE ?
                          extendedMetaData.demandPackage(uriString) :
                          result;
                    }

                    @Override
                    protected void processSchemaLocations(String prefix, String name)
                    {
                      // Completely ignore schema locations.
                    }
                  };
              }
            };
        }
      };
  }

  /**
   * Returns the character set of the input stream; it's computed from the encoding specified in the XML content.
   * @param uri the URI of the input stream.
   * @param inputStream the input stream of the XML content.
   * @param options any options that might influence the loading of the XML content.
   * @param context a cache for previously computed information.
   * @return the character set of the input stream.
   * @throws IOException if there is a problem loading the XML content.
   */
  @Override
  protected String getCharset(URI uri, InputStream inputStream, Map<?, ?> options, Map<Object, Object> context) throws IOException
  {
    String result = (String)context.get(ContentHandler.CHARSET_PROPERTY);
    if (result == null)
    {
      result = load(uri, inputStream, options, context).getEncoding();
      context.put(ContentHandler.CHARSET_PROPERTY, result);
    }
    return result;
  }

  /**
   * Returns the XML resource loaded from the input stream.
   * @param uri the URI of the input stream.
   * @param inputStream the input stream of the XML content.
   * @param options any options that might influence the loading the the XML content.
   * @param context a cache for previously computed information.
   * @return the XML resource loaded from the input stream.
   * @throws IOException if there is a problem loading the XML content.
   */
  protected XMLResource load(URI uri, InputStream inputStream, Map<?, ?> options, Map<Object, Object> context) throws IOException
  {
    XMLResource result = (XMLResource)context.get("XMLResource");
    if (result == null)
    {
      try
      {
        result = createXMLResource();
        result.setURI(uri);
        result.load
          (new BufferedInputStream(inputStream)
           {
             @Override
             public void close() throws IOException
             {
               // Ignore close since we don't want to let the parser close the stream when it's done.
             }
           },
           null);
      }
      catch (Throwable exception)
      {
        // Ignore since we expect this to fail because of early termination
        // and if there is some other kind of failure, then we'll simply not be able to match the content type.
      }
      finally
      {
        inputStream.reset();
      }
      context.put("XMLResource", result);
    }
    return result;
  }
  
  public static class XMI extends XMLContentHandlerImpl
  {
    /**
     * Returns a valid content description if the XML content of the input stream has an XMI version attribute.
     */
    @Override
    public Map<String, Object> contentDescription(URI uri, InputStream inputStream, Map<?, ?> options, Map<Object, Object> context) throws IOException
    {
      Map<String, Object> result = super.contentDescription(uri, inputStream, options, context);

      XMLResource xmlResource = load(uri, inputStream, options, context);
      EList<EObject> contents = xmlResource.getContents();
      if (!contents.isEmpty())
      {
        EObject eObject = contents.get(0);
        if (eObject instanceof XMLTypeDocumentRoot)
        {
          XMLTypeDocumentRoot documentRoot = (XMLTypeDocumentRoot)eObject;
          EList<EObject> rootContents = documentRoot.eContents();
          if (!rootContents.isEmpty())
          {
            EObject root = rootContents.get(0);
            if (root instanceof AnyType)
            {
              for (FeatureMap.Entry entry : ((AnyType)root).getAnyAttribute())
              {
                EStructuralFeature attributeFeature = entry.getEStructuralFeature();
                if ("version".equals(ExtendedMetaData.INSTANCE.getName(attributeFeature)) &&
                      isXMINamespace(ExtendedMetaData.INSTANCE.getNamespace(attributeFeature)))
                {
                  result.put(VALIDITY_PROPERTY, ContentHandler.Validity.VALID);
                  result.put(CONTENT_TYPE_PROPERTY, "org.eclipse.emf.ecore.xmi");
                  break;
                }
              }
            }
          }
        }
      }
      return result;
    }

    /**
     * A describer that {@link #createContentHandler(Map) creates} an {@link XMI} instance.
     */
    public static class Describer extends ContentHandlerImpl.Describer
    {
      /**
       * Creates a {@link RootXMLContentHandlerImpl} instance.
       */
      @Override
      protected ContentHandler createContentHandler(Map<String, String> parameters)
      {
        return new XMI();
      }
    }
  }
}
