/**
 * Copyright (c) 2002-2007 IBM Corporation 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
 */
package org.eclipse.emf.ecore.xmi.impl;


import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.w3c.dom.CDATASection;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.ProcessingInstruction;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.AttributesImpl;
import org.xml.sax.helpers.DefaultHandler;

import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.URIConverter;
import org.eclipse.emf.ecore.util.ExtendedMetaData;
import org.eclipse.emf.ecore.xmi.XMLDefaultHandler;
import org.eclipse.emf.ecore.xmi.XMLHelper;
import org.eclipse.emf.ecore.xmi.XMLLoad;
import org.eclipse.emf.ecore.xmi.XMLParserPool;
import org.eclipse.emf.ecore.xmi.XMLResource;


/**
 * This class begins parsing with the given input stream using the XML
 * deserializer.
 */
public class XMLLoadImpl implements XMLLoad
{
  protected static final String SAX_LEXICAL_PROPERTY = "http://xml.org/sax/properties/lexical-handler";
  protected static final int BUFFER_SIZE = 200;
  protected XMLResource resource;
  protected InputStream is;
  protected XMLHelper helper;
  protected Map<?, ?> options;
  protected boolean namespaceAware;

  public XMLLoadImpl(XMLHelper helper)
  {
    this.helper = helper;
  }

  protected void handleErrors() throws IOException
  {
    if (!resource.getErrors().isEmpty())
    {
      Resource.Diagnostic error = resource.getErrors().get(0);
      if (error instanceof Exception)
      {
        throw new Resource.IOWrappedException((Exception)error);
      }
      else
      {
        throw new IOException(error.getMessage());
      }
    }
  }

  /**
   * Start parsing with the default handler; either XMI
   * or XML.
   */
  public void load(XMLResource resource, InputStream inputStream, Map<?, ?> options) throws IOException
  {
    if (inputStream instanceof URIConverter.Readable)
    {
      URIConverter.Readable readable = (URIConverter.Readable)inputStream;
      resource.setEncoding(readable.getEncoding());
      
      InputSource inputSource =  new InputSource(readable.asReader());
      if (resource.getURI() != null)
      {
        String resourceURI = resource.getURI().toString();
        inputSource.setPublicId(resourceURI);
        inputSource.setSystemId(resourceURI);
        inputSource.setEncoding(resource.getEncoding());
      }
      load(resource, inputSource, options);
      return;
    }
    
    this.resource = resource;
    is = inputStream;
    this.options = options;
    XMLParserPool pool = (XMLParserPool)options.get(XMLResource.OPTION_USE_PARSER_POOL);
    @SuppressWarnings("unchecked") Map<String, Boolean> parserFeatures = (Map<String, Boolean>)options.get(XMLResource.OPTION_PARSER_FEATURES);
    @SuppressWarnings("unchecked") Map<String, ?> parserProperties = (Map<String, ?>)options.get(XMLResource.OPTION_PARSER_PROPERTIES);
    parserFeatures = (parserFeatures == null) ? Collections.<String, Boolean>emptyMap() : parserFeatures;
    parserProperties = (parserProperties == null) ? Collections.<String, Object>emptyMap() : parserProperties;

    // HACK: reading encoding
    String encoding = null;
    if (!Boolean.FALSE.equals(options.get(XMLResource.OPTION_USE_DEPRECATED_METHODS)))
    {
      encoding = getEncoding();
      resource.setEncoding(encoding);
    }
    try
    {
      SAXParser parser;  
      DefaultHandler handler;

      if (pool != null)
      {
        // use the pool to retrieve the parser
        parser = pool.get(parserFeatures, parserProperties, Boolean.TRUE.equals(options.get(XMLResource.OPTION_USE_LEXICAL_HANDLER)));
        handler = (DefaultHandler)pool.getDefaultHandler(resource, this, helper, options);
      } 
      else 
      {
        parser = makeParser();
        handler = makeDefaultHandler();
        // set features and properties
        if (parserFeatures != null)
        {
          for (Map.Entry<String, Boolean> entry : parserFeatures.entrySet())
          {
            parser.getXMLReader().setFeature(entry.getKey(), entry.getValue());
          }
        }
        if (parserProperties !=null)
        {
          for (Map.Entry<String, ?> entry : parserProperties.entrySet())
          {
            parser.getXMLReader().setProperty(entry.getKey(), entry.getValue());
          }
        }
      }

      InputSource inputSource = new InputSource(is);
      if (resource.getURI() != null)
      {
        String resourceURI = resource.getURI().toString();
        inputSource.setPublicId(resourceURI);
        inputSource.setSystemId(resourceURI);
        inputSource.setEncoding(encoding);
      }
    
      // set lexical handler
      if (Boolean.TRUE.equals(options.get(XMLResource.OPTION_USE_LEXICAL_HANDLER)))
      {
        if (parserProperties == null || parserProperties.get(SAX_LEXICAL_PROPERTY) == null) 
        {
          parser.setProperty(SAX_LEXICAL_PROPERTY, handler);
        }
      }
      
      parser.parse(inputSource, handler);
      
      // release parser back to the pool
      if (pool != null)
      {
        pool.release(parser, parserFeatures, parserProperties, Boolean.TRUE.equals(options.get(XMLResource.OPTION_USE_LEXICAL_HANDLER)));
        pool.releaseDefaultHandler((XMLDefaultHandler)handler, options);
      }
      
      helper = null;
      handleErrors();
    }
    catch (SAXException exception)
    {
      if (exception.getException() != null)
      {
        throw new Resource.IOWrappedException(exception.getException());
      }
      else
      {
        throw new Resource.IOWrappedException(exception);
      }
    }
    catch (ParserConfigurationException exception)
    {
      throw new Resource.IOWrappedException(exception);
    }
  }
  
