/**
 * 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.common.util;


import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

import org.eclipse.emf.common.CommonPlugin;
import org.eclipse.emf.common.util.ResourceLocator;


/**
 * An abstract resource locator implementation
 * comprising a {@link #getPrimaryResourceLocator() primary locator}
 * and a series {@link #getDelegateResourceLocators() delegate locators}.
 */
public abstract class DelegatingResourceLocator implements ResourceLocator
{
  /**
   * The cached base URL.
   */
  protected URL baseURL;

  /**
   * The resource bundle containing untranslated strings.
   */
  protected ResourceBundle untranslatedResourceBundle;

  /**
   * The resource bundle containing translated strings.
   */
  protected ResourceBundle resourceBundle;

  /**
   * A cache of the translated strings.
   */
  protected Map<String, String> strings = new HashMap<String, String>();

  /**
   * A cache of the untranslated strings.
   */
  protected Map<String, String> untranslatedStrings = new HashMap<String, String>();

  /**
   * A cache of the image descriptions.
   */
  protected Map<String, Object> images = new HashMap<String, Object>();

  /**
   * Whether to translate strings by default.
   */
  protected boolean shouldTranslate = true;

  /**
   * The default localization properties file.
   * @since 2.14
   */
  protected String bundleLocalization = "plugin.properties";

  /**
   * Creates an instance.
   */
  public DelegatingResourceLocator()
  {
    super();
  }

  /**
   * Returns the primary resource locator.
   * @return the primary resource locator.
   */
  protected abstract ResourceLocator getPrimaryResourceLocator();

  /**
   * Returns the delegate resource locators.
   * @return the delegate resource locators.
   */
  protected abstract ResourceLocator [] getDelegateResourceLocators();

  private static final URI DOT = URI.createURI(".");

  /*
   * Javadoc copied from interface.
   */
  public URL getBaseURL()
  {
    if (baseURL == null)
    {
      if (getPrimaryResourceLocator() == null)
      {
        try
        {
          // Determine the base URL by looking for the plugin.properties file in the standard way.
          //
          Class<? extends DelegatingResourceLocator> theClass = getClass();
          URL pluginPropertiesURL = theClass.getResource("plugin.properties");
          if (pluginPropertiesURL == null)
          {
            // If that fails, determine the URL for the class itself.
            // The URL will be of one of the following forms,
            // so there are a few good places to consider looking for the plugin.properties.
            //
            // For a plugin.xml with runtime="common.jar":
            // jar:file:/D:/sandbox/unpackage1-3.1M7/eclipse/plugins/org.eclipse.emf.common/common.jar!/org/eclipse/common/CommonPlugin.class
            //
            // For a plugin.xml with runtime="runtime/common.jar":
            // jar:file:/D:/sandbox/unpackage1-3.1M7/eclipse/plugins/org.eclipse.emf.common/runtime/common.jar!/org/eclipse/common/CommonPlugin.class
            //
            // For a plugin.xml with runtime="." where the plugin is jarred:
            // jar:file:/D:/sandbox/unpackage1-3.1M7/eclipse/plugins/org.eclipse.emf.common.jar!/org/eclipse/common/CommonPlugin.class
            //
            // For a plugin.xml with runtime="." where the plugin is not jarred.
            // file:/D:/sandbox/unpackage1-3.1M7/eclipse/plugins/org.eclipse.emf.common/org/eclipse/emf/common/CommonPlugin.class
            //
            // Running in PDE with bin on classpath:
            // file:/D:/sandbox/unpackage1-3.1M7/eclipse/plugins/org.eclipse.emf.common/bin/org/eclipse/emf/common/CommonPlugin.class
            //
            String className = theClass.getName();
            int index = className.lastIndexOf(".");
            URL classURL = theClass.getResource((index == -1 ? className : className.substring(index + 1)) + ".class");
            URI uri = URI.createURI(classURL.toString());

            // Trim off the segments corresponding to the package nesting.
            //
            int count = 1;
            for (int i = 0; (i = className.indexOf('.', i)) != -1; ++i)
            {
              ++count;
            }
            uri = uri.trimSegments(count);

            // For an archive URI, check for the plugin.properties in the archive.
            //
            if (URI.isArchiveScheme(uri.scheme()))
            {
              try
              {
                String bundleLocalization = getBundleLocalization(new URL(uri.appendSegment("META-INF").appendSegment("MANIFEST.MF").toString()));
                if (bundleLocalization != null)
                {
                  this.bundleLocalization = bundleLocalization;
                }
                baseURL = new URL(uri.toString());
              }
              catch (IOException exception)
              {
                try
                {
                  // If we can open  an input stream, then the plugin.properties is there, and we have a good base URL.
                  //
                  InputStream inputStream =  new URL(uri.appendSegment("plugin.properties").toString()).openStream();
                  inputStream.close();
                  baseURL = new URL(uri.toString());
                }
                catch (IOException nestedException)
                {
                  // If the plugin.properties isn't within the root of the archive,
                  // create a new URI for the folder location of the archive,
                  // so we can look in the folder that contains it.
                  //
                  uri = URI.createURI(uri.authority()).trimSegments(1);
                }
              }
            }

            // If we didn't find the plugin.properties in the usual place nor in the archive...
            //
            if (baseURL == null)
            {
              // Trim off the "bin" or "runtime" segment.
              //
              String lastSegment = uri.lastSegment();
              if ("bin".equals(lastSegment) || "runtime".equals(lastSegment))
              {
                uri = uri.trimSegments(1);
              }

              try
              {
                String bundleLocalization = getBundleLocalization(new URL(uri.appendSegment("META-INF").appendSegment("MANIFEST.MF").toString()));
                if (bundleLocalization != null)
                {
                  this.bundleLocalization = bundleLocalization;
                }
                baseURL = new URL(uri.toString() + "/");
              }
              catch (IOException exception)
              {
                uri = uri.appendSegment("plugin.properties");
                try
                {
                  // If we can open  an input stream, then the plugin.properties is in the folder, and we have a good base URL.
                  //
                  InputStream inputStream = new URL(uri.toString()).openStream();
                  inputStream.close();
                  baseURL = new URL(DOT.resolve(uri).toString());
                }
                catch (IOException nextedException)
                {
                  // We're going to fail.
                }
              }
            }

            // If we still don't have a good base URL, complain about it.
            //
            if (baseURL == null)
            {
              String resourceName =
                index == -1 ?
                  "plugin.properties" :
                  className.substring(0, index + 1).replace('.','/') + "plugin.properties";
              throw new MissingResourceException("Missing properties: " + resourceName, theClass.getName(), "plugin.properties");
            }
          }
          else
          {
            baseURL = new URL(DOT.resolve(URI.createURI(pluginPropertiesURL.toString())).toString());
          }
        }
        catch (IOException exception)
        {
          throw new WrappedException(exception);
        }
      }
      else
      {
        baseURL = getPrimaryResourceLocator().getBaseURL();
      }
    }

    return baseURL;
  }

