/**
 * Copyright (c) 2005-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.edit.ui.util;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IStorage;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.ui.IEditorDescriptor;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorRegistry;
import org.eclipse.ui.IPathEditorInput;
import org.eclipse.ui.IStorageEditorInput;
import org.eclipse.ui.IURIEditorInput;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.emf.common.EMFPlugin;
import org.eclipse.emf.common.ui.URIEditorInput;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.common.util.UniqueEList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.URIConverter;
import org.eclipse.emf.ecore.resource.URIHandler;
import org.eclipse.emf.ecore.resource.impl.URIHandlerImpl;
import org.eclipse.emf.edit.ui.EMFEditUIPlugin;
import org.osgi.framework.Bundle;

/**
 * @since 2.2.0
 */
public class EditUIUtil
{
  /**
   * Opens the default editor for the resource that contains the specified
   * EObject.  This method only works if the resource's URI is a 
   * platform resource URI.
   */
  public static boolean openEditor(EObject eObject) throws PartInitException
  {
    if (eObject != null)
    {
      Resource resource = eObject.eResource();
      if (resource != null)
      {
        URI uri = resource.getURI();
        if (uri != null)
        {
          IEditorInput editorInput = null;
          if (uri.isPlatformResource())
          {
            String path = uri.toPlatformString(true);
            IResource workspaceResource = ResourcesPlugin.getWorkspace().getRoot().findMember(new Path(path));
            if (workspaceResource instanceof IFile)
            {
              editorInput = EclipseUtil.createFileEditorInput((IFile)workspaceResource);
            }
          }
          else
          {
            editorInput = new URIEditorInput(uri);
          }
          if (editorInput != null)
          {
            IWorkbench workbench = PlatformUI.getWorkbench();
            IWorkbenchPage page = workbench.getActiveWorkbenchWindow().getActivePage();
            IEditorPart editorPart = page.openEditor(editorInput, workbench.getEditorRegistry().getDefaultEditor(uri.lastSegment()).getId());
            return editorPart != null;
          }
        }
      }
    }
    return false;
  }

  public static URI getURI(IEditorInput editorInput)
  {
    return getURI(editorInput, null);
  }

  /**
   * @since 2.11
   */
  public static URI getURI(IEditorInput editorInput, URIConverter uriConverter)
  {
    URI result = null;
    if (EMFPlugin.IS_ECLIPSE_RUNNING)
    {
      result = EclipseUtil.getURI(editorInput, uriConverter);
    }
    if (result == null)
    {
      if (editorInput instanceof URIEditorInput)
      {
        result = ((URIEditorInput)editorInput).getURI().trimFragment();
      }
      else
      {
        result = URI.createURI(editorInput.getName());
      }
    }
    return result;
  }

  /**
   * Returns the default editor for a given file name. This method is like {@link IEditorRegistry#getDefaultEditor(String)},
   * but it will not return <code>null</code> unless all applicable content types have no associated editor.
   * 
   * @param fileName the file name in the system
   * @return the descriptor of the default editor, or <code>null</code> if not found
   * @since 2.4
   */
  public static IEditorDescriptor getDefaultEditor(String fileName)
  {
    return fileName != null && fileName.length() != 0 ?
      getDefaultEditor(fileName, Platform.getContentTypeManager().findContentTypesFor(fileName)) :
      null;
  }

  /**
   * Returns the default editor for a given contents and file name.
   * <p>
   * If a file name is not provided, the entire content type registry will be queried. For performance reasons, it is
   * highly recommended to provide a file name if available.
   *
   * @param contents an input stream
   * @param fileName the file name associated to the contents, or <code>null</code> 
   * @return the descriptor of the default editor, or <code>null</code> if not found
   * @since 2.4
   */ 
  public static IEditorDescriptor getDefaultEditor(InputStream contents, String fileName)
  {
    if (contents != null)
    {
      try
      {
        return getDefaultEditor(fileName, Platform.getContentTypeManager().findContentTypesFor(contents, fileName));
      }
      catch (IOException e)
      {
        EMFEditUIPlugin.INSTANCE.log(e);
      }
    }
    return getDefaultEditor(fileName);
  }