  public void load(XMLResource resource, InputSource inputSource, Map<?, ?> options) throws IOException
  {
    this.resource = resource;
   
    this.options = options;
    XMLParserPool pool = (XMLParserPool)options.get(XMLResource.OPTION_USE_PARSER_POOL);
    @SuppressWarnings("unchecked") Map<String, Boolean> parserFeatures = (Map<String, Boolean>)options.get(XMLResource.OPTION_PARSER_FEATURES);
    @SuppressWarnings("unchecked") Map<String, ?> parserProperties = (Map<String, ?>)options.get(XMLResource.OPTION_PARSER_PROPERTIES);
    parserFeatures = (parserFeatures == null) ? Collections.<String, Boolean>emptyMap() : parserFeatures;
    parserProperties = (parserProperties == null) ? Collections.<String, Object>emptyMap() : parserProperties;

    // Don't read encoding - rely on XML parser to provide one via Locator2

    try
    {
      SAXParser parser;  
      DefaultHandler handler;

      if (pool != null)
      {
        // use the pool to retrieve the parser
        parser = pool.get(parserFeatures, parserProperties, Boolean.TRUE.equals(options.get(XMLResource.OPTION_USE_LEXICAL_HANDLER)));
        handler = (DefaultHandler)pool.getDefaultHandler(resource, this, helper, options);
      } 
      else 
      {
        parser = makeParser();
        handler = makeDefaultHandler();
        // set features and properties
        if (parserFeatures != null)
        {
          for (Map.Entry<String, Boolean> feature : parserFeatures.entrySet())
          {
            parser.getXMLReader().setFeature(feature.getKey(),  feature.getValue());
          }
        }
        if (parserProperties !=null)
        {
          for (Map.Entry<String, ?> property : parserProperties.entrySet())
          {
            parser.getXMLReader().setProperty(property.getKey(), property.getValue());
          }
        }
      }
  
      // set lexical handler
      if (Boolean.TRUE.equals(options.get(XMLResource.OPTION_USE_LEXICAL_HANDLER)))
      {
        if (parserProperties == null || parserProperties.get(SAX_LEXICAL_PROPERTY) == null) 
        {
          parser.setProperty(SAX_LEXICAL_PROPERTY, handler);
        }
      }
      
      parser.parse(inputSource, handler);

      // release parser back to the pool
      if (pool != null)
      {
        pool.release(parser, parserFeatures, parserProperties, Boolean.TRUE.equals(options.get(XMLResource.OPTION_USE_LEXICAL_HANDLER)));
        pool.releaseDefaultHandler((XMLDefaultHandler)handler, options);
      }

      helper = null;
      handleErrors();
    }
    catch (SAXException exception)
    {
      if (exception.getException() != null)
      {
        throw new Resource.IOWrappedException(exception.getException());
      }
      else
      {
        throw new Resource.IOWrappedException(exception);
      }
    }
    catch (ParserConfigurationException exception)
    {
      throw new Resource.IOWrappedException(exception);
    }
  }

