/**
 * Copyright (c) 2005-2014 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
 *   Aurelien pupier (Bonitasoft S.A.) - Bug 441484: respect encoding provided in saveToString Method
 */
package org.eclipse.emf.ecore.xmi.util;


import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.EcorePackage;
import org.eclipse.emf.ecore.impl.EPackageRegistryImpl;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.emf.ecore.util.BasicExtendedMetaData;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.util.ExtendedMetaData;
import org.eclipse.emf.ecore.xmi.DOMHandler;
import org.eclipse.emf.ecore.xmi.EcoreBuilder;
import org.eclipse.emf.ecore.xmi.XMLResource;
import org.eclipse.emf.ecore.xmi.impl.XMLParserPoolImpl;
import org.eclipse.emf.ecore.xmi.impl.XMLResourceFactoryImpl;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;


/**
 * XMLProcessor provides helper methods to serialize and deserialize XML using EMF framework.
 *
 */
public class XMLProcessor
{
  protected EcoreBuilder ecoreBuilder;

  protected final static String XML_EXTENSION = "xml";

  protected final static String STAR_EXTENSION = "*";

  protected final static URI XML_URI = URI.createFileURI(XML_EXTENSION);

  protected Map<String, Resource.Factory> registrations;

  protected Map<Object, Object> loadOptions = new HashMap<Object, Object>();

  protected Map<Object, Object> saveOptions = new HashMap<Object, Object>();

  protected ExtendedMetaData extendedMetaData;

  protected EPackage.Registry registry;

  /**
   * Protected constructor - should be used for in statically generated models, 
   * or in the case schema(s) is known when XMLProcessor is created
   */
  protected XMLProcessor(EPackage.Registry registry)
  {
    this.registry = registry;
    this.extendedMetaData = createExtendedMetaData();
    ecoreBuilder = createEcoreBuilder();
    loadOptions.put(XMLResource.OPTION_EXTENDED_META_DATA, extendedMetaData);
    loadOptions.put(XMLResource.OPTION_USE_PARSER_POOL, new XMLParserPoolImpl(true));
    loadOptions.put(XMLResource.OPTION_USE_XML_NAME_TO_FEATURE_MAP, new HashMap<String, EStructuralFeature>());
    loadOptions.put(XMLResource.OPTION_USE_DEPRECATED_METHODS, Boolean.FALSE);
    loadOptions.put(XMLResource.OPTION_CONFIGURATION_CACHE, Boolean.TRUE);
    saveOptions.put(XMLResource.OPTION_EXTENDED_META_DATA, extendedMetaData);
    saveOptions.put(XMLResource.OPTION_USE_CACHED_LOOKUP_TABLE, new ArrayList<Object>()); // TODO
    saveOptions.put(XMLResource.OPTION_CONFIGURATION_CACHE, Boolean.TRUE);
  }

  /**
   * Use this constructor to create an XML processor that is not aware of any schema.
   * Use {@link org.eclipse.emf.ecore.xmi.XMLOptions} to configure this processor to 
   * process, for example, schemaLocation/noNamespaceSchema location attributes.
   */
  public XMLProcessor()
  {
    this.extendedMetaData = createExtendedMetaData();
    ecoreBuilder = createEcoreBuilder();
    // register default options
    loadOptions.put(XMLResource.OPTION_EXTENDED_META_DATA, extendedMetaData);
    loadOptions.put(XMLResource.OPTION_USE_PARSER_POOL, new XMLParserPoolImpl(true));
    loadOptions.put(XMLResource.OPTION_USE_DEPRECATED_METHODS, Boolean.FALSE);
    loadOptions.put(XMLResource.OPTION_CONFIGURATION_CACHE, Boolean.TRUE);
    saveOptions.put(XMLResource.OPTION_EXTENDED_META_DATA, extendedMetaData);
    saveOptions.put(XMLResource.OPTION_CONFIGURATION_CACHE, Boolean.TRUE);
  }