  /**
   * @since 2.14
   */
  protected String getBundleLocalization(URL manifestURL) throws IOException
  {
    InputStream inputStream =  null;
    try
    {
      inputStream =  manifestURL.openStream();
      Manifest manifest = new Manifest(inputStream);
      Attributes mainAttributes = manifest.getMainAttributes();
      String value = mainAttributes.getValue("Bundle-Localization");
      if (value != null)
      {
        return value + ".properties";
      }
    }
    finally
    {
      if (inputStream != null)
      {
        inputStream.close();
      }
    }
    
    return null;
  }

  /*
   * Javadoc copied from interface.
   */
  public Object getImage(String key)
  {
    Object result = images.get(key);
    if (result == null)
    {
      ResourceLocator pluginResourceLocator = getPrimaryResourceLocator();
      if (pluginResourceLocator == null)
      {
        try
        {
          result = doGetImage(key);
        }
        catch (MalformedURLException exception)
        {
          throw new WrappedException(exception);
        }
        catch (IOException exception)
        {
          result = delegatedGetImage(key);
        }
      }
      else
      {
        try
        {
          result = pluginResourceLocator.getImage(key);
        }
        catch (MissingResourceException exception)
        {
          result = delegatedGetImage(key);
        }
      }

      images.put(key, result);
    }

    return result;
  }

  /**
   * Does the work of fetching the image associated with the key.
   * It ensures that the image exists.
   * @param key the key of the image to fetch.
   * @exception IOException if an image doesn't exist.
   * @return the description of the image associated with the key.
   */
  protected Object doGetImage(String key) throws IOException
  {
    URL url = new URL(getBaseURL() + "icons/" + key + extensionFor(key));
    InputStream inputStream = url.openStream();
    inputStream.close();
    return url;
  }

  /**
   * Computes the file extension to be used with the key to specify an image resource.
   * @param key the key for the imagine.
   * @return the file extension to be used with the key to specify an image resource.
   */
  protected static String extensionFor(String key)
  {    String result = ".gif";
    int index = key.lastIndexOf('.');
    if (index != -1)
    {
      String extension = key.substring(index + 1);
      if ("png".equalsIgnoreCase(extension) || 
            "gif".equalsIgnoreCase(extension) ||
            "bmp".equalsIgnoreCase(extension) ||
            "ico".equalsIgnoreCase(extension) ||
            "jpg".equalsIgnoreCase(extension) ||
            "jpeg".equalsIgnoreCase(extension) ||
            "tif".equalsIgnoreCase(extension) ||
            "tiff".equalsIgnoreCase(extension))
      {
        result = "";
      }
    }
    return result;
  }

