/*
 * *****************************************************************************
 * Copyright (C) 2005-2011, International Business Machines Corporation and    *
 * others. All Rights Reserved.                                                *
 * *****************************************************************************
 */

package com.ibm.icu.impl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.ref.SoftReference;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;

import com.ibm.icu.impl.URLHandler.URLVisitor;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
import com.ibm.icu.util.UResourceBundleIterator;
import com.ibm.icu.util.VersionInfo;

public  class ICUResourceBundle extends UResourceBundle {
    /**
     * The data path to be used with getBundleInstance API
     */
    protected static final String ICU_DATA_PATH = "com/ibm/icu/impl/";
    /**
     * The data path to be used with getBundleInstance API
     */
    public static final String ICU_BUNDLE = "data/icudt" + VersionInfo.ICU_DATA_VERSION_PATH;

    /**
     * The base name of ICU data to be used with getBundleInstance API
     */
    public static final String ICU_BASE_NAME = ICU_DATA_PATH + ICU_BUNDLE;

    /**
     * The base name of collation data to be used with getBundleInstance API
     */
    public static final String ICU_COLLATION_BASE_NAME = ICU_BASE_NAME + "/coll";

    /**
     * The base name of rbbi data to be used with getData API
     */
    public static final String ICU_BRKITR_NAME = "/brkitr";

    /**
     * The base name of rbbi data to be used with getBundleInstance API
     */
    public static final String ICU_BRKITR_BASE_NAME = ICU_BASE_NAME + ICU_BRKITR_NAME;

    /**
     * The base name of rbnf data to be used with getBundleInstance API
     */
    public static final String ICU_RBNF_BASE_NAME = ICU_BASE_NAME + "/rbnf";

    /**
     * The base name of transliterator data to be used with getBundleInstance API
     */
    public static final String ICU_TRANSLIT_BASE_NAME = ICU_BASE_NAME + "/translit";

    public static final String ICU_LANG_BASE_NAME = ICU_BASE_NAME + "/lang";
    public static final String ICU_CURR_BASE_NAME = ICU_BASE_NAME + "/curr";
    public static final String ICU_REGION_BASE_NAME = ICU_BASE_NAME + "/region";
    public static final String ICU_ZONE_BASE_NAME = ICU_BASE_NAME + "/zone";

    /**
     * The actual path of the resource
     */
    protected String resPath;

    /**
     * The class loader constant to be used with getBundleInstance API
     */
    public static final ClassLoader ICU_DATA_CLASS_LOADER;
    static {
        ClassLoader loader = ICUData.class.getClassLoader();
        if (loader == null) {
            loader = Utility.getFallbackClassLoader();
        }
        ICU_DATA_CLASS_LOADER = loader;
    }

    /**
     * The name of the resource containing the installed locales
     */
    protected static final String INSTALLED_LOCALES = "InstalledLocales";

    public static final int FROM_FALLBACK = 1, FROM_ROOT = 2, FROM_DEFAULT = 3, FROM_LOCALE = 4;

    private int loadingStatus = -1;

    public void setLoadingStatus(int newStatus) {
        loadingStatus = newStatus;
    }
    /**
     * Returns the loading status of a particular resource.
     *
     * @return FROM_FALLBACK if the resource is fetched from fallback bundle
     *         FROM_ROOT if the resource is fetched from root bundle.
     *         FROM_DEFAULT if the resource is fetched from the default locale.
     */
    public int getLoadingStatus() {
        return loadingStatus;
    }

    public void setLoadingStatus(String requestedLocale){
        String locale = getLocaleID();
        if(locale.equals("root")) {
            setLoadingStatus(FROM_ROOT);
        } else if(locale.equals(requestedLocale)) {
            setLoadingStatus(FROM_LOCALE);
        } else {
            setLoadingStatus(FROM_FALLBACK);
        }
     }

    /**
     * Returns the respath of this bundle
     * @return the respath of the bundle
     */
    public String getResPath(){
        return resPath;
    }