  /**
   * Make either a validating or non-validating parser;
   * throw an if one could not be made.
   */
  protected SAXParser makeParser() throws ParserConfigurationException, SAXException
  {
    SAXParserFactory f = SAXParserFactory.newInstance();
    return f.newSAXParser();
  }
  
  public XMLDefaultHandler createDefaultHandler()
  {
    return (XMLDefaultHandler)makeDefaultHandler();
  }

  protected DefaultHandler makeDefaultHandler()
  {
    return new SAXXMLHandler(resource, helper, options);
  }

  /** 
   * @deprecated since 2.2
   * The encoding will be reported by the parser using SAX 2 Locator
   */
  @Deprecated
  protected String getEncoding() throws IOException
  {
    if (!is.markSupported())
      is = new BufferedInputStream(is);
    
    byte[] buffer = readBuffer();
    return XMLHandler.getXMLEncoding(buffer);
  }  

  protected byte[] readBuffer() throws IOException
  {
    if (is.available() == 0)
    {
      return new byte[0];
    }

    byte[] buffer = new byte[BUFFER_SIZE];
    is.mark(BUFFER_SIZE);
    int bytesRead = is.read(buffer, 0, BUFFER_SIZE);
    int totalBytesRead = bytesRead;
    
    while (bytesRead != -1 && (totalBytesRead < BUFFER_SIZE))
    {
      bytesRead = is.read(buffer, totalBytesRead, BUFFER_SIZE - totalBytesRead);

      if (bytesRead != -1)
        totalBytesRead += bytesRead;
    }

    if (totalBytesRead < 0)
    {
      buffer = new byte[0];
    }
    else if (totalBytesRead < BUFFER_SIZE)
    {
      byte[] smallerBuffer = new byte[totalBytesRead];
      System.arraycopy(buffer, 0, smallerBuffer, 0, totalBytesRead);
      buffer = smallerBuffer;
    }
      
    is.reset();
    return buffer;
  }

  /* (non-Javadoc)
   * @see org.eclipse.emf.ecore.xmi.XMLLoad#load(org.eclipse.emf.ecore.xmi.XMLResource, org.w3c.dom.Node, java.util.Map)
   */
  public void load(XMLResource resource, Node node, Map<?, ?> options) throws IOException
  {
    this.resource = resource;
    this.options = options;
    this.namespaceAware = Boolean.FALSE.equals(options.get(XMLResource.OPTION_USE_DEPRECATED_METHODS));
    DefaultHandler handler;
    XMLParserPool pool = (XMLParserPool)options.get(XMLResource.OPTION_USE_PARSER_POOL);
    if (pool != null)
    {
      handler = (DefaultHandler)pool.getDefaultHandler(resource, this, helper, options);
    }
    else
    {
      handler = makeDefaultHandler();
    }
    LexicalHandler lexicalHandler = null;

    if (Boolean.TRUE.equals(options.get(XMLResource.OPTION_USE_LEXICAL_HANDLER)))
    {
      lexicalHandler = (LexicalHandler)handler;
    }

    AttributesProxy attributesProxy = new AttributesProxy();
    try
    {
      short type = node.getNodeType();
      if (type == Node.ELEMENT_NODE)
      {
        handler.startDocument();
        if (Boolean.TRUE.equals(options.get(XMLResource.OPTION_DOM_USE_NAMESPACES_IN_SCOPE)))
        {
          traverseElement((Element)node, attributesProxy, handler, lexicalHandler);
        }
        else
        {
          traverse(node, attributesProxy, handler, lexicalHandler);
        }
        handler.endDocument();      
      }
      else
      {
        traverse(node, attributesProxy, handler, lexicalHandler);
      }
    }
    catch (SAXException e)
    {
      // ignore
    }
    
    if (pool != null)
    {
      pool.releaseDefaultHandler((XMLDefaultHandler)handler, options);
    }
    
    attributesProxy = null;
    handler = null;
    lexicalHandler = null;
    helper = null;

    handleErrors();
  }
  