  private static IEditorDescriptor getDefaultEditor(String fileName, IContentType[] contentTypes)
  {
    IEditorRegistry editorRegistry = PlatformUI.getWorkbench().getEditorRegistry();

    if (contentTypes.length == 0)
    {
      return editorRegistry.getDefaultEditor(fileName, null);
    }

    IEditorDescriptor result = null;
    for (int i = 0; result == null && i < contentTypes.length; i++)
    {
      result = editorRegistry.getDefaultEditor(fileName, contentTypes[i]);
    }
    return result;
  }

  /**
   * Returns the default editor for a given URI. This method actually attempts to open an input stream for the URI and
   * uses its contents, along with the filename (the URI's last segment), to obtain appropriate content types.
   * <p>
   * If a URI converter is specified, it is used to open the stream. Otherwise, the global default
   * {@link URIConverter#INSTANCE instance} is used.
   *
   * @param uri a URI
   * @param uriConverter URI converter from which to obtain an input stream, or <code>null</code>
   * @return the descriptor of the default editor, or <code>null</code> if not found
   * @since 2.4
   */ 
  public static IEditorDescriptor getDefaultEditor(URI uri, URIConverter uriConverter)
  {
    String fileName = URI.decode(uri.lastSegment());
    if (uriConverter == null)
    {
      uriConverter = URIConverter.INSTANCE;
    }
    InputStream stream = null;

    try
    {
      stream = uriConverter.createInputStream(uri);
      return getDefaultEditor(stream, fileName);
    }
    catch (IOException e)
    {
      EMFEditUIPlugin.INSTANCE.log(e);
      return getDefaultEditor(fileName);
    }
    finally
    {
      close(stream);
    }
  }

  private static void close(InputStream stream)
  {
    if (stream != null)
    {
      try
      {
        stream.close();
      }
      catch (IOException e) { EMFEditUIPlugin.INSTANCE.log(e); }
    }
  }

  /**
   * Returns the editors for a given file name. This method is like {@link IEditorRegistry#getEditors(String)},
   * but its result will include editors for all applicable content types.
   * 
   * @param fileName the file name in the system
   * @param defaultsOnly if <code>true</code>, only the default editor for each content type will be included in the result 
   * @return the descriptors of the editors
   * @since 2.4
   */
  public static IEditorDescriptor[] getEditors(String fileName, boolean defaultsOnly)
  {
    return fileName != null && fileName.length() != 0 ?
      getEditors(fileName, Platform.getContentTypeManager().findContentTypesFor(fileName), defaultsOnly) :
      new IEditorDescriptor[0];
  }

  /**
   * Returns the editors for a given contents and file name.
   * <p>
   * If a file name is not provided, the entire content type registry will be queried. For performance reasons, it is
   * highly recommended to provide a file name if available.
   * 
   * @param contents an input stream
   * @param fileName the file name associated to the contents, or <code>null</code> 
   * @param defaultsOnly if <code>true</code>, only the default editor for each content type will be included in the result 
   * @return the descriptors of the editors
   * @since 2.4
   */
  public static IEditorDescriptor[] getEditors(InputStream contents, String fileName, boolean defaultsOnly)
  {
    if (contents != null)
    {
      try
      {
        return getEditors(fileName, Platform.getContentTypeManager().findContentTypesFor(contents, fileName), defaultsOnly);
      }
      catch (IOException e)
      {
        EMFEditUIPlugin.INSTANCE.log(e);
      }
    }    
    return getEditors(fileName, defaultsOnly);
  }

  private static IEditorDescriptor[] getEditors(String fileName, IContentType[] contentTypes, boolean defaultsOnly)
  {
    IEditorRegistry editorRegistry = PlatformUI.getWorkbench().getEditorRegistry();

    if (contentTypes.length == 0)
    {
      return editorRegistry.getEditors(fileName, null);
    }

    List<IEditorDescriptor> result = new UniqueEList<IEditorDescriptor>();
    for (IContentType contentType : contentTypes)
    {
      if (defaultsOnly)
      {
        IEditorDescriptor editor = editorRegistry.getDefaultEditor(fileName, contentType);
        if (editor != null)
        {
          result.add(editor);
        }
      }
      else
      {
        result.addAll(Arrays.asList(editorRegistry.getEditors(fileName, contentType)));        
      }
    }
    return result.toArray(new IEditorDescriptor[result.size()]);
  }