  /*
   * Create an XML processor that is aware of XML Schema specified.
   * @param schemaURIs - a URI {@link org.eclipse.common.util.URI} which point to an XML Schema
   */
  public XMLProcessor(URI schemaURI) throws SAXException
  {
    this(Collections.singleton(schemaURI));
  }

  /**
   * Create an XML processor that is aware of XML Schemas specified.
   * @param schemaURIs - a list of URI {@link URI} which point to XML Schemas
   * @throws SAXException
   */
  public XMLProcessor(Collection<URI> schemaURIs) throws SAXException
  {
    this(new EPackageRegistryImpl());
    loadOptions.put(XMLResource.OPTION_USE_ENCODED_ATTRIBUTE_STYLE, Boolean.TRUE);
    loadOptions.put(XMLResource.OPTION_USE_LEXICAL_HANDLER, Boolean.TRUE);
    saveOptions.put(XMLResource.OPTION_USE_ENCODED_ATTRIBUTE_STYLE, Boolean.TRUE); 
    saveOptions.put(XMLResource.OPTION_SCHEMA_LOCATION, Boolean.TRUE);
    try
    {
      for (Resource resource : ecoreBuilder.generate(schemaURIs))
      {
        for (EPackage ePackage : EcoreUtil.<EPackage>getObjectsByType(resource.getContents(), EcorePackage.Literals.EPACKAGE))
        {
          EcoreUtil.freeze(ePackage);
        }
      }
    }
    catch (InvocationTargetException ie)
    {
      throw new SAXException((Exception)ie.getTargetException());
    }
    catch (Exception e)
    {
      throw new SAXException(e);
    }
  }

  protected Map<String, Resource.Factory> getRegistrations()
  {
    if (registrations == null)
    {
      Map<String, Resource.Factory> result = new HashMap<String, Resource.Factory>();
      result.put(STAR_EXTENSION, new XMLResourceFactoryImpl());
      registrations = result;
    }

    return registrations;
  }

  /**
   * @return a map of namespace URI to EPackage. This registry should be treated as read-only. The registry
   * does not include packages loaded on demand (@see #getExtendedMetaData to retrieve demand loaded packages)
   */
  public EPackage.Registry getEPackageRegistry()
  {
    return registry;
  }

  /**
   * @return The ExtendedMetaData used by XMLProcessor to register all Ecore packages. 
   * The ExtendedMetaData can be used to retieve information about the model (e.g. @see ExtendedMetaData#getElement(String, String)).
   * The ExtendedMetaData should be treated as read-only.
   */
  public ExtendedMetaData getExtendedMetaData()
  {
    return extendedMetaData;
  }

  /**
   * Given a system identifier and option, this methods creates an EMF Resource 
   * (using URI#createURI method) and loads the resource data.
   * @param systemId - system identifier
   * @param options - options map
   * @return Loaded resource
   * @throws IOException
   * @see org.eclipse.emf.ecore.resource.Resource
   * @see org.eclipse.emf.common.util.URI
   */
  
  public Resource load(String systemId, Map<?, ?> options) throws IOException
  {
    ResourceSet resourceSet = createResourceSet();
    XMLResource resource = (XMLResource)resourceSet.createResource(URI.createURI(systemId));
    InputSource inputSource = new InputSource();
    inputSource.setSystemId(systemId);
    if (options != null)
    {
      Map<Object, Object> mergedOptions = new HashMap<Object, Object>(loadOptions);
      mergedOptions.putAll(options);
      resource.load(inputSource, mergedOptions);
    }
    else
    {
      resource.load(inputSource, loadOptions);
    }
    resourceSet.getPackageRegistry().putAll(registry);
    return resource;
  }

  public Resource load(InputStream is, Map<?, ?> options) throws IOException
  {
    ResourceSet resourceSet = createResourceSet();
    Resource resource = resourceSet.createResource(XML_URI);
    if (options != null)
    {
      Map<Object, Object> mergedOptions = new HashMap<Object, Object>(loadOptions);
      mergedOptions.putAll(options);
      resource.load(is, mergedOptions);
    }
    else
    {
      resource.load(is, loadOptions);
    }
    resourceSet.getPackageRegistry().putAll(registry);
    return resource;
  }