  /**
   * Special case: traversing root element using namespaces in scope
   */
  protected void traverseElement(Element element, AttributesProxy attributesProxy, DefaultHandler handler, LexicalHandler lexicalHandler) throws SAXException
  {   
    // temporary structure to hold node's attributes + namespaces in scope
    AttributesImpl attrs = new AttributesImpl();
    Set<String> prefixes = new HashSet<String>();
    
    // record node's attributes
    if (element.hasAttributes())
    {
      NamedNodeMap attributes = element.getAttributes();
      for (int i = 0; i < attributes.getLength(); i++)
      {
        Node attr = attributes.item(i);
        String namespaceURI = attr.getNamespaceURI();
        if (namespaceURI == null)
        {
          namespaceURI = "";
        }
        String nodeName = attr.getNodeName();
        String localName = attr.getLocalName();
        String nodeValue = attr.getNodeValue();
        if (ExtendedMetaData.XMLNS_URI.equals(namespaceURI))
        {
          // Include only non-duplicate namespace declarations.
          //
          if (namespaceAware)
          {
            if (prefixes.add(localName))
            {
              handler.startPrefixMapping(localName, nodeValue);
            }
          }
          else if (attrs.getIndex(nodeName) < 0)
          {
            attrs.addAttribute(namespaceURI, localName, nodeName, "CDATA", nodeValue);
          }
        }
        else
        {
          attrs.addAttribute(namespaceURI, localName, nodeName, "CDATA", nodeValue);
        }
      }
    }
    
    // record namespaces in scope
    //
    for (Node parent = element.getParentNode();  parent != null && parent.getNodeType() != Node.DOCUMENT_NODE;  parent = parent.getParentNode())
    {
      if (parent.hasAttributes())
      {
        NamedNodeMap attributes = parent.getAttributes();
        for (int i = 0; i < attributes.getLength(); i++)
        {
          Node attr = attributes.item(i);
          String namespaceURI = attr.getNamespaceURI();
          if (ExtendedMetaData.XMLNS_URI.equals(namespaceURI))
          {          
            // Include only non-duplicate namespace declarations.
            //
            String localName = attr.getLocalName();
            String nodeValue = attr.getNodeValue();
            if (namespaceAware)
            {
              if (prefixes.add(localName))
              {
                handler.startPrefixMapping(localName, nodeValue);
              }
            }
            else 
            {
              String nodeName = attr.getNodeName();
              if (attrs.getIndex(nodeName) < 0)
              {
                attrs.addAttribute(namespaceURI, localName, nodeName, "CDATA", nodeValue);
              }
            }
          }
        }
      }
    }  

    // traverse element node
    String namespaceURI = element.getNamespaceURI();
    if (namespaceURI == null)
    {
      namespaceURI = "";
    }
    String localName = element.getLocalName();
    String qname = element.getNodeName();   
    
    handler.startElement(namespaceURI, localName , qname, attrs);
    Node child = element.getFirstChild();
    while (child != null)
    {
      traverse(child, attributesProxy, handler, lexicalHandler);
      child = child.getNextSibling();
    }
    handler.endElement(namespaceURI, localName , qname);
  }