  /**
   * Returns the editors for a given URI. This method actually attempts to open an input stream for the URI and
   * uses its contents, along with the filename (the URI's last segment), to obtain appropriate content types.
   * <p>
   * If a URI converter is specified, it is used to open the stream. Otherwise, the global default
   * {@link URIConverter#INSTANCE instance} is used.
   *
   * @param uri a URI
   * @param uriConverter a URI converter from which to obtain an input stream, or <code>null</code>
   * @param defaultsOnly if <code>true</code>, only the default editor for each content type will be included in the result 
   * @return the descriptors of the editors
   * @since 2.4
   */ 
  public static IEditorDescriptor[] getEditors(URI uri, URIConverter uriConverter, boolean defaultsOnly)
  {
    String fileName = URI.decode(uri.lastSegment());
    if (uriConverter == null)
    {
      uriConverter = URIConverter.INSTANCE;
    }
    InputStream stream = null;

    try
    {
      stream = uriConverter.createInputStream(uri);
      return getEditors(stream, fileName, defaultsOnly);
    }
    catch (IOException e)
    {
      EMFEditUIPlugin.INSTANCE.log(e);
      return getEditors(fileName, defaultsOnly);
    }
    finally
    {
      close(stream);
    }
  }

  private static class StorageURIHandler extends URIHandlerImpl
  {
    final private URI uri;
    final private IStorage storage;

    private StorageURIHandler(URI uri, IStorage storage)
    {
      this.uri = uri;
      this.storage = storage;
    }

    @Override
    public boolean canHandle(URI uri)
    {
      return uri.equals(this.uri);
    }

    @Override
    public InputStream createInputStream(URI uri, Map<?, ?> options) throws IOException
    {
      try
      {
        return storage.getContents();
      }
      catch (CoreException exception)
      {
        throw new Resource.IOWrappedException(exception);
      }
    }
    
    @Override
    public boolean exists(URI uri, Map<?, ?> options)
    {
      return true;
    }

    @Override
    public Map<String, ?> getAttributes(URI uri, Map<?, ?> options)
    {
      Map<String, Object> result = new HashMap<String, Object>();
      Set<String> requestedAttributes = getRequestedAttributes(options);
      if (requestedAttributes == null || requestedAttributes.contains(URIConverter.ATTRIBUTE_READ_ONLY))
      {
          result.put(URIConverter.ATTRIBUTE_READ_ONLY, storage.isReadOnly());
      }

      return result;
    }
  }

  private static class EclipseUtil
  {
    static final Class<?> FILE_CLASS;
    static
    {
      Class<?> fileClass = null;
      try
      {
        fileClass = IFile.class;
      }
      catch (Throwable exception)
      {
        // Ignore any exceptions and assume the class isn't available.
      }
      FILE_CLASS = fileClass;
    }

    static final Class<?> FILE_REVISION_CLASS;
    static final Method FILE_REVISION_GET_URI_METHOD;
    static final Method FILE_REVISION_GET_STORAGE;
    static
    {
      FILE_REVISION_CLASS = loadClass("org.eclipse.team.core", "org.eclipse.team.core.history.IFileRevision");
      FILE_REVISION_GET_URI_METHOD = getMethod(FILE_REVISION_CLASS, "getURI");
      FILE_REVISION_GET_STORAGE = getMethod(FILE_REVISION_CLASS, "getStorage", IProgressMonitor.class);
    }

    static final Class<?> URI_EDITOR_INPUT_CLASS;
    static
    {
      Class<?> uriEditorInputClass = null;
      try
      {
        uriEditorInputClass = IURIEditorInput.class;
      }
      catch (Throwable exception)
      {
        // The class is not available.
      }
      URI_EDITOR_INPUT_CLASS = uriEditorInputClass;
    }

