/**
 * Copyright (c) 2002-2012 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.resource.impl;


import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import org.eclipse.emf.common.util.BasicEList;
import org.eclipse.emf.common.util.Callback;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.ContentHandler;
import org.eclipse.emf.ecore.resource.URIConverter;
import org.eclipse.emf.ecore.resource.URIHandler;


/**
 * A highly functional and extensible URI converter implementation.
 * <p>
 * This implementation provides seamless transparent Eclipse integration
 * by supporting the <code>platform:/resource</code> mechanism both inside of Eclipse and outside of Eclipse.
 * Furthermore, although the implementation imports
 * both {@code org.eclipse.core.runtime} and {@code org.eclipse.core.resources},
 * and hence requires the Eclipse libraries at development time,
 * the implementation does <b>not</b> require them at runtime.
 * Clients of this implementation must be cautious if they wish to maintain this platform neutral behaviour.
 * </p>
 */
public class ExtensibleURIConverterImpl implements URIConverter
{
  /**
   * A map that remaps URIs.
   */
  public interface URIMap extends Map<URI, URI>
  {
    /**
     * Returns the remapped URI, or the URI itself.
     * @param uri the URI to remap.
     * @return the remapped URI, or the URI itself.
     */
    URI getURI(URI uri);
  }
 
  protected static class URIHandlerList extends BasicEList<URIHandler>
  {
    private static final long serialVersionUID = 1L;

    public URIHandlerList()
    {
      super();
    }

    @Override
    protected boolean canContainNull()
    {
      return false;
    }

    @Override
    protected Object [] newData(int capacity)
    {
      return new URIHandler [capacity];
    }

    @Override
    public URIHandler [] data()
    {
      return (URIHandler[])data;
    }
  }
 
  protected URIHandlerList uriHandlers;
 
  protected static class ContentHandlerList extends BasicEList<ContentHandler>
  {
    private static final long serialVersionUID = 1L;

    public ContentHandlerList()
    {
      super();
    }

    @Override
    protected boolean canContainNull()
    {
      return false;
    }

    @Override
    protected Object [] newData(int capacity)
    {
      return new ContentHandler [capacity];
    }

    @Override
    public ContentHandler [] data()
    {
      return (ContentHandler[])data;
    }
  }
 
  protected ContentHandlerList contentHandlers;

  /**
   * The URI map.
   */
  protected URIMap uriMap;

  /**
   * Creates an instance.
   */
  public ExtensibleURIConverterImpl()
  {
    this(URIHandler.DEFAULT_HANDLERS, ContentHandler.Registry.INSTANCE.contentHandlers());
  }

  /**
   * Creates an instance.
   */
  public ExtensibleURIConverterImpl(Collection<URIHandler> uriHandlers, Collection<ContentHandler> contentHandlers)
  {
    getURIHandlers().addAll(uriHandlers);
    getContentHandlers().addAll(contentHandlers);
  }

  public EList<URIHandler> getURIHandlers()
  {
    if (uriHandlers == null)
    {
      uriHandlers = new URIHandlerList();
    }
    return uriHandlers;
  }

  public URIHandler getURIHandler(URI uri)
  {
    int size = uriHandlers.size();
    if (size > 0)
    {
      URIHandler[] data = uriHandlers.data();
      for (int i = 0; i < size; ++i)
      {
        URIHandler uriHandler = data[i];
        if (uriHandler.canHandle(uri))
        {
          return uriHandler;
        }
      }
    }
    throw new RuntimeException("There is no URIHandler to handle " + uri);
  }

  public EList<ContentHandler> getContentHandlers()
  {
    if (contentHandlers == null)
    {
      contentHandlers = new ContentHandlerList();
    }
    return contentHandlers;
  }

  public OutputStream createOutputStream(URI uri) throws IOException
  {
    return createOutputStream(uri, null);
  }

  public void store(URI uri, byte[] bytes, Map<?, ?> options, Callback<Map<?, ?>> callback)
  {
    URI normalizedURI = normalize(uri);
    getURIHandler(normalizedURI).store(normalizedURI, bytes, new OptionsMap(OPTION_URI_CONVERTER, this, options), callback);
  }

  public void delete(URI uri, Map<?, ?> options, Callback<Map<?, ?>> callback)
  {
    URI normalizedURI = normalize(uri);
    getURIHandler(normalizedURI).delete(normalizedURI, new OptionsMap(OPTION_URI_CONVERTER, this, options), callback);
  }

  static class OptionsMap implements Map<Object, Object>
  {
    protected Object key;
    protected Object value;
    protected Map<?, ?> options;
    protected Map<Object, Object> mergedMap;

    public OptionsMap(Object key, Object value, Map<?, ?> options)
    {
      this.options = options == null ? Collections.EMPTY_MAP : options;
      this.key = key;
      this.value = value;
    }

    protected Map<Object, Object> mergedMap()
    {
      if (mergedMap == null)
      {
        mergedMap = new LinkedHashMap<Object, Object>(options);
        mergedMap.put(key, value);
      }
      return mergedMap;
    }

    public void clear()
    {
      throw new UnsupportedOperationException();
    }

    public boolean containsKey(Object key)
    {
      return this.key == key || this.key.equals(key) || options.containsKey(key);
    }

    public boolean containsValue(Object value)
    {
      return this.value == value || options.containsValue(value);
    }

    public Set<Map.Entry<Object, Object>> entrySet()
    {
      return mergedMap().entrySet();
    }