    /**
     * Returns a functionally equivalent locale, considering keywords as well, for the specified keyword.
     * @param baseName resource specifier
     * @param resName top level resource to consider (such as "collations")
     * @param keyword a particular keyword to consider (such as "collation" )
     * @param locID The requested locale
     * @param isAvailable If non-null, 1-element array of fillin parameter that indicates whether the
     * requested locale was available. The locale is defined as 'available' if it physically
     * exists within the specified tree and included in 'InstalledLocales'.
     * @param omitDefault  if true, omit keyword and value if default.
     * 'de_DE\@collation=standard' -> 'de_DE'
     * @return the locale
     * @internal ICU 3.0
     */
    public static final ULocale getFunctionalEquivalent(String baseName, ClassLoader loader,
            String resName, String keyword, ULocale locID,
            boolean isAvailable[], boolean omitDefault) {
        String kwVal = locID.getKeywordValue(keyword);
        String baseLoc = locID.getBaseName();
        String defStr = null;
        ULocale parent = new ULocale(baseLoc);
        ULocale defLoc = null; // locale where default (found) resource is
        boolean lookForDefault = false; // true if kwVal needs to be set
        ULocale fullBase = null; // base locale of found (target) resource
        int defDepth = 0; // depth of 'default' marker
        int resDepth = 0; // depth of found resource;

        if ((kwVal == null) || (kwVal.length() == 0)
                || kwVal.equals(DEFAULT_TAG)) {
            kwVal = ""; // default tag is treated as no keyword
            lookForDefault = true;
        }

        // Check top level locale first
        ICUResourceBundle r = null;

        r = (ICUResourceBundle) UResourceBundle.getBundleInstance(baseName, parent);
        if (isAvailable != null) {
            isAvailable[0] = false;
            ULocale[] availableULocales = getAvailEntry(baseName, loader).getULocaleList();
            for (int i = 0; i < availableULocales.length; i++) {
                if (parent.equals(availableULocales[i])) {
                    isAvailable[0] = true;
                    break;
                }
            }
        }
        // determine in which locale (if any) the currently relevant 'default' is
        do {
            try {
                ICUResourceBundle irb = (ICUResourceBundle) r.get(resName);
                defStr = irb.getString(DEFAULT_TAG);
                if (lookForDefault == true) {
                    kwVal = defStr;
                    lookForDefault = false;
                }
                defLoc = r.getULocale();
            } catch (MissingResourceException t) {
                // Ignore error and continue search.
            }
            if (defLoc == null) {
                r = (ICUResourceBundle) r.getParent();
                defDepth++;
            }
        } while ((r != null) && (defLoc == null));

        // Now, search for the named resource
        parent = new ULocale(baseLoc);
        r = (ICUResourceBundle) UResourceBundle.getBundleInstance(baseName, parent);
        // determine in which locale (if any) the named resource is located
        do {
            try {
                ICUResourceBundle irb = (ICUResourceBundle)r.get(resName);
                /* UResourceBundle urb = */irb.get(kwVal);
                fullBase = irb.getULocale();
                // If the get() completed, we have the full base locale
                // If we fell back to an ancestor of the old 'default',
                // we need to re calculate the "default" keyword.
                if ((fullBase != null) && ((resDepth) > defDepth)) {
                    defStr = irb.getString(DEFAULT_TAG);
                    defLoc = r.getULocale();
                    defDepth = resDepth;
                }
            } catch (MissingResourceException t) {
                // Ignore error,
            }
            if (fullBase == null) {
                r = (ICUResourceBundle) r.getParent();
                resDepth++;
            }
        } while ((r != null) && (fullBase == null));

        if (fullBase == null && // Could not find resource 'kwVal'
                (defStr != null) && // default was defined
                !defStr.equals(kwVal)) { // kwVal is not default
            // couldn't find requested resource. Fall back to default.
            kwVal = defStr; // Fall back to default.
            parent = new ULocale(baseLoc);
            r = (ICUResourceBundle) UResourceBundle.getBundleInstance(baseName, parent);
            resDepth = 0;
            // determine in which locale (if any) the named resource is located
            do {
                try {
                    ICUResourceBundle irb = (ICUResourceBundle)r.get(resName);
                    UResourceBundle urb = irb.get(kwVal);

                    // if we didn't fail before this..
                    fullBase = r.getULocale();

                    // If the fetched item (urb) is in a different locale than our outer locale (r/fullBase)
                    // then we are in a 'fallback' situation. treat as a missing resource situation.
                    if(!fullBase.toString().equals(urb.getLocale().toString())) {
                        fullBase = null; // fallback condition. Loop and try again.
                    }

                    // If we fell back to an ancestor of the old 'default',
                    // we need to re calculate the "default" keyword.
                    if ((fullBase != null) && ((resDepth) > defDepth)) {
                        defStr = irb.getString(DEFAULT_TAG);
                        defLoc = r.getULocale();
                        defDepth = resDepth;
                    }
                } catch (MissingResourceException t) {
                    // Ignore error, continue search.
                }
                if (fullBase == null) {
                    r = (ICUResourceBundle) r.getParent();
                    resDepth++;
                }
            } while ((r != null) && (fullBase == null));
        }

        if (fullBase == null) {
            throw new MissingResourceException(
                "Could not find locale containing requested or default keyword.",
                baseName, keyword + "=" + kwVal);
        }

        if (omitDefault
            && defStr.equals(kwVal) // if default was requested and
            && resDepth <= defDepth) { // default was set in same locale or child
            return fullBase; // Keyword value is default - no keyword needed in locale
        } else {
            return new ULocale(fullBase.toString() + "@" + keyword + "=" + kwVal);
        }
    }

    /**
     * Given a tree path and keyword, return a string enumeration of all possible values for that keyword.
     * @param baseName resource specifier
     * @param keyword a particular keyword to consider, must match a top level resource name
     * within the tree. (i.e. "collations")
     * @internal ICU 3.0
     */
    public static final String[] getKeywordValues(String baseName, String keyword) {
        Set<String> keywords = new HashSet<String>();
        ULocale locales[] = createULocaleList(baseName, ICU_DATA_CLASS_LOADER);
        int i;

        for (i = 0; i < locales.length; i++) {
            try {
                UResourceBundle b = UResourceBundle.getBundleInstance(baseName, locales[i]);
                // downcast to ICUResourceBundle?
                ICUResourceBundle irb = (ICUResourceBundle) (b.getObject(keyword));
                Enumeration<String> e = irb.getKeys();
                while (e.hasMoreElements()) {
                    String s = e.nextElement();
                    if (!DEFAULT_TAG.equals(s)) {
                        // don't add 'default' items
                        keywords.add(s);
                    }
                }
            } catch (Throwable t) {
                //System.err.println("Error in - " + new Integer(i).toString()
                // + " - " + t.toString());
                // ignore the err - just skip that resource
            }
        }
        return keywords.toArray(new String[0]);
    }

    /**
     * This method performs multilevel fallback for fetching items from the
     * bundle e.g: If resource is in the form de__PHONEBOOK{ collations{
     * default{ "phonebook"} } } If the value of "default" key needs to be
     * accessed, then do: <code>
     *  UResourceBundle bundle = UResourceBundle.getBundleInstance("de__PHONEBOOK");
     *  ICUResourceBundle result = null;
     *  if(bundle instanceof ICUResourceBundle){
     *      result = ((ICUResourceBundle) bundle).getWithFallback("collations/default");
     *  }
     * </code>
     *
     * @param path The path to the required resource key
     * @return resource represented by the key
     * @exception MissingResourceException If a resource was not found.
     */
    public ICUResourceBundle getWithFallback(String path) throws MissingResourceException {
        ICUResourceBundle result = null;
        ICUResourceBundle actualBundle = this;

        // now recurse to pick up sub levels of the items
        result = findResourceWithFallback(path, actualBundle, null);

        if (result == null) {
            throw new MissingResourceException(
                "Can't find resource for bundle "
                + this.getClass().getName() + ", key " + getType(),
                path, getKey());
        }
        return result;
    }
    
    public ICUResourceBundle at(int index) {
        return (ICUResourceBundle) handleGet(index, null, this);
    }
    
    public ICUResourceBundle at(String key) {
        // don't ever presume the key is an int in disguise, like ResourceArray does.
        if (this instanceof ICUResourceBundleImpl.ResourceTable) {
            return (ICUResourceBundle) handleGet(key, null, this);
        }
        return null;
    }
    