  protected void traverse(Node node, AttributesProxy attributesProxy, DefaultHandler handler, LexicalHandler lexicalHandler) throws SAXException
  {
    if (node == null)
    {
      return;
    }

    short type = node.getNodeType();
    switch (type)
    {
      case Node.DOCUMENT_NODE:
      {
        Document document = (Document)node;
        handler.startDocument();
        Node root = document.getDocumentElement();
        if (lexicalHandler != null)
        {
          DocumentType doctype = document.getDoctype();
          if (doctype != null)
          {
            String publicId = doctype.getPublicId();
            String systemId = doctype.getSystemId();
            lexicalHandler.startDTD(root.getNodeName(), publicId, systemId);
          }
        }
        traverse(root, attributesProxy, handler, lexicalHandler);
        handler.endDocument();
        break;
      }
      case Node.ELEMENT_NODE:
      {
        AttributesImpl filteredAttributes = null;
        NamedNodeMap attributes = node.getAttributes();
        if (namespaceAware)
        {
          for (int i = 0, length = attributes.getLength(); i < length; i++)
          {
            Node attr = attributes.item(i);
            String namespaceURI = attr.getNamespaceURI();
            if (ExtendedMetaData.XMLNS_URI.equals(namespaceURI))
            {
              handler.startPrefixMapping(attr.getLocalName(), attr.getNodeValue());
              if (filteredAttributes == null)
              {
                filteredAttributes = new AttributesImpl();
                for (int j = 0; j < i; ++j)
                {
                  attr = attributes.item(j);
                  namespaceURI = attr.getNamespaceURI();
                  if (namespaceURI == null)
                  {
                    namespaceURI = "";
                  }
                  filteredAttributes.addAttribute(namespaceURI, attr.getLocalName(), attr.getNodeName(), "CDATA", attr.getNodeValue());
                }
              }
            }
            else if (filteredAttributes != null)
            {
              if (namespaceURI == null)
              {
                namespaceURI = "";
              }
              filteredAttributes.addAttribute(namespaceURI, attr.getLocalName(), attr.getNodeName(), "CDATA", attr.getNodeValue());
            }
          }
        }
        if (filteredAttributes == null)
        {
          attributesProxy.setAttributes(attributes);
        }
        String namespaceURI = node.getNamespaceURI();
        if (namespaceURI == null)
        {
          namespaceURI = "";
        }
        String localName = node.getLocalName();
        String qname = node.getNodeName();   
        
        handler.startElement(namespaceURI, localName, qname, filteredAttributes == null ? attributesProxy: filteredAttributes);

        Node child = node.getFirstChild();
        while (child != null)
        {
          traverse(child, attributesProxy, handler, lexicalHandler);
          child = child.getNextSibling();
        }
        handler.endElement(namespaceURI, localName, qname);
        break;
      }

      case Node.CDATA_SECTION_NODE:
      {
        if (lexicalHandler != null)
        {
          lexicalHandler.startCDATA();
        }
        char[] chars = ((CDATASection)node).getData().toCharArray();
        handler.characters(chars, 0, chars.length);
        if (lexicalHandler != null)
        {
          lexicalHandler.endCDATA();
        }
        break;
      }
      case Node.TEXT_NODE:
      {
        char[] chars = node.getNodeValue().toCharArray();
        handler.characters(chars, 0, chars.length);
        break;
      }
      case Node.COMMENT_NODE:
      {
        if (lexicalHandler != null)
        {
          char[] chars = ((Comment)node).getData().toCharArray();
          lexicalHandler.comment(chars, 0, chars.length);
        }
        break;
      }
      case Node.PROCESSING_INSTRUCTION_NODE:
      {
        ProcessingInstruction pi = (ProcessingInstruction) node;
        handler.processingInstruction(pi.getTarget(), pi.getData());
        break;
      }
    }
  }

  protected static final class AttributesProxy implements Attributes
  {
    /** DOM attributes. */
    protected NamedNodeMap attributes;

    /** Sets the DOM attributes. */
    public void setAttributes(NamedNodeMap attributes)
    {
      this.attributes = attributes;
    }

    public int getLength()
    {
      return attributes.getLength();
    }

    public String getQName(int index)
    {
      Node node = attributes.item(index);
      return (node != null) ? node.getNodeName() : null;
    }

    public String getURI(int index)
    {
      Node node = attributes.item(index);
      if (node != null)
      {
        String namespaceURI = node.getNamespaceURI();
        if (ExtendedMetaData.XMLNS_URI.equals(namespaceURI))
        {
          return "";
        }
        return namespaceURI;
      }
      return null;
    }

    public String getLocalName(int index)
    {
      Node node = attributes.item(index);
      if (node != null)
      {
        String prefix = node.getPrefix();
        if (ExtendedMetaData.XMLNS_PREFIX.equals(prefix))
        {
          return "";
        }
        return node.getLocalName();
      }
      return null;
    }

    public String getType(int i)
    {
      return "CDATA";
    }

    public String getType(String name)
    {
      return "CDATA";
    }

    public String getType(String uri, String localName)
    {
      return "CDATA";
    }

    public String getValue(int i)
    {
      Node node = attributes.item(i);
      return (node != null) ? node.getNodeValue() : null;
    }

    public String getValue(String name)
    {
      Node node = attributes.getNamedItem(name);
      return (node != null) ? node.getNodeValue() : null;
    }

    public String getValue(String uri, String localName)
    {
      Node node = attributes.getNamedItemNS(uri, localName);
      return (node != null) ? node.getNodeValue() : null;
    }

    public int getIndex(String qName)
    {
      Node node = attributes.getNamedItem(qName);
      if (node != null)
      {
        for (int i = 0; i < attributes.getLength(); i++)
        {
          Node item = attributes.item(i);
          if (item == node)
          {
            return i;
          }
        }
      }
      return -1;
    }

    public int getIndex(String uri, String localPart)
    {
      Node node = attributes.getNamedItemNS(uri, localPart);
      if (node != null)
      {
        for (int i = 0; i < attributes.getLength(); i++)
        {
          Node item = attributes.item(i);
          if (item == node)
          {
            return i;
          }
        }
      }
      return -1;
    }
  } // class AttributesProxy

} // XMLLoad