    public Object get(Object key)
    {
      return this.key == key || this.key.equals(key) ? value : options.get(key);
    }

    public boolean isEmpty()
    {
      return false;
    }

    public Set<Object> keySet()
    {
      return mergedMap().keySet();
    }

    public Object put(Object key, Object value)
    {
      throw new UnsupportedOperationException();
    }

    public void putAll(Map<? extends Object, ? extends Object> t)
    {
      throw new UnsupportedOperationException();
    }

    public Object remove(Object key)
    {
      throw new UnsupportedOperationException();
    }

    public int size()
    {
      return mergedMap().size();
    }

    public Collection<Object> values()
    {
      return mergedMap().values();
    }

    @Override
    public int hashCode()
    {
      return mergedMap().hashCode();
    }

    @Override
    public boolean equals(Object o)
    {
      return mergedMap().equals(o);
    }
  }

  public OutputStream createOutputStream(URI uri, Map<?, ?> options) throws IOException
  {
    URI normalizedURI = normalize(uri);
    return getURIHandler(normalizedURI).createOutputStream(normalizedURI, new OptionsMap(OPTION_URI_CONVERTER, this, options));
  }

  public InputStream createInputStream(URI uri) throws IOException
  {
    return createInputStream(uri, null);
  }

  public InputStream createInputStream(URI uri, Map<?, ?> options) throws IOException
  {
    URI normalizedURI = normalize(uri);
    return getURIHandler(normalizedURI).createInputStream(normalizedURI, new OptionsMap(OPTION_URI_CONVERTER, this, options));
  }

  public void createInputStream(URI uri, Map<?, ?> options, Callback<Map<?, ?>> callback)
  {
    URI normalizedURI = normalize(uri);
    getURIHandler(normalizedURI).createInputStream(normalizedURI, new OptionsMap(OPTION_URI_CONVERTER, this, options), callback);
  }

  public void delete(URI uri, Map<?, ?> options) throws IOException
  {
    URI normalizedURI = normalize(uri);
    getURIHandler(normalizedURI).delete(normalizedURI, new OptionsMap(OPTION_URI_CONVERTER, this, options));
  }

  public Map<String, ?> contentDescription(URI uri, Map<?, ?> options) throws IOException
  {
    URI normalizedURI = normalize(uri);
    return getURIHandler(normalizedURI).contentDescription(normalizedURI, new OptionsMap(OPTION_URI_CONVERTER, this, options));
  }

  public boolean exists(URI uri, Map<?, ?> options)
  {
    URI normalizedURI = normalize(uri);
    return getURIHandler(normalizedURI).exists(normalizedURI, new OptionsMap(OPTION_URI_CONVERTER, this, options));
  }

  public void exists(URI uri, Map<?, ?> options, Callback<Boolean> callback)
  {
    URI normalizedURI = normalize(uri);
    getURIHandler(normalizedURI).exists(normalizedURI, new OptionsMap(OPTION_URI_CONVERTER, this, options), callback);
  }

  public Map<String, ?> getAttributes(URI uri, Map<?, ?> options)
  {
    URI normalizedURI = normalize(uri);
    return getURIHandler(normalizedURI).getAttributes(normalizedURI, new OptionsMap(OPTION_URI_CONVERTER, this, options));
  }

  public void setAttributes(URI uri, Map<String, ?> attributes, Map<?, ?> options) throws IOException
  {
    URI normalizedURI = normalize(uri);
    getURIHandler(normalizedURI).setAttributes(normalizedURI, attributes, new OptionsMap(OPTION_URI_CONVERTER, this, options));
  }

  /**
   * Returns the normalized form of the URI.
   * <p>
   * This implementation does precisely and only the {@link URIConverter#normalize typical} thing.
   * It calls itself recursively so that mapped chains are followed.
   * </p>
   * @param uri the URI to normalize.
   * @return the normalized form.
   * @see org.eclipse.emf.ecore.plugin.EcorePlugin#getPlatformResourceMap
   */
  public URI normalize(URI uri)
  {
    String fragment = uri.fragment();
    String query = uri.query();
    URI result = getInternalURIMap().getURI(uri.trimFragment().trimQuery());
    String scheme = result.scheme();
    if (scheme == null)
    {
      if (result.hasAbsolutePath())
      {
        result = URI.createURI("file:" + result);
      }
      else
      {
        result = URI.createFileURI(result.toString());
      }
    }
    if (fragment != null)
    {
      result = result.appendFragment(fragment);
    }
    if (query != null)
    {
      result = result.appendQuery(query);
    }

    if (result.equals(uri))
    {
      return uri;
    }
    else
    {
      return normalize(result);
    }
  }

  /*
   * Javadoc copied from interface.
   */
  public Map<URI, URI> getURIMap()
  {
    return getInternalURIMap();
  }

  /**
   * Returns the internal version of the URI map.
   * @return the internal version of the URI map.
   */
  protected URIMap getInternalURIMap()
  {
    if (uriMap == null)
    {
      URIMappingRegistryImpl mappingRegistryImpl =
        new URIMappingRegistryImpl()
        {
          private static final long serialVersionUID = 1L;

          @Override
          protected URI delegatedGetURI(URI uri)
          {
            return URIMappingRegistryImpl.INSTANCE.getURI(uri);
          }
        };

      uriMap = (URIMap)mappingRegistryImpl.map();
    }

    return uriMap;
  }
}