    @Override
    public ICUResourceBundle findTopLevel(int index) {
        return (ICUResourceBundle) super.findTopLevel(index);
    }
    
    @Override
    public ICUResourceBundle findTopLevel(String aKey) {
        return (ICUResourceBundle) super.findTopLevel(aKey);
    }
    
    /**
     * Like getWithFallback, but returns null if the resource is not found instead of
     * throwing an exception.
     * @param path the path to the resource
     * @return the resource, or null
     */
    public ICUResourceBundle findWithFallback(String path) {
        return findResourceWithFallback(path, this, null);
    }

    // will throw type mismatch exception if the resource is not a string
    public String getStringWithFallback(String path) throws MissingResourceException {
        return getWithFallback(path).getString();
    }

    /**
     * Return a set of the locale names supported by a collection of resource
     * bundles.
     *
     * @param bundlePrefix the prefix of the resource bundles to use.
     */
    public static Set<String> getAvailableLocaleNameSet(String bundlePrefix, ClassLoader loader) {
        return getAvailEntry(bundlePrefix, loader).getLocaleNameSet();
    }

    /**
     * Return a set of all the locale names supported by a collection of
     * resource bundles.
     */
    public static Set<String> getFullLocaleNameSet() {
        return getFullLocaleNameSet(ICU_BASE_NAME, ICU_DATA_CLASS_LOADER);
    }

    /**
     * Return a set of all the locale names supported by a collection of
     * resource bundles.
     *
     * @param bundlePrefix the prefix of the resource bundles to use.
     */
    public static Set<String> getFullLocaleNameSet(String bundlePrefix, ClassLoader loader) {
        return getAvailEntry(bundlePrefix, loader).getFullLocaleNameSet();
    }

    /**
     * Return a set of the locale names supported by a collection of resource
     * bundles.
     */
    public static Set<String> getAvailableLocaleNameSet() {
        return getAvailableLocaleNameSet(ICU_BASE_NAME, ICU_DATA_CLASS_LOADER);
    }

    /**
     * Get the set of Locales installed in the specified bundles.
     * @return the list of available locales
     */
    public static final ULocale[] getAvailableULocales(String baseName, ClassLoader loader) {
        return getAvailEntry(baseName, loader).getULocaleList();
    }

    /**
     * Get the set of ULocales installed the base bundle.
     * @return the list of available locales
     */
    public static final ULocale[] getAvailableULocales() {
        return getAvailableULocales(ICU_BASE_NAME, ICU_DATA_CLASS_LOADER);
    }

    /**
     * Get the set of Locales installed in the specified bundles.
     * @return the list of available locales
     */
    public static final Locale[] getAvailableLocales(String baseName, ClassLoader loader) {
        return getAvailEntry(baseName, loader).getLocaleList();
    }

   /**
     * Get the set of Locales installed the base bundle.
     * @return the list of available locales
     */
    public static final Locale[] getAvailableLocales() {
        return getAvailEntry(ICU_BASE_NAME, ICU_DATA_CLASS_LOADER).getLocaleList();
    }

    /**
     * Convert a list of ULocales to a list of Locales.  ULocales with a script code will not be converted
     * since they cannot be represented as a Locale.  This means that the two lists will <b>not</b> match
     * one-to-one, and that the returned list might be shorter than the input list.
     * @param ulocales a list of ULocales to convert to a list of Locales.
     * @return the list of converted ULocales
     */
    public static final Locale[] getLocaleList(ULocale[] ulocales) {
        ArrayList<Locale> list = new ArrayList<Locale>(ulocales.length);
        HashSet<Locale> uniqueSet = new HashSet<Locale>();
        for (int i = 0; i < ulocales.length; i++) {
            Locale loc = ulocales[i].toLocale();
            if (!uniqueSet.contains(loc)) {
                list.add(loc);
                uniqueSet.add(loc);
            }
        }
        return list.toArray(new Locale[list.size()]);
    }

    /**
     * Returns the locale of this resource bundle. This method can be used after
     * a call to getBundle() to determine whether the resource bundle returned
     * really corresponds to the requested locale or is a fallback.
     *
     * @return the locale of this resource bundle
     */
    public Locale getLocale() {
        return getULocale().toLocale();
    }


    // ========== privates ==========
    private static final String ICU_RESOURCE_INDEX = "res_index";

    private static final String DEFAULT_TAG = "default";

    // The name of text file generated by ICU4J build script including all locale names
    // (canonical, alias and root)
    private static final String FULL_LOCALE_NAMES_LIST = "fullLocaleNames.lst";

    // Flag for enabling/disabling debugging code
    private static final boolean DEBUG = ICUDebug.enabled("localedata");

    // Cache for getAvailableLocales
    private static SoftReference<Map<String, AvailEntry>> GET_AVAILABLE_CACHE;
    private static final ULocale[] createULocaleList(String baseName,
            ClassLoader root) {
        // the canned list is a subset of all the available .res files, the idea
        // is we don't export them
        // all. gotta be a better way to do this, since to add a locale you have
        // to update this list,
        // and it's embedded in our binary resources.
        ICUResourceBundle bundle = (ICUResourceBundle) UResourceBundle.instantiateBundle(baseName, ICU_RESOURCE_INDEX, root, true);

        bundle = (ICUResourceBundle)bundle.get(INSTALLED_LOCALES);
        int length = bundle.getSize();
        int i = 0;
        ULocale[] locales = new ULocale[length];
        UResourceBundleIterator iter = bundle.getIterator();
        iter.reset();
        while (iter.hasNext()) {
            String locstr = iter.next().getKey();
            if (locstr.equals("root")) {
                locales[i++] = ULocale.ROOT;
            } else {
                locales[i++] = new ULocale(locstr);
            }
        }
        bundle = null;
        return locales;
    }

    private static final Locale[] createLocaleList(String baseName, ClassLoader loader) {
        ULocale[] ulocales = getAvailEntry(baseName, loader).getULocaleList();
        return getLocaleList(ulocales);
    }