    static final Class<?> STORAGE_CLASS;
    static
    {
      Class<?> storageClass = null;
      try
      {
        storageClass = IStorage.class;
      }
      catch (Throwable exception)
      {
        // Ignore any exceptions and assume the class isn't available.
      }
      STORAGE_CLASS = storageClass;
    }

    static final Class<?> STORAGE_EDITOR_INPUT_CLASS;
    static
    {
      Class<?> storageEditorInputClass = null;
      try
      {
        storageEditorInputClass = IStorageEditorInput.class;
      }
      catch (Throwable exception)
      {
        // The class is not available.
      }
      STORAGE_EDITOR_INPUT_CLASS = storageEditorInputClass;
    }

    static final Class<?> JAR_ENTRY_RESOURCE_CLASS;
    static final Method JAR_ENTRY_RESOURCE_GET_PACKAGE_FRAGMENT_ROOT;
    static
    {
      JAR_ENTRY_RESOURCE_CLASS = loadClass("org.eclipse.jdt.core", "org.eclipse.jdt.core.IJarEntryResource");
      JAR_ENTRY_RESOURCE_GET_PACKAGE_FRAGMENT_ROOT = getMethod(JAR_ENTRY_RESOURCE_CLASS, "getPackageFragmentRoot");
    }

    static final Class<?> NON_JAVA_RESOURCE_CLASS = loadClass("org.eclipse.jdt.core", "org.eclipse.jdt.internal.core.NonJavaResource");
    static final Field NON_JAVA_RESOURCE_FIELD;
    static
    {
      Field nonJavaResourceField = null;
      if (NON_JAVA_RESOURCE_CLASS != null)
      {
        try
        {
          nonJavaResourceField = NON_JAVA_RESOURCE_CLASS.getDeclaredField("resource");
          nonJavaResourceField.setAccessible(true);
        }
        catch (Throwable throwable)
        {
          // Ignore.
        }
      }
      NON_JAVA_RESOURCE_FIELD = nonJavaResourceField;
    }

    private static Class<?> loadClass(String bundleID, String className)
    {
      Bundle bundle = Platform.getBundle(bundleID);
      if (bundle != null && (bundle.getState() & (Bundle.ACTIVE | Bundle.STARTING | Bundle.RESOLVED)) != 0)
      {
        try
        {
          return bundle.loadClass(className);
        }
        catch (Throwable exeption)
        {
          // Ignore any exceptions and assume the class isn't available.
        }
      }
      return null;
    }

    private static Method getMethod(Class<?> theClass, String name, Class<?>... parameterTypes)
    {
      try
      {
        return theClass == null ? null : theClass.getMethod(name, parameterTypes);
      }
      catch (Throwable exception)
      {
        return null;
      }
    }

    private static Object invoke(Method method, Object object, Object... arguments)
    {
      try
      {
        return method == null ? null : method.invoke(object, arguments);
      }
      catch (Throwable throwable)
      {
        return null;
      }
    }

    private static Object invoke(String methodName, Object object)
    {
      if (object != null)
      {
        Method method = getMethod(object.getClass(), methodName);
        if (method != null)
        {
          return invoke(method, object);
        }
      }
      return null;
    }