  public Resource load(InputSource inputSource, Map<?, ?> options) throws IOException
  {
    ResourceSet resourceSet = createResourceSet();
    XMLResource resource = (XMLResource)resourceSet.createResource(XML_URI);
    if (options != null)
    {
      Map<Object, Object> mergedOptions = new HashMap<Object, Object>(loadOptions);
      mergedOptions.putAll(options);
      resource.load(inputSource, mergedOptions);
    }
    else
    {
      resource.load(inputSource, loadOptions);
    }
    resourceSet.getPackageRegistry().putAll(registry);
    return resource;
  }

  public Resource load(Node node, Map<?, ?> options) throws IOException
  {
    ResourceSet resourceSet = createResourceSet();
    XMLResource resource = (XMLResource)resourceSet.createResource(XML_URI);
    if (options != null)
    {
      Map<Object, Object> mergedOptions = new HashMap<Object, Object>(loadOptions);
      mergedOptions.putAll(options);
      resource.load(node, mergedOptions);
    }
    else
    {
      resource.load(node, loadOptions);
    }
    resourceSet.getPackageRegistry().putAll(registry);
    return resource;
  }

  public void save(OutputStream outputStream, Resource resource, Map<?, ?> options) throws IOException
  {
    if (options != null)
    {
      Map<Object, Object> mergedOptions = new HashMap<Object, Object>(saveOptions);
      mergedOptions.putAll(options);
      resource.save(outputStream, mergedOptions);
    }
    else
    {
      resource.save(outputStream, saveOptions);
    }
  }
  
  public void save(Writer writer, Resource resource, Map<?, ?> options) throws IOException
  {
    if (options != null)
    {
      Map<Object, Object> mergedOptions = new HashMap<Object, Object>(saveOptions);
      mergedOptions.putAll(options);
      ((XMLResource)resource).save(writer, mergedOptions);
    }
    else
    {
      ((XMLResource)resource).save(writer, saveOptions);
    }
  }

  public void save(Document document, Resource resource, DOMHandler handler, Map<?, ?> options) throws IOException
  {
    if (options != null)
    {
      Map<Object, Object> mergedOptions = new HashMap<Object, Object>(saveOptions);
      mergedOptions.putAll(options);
      ((XMLResource)resource).save(document, mergedOptions, handler);
    }
    else
    {
      ((XMLResource)resource).save(document, saveOptions, handler);
    }
  }
  
  public String saveToString(Resource resource, Map<?, ?> options) throws IOException
  {
    Map<Object, Object> mergedOptions = new HashMap<Object, Object>(saveOptions);
    if (options != null)
    {
      mergedOptions.putAll(options);
    }
    ByteArrayOutputStream os = new ByteArrayOutputStream();
    ((XMLResource)resource).save(os, mergedOptions);
    final Object encoding = mergedOptions.get(XMLResource.OPTION_ENCODING);
    if (encoding != null && encoding instanceof String)
    {
        return os.toString((String) encoding);
    }
    else
    {
        return os.toString();
    }
  }

  protected ResourceSet createResourceSet()
  {
    ResourceSet resourceSet = new ResourceSetImpl();
    resourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap().putAll(getRegistrations());
    return resourceSet;
  }

  protected EcoreBuilder createEcoreBuilder()
  {
    return new DefaultEcoreBuilder(extendedMetaData);
  }

  /**
   * This methods can be used to provide a different ExtendedMetaData. 
   * Note that if this method creates a new EPackage.Registry it must also assign the global registry
   * variable.
   * @return ExtendedMetaData
   */
  protected ExtendedMetaData createExtendedMetaData()
  {
    if (registry == null)
    {
      registry = new EPackageRegistryImpl();
    }
    return new BasicExtendedMetaData(registry);
  }
}