    private static final String[] createLocaleNameArray(String baseName,
            ClassLoader root) {
        ICUResourceBundle bundle = (ICUResourceBundle) UResourceBundle.instantiateBundle( baseName, ICU_RESOURCE_INDEX, root, true);
        bundle = (ICUResourceBundle)bundle.get(INSTALLED_LOCALES);
        int length = bundle.getSize();
        int i = 0;
        String[] locales = new String[length];
        UResourceBundleIterator iter = bundle.getIterator();
        iter.reset();
        while (iter.hasNext()) {
            String locstr = iter.next(). getKey();
            if (locstr.equals("root")) {
                locales[i++] = ULocale.ROOT.toString();
            } else {
                locales[i++] = locstr;
            }
        }
        bundle = null;
        return locales;
    }

    private static final List<String> createFullLocaleNameArray(
            final String baseName, final ClassLoader root) {

        List<String> list = java.security.AccessController
            .doPrivileged(new java.security.PrivilegedAction<List<String>>() {
                public List<String> run() {
                    // WebSphere class loader will return null for a raw
                    // directory name without trailing slash
                    String bn = baseName.endsWith("/")
                        ? baseName
                        : baseName + "/";

                    List<String> resList = null;

                    String skipScan = ICUConfig.get("com.ibm.icu.impl.ICUResourceBundle.skipRuntimeLocaleResourceScan", "false");
                    if (!skipScan.equalsIgnoreCase("true")) {
                        // scan available locale resources under the base url first
                        try {
                            Enumeration<URL> urls = root.getResources(bn);
                            while (urls.hasMoreElements()) {
                                URL url = urls.nextElement();
                                URLHandler handler = URLHandler.get(url);
                                if (handler != null) {
                                    final List<String> lst = new ArrayList<String>();
                                    URLVisitor v = new URLVisitor() {
                                            public void visit(String s) {
                                                //TODO: This is ugly hack.  We have to figure out how
                                                // we can distinguish locale data from others
                                                if (s.endsWith(".res")) {
                                                    String locstr = s.substring(0, s.length() - 4);
                                                    if (locstr.contains("_") && !locstr.equals("res_index")) {
                                                        // locale data with country/script contain "_",
                                                        // except for res_index.res
                                                        lst.add(locstr);
                                                    } else if (locstr.length() == 2 || locstr.length() == 3) {
                                                        // all 2-letter or 3-letter entries are all locale
                                                        // data at least for now
                                                        lst.add(locstr);
                                                    } else if (locstr.equalsIgnoreCase("root")) {
                                                        // root locale is a special case
                                                        lst.add(ULocale.ROOT.toString());
                                                    }
                                                }
                                            }
                                        };
                                    handler.guide(v, false);

                                    if (resList == null) {
                                        resList = new ArrayList<String>(lst);
                                    } else {
                                        resList.addAll(lst);
                                    }
                                } else {
                                    if (DEBUG) System.out.println("handler for " + url + " is null");
                                }
                            }
                        } catch (IOException e) {
                            if (DEBUG) System.out.println("ouch: " + e.getMessage());
                            resList = null;
                        }
                    }

                    if (resList == null) {
                        // look for prebuilt full locale names list next
                        try {
                            InputStream s = root.getResourceAsStream(bn + FULL_LOCALE_NAMES_LIST);
                            if (s != null) {
                                resList = new ArrayList<String>();
                                BufferedReader br = new BufferedReader(new InputStreamReader(s, "ASCII"));
                                String line;
                                while ((line = br.readLine()) != null) {
                                    if (line.length() != 0 && !line.startsWith("#")) {
                                        if (line.equalsIgnoreCase("root")) {
                                            resList.add(ULocale.ROOT.toString());
                                        } else {
                                            resList.add(line);
                                        }
                                    }
                                }
                            }
                        } catch (IOException e) {
                            // swallow it
                        }
                    }

                    return resList;
                }
            });

        return list;
    }

    private static Set<String> createFullLocaleNameSet(String baseName, ClassLoader loader) {
        List<String> list = createFullLocaleNameArray(baseName, loader);
        if(list == null){
            if (DEBUG) System.out.println("createFullLocaleNameArray returned null");
            // Use locale name set as the last resort fallback
            Set<String> locNameSet = createLocaleNameSet(baseName, loader);
            String rootLocaleID = ULocale.ROOT.toString();
            if (!locNameSet.contains(rootLocaleID)) {
                // We need to add the root locale in the set
                Set<String> tmp = new HashSet<String>(locNameSet);
                tmp.add(rootLocaleID);
                locNameSet = Collections.unmodifiableSet(tmp);
            }
            return locNameSet;
        }
        Set<String> fullLocNameSet = new HashSet<String>();
        fullLocNameSet.addAll(list);
        return Collections.unmodifiableSet(fullLocNameSet);
    }

    private static Set<String> createLocaleNameSet(String baseName, ClassLoader loader) {
        try {
            String[] locales = createLocaleNameArray(baseName, loader);

            HashSet<String> set = new HashSet<String>();
            set.addAll(Arrays.asList(locales));
            return Collections.unmodifiableSet(set);
        } catch (MissingResourceException e) {
            if (DEBUG) {
                System.out.println("couldn't find index for bundleName: " + baseName);
                Thread.dumpStack();
            }
        }
        return Collections.emptySet();
    }

    /**
     * Holds the prefix, and lazily creates the Locale[] list or the locale name
     * Set as needed.
     */
    private static final class AvailEntry {
        private String prefix;
        private ClassLoader loader;
        private ULocale[] ulocales;
        private Locale[] locales;
        private Set<String> nameSet;
        private Set<String> fullNameSet;

        AvailEntry(String prefix, ClassLoader loader) {
            this.prefix = prefix;
            this.loader = loader;
        }

        ULocale[] getULocaleList() {
            if (ulocales == null) {
                ulocales = createULocaleList(prefix, loader);
            }
            return ulocales;
        }
        Locale[] getLocaleList() {
            if (locales == null) {
              locales = createLocaleList(prefix, loader);
            }
            return locales;
        }
        Set<String> getLocaleNameSet() {
            if (nameSet == null) {
              nameSet = createLocaleNameSet(prefix, loader);
            }
            return nameSet;
        }
        Set<String> getFullLocaleNameSet() {
            if (fullNameSet == null) {
              fullNameSet = createFullLocaleNameSet(prefix, loader);
            }
            return fullNameSet;
        }
    }