    public static URI getURI(IEditorInput editorInput, URIConverter uriConverter)
    {
      if (FILE_CLASS != null)
      {
        IFile file = (IFile)editorInput.getAdapter(FILE_CLASS);
        if (file != null)
        {
          return URI.createPlatformResourceURI(file.getFullPath().toString(), true);
        }
      }

      if (FILE_REVISION_CLASS != null)
      {
        Object fileRevision = editorInput.getAdapter(FILE_REVISION_CLASS);
        if (fileRevision != null)
        {
          try
          {
            URI uri = URI.createURI(((java.net.URI)FILE_REVISION_GET_URI_METHOD.invoke(fileRevision)).toString());
            IStorage storage = (IStorage)invoke(FILE_REVISION_GET_STORAGE, fileRevision, new NullProgressMonitor());
            if (storage != null)
            {
              registerHandler(uri, storage, uriConverter);
            }
            return uri;
          }
          catch (Throwable exception)
          {
            EMFEditUIPlugin.INSTANCE.log(exception);
          }
        }
      }

      if (STORAGE_CLASS != null)
      {
        IStorage storage = (IStorage)editorInput.getAdapter(STORAGE_CLASS);
        URI uri = handleStorage(storage, uriConverter);
        if (uri != null)
        {
          return uri;
        }
      }

      if (editorInput instanceof IPathEditorInput)
      {
        IPathEditorInput pathEditorInput = (IPathEditorInput)editorInput;
        IPath path = pathEditorInput.getPath();
        if (path != null)
        {
          return URI.createFileURI(path.toString());
        }
      }

      if (STORAGE_EDITOR_INPUT_CLASS != null)
      {
        if (editorInput instanceof IStorageEditorInput)
        {
          IStorageEditorInput storageEditorInput = (IStorageEditorInput)editorInput;
          try
          {
            IStorage storage = storageEditorInput.getStorage();
            URI uri = handleStorage(storage, uriConverter);
            if (uri != null)
            {
              return uri;
            }
          }
          catch (CoreException e)
          {
            // Ignore
          }
        }
      }

      if (URI_EDITOR_INPUT_CLASS != null)
      {
        if (editorInput instanceof IURIEditorInput)
        {
          return URI.createURI(((IURIEditorInput)editorInput).getURI().toString()).trimFragment();
        }
      }

      URI uri = (URI)editorInput.getAdapter(URI.class);
      if (uri == null)
      {
        java.net.URI javaNetURI = (java.net.URI)editorInput.getAdapter(java.net.URI.class);
        if (javaNetURI != null)
        {
          uri = URI.createURI(javaNetURI.toString());
        }
      }

      return uri;
    }

    private static IFile getNonJavaResourceFieldValue(Object object)
    {
      // This is a very ugly workaround for the fact that org.eclipse.jdt.internal.core.NonJavaResource
      // does not return a meaningful entry name relative to its parent.
      // The entry name will just be the file name and the parent will be the package fragment root
      // which can be arbitrarily many folders up from the entry.
      // There are no methods on the implementation to determine the correct path.
      if (NON_JAVA_RESOURCE_CLASS != null && NON_JAVA_RESOURCE_CLASS.isInstance(object))
      {
        try
        {
          return (IFile)NON_JAVA_RESOURCE_FIELD.get(object);
        }
        catch (Throwable throwable)
        {
          return null;
        }
      }
      return null;
    }

    private static URI handleStorage(IStorage storage, URIConverter uriConverter)
    {
      if (storage != null)
      {
        IPath path = storage.getFullPath();
        if (path != null)
        {
          if (JAR_ENTRY_RESOURCE_CLASS != null && JAR_ENTRY_RESOURCE_CLASS.isInstance(storage))
          {
            IFile  file = getNonJavaResourceFieldValue(storage);
            if (file != null)
            {
              return URI.createPlatformResourceURI(file.getFullPath().toString(), true);
            }

            Object packageFragmentRoot = invoke(JAR_ENTRY_RESOURCE_GET_PACKAGE_FRAGMENT_ROOT, storage);
            if (packageFragmentRoot != null)
            {
              IPath archivePath = (IPath)invoke("getPath", packageFragmentRoot);
              if (archivePath != null)
              {
                return URI.createURI("archive:" + URI.createFileURI(archivePath.toString()) + "!" + path);
              }
            }
          }

          URI uri = URI.createURI(path.toString());
          registerHandler(uri, storage, uriConverter);
          return uri;
        }
      }

      return null;
    }

    private static void registerHandler(URI uri, IStorage storage, URIConverter uriConverter)
    {
      if (uriConverter != null)
      {
        EList<URIHandler> uriHandlers = uriConverter.getURIHandlers();
        for (URIHandler uriHandler : uriHandlers)
        {
          if (uriHandler instanceof StorageURIHandler)
          {
            return;
          }
        }

        URI logicalURI = URI.createURI("storage:/").appendSegments(uri.segments());
        uriConverter.getURIMap().put(uri, logicalURI);
        uriHandlers.add(0, new StorageURIHandler(logicalURI, storage));
      }
    }

    public static IEditorInput createFileEditorInput(IFile file)
    {
      return new FileEditorInput(file);
    }
  }
}