  /**
   * Does the work of fetching the image associated with the key,
   * when the image resource is not available locally.
   * @param key the key of the image to fetch.
   * @exception MissingResourceException if the image resource doesn't exist anywhere.
   * @see #getDelegateResourceLocators()
   */
  protected Object delegatedGetImage(String key) throws MissingResourceException
  {
    ResourceLocator[] delegateResourceLocators = getDelegateResourceLocators();
    for (int i = 0; i < delegateResourceLocators.length; ++i)
    {
      try
      {
        return delegateResourceLocators[i].getImage(key);
      }
      catch (MissingResourceException exception)
      {
        // Ignore the exception since we will throw one when all else fails.
      }
    }

    throw
      new MissingResourceException
        (CommonPlugin.INSTANCE.getString("_UI_ImageResourceNotFound_exception", new Object [] { key }),
         getClass().getName(),
         key);
  }

  /**
   * Indicates whether strings should be translated by default.
   *
   * @return <code>true</code> if strings should be translated by default; <code>false</code> otherwise.
   */
  public boolean shouldTranslate()
  {
    return shouldTranslate;
  }

  /**
   * Sets whether strings should be translated by default.
   *
   * @param shouldTranslate whether strings should be translated by default.
   */
  public void setShouldTranslate(boolean shouldTranslate)
  {
    this.shouldTranslate = shouldTranslate;
  }

  /*
   * Javadoc copied from interface.
   */
  public String getString(String key)
  {
    return getString(key, shouldTranslate());
  }

  /*
   * Javadoc copied from interface.
   */
  public String getString(String key, boolean translate)
  {
    Map<String, String> stringMap = translate ? strings : untranslatedStrings;
    String result = stringMap.get(key);
    if (result == null)
    {
      try
      {
        ResourceLocator pluginResourceLocator = getPrimaryResourceLocator();
        if (pluginResourceLocator == null)
        {
          result = doGetString(key, translate);
        }
        else
        {
          result = pluginResourceLocator.getString(key, translate);
        }
      }
      catch (MissingResourceException exception)
      {
        result = delegatedGetString(key, translate);
      }

      stringMap.put(key, result);
    }

    return result;
  }

  /**
   * Does the work of fetching the string associated with the key.
   * It ensures that the string exists.
   * @param key the key of the string to fetch.
   * @exception MissingResourceException if a string doesn't exist.
   * @return the string associated with the key.
   */
  protected String doGetString(String key, boolean translate) throws MissingResourceException
  {
    ResourceBundle bundle = translate ? resourceBundle : untranslatedResourceBundle;
    if (bundle == null)
    {
      String packageName = getClass().getName();
      int index = packageName.lastIndexOf(".");
      if (index != -1)
      {
        packageName = packageName.substring(0, index);
      }
      URL baseURL = getBaseURL();
      if (translate)
      {
        try
        {
          bundle = resourceBundle = ResourceBundle.getBundle(packageName + ".plugin");
        }
        catch (MissingResourceException exception)
        {
          // If the bundle can't be found the normal way, try to find it as the base URL.
          // If that also doesn't work, rethrow the original exception.
          //
          try
          {
            InputStream inputStream =  new URL(baseURL.toString() + bundleLocalization).openStream();
            bundle = untranslatedResourceBundle = resourceBundle = new PropertyResourceBundle(inputStream);
            inputStream.close();
          }
          catch (IOException ioException)
          {
            // We'll rethrow the original exception, not this one.
          }
          if (bundle == null)
          {
            throw exception;
          }
        }
      }
      else
      {
        String resourceName = baseURL.toString() + bundleLocalization;
        try
        {
          InputStream inputStream =  new URL(resourceName).openStream();
          bundle = untranslatedResourceBundle = new PropertyResourceBundle(inputStream);
          inputStream.close();
        }
        catch (IOException ioException)
        {
          throw new MissingResourceException("Missing properties: " + resourceName, getClass().getName(), "plugin.properties");
        }
      }
    }
    return bundle.getString(key);
  }

  /**
   * Does the work of fetching the string associated with the key,
   * when the string resource is not available locally.
   * @param key the key of the string to fetch.
   * @exception MissingResourceException if the string resource doesn't exist anywhere.
   * @see #getDelegateResourceLocators()
   */
  protected String delegatedGetString(String key, boolean translate)
  {
    ResourceLocator[] delegateResourceLocators = getDelegateResourceLocators();
    for (int i = 0; i < delegateResourceLocators.length; ++i)
    {
      try
      {
        return delegateResourceLocators[i].getString(key, translate);
      }
      catch (MissingResourceException exception)
      {
        // Ignore this since we will throw an exception when all else fails.
      }
    }

    throw
      new MissingResourceException
        (MessageFormat.format("The string resource ''{0}'' could not be located", new Object [] { key }),
         getClass().getName(),
         key);
  }

  /*
   * Javadoc copied from interface.
   */
  public String getString(String key, Object [] substitutions)
  {
    return getString(key, substitutions, shouldTranslate());
  }

  /*
   * Javadoc copied from interface.
   */
  public String getString(String key, Object [] substitutions, boolean translate)
  {
    return MessageFormat.format(getString(key, translate), substitutions);
  }
}