    /**
     * Stores the locale information in a cache accessed by key (bundle prefix).
     * The cached objects are AvailEntries. The cache is held by a SoftReference
     * so it can be GC'd.
     */
  private static AvailEntry getAvailEntry(String key, ClassLoader loader) {
        AvailEntry ae = null;
        Map<String, AvailEntry> lcache = null;
        if (GET_AVAILABLE_CACHE != null) {
            lcache = GET_AVAILABLE_CACHE.get();
            if (lcache != null) {
                ae = lcache.get(key);
            }
        }

        if (ae == null) {
          ae = new AvailEntry(key, loader);
            if (lcache == null) {
                lcache = new HashMap<String, AvailEntry>();
                lcache.put(key, ae);
                GET_AVAILABLE_CACHE = new SoftReference<Map<String, AvailEntry>>(lcache);
            } else {
                lcache.put(key, ae);
            }
        }

        return ae;
    }

    protected static final ICUResourceBundle findResourceWithFallback(String path,
            UResourceBundle actualBundle, UResourceBundle requested) {
        ICUResourceBundle sub = null;
        if (requested == null) {
            requested = actualBundle;
        }
        while (actualBundle != null) {
            ICUResourceBundle current = (ICUResourceBundle) actualBundle;
            if (path.indexOf('/') == -1) { // skip the tokenizer
                sub = (ICUResourceBundle) current.handleGet(path, null, requested);
                if (sub != null) {
                    current = sub;
                    break;
                }
            } else {
                StringTokenizer st = new StringTokenizer(path, "/");
                while (st.hasMoreTokens()) {
                    String subKey = st.nextToken();
                    sub = (ICUResourceBundle) current.handleGet(subKey, null, requested);
                    if (sub == null) {
                        break;
                    }
                    current = sub;
                }
                if (sub != null) {
                    //we found it
                    break;
                }
            }
            if (((ICUResourceBundle)actualBundle).resPath.length() != 0) {
                path = ((ICUResourceBundle)actualBundle).resPath + "/" + path;
            }
            // if not try the parent bundle
            actualBundle = ((ICUResourceBundle) actualBundle).getParent();

        }
        if(sub != null){
            sub.setLoadingStatus(((ICUResourceBundle)requested).getLocaleID());
        }
        return sub;
    }
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (other instanceof ICUResourceBundle) {
            ICUResourceBundle o = (ICUResourceBundle) other;
            if (getBaseName().equals(o.getBaseName())
                    && getLocaleID().equals(o.getLocaleID())) {
                return true;
            }
        }
        return false;
    }
    // This method is for super class's instantiateBundle method
    public static UResourceBundle getBundleInstance(String baseName, String localeID,
                                                    ClassLoader root, boolean disableFallback){
        UResourceBundle b = instantiateBundle(baseName, localeID, root, disableFallback);
        if(b==null){
            throw new MissingResourceException("Could not find the bundle "+ baseName+"/"+ localeID+".res","","");
        }
        return b;
    }
    //  recursively build bundle .. over-ride super class method.
    protected synchronized static UResourceBundle instantiateBundle(String baseName, String localeID,
                                                                    ClassLoader root, boolean disableFallback){
        ULocale defaultLocale = ULocale.getDefault();
        String localeName = localeID;
        if(localeName.indexOf('@')>0){
            localeName = ULocale.getBaseName(localeID);
        }
        String fullName = getFullName(baseName, localeName);
        ICUResourceBundle b = (ICUResourceBundle)loadFromCache(root, fullName, defaultLocale);

        // here we assume that java type resource bundle organization
        // is required then the base name contains '.' else
        // the resource organization is of ICU type
        // so clients can instantiate resources of the type
        // com.mycompany.data.MyLocaleElements_en.res and
        // com.mycompany.data.MyLocaleElements.res
        //
        final String rootLocale = (baseName.indexOf('.')==-1) ? "root" : "";
        final String defaultID = defaultLocale.toString();

        if(localeName.equals("")){
            localeName = rootLocale;
        }
        if(DEBUG) System.out.println("Creating "+fullName+ " currently b is "+b);
        if (b == null) {
            b = ICUResourceBundle.createBundle(baseName, localeName, root);

            if(DEBUG)System.out.println("The bundle created is: "+b+" and disableFallback="+disableFallback+" and bundle.getNoFallback="+(b!=null && b.getNoFallback()));
            if(disableFallback || (b!=null && b.getNoFallback())){
                // no fallback because the caller said so or because the bundle says so
                return addToCache(root, fullName, defaultLocale, b);
            }

            // fallback to locale ID parent
            if(b == null){
                int i = localeName.lastIndexOf('_');
                if (i != -1) {
                    String temp = localeName.substring(0, i);
                    b = (ICUResourceBundle)instantiateBundle(baseName, temp, root, disableFallback);
                    if(b!=null && b.getULocale().equals(temp)){
                        b.setLoadingStatus(ICUResourceBundle.FROM_FALLBACK);
                    }
                }else{
                    if(defaultID.indexOf(localeName)==-1){
                        b = (ICUResourceBundle)instantiateBundle(baseName, defaultID, root, disableFallback);
                        if(b!=null){
                            b.setLoadingStatus(ICUResourceBundle.FROM_DEFAULT);
                        }
                    }else if(rootLocale.length()!=0){
                        b = ICUResourceBundle.createBundle(baseName, rootLocale, root);
                        if(b!=null){
                            b.setLoadingStatus(ICUResourceBundle.FROM_ROOT);
                        }
                    }
                }
            }else{
                UResourceBundle parent = null;
                localeName = b.getLocaleID();
                int i = localeName.lastIndexOf('_');

                b = (ICUResourceBundle)addToCache(root, fullName, defaultLocale, b);

                boolean ParentIsRoot = false;
                if (b.getTableResource("%%ParentIsRoot") != RES_BOGUS) {
                    ParentIsRoot = true;
                }

                if (i != -1 && !ParentIsRoot) {
                    parent = instantiateBundle(baseName, localeName.substring(0, i), root, disableFallback);
                } else if (!localeName.equals(rootLocale)){
                    parent = instantiateBundle(baseName, rootLocale, root, true);
                }

                if (!b.equals(parent)){
                    b.setParent(parent);
                }
            }
        }
        return b;
    }
    UResourceBundle get(String aKey, HashMap<String, String> table, UResourceBundle requested) {
        ICUResourceBundle obj = (ICUResourceBundle)handleGet(aKey, table, requested);
        if (obj == null) {
            obj = (ICUResourceBundle)getParent();
            if (obj != null) {
                //call the get method to recursively fetch the resource
                obj = (ICUResourceBundle)obj.get(aKey, table, requested);
            }
            if (obj == null) {
                String fullName = getFullName(getBaseName(), getLocaleID());
                throw new MissingResourceException(
                        "Can't find resource for bundle " + fullName + ", key "
                                + aKey, this.getClass().getName(), aKey);
            }
        }
        obj.setLoadingStatus(((ICUResourceBundle)requested).getLocaleID());
        return obj;
    }

    private static final String ICU_RESOURCE_SUFFIX = ".res";
    /**
     * Gets the full name of the resource with suffix.
     */
    public static String getFullName(String baseName, String localeName){
        if(baseName==null || baseName.length()==0){
            if(localeName.length()==0){
                return localeName=ULocale.getDefault().toString();   
            }
            return localeName+ICU_RESOURCE_SUFFIX;
        }else{
            if(baseName.indexOf('.')==-1){
                if(baseName.charAt(baseName.length()-1)!= '/'){
                    return baseName+"/"+localeName+ICU_RESOURCE_SUFFIX;
                }else{
                    return baseName+localeName+ICU_RESOURCE_SUFFIX;   
                }
            }else{
                baseName = baseName.replace('.','/');
                if(localeName.length()==0){
                    return baseName+ICU_RESOURCE_SUFFIX;   
                }else{
                    return baseName+"_"+localeName+ICU_RESOURCE_SUFFIX;
                }
            }
        }
    }

    protected String localeID;
    protected String baseName;
    protected ULocale ulocale;
    protected ClassLoader loader;

    /**
     * Access to the bits and bytes of the resource bundle.
     * Hides low-level details.
     */
    protected ICUResourceBundleReader reader;
    /** Data member where the subclasses store the key. */
    protected String key;
    /** Data member where the subclasses store the offset within resource data. */
    protected int resource;

    /**
     * A resource word value that means "no resource".
     * Note: 0xffffffff == -1
     * This has the same value as UResourceBundle.NONE, but they are semantically
     * different and should be used appropriately according to context:
     * NONE means "no type".
     * (The type of RES_BOGUS is RES_RESERVED=15 which was defined in ICU4C ures.h.)
     */
    public static final int RES_BOGUS = 0xffffffff;

    /**
     * Resource type constant for aliases;
     * internally stores a string which identifies the actual resource
     * storing the data (can be in a different resource bundle).
     * Resolved internally before delivering the actual resource through the API.
     */
    public static final int ALIAS = 3;

    /** Resource type constant for tables with 32-bit count, key offsets and values. */
    public static final int TABLE32 = 4;

    /**
     * Resource type constant for tables with 16-bit count, key offsets and values.
     * All values are STRING_V2 strings.
     */
    public static final int TABLE16 = 5;

    /** Resource type constant for 16-bit Unicode strings in formatVersion 2. */
    public static final int STRING_V2 = 6;

    /**
     * Resource type constant for arrays with 16-bit count and values.
     * All values are STRING_V2 strings.
     */
    public static final int ARRAY16 = 9;

    private static final ConcurrentHashMap<String, ICUResourceBundle> cache = 
        new ConcurrentHashMap<String, ICUResourceBundle>();
    private static final ICUResourceBundle NULL_BUNDLE = 
        new ICUResourceBundle(null, null, null, 0, null) {
        public int hashCode() {
            return 0;
        }
        public boolean equals(Object rhs) {
            return this == rhs;
        }
    };
    
   /**
    *
    * @param baseName The name for the bundle.
    * @param localeID The locale identification.
    * @param root The ClassLoader object root.
    * @return the new bundle
    */
    public static ICUResourceBundle createBundle(String baseName, String localeID, 
            ClassLoader root) {
        
        String resKey = Integer.toHexString(root.hashCode()) + baseName + localeID;
        ICUResourceBundle b = cache.get(resKey);
        if (b == null) {
            String resolvedName = getFullName(baseName, localeID);
            ICUResourceBundleReader reader = ICUResourceBundleReader.getReader(resolvedName, root);
            // could not open the .res file so return null
            if (reader == null) {
                b = NULL_BUNDLE;
            } else {
                b = getBundle(reader, baseName, localeID, root);
            }
            cache.put(resKey, b);
        }
        return b == NULL_BUNDLE ? null : b;
    }

    protected String getLocaleID() {
        return localeID;
    }

    protected String getBaseName() {
        return baseName;
    }

    public ULocale getULocale() {
        return ulocale;
    }

    public UResourceBundle getParent() {
        return (UResourceBundle) parent;
    }

    protected void setParent(ResourceBundle parent) {
        this.parent = parent;
    }

    public String getKey() {
        return key;
    }

    private static final int[] gPublicTypes = new int[] {
        STRING,
        BINARY,
        TABLE,
        ALIAS,

        TABLE,      /* TABLE32 */
        TABLE,      /* TABLE16 */
        STRING,     /* STRING_V2 */
        INT,

        ARRAY,
        ARRAY,      /* ARRAY16 */
        NONE,
        NONE,

        NONE,
        NONE,
        INT_VECTOR,
        NONE
    };

    public int getType() {
        return gPublicTypes[ICUResourceBundleReader.RES_GET_TYPE(resource)];
    }

    /**
     * Get the noFallback flag specified in the loaded bundle.
     * @return The noFallback flag.
     */
    private boolean getNoFallback() {
        return reader.getNoFallback();
    }

    private static ICUResourceBundle getBundle(ICUResourceBundleReader reader,
                                               String baseName, String localeID,
                                               ClassLoader loader) {
        ICUResourceBundleImpl bundle;
        int rootRes = reader.getRootResource();
        if(gPublicTypes[ICUResourceBundleReader.RES_GET_TYPE(rootRes)] == TABLE) {
            bundle = new ICUResourceBundleImpl.ResourceTable(reader, null, "", rootRes, null);
        } else {
            throw new IllegalStateException("Invalid format error");
        }
        bundle.baseName = baseName;
        bundle.localeID = localeID;
        bundle.ulocale = new ULocale(localeID);
        bundle.loader = loader;
        if(bundle.reader.getUsesPoolBundle()) {
            bundle.reader.setPoolBundleKeys(
                ((ICUResourceBundleImpl)getBundleInstance(baseName, "pool", loader, true)).reader);
        }
        UResourceBundle alias = bundle.handleGetImpl("%%ALIAS", null, bundle, null, null); // handleGet will cache the bundle with no parent set
        if(alias != null) {
            return (ICUResourceBundle)UResourceBundle.getBundleInstance(baseName, alias.getString());
        } else {
            return bundle;
        }
    }
    // constructor for inner classes
    protected ICUResourceBundle(ICUResourceBundleReader reader, String key, String resPath, int resource,
                                ICUResourceBundle container) {
        this.reader = reader;
        this.key = key;
        this.resPath = resPath;
        this.resource = resource;
        if(container != null) {
            baseName = container.baseName;
            localeID = container.localeID;
            ulocale = container.ulocale;
            loader = container.loader;
            this.parent = container.parent;
        }
    }

    private String getAliasValue(int res) {
        String result = reader.getAlias(res);
        return result != null ? result : "";
    }
    private static final char RES_PATH_SEP_CHAR = '/';
    private static final String RES_PATH_SEP_STR = "/";
    private static final String ICUDATA = "ICUDATA";
    private static final char HYPHEN = '-';
    private static final String LOCALE = "LOCALE";

    protected ICUResourceBundle findResource(String _key, int _resource,
                                             HashMap<String, String> table,
                                             UResourceBundle requested) {
        ClassLoader loaderToUse = loader;
        String locale = null, keyPath = null;
        String bundleName;
        String rpath = getAliasValue(_resource);
        if (table == null) {
            table = new HashMap<String, String>();
        }
        if (table.get(rpath) != null) {
            throw new IllegalArgumentException(
                    "Circular references in the resource bundles");
        }
        table.put(rpath, "");
        if (rpath.indexOf(RES_PATH_SEP_CHAR) == 0) {
            int i = rpath.indexOf(RES_PATH_SEP_CHAR, 1);
            int j = rpath.indexOf(RES_PATH_SEP_CHAR, i + 1);
            bundleName = rpath.substring(1, i);
            if (j < 0) {
                locale = rpath.substring(i + 1);
            } else {
                locale = rpath.substring(i + 1, j);
                keyPath = rpath.substring(j + 1, rpath.length());
            }
            //there is a path included
            if (bundleName.equals(ICUDATA)) {
                bundleName = ICU_BASE_NAME;
                loaderToUse = ICU_DATA_CLASS_LOADER;
            }else if(bundleName.indexOf(ICUDATA)>-1){
                int idx = bundleName.indexOf(HYPHEN);
                if(idx>-1){
                    bundleName = ICU_BASE_NAME+RES_PATH_SEP_STR+bundleName.substring(idx+1,bundleName.length());
                    loaderToUse = ICU_DATA_CLASS_LOADER;
                }
            }
        } else {
            //no path start with locale
            int i = rpath.indexOf(RES_PATH_SEP_CHAR);
            keyPath = rpath.substring(i + 1);
            if (i != -1) {
                locale = rpath.substring(0, i);
            } else {
                locale = keyPath;
                keyPath = null;//keyPath.substring(i, keyPath.length());
            }
            bundleName = baseName;
        }
        ICUResourceBundle bundle = null;
        ICUResourceBundle sub = null;
        if(bundleName.equals(LOCALE)){
            bundleName = baseName;
            bundle = (ICUResourceBundle)requested;
            keyPath = rpath.substring(LOCALE.length() + 2/* prepending and appending / */, rpath.length());
            locale = ((ICUResourceBundle)requested).getLocaleID();
            sub = ICUResourceBundle.findResourceWithFallback(keyPath, requested, null);
            if (sub != null) {
                sub.resPath = "/" + sub.getLocaleID() + "/" + keyPath;
            }
        }else{
            if (locale == null) {
                // {dlf} must use requestor's class loader to get resources from same jar
                bundle = (ICUResourceBundle) getBundleInstance(bundleName, "",
                         loaderToUse, false);
            } else {
                bundle = (ICUResourceBundle) getBundleInstance(bundleName, locale,
                         loaderToUse, false);
            }
            if (keyPath != null) {
                StringTokenizer st = new StringTokenizer(keyPath, "/");
                ICUResourceBundle current = bundle;
                while (st.hasMoreTokens()) {
                    String subKey = st.nextToken();
                    sub = (ICUResourceBundle)current.get(subKey, table, requested);
                    if (sub == null) {
                        break;
                    }
                    current = sub;
                }
            } else {
                // if the sub resource is not found
                // try fetching the sub resource with
                // the key of this alias resource
                sub = (ICUResourceBundle)bundle.get(_key);
            }
            if (sub != null) {
                sub.resPath = rpath;
            }
        }
        if (sub == null) {
            throw new MissingResourceException(localeID, baseName, _key);
        }
        return sub;
    }

    // Resource bundle lookup cache, which may be used by subclasses
    // which have nested resources
    protected ICUCache<Object, UResourceBundle> lookup;
    private static final int MAX_INITIAL_LOOKUP_SIZE = 64;

    protected void createLookupCache() {
        lookup = new SimpleCache<Object, UResourceBundle>(ICUCache.WEAK, Math.max(getSize()*2, MAX_INITIAL_LOOKUP_SIZE));
    }

    protected UResourceBundle handleGet(String resKey, HashMap<String, String> table, UResourceBundle requested) {
        UResourceBundle res = null;
        if (lookup != null) {
            res = lookup.get(resKey);
        }
        if (res == null) {
            int[] index = new int[1];
            boolean[] alias = new boolean[1];
            res = handleGetImpl(resKey, table, requested, index, alias);
            if (res != null && lookup != null && !alias[0]) {
                // We do not want to cache a result from alias entry
                lookup.put(resKey, res);
                lookup.put(Integer.valueOf(index[0]), res);
            }
        }
        return res;
    }

    protected UResourceBundle handleGet(int index, HashMap<String, String> table, UResourceBundle requested) {
        UResourceBundle res = null;
        Integer indexKey = null;
        if (lookup != null) {
            indexKey = Integer.valueOf(index);
            res = lookup.get(indexKey);
        }
        if (res == null) {
            boolean[] alias = new boolean[1];
            res = handleGetImpl(index, table, requested, alias);
            if (res != null && lookup != null && !alias[0]) {
                // We do not want to cache a result from alias entry
                lookup.put(res.getKey(), res);
                lookup.put(indexKey, res);
            }
        }
        return res;
    }

    // Subclass which supports key based resource access to implement this method
    protected UResourceBundle handleGetImpl(String resKey, HashMap<String, String> table, UResourceBundle requested,
            int[] index, boolean[] isAlias) {
        return null;
    }

    // Subclass which supports index based resource access to implement this method
    protected UResourceBundle handleGetImpl(int index, HashMap<String, String> table, UResourceBundle requested,
            boolean[] isAlias) {
        return null;
    }


     // TODO Below is a set of workarounds created for org.unicode.cldr.icu.ICU2LDMLWriter
     /* 
      * Calling getKeys() on a table that has alias's can throw a NullPointerException if parent is not set, 
      * see trac bug: 6514
      * -Brian Rower - IBM - Sept. 2008
      */
    
    /**
     * Returns the resource handle for the given key within the calling resource table.
     * 
     * @internal
     * @deprecated This API is ICU internal only and a workaround see ticket #6514.
     * @author Brian Rower
     */
    protected int getTableResource(String resKey) {
        return RES_BOGUS;
    }
    protected int getTableResource(int index) {
        return RES_BOGUS;
    }

    /**
     * Determines if the object at the specified index of the calling resource table
     * is an alias. If it is, returns true
     * 
     * @param index The index of the resource to check
     * @returns True if the resource at 'index' is an alias, false otherwise.
     * 
     * @internal
     * @deprecated This API is ICU internal only and part of a work around see ticket #6514
     * @author Brian Rower
     */
    public boolean isAlias(int index)
    {
        //TODO this is part of a workaround for ticket #6514
        //if index is out of the resource, return false.
        return ICUResourceBundleReader.RES_GET_TYPE(getTableResource(index)) == ALIAS;
    }

    /**
     * 
     * @internal
     * @deprecated This API is ICU internal only and part of a workaround see ticket #6514.
     * @author Brian Rower
     */
    public boolean isAlias()
    {
        //TODO this is part of a workaround for ticket #6514
        return ICUResourceBundleReader.RES_GET_TYPE(resource) == ALIAS;
    }

    /**
     * Determines if the object with the specified key 
     * is an alias. If it is, returns true
     * 
     * @returns True if the resource with 'key' is an alias, false otherwise.
     * 
     * @internal
     * @deprecated This API is ICU internal only and part of a workaround see ticket #6514.
     * @author Brian Rower
     */
    public boolean isAlias(String k)
    {
        //TODO this is part of a workaround for ticket #6514
        //this only applies to tables
        return ICUResourceBundleReader.RES_GET_TYPE(getTableResource(k)) == ALIAS;
    }

    /**
     * This method can be used to retrieve the underlying alias path (aka where the alias points to)
     * This method was written to allow conversion from ICU back to LDML format.
     * 
     * @param index The index where the alias path points to.
     * @return The alias path.
     * @author Brian Rower
     * @internal
     * @deprecated This API is ICU internal only.
     * @author Brian Rower
     */
    public String getAliasPath(int index)
    {
        return getAliasValue(getTableResource(index));
    }

    /**
     * 
     * @internal
     * @deprecated This API is ICU internal only
     * @author Brian Rower
     */
    public String getAliasPath()
    {
        //TODO cannot allow alias path to end up in public API
        return getAliasValue(resource);
    }

    /**
     * 
     * @internal
     * @deprecated This API is ICU internal only
     * @author Brian Rower
     */
    public String getAliasPath(String k)
    {
        //TODO cannot allow alias path to end up in public API
        return getAliasValue(getTableResource(k));
    }
    
    /*
     * Helper method for getKeysSafe
     */
    protected String getKey(int index) {
        return null;
    }

    /**
     * Returns an Enumeration of the keys belonging to this table or array.
     * This method differs from the getKeys() method by not following alias paths. This method exposes 
     * underlying alias's. For all general purposes of the ICU resource bundle please use getKeys().
     * 
     * @return Keys in this table or array.
     * @internal
     * @deprecated This API is ICU internal only and a workaround see ticket #6514.
     * @author Brian Rower
     */
    public Enumeration<String> getKeysSafe()
    {
        //TODO this is part of a workaround for ticket #6514
        //the safeness only applies to tables, so use the other method if it's not a table
        if(!ICUResourceBundleReader.URES_IS_TABLE(resource))
        {
            return getKeys();
        }
        Vector<String> v = new Vector<String>();
        int size = getSize();
        for(int index = 0; index < size; index++)
        {
            String curKey = getKey(index); 
            v.add(curKey);
        }
        return v.elements();
    }

    // This is the worker function for the public getKeys().
    // TODO: Now that UResourceBundle uses handleKeySet(), this function is obsolete.
    // It is also not inherited from ResourceBundle, and it is not implemented
    // by ResourceBundleWrapper despite its documentation requiring all subclasses to
    // implement it.
    // Consider deprecating UResourceBundle.handleGetKeys(), and consider making it always return null.
    protected Enumeration<String> handleGetKeys() {
        return Collections.enumeration(handleKeySet());
    }

    protected boolean isTopLevelResource() {
        return resPath.length() == 0;
    }
}
