/*
 * Copyright (c) 1996, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.awt;

import java.awt.Font;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.Vector;
import sun.font.CompositeFontDescriptor;
import sun.font.SunFontManager;
import sun.font.FontManagerFactory;
import sun.font.FontUtilities;
import sun.util.logging.PlatformLogger;

/**
 * Provides the definitions of the five logical fonts: Serif, SansSerif,
 * Monospaced, Dialog, and DialogInput. The necessary information
 * is obtained from fontconfig files.
 */
public abstract class FontConfiguration {

    //static global runtime env
    protected static String osVersion;
    protected static String osName;
    protected static String encoding; // canonical name of default nio charset
    protected static Locale startupLocale = null;
    protected static Hashtable<String, String> localeMap = null;
    private static FontConfiguration fontConfig;
    private static PlatformLogger logger;
    protected static boolean isProperties = true;

    protected SunFontManager fontManager;
    protected boolean preferLocaleFonts;
    protected boolean preferPropFonts;

    private File fontConfigFile;
    private boolean foundOsSpecificFile;
    private boolean inited;
    private String javaLib;

    /* A default FontConfiguration must be created before an alternate
     * one to ensure proper static initialisation takes place.
     */
    public FontConfiguration(SunFontManager fm) {
        if (FontUtilities.debugFonts()) {
            FontUtilities.getLogger()
                .info("Creating standard Font Configuration");
        }
        if (FontUtilities.debugFonts() && logger == null) {
            logger = PlatformLogger.getLogger("sun.awt.FontConfiguration");
        }
        fontManager = fm;
        setOsNameAndVersion();  /* static initialization */
        setEncoding();          /* static initialization */
        /* Separating out the file location from the rest of the
         * initialisation, so the caller has the option of doing
         * something else if a suitable file isn't found.
         */
        findFontConfigFile();
    }

    public synchronized boolean init() {
        if (!inited) {
            this.preferLocaleFonts = false;
            this.preferPropFonts = false;
            setFontConfiguration();
            readFontConfigFile(fontConfigFile);
            initFontConfig();
            inited = true;
        }
        return true;
    }

    public FontConfiguration(SunFontManager fm,
                             boolean preferLocaleFonts,
                             boolean preferPropFonts) {
        fontManager = fm;
        if (FontUtilities.debugFonts()) {
            FontUtilities.getLogger()
                .info("Creating alternate Font Configuration");
        }
        this.preferLocaleFonts = preferLocaleFonts;
        this.preferPropFonts = preferPropFonts;
        /* fontConfig should be initialised by default constructor, and
         * its data tables can be shared, since readFontConfigFile doesn't
         * update any other state. Also avoid a doPrivileged block.
         */
        initFontConfig();
    }

    /**
     * Fills in this instance's osVersion and osName members. By
     * default uses the system properties os.name and os.version;
     * subclasses may override.
     */
    protected void setOsNameAndVersion() {
        osName = System.getProperty("os.name");
        osVersion = System.getProperty("os.version");
    }

    private void setEncoding() {
        encoding = Charset.defaultCharset().name();
        startupLocale = SunToolkit.getStartupLocale();
    }

    /////////////////////////////////////////////////////////////////////
    // methods for loading the FontConfig file                         //
    /////////////////////////////////////////////////////////////////////

    public boolean foundOsSpecificFile() {
        return foundOsSpecificFile;
    }

    /* Smoke test to see if we can trust this configuration by testing if
     * the first slot of a composite font maps to an installed file.
     */
    public boolean fontFilesArePresent() {
        init();
        short fontNameID = compFontNameIDs[0][0][0];
        short fileNameID = getComponentFileID(fontNameID);
        final String fileName = mapFileName(getComponentFileName(fileNameID));
        Boolean exists = java.security.AccessController.doPrivileged(
            new java.security.PrivilegedAction<Boolean>() {
                 public Boolean run() {
                     try {
                         File f = new File(fileName);
                         return Boolean.valueOf(f.exists());
                     }
                     catch (Exception e) {
                         return Boolean.FALSE;
                     }
                 }
                });
        return exists.booleanValue();
    }

    private void findFontConfigFile() {

        foundOsSpecificFile = true; // default assumption.
        String javaHome = System.getProperty("java.home");
        if (javaHome == null) {
            throw new Error("java.home property not set");
        }
        javaLib = javaHome + File.separator + "lib";
        String javaConfFonts = javaHome +
                               File.separator + "conf" +
                               File.separator + "fonts";
        String userConfigFile = System.getProperty("sun.awt.fontconfig");
        if (userConfigFile != null) {
            fontConfigFile = new File(userConfigFile);
        } else {
            fontConfigFile = findFontConfigFile(javaConfFonts);
            if (fontConfigFile == null) {
                fontConfigFile = findFontConfigFile(javaLib);
            }
        }
    }

    private void readFontConfigFile(File f) {
        /* This is invoked here as readFontConfigFile is only invoked
         * once per VM, and always in a privileged context, thus the
         * directory containing installed fall back fonts is accessed
         * from this context
         */
        getInstalledFallbackFonts(javaLib);

        if (f != null) {
            try {
                FileInputStream in = new FileInputStream(f.getPath());
                if (isProperties) {
                    loadProperties(in);
                } else {
                    loadBinary(in);
                }
                in.close();
                if (FontUtilities.debugFonts()) {
                    logger.config("Read logical font configuration from " + f);
                }
            } catch (IOException e) {
                if (FontUtilities.debugFonts()) {
                    logger.config("Failed to read logical font configuration from " + f);
                }
            }
        }
        String version = getVersion();
        if (!"1".equals(version) && FontUtilities.debugFonts()) {
            logger.config("Unsupported fontconfig version: " + version);
        }
    }

    protected void getInstalledFallbackFonts(String javaLib) {
        String fallbackDirName = javaLib + File.separator +
            "fonts" + File.separator + "fallback";

        File fallbackDir = new File(fallbackDirName);
        if (fallbackDir.exists() && fallbackDir.isDirectory()) {
            String[] ttfs = fallbackDir.list(fontManager.getTrueTypeFilter());
            String[] t1s = fallbackDir.list(fontManager.getType1Filter());
            int numTTFs = (ttfs == null) ? 0 : ttfs.length;
            int numT1s = (t1s == null) ? 0 : t1s.length;
            int len = numTTFs + numT1s;
            if (numTTFs + numT1s == 0) {
                return;
            }
            installedFallbackFontFiles = new String[len];
            for (int i=0; i<numTTFs; i++) {
                installedFallbackFontFiles[i] =
                    fallbackDir + File.separator + ttfs[i];
            }
            for (int i=0; i<numT1s; i++) {
                installedFallbackFontFiles[i+numTTFs] =
                    fallbackDir + File.separator + t1s[i];
            }
            fontManager.registerFontsInDir(fallbackDirName);
        }
    }

    private File findImpl(String fname) {
        File f = new File(fname + ".properties");
        if (FontUtilities.debugFonts()) {
            logger.info("Looking for text fontconfig file : " + f);
        }
        if (f.canRead()) {
            if (FontUtilities.debugFonts()) {
                logger.info("Found file : " + f);
            }
            isProperties = true;
            return f;
        }
        f = new File(fname + ".bfc");
        if (FontUtilities.debugFonts()) {
            logger.info("Looking for binary fontconfig file : " + f);
        }
        if (f.canRead()) {
            if (FontUtilities.debugFonts()) {
                logger.info("Found file : " + f);
            }
            isProperties = false;
            return f;
        }
        return null;
    }

    private File findFontConfigFile(String dir) {
        if (!(new File(dir)).exists()) {
            return null;
        }
        String baseName = dir + File.separator + "fontconfig";
        File configFile;
        String osMajorVersion = null;
        if (osVersion != null && osName != null) {
            configFile = findImpl(baseName + "." + osName + "." + osVersion);
            if (configFile != null) {
                return configFile;
            }
            int decimalPointIndex = osVersion.indexOf('.');
            if (decimalPointIndex != -1) {
                osMajorVersion = osVersion.substring(0, osVersion.indexOf('.'));
                configFile = findImpl(baseName + "." + osName + "." + osMajorVersion);
                if (configFile != null) {
                    return configFile;
                }
            }
        }
        if (osName != null) {
            configFile = findImpl(baseName + "." + osName);
            if (configFile != null) {
                return configFile;
            }
        }
        if (osVersion != null) {
            configFile = findImpl(baseName + "." + osVersion);
            if (configFile != null) {
                return configFile;
            }
            if (osMajorVersion != null) {
                configFile = findImpl(baseName + "." + osMajorVersion);
                if (configFile != null) {
                    return configFile;
                }
            }
        }
        foundOsSpecificFile = false;

        configFile = findImpl(baseName);
        if (configFile != null) {
            return configFile;
        }
        if (FontUtilities.debugFonts()) {
            logger.info("Did not find a fontconfig file.");
        }
        return null;
    }

    /* Initialize the internal data tables from binary format font
     * configuration file.
     */
    public static void loadBinary(InputStream inStream) throws IOException {
        DataInputStream in = new DataInputStream(inStream);
        head = readShortTable(in, HEAD_LENGTH);
        int[] tableSizes = new int[INDEX_TABLEEND];
        for (int i = 0; i < INDEX_TABLEEND; i++) {
            tableSizes[i] = head[i + 1] - head[i];
        }
        table_scriptIDs       = readShortTable(in, tableSizes[INDEX_scriptIDs]);
        table_scriptFonts     = readShortTable(in, tableSizes[INDEX_scriptFonts]);
        table_elcIDs          = readShortTable(in, tableSizes[INDEX_elcIDs]);
        table_sequences        = readShortTable(in, tableSizes[INDEX_sequences]);
        table_fontfileNameIDs = readShortTable(in, tableSizes[INDEX_fontfileNameIDs]);
        table_componentFontNameIDs = readShortTable(in, tableSizes[INDEX_componentFontNameIDs]);
        table_filenames       = readShortTable(in, tableSizes[INDEX_filenames]);
        table_awtfontpaths    = readShortTable(in, tableSizes[INDEX_awtfontpaths]);
        table_exclusions      = readShortTable(in, tableSizes[INDEX_exclusions]);
        table_proportionals   = readShortTable(in, tableSizes[INDEX_proportionals]);
        table_scriptFontsMotif   = readShortTable(in, tableSizes[INDEX_scriptFontsMotif]);
        table_alphabeticSuffix   = readShortTable(in, tableSizes[INDEX_alphabeticSuffix]);
        table_stringIDs       = readShortTable(in, tableSizes[INDEX_stringIDs]);

        //StringTable cache
        stringCache = new String[table_stringIDs.length + 1];

        int len = tableSizes[INDEX_stringTable];
        byte[] bb = new byte[len * 2];
        table_stringTable = new char[len];
        in.read(bb);
        int i = 0, j = 0;
        while (i < len) {
           table_stringTable[i++] = (char)(bb[j++] << 8 | (bb[j++] & 0xff));
        }
        if (verbose) {
            dump();
        }
    }

    /* Generate a binary format font configuration from internal data
     * tables.
     */
    public static void saveBinary(OutputStream out) throws IOException {
        sanityCheck();

        DataOutputStream dataOut = new DataOutputStream(out);
        writeShortTable(dataOut, head);
        writeShortTable(dataOut, table_scriptIDs);
        writeShortTable(dataOut, table_scriptFonts);
        writeShortTable(dataOut, table_elcIDs);
        writeShortTable(dataOut, table_sequences);
        writeShortTable(dataOut, table_fontfileNameIDs);
        writeShortTable(dataOut, table_componentFontNameIDs);
        writeShortTable(dataOut, table_filenames);
        writeShortTable(dataOut, table_awtfontpaths);
        writeShortTable(dataOut, table_exclusions);
        writeShortTable(dataOut, table_proportionals);
        writeShortTable(dataOut, table_scriptFontsMotif);
        writeShortTable(dataOut, table_alphabeticSuffix);
        writeShortTable(dataOut, table_stringIDs);
        //stringTable
        dataOut.writeChars(new String(table_stringTable));
        out.close();
        if (verbose) {
            dump();
        }
    }

    //private static boolean loadingProperties;
    private static short stringIDNum;
    private static short[] stringIDs;
    private static StringBuilder stringTable;

    public static void loadProperties(InputStream in) throws IOException {
        //loadingProperties = true;
        //StringID starts from "1", "0" is reserved for "not defined"
        stringIDNum = 1;
        stringIDs = new short[1000];
        stringTable = new StringBuilder(4096);

        if (verbose && logger == null) {
            logger = PlatformLogger.getLogger("sun.awt.FontConfiguration");
        }
        new PropertiesHandler().load(in);

        //loadingProperties = false;
        stringIDs = null;
        stringTable = null;
    }


    /////////////////////////////////////////////////////////////////////
    // methods for initializing the FontConfig                         //
    /////////////////////////////////////////////////////////////////////

    /**
     *  set initLocale, initEncoding and initELC for this FontConfig object
     *  currently we just simply use the startup locale and encoding
     */
    private void initFontConfig() {
        initLocale = startupLocale;
        initEncoding = encoding;
        if (preferLocaleFonts && !willReorderForStartupLocale()) {
            preferLocaleFonts = false;
        }
        initELC = getInitELC();
        initAllComponentFonts();
    }

    //"ELC" stands for "Encoding.Language.Country". This method returns
    //the ID of the matched elc setting of "initLocale" in elcIDs table.
    //If no match is found, it returns the default ID, which is
    //"NULL.NULL.NULL" in elcIDs table.
    private short getInitELC() {
        if (initELC != -1) {
            return initELC;
        }
        HashMap <String, Integer> elcIDs = new HashMap<String, Integer>();
        for (int i = 0; i < table_elcIDs.length; i++) {
            elcIDs.put(getString(table_elcIDs[i]), i);
        }
        String language = initLocale.getLanguage();
        String country = initLocale.getCountry();
        String elc;
        if (elcIDs.containsKey(elc=initEncoding + "." + language + "." + country)
            || elcIDs.containsKey(elc=initEncoding + "." + language)
            || elcIDs.containsKey(elc=initEncoding)) {
            initELC = elcIDs.get(elc).shortValue();
        } else {
            initELC = elcIDs.get("NULL.NULL.NULL").shortValue();
        }
        int i = 0;
        while (i < table_alphabeticSuffix.length) {
            if (initELC == table_alphabeticSuffix[i]) {
                alphabeticSuffix = getString(table_alphabeticSuffix[i + 1]);
                return initELC;
            }
            i += 2;
        }
        return initELC;
    }

    public static boolean verbose;
    private short    initELC = -1;
    private Locale   initLocale;
    private String   initEncoding;
    private String   alphabeticSuffix;

    private short[][][] compFontNameIDs = new short[NUM_FONTS][NUM_STYLES][];
    private int[][][] compExclusions = new int[NUM_FONTS][][];
    private int[] compCoreNum = new int[NUM_FONTS];

    private Set<Short> coreFontNameIDs = new HashSet<Short>();
    private Set<Short> fallbackFontNameIDs = new HashSet<Short>();

    private void initAllComponentFonts() {
        short[] fallbackScripts = getFallbackScripts();
        for (int fontIndex = 0; fontIndex < NUM_FONTS; fontIndex++) {
            short[] coreScripts = getCoreScripts(fontIndex);
            compCoreNum[fontIndex] = coreScripts.length;
            /*
            System.out.println("coreScriptID=" + table_sequences[initELC * 5 + fontIndex]);
            for (int i = 0; i < coreScripts.length; i++) {
            System.out.println("  " + i + " :" + getString(table_scriptIDs[coreScripts[i]]));
            }
            */
            //init exclusionRanges
            int[][] exclusions = new int[coreScripts.length][];
            for (int i = 0; i < coreScripts.length; i++) {
                exclusions[i] = getExclusionRanges(coreScripts[i]);
            }
            compExclusions[fontIndex] = exclusions;
            //init componentFontNames
            for (int styleIndex = 0; styleIndex < NUM_STYLES; styleIndex++) {
                int index;
                short[] nameIDs = new short[coreScripts.length + fallbackScripts.length];
                //core
                for (index = 0; index < coreScripts.length; index++) {
                    nameIDs[index] = getComponentFontID(coreScripts[index],
                                               fontIndex, styleIndex);
                    if (preferLocaleFonts && localeMap != null &&
                            fontManager.usingAlternateFontforJALocales()) {
                        nameIDs[index] = remapLocaleMap(fontIndex, styleIndex,
                                                        coreScripts[index], nameIDs[index]);
                    }
                    if (preferPropFonts) {
                        nameIDs[index] = remapProportional(fontIndex, nameIDs[index]);
                    }
                    //System.out.println("nameid=" + nameIDs[index]);
                    coreFontNameIDs.add(nameIDs[index]);
                }
                //fallback
                for (int i = 0; i < fallbackScripts.length; i++) {
                    short id = getComponentFontID(fallbackScripts[i],
                                               fontIndex, styleIndex);
                    if (preferLocaleFonts && localeMap != null &&
                            fontManager.usingAlternateFontforJALocales()) {
                        id = remapLocaleMap(fontIndex, styleIndex, fallbackScripts[i], id);
                    }
                    if (preferPropFonts) {
                        id = remapProportional(fontIndex, id);
                    }
                    if (contains(nameIDs, id, index)) {
                        continue;
                    }
                    /*
                      System.out.println("fontIndex=" + fontIndex + ", styleIndex=" + styleIndex
                           + ", fbIndex=" + i + ",fbS=" + fallbackScripts[i] + ", id=" + id);
                    */
                    fallbackFontNameIDs.add(id);
                    nameIDs[index++] = id;
                }
                if (index < nameIDs.length) {
                    short[] newNameIDs = new short[index];
                    System.arraycopy(nameIDs, 0, newNameIDs, 0, index);
                    nameIDs = newNameIDs;
                }
                compFontNameIDs[fontIndex][styleIndex] = nameIDs;
            }
        }
   }

   private short remapLocaleMap(int fontIndex, int styleIndex, short scriptID, short fontID) {
        String scriptName = getString(table_scriptIDs[scriptID]);

        String value = localeMap.get(scriptName);
        if (value == null) {
            String fontName = fontNames[fontIndex];
            String styleName = styleNames[styleIndex];
            value = localeMap.get(fontName + "." + styleName + "." + scriptName);
        }
        if (value == null) {
            return fontID;
        }

        for (int i = 0; i < table_componentFontNameIDs.length; i++) {
            String name = getString(table_componentFontNameIDs[i]);
            if (value.equalsIgnoreCase(name)) {
                fontID = (short)i;
                break;
            }
        }
        return fontID;
    }

    public static boolean hasMonoToPropMap() {
        return table_proportionals != null && table_proportionals.length != 0;
    }

    private short remapProportional(int fontIndex, short id) {
    if (preferPropFonts &&
        table_proportionals.length != 0 &&
        fontIndex != 2 &&         //"monospaced"
        fontIndex != 4) {         //"dialoginput"
            int i = 0;
            while (i < table_proportionals.length) {
                if (table_proportionals[i] == id) {
                    return table_proportionals[i + 1];
                }
                i += 2;
            }
        }
        return id;
    }

    /////////////////////////////////////////////////////////////////////
    // Methods for handling font and style names                       //
    /////////////////////////////////////////////////////////////////////
    protected static final int NUM_FONTS = 5;
    protected static final int NUM_STYLES = 4;
    protected static final String[] fontNames
            = {"serif", "sansserif", "monospaced", "dialog", "dialoginput"};
    protected static final String[] publicFontNames
            = {Font.SERIF, Font.SANS_SERIF, Font.MONOSPACED, Font.DIALOG,
               Font.DIALOG_INPUT};
    protected static final String[] styleNames
            = {"plain", "bold", "italic", "bolditalic"};

    /**
     * Checks whether the given font family name is a valid logical font name.
     * The check is case insensitive.
     */
    public static boolean isLogicalFontFamilyName(String fontName) {
        return isLogicalFontFamilyNameLC(fontName.toLowerCase(Locale.ENGLISH));
    }

    /**
     * Checks whether the given font family name is a valid logical font name.
     * The check is case sensitive.
     */
    public static boolean isLogicalFontFamilyNameLC(String fontName) {
        for (int i = 0; i < fontNames.length; i++) {
            if (fontName.equals(fontNames[i])) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks whether the given style name is a valid logical font style name.
     */
    private static boolean isLogicalFontStyleName(String styleName) {
        for (int i = 0; i < styleNames.length; i++) {
            if (styleName.equals(styleNames[i])) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks whether the given font face name is a valid logical font name.
     * The check is case insensitive.
     */
    public static boolean isLogicalFontFaceName(String fontName) {
        return isLogicalFontFaceNameLC(fontName.toLowerCase(Locale.ENGLISH));
    }

   /**
    * Checks whether the given font face name is a valid logical font name.
    * The check is case sensitive.
    */
    public static boolean isLogicalFontFaceNameLC(String fontName) {
        int period = fontName.indexOf('.');
        if (period >= 0) {
            String familyName = fontName.substring(0, period);
            String styleName = fontName.substring(period + 1);
            return isLogicalFontFamilyName(familyName) &&
                    isLogicalFontStyleName(styleName);
        } else {
            return isLogicalFontFamilyName(fontName);
        }
    }

    protected static int getFontIndex(String fontName) {
        return getArrayIndex(fontNames, fontName);
    }

    protected static int getStyleIndex(String styleName) {
        return getArrayIndex(styleNames, styleName);
    }

    private static int getArrayIndex(String[] names, String name) {
        for (int i = 0; i < names.length; i++) {
            if (name.equals(names[i])) {
                return i;
            }
        }
        assert false;
        return 0;
    }

    protected static int getStyleIndex(int style) {
        switch (style) {
            case Font.PLAIN:
                return 0;
            case Font.BOLD:
                return 1;
            case Font.ITALIC:
                return 2;
            case Font.BOLD | Font.ITALIC:
                return 3;
            default:
                return 0;
        }
    }

    protected static String getFontName(int fontIndex) {
        return fontNames[fontIndex];
    }

    protected static String getStyleName(int styleIndex) {
        return styleNames[styleIndex];
    }

    /**
     * Returns the font face name for the given logical font
     * family name and style.
     * The style argument is interpreted as in java.awt.Font.Font.
     */
    public static String getLogicalFontFaceName(String familyName, int style) {
        assert isLogicalFontFamilyName(familyName);
        return familyName.toLowerCase(Locale.ENGLISH) + "." + getStyleString(style);
    }

    /**
     * Returns the string typically used in properties files
     * for the given style.
     * The style argument is interpreted as in java.awt.Font.Font.
     */
    public static String getStyleString(int style) {
        return getStyleName(getStyleIndex(style));
    }

    /**
     * Returns a fallback name for the given font name. For a few known
     * font names, matching logical font names are returned. For all
     * other font names, defaultFallback is returned.
     * defaultFallback differs between AWT and 2D.
     */
    public abstract String getFallbackFamilyName(String fontName, String defaultFallback);

    /**
     * Returns the 1.1 equivalent for some old 1.0 font family names for
     * which we need to maintain compatibility in some configurations.
     * Returns null for other font names.
     */
    protected String getCompatibilityFamilyName(String fontName) {
        fontName = fontName.toLowerCase(Locale.ENGLISH);
        if (fontName.equals("timesroman")) {
            return "serif";
        } else if (fontName.equals("helvetica")) {
            return "sansserif";
        } else if (fontName.equals("courier")) {
            return "monospaced";
        }
        return null;
    }

    protected static String[] installedFallbackFontFiles = null;

    /**
     * Maps a file name given in the font configuration file
     * to a format appropriate for the platform.
     */
    protected String mapFileName(String fileName) {
        return fileName;
    }

    //////////////////////////////////////////////////////////////////////
    //  reordering                                                      //
    //////////////////////////////////////////////////////////////////////

    /* Mappings from file encoding to font config name for font supporting
     * the corresponding language. This is filled in by initReorderMap()
     */
    protected HashMap<String, Object> reorderMap = null;

    /* Platform-specific mappings */
    protected abstract void initReorderMap();

    /* Move item at index "src" to "dst", shuffling all values in
     * between down
     */
    private void shuffle(String[] seq, int src, int dst) {
        if (dst >= src) {
            return;
        }
        String tmp = seq[src];
        for (int i=src; i>dst; i--) {
            seq[i] = seq[i-1];
        }
        seq[dst] = tmp;
    }

    /* Called to determine if there's a re-order sequence for this locale/
     * encoding. If there's none then the caller can "bail" and avoid
     * unnecessary work
     */
    public static boolean willReorderForStartupLocale() {
        return getReorderSequence() != null;
    }

    private static Object getReorderSequence() {
        if (fontConfig.reorderMap == null) {
             fontConfig.initReorderMap();
        }
        HashMap<String, Object> reorderMap = fontConfig.reorderMap;

        /* Find the most specific mapping */
        String language = startupLocale.getLanguage();
        String country = startupLocale.getCountry();
        Object val = reorderMap.get(encoding + "." + language + "." + country);
        if (val == null) {
            val = reorderMap.get(encoding + "." + language);
        }
        if (val == null) {
            val = reorderMap.get(encoding);
        }
        return val;
    }

    /* This method reorders the sequence such that the matches for the
     * file encoding are moved ahead of other elements.
     * If an encoding uses more than one font, they are all moved up.
     */
     private void reorderSequenceForLocale(String[] seq) {
        Object val =  getReorderSequence();
        if (val instanceof String) {
            for (int i=0; i< seq.length; i++) {
                if (seq[i].equals(val)) {
                    shuffle(seq, i, 0);
                    return;
                }
            }
        } else if (val instanceof String[]) {
            String[] fontLangs = (String[])val;
            for (int l=0; l<fontLangs.length;l++) {
                for (int i=0; i<seq.length;i++) {
                    if (seq[i].equals(fontLangs[l])) {
                        shuffle(seq, i, l);
                    }
                }
            }
        }
    }

    private static Vector<String> splitSequence(String sequence) {
        //String.split would be more convenient, but incurs big performance penalty
        Vector<String> parts = new Vector<>();
        int start = 0;
        int end;
        while ((end = sequence.indexOf(',', start)) >= 0) {
            parts.add(sequence.substring(start, end));
            start = end + 1;
        }
        if (sequence.length() > start) {
            parts.add(sequence.substring(start, sequence.length()));
        }
        return parts;
    }

    protected String[] split(String sequence) {
        Vector<String> v = splitSequence(sequence);
        return v.toArray(new String[0]);
    }

    ////////////////////////////////////////////////////////////////////////
    // Methods for extracting information from the fontconfig data for AWT//
    ////////////////////////////////////////////////////////////////////////
    private Hashtable<String, Charset> charsetRegistry = new Hashtable<>(5);

    /**
     * Returns FontDescriptors describing the physical fonts used for the
     * given logical font name and style. The font name is interpreted
     * in a case insensitive way.
     * The style argument is interpreted as in java.awt.Font.Font.
     */
    public FontDescriptor[] getFontDescriptors(String fontName, int style) {
        assert isLogicalFontFamilyName(fontName);
        fontName = fontName.toLowerCase(Locale.ENGLISH);
        int fontIndex = getFontIndex(fontName);
        int styleIndex = getStyleIndex(style);
        return getFontDescriptors(fontIndex, styleIndex);
    }
    private FontDescriptor[][][] fontDescriptors =
        new FontDescriptor[NUM_FONTS][NUM_STYLES][];

    private FontDescriptor[] getFontDescriptors(int fontIndex, int styleIndex) {
        FontDescriptor[] descriptors = fontDescriptors[fontIndex][styleIndex];
        if (descriptors == null) {
            descriptors = buildFontDescriptors(fontIndex, styleIndex);
            fontDescriptors[fontIndex][styleIndex] = descriptors;
        }
        return descriptors;
    }

    protected FontDescriptor[] buildFontDescriptors(int fontIndex, int styleIndex) {
        String fontName = fontNames[fontIndex];
        String styleName = styleNames[styleIndex];

        short[] scriptIDs = getCoreScripts(fontIndex);
        short[] nameIDs = compFontNameIDs[fontIndex][styleIndex];
        String[] sequence = new String[scriptIDs.length];
        String[] names = new String[scriptIDs.length];
        for (int i = 0; i < sequence.length; i++) {
            names[i] = getComponentFontName(nameIDs[i]);
            sequence[i] = getScriptName(scriptIDs[i]);
            if (alphabeticSuffix != null && "alphabetic".equals(sequence[i])) {
                sequence[i] = sequence[i] + "/" + alphabeticSuffix;
            }
        }
        int[][] fontExclusionRanges = compExclusions[fontIndex];

        FontDescriptor[] descriptors = new FontDescriptor[names.length];

        for (int i = 0; i < names.length; i++) {
            String awtFontName;
            String encoding;

            awtFontName = makeAWTFontName(names[i], sequence[i]);

            // look up character encoding
            encoding = getEncoding(names[i], sequence[i]);
            if (encoding == null) {
                encoding = "default";
            }
            CharsetEncoder enc
                    = getFontCharsetEncoder(encoding.trim(), awtFontName);

            // we already have the exclusion ranges
            int[] exclusionRanges = fontExclusionRanges[i];

            // create descriptor
            descriptors[i] = new FontDescriptor(awtFontName, enc, exclusionRanges);
        }
        return descriptors;
    }

    /**
     * Returns the AWT font name for the given platform font name and
     * character subset.
     */
    protected String makeAWTFontName(String platformFontName,
            String characterSubsetName) {
        return platformFontName;
    }

    /**
     * Returns the java.io name of the platform character encoding for the
     * given AWT font name and character subset. May return "default"
     * to indicate that getDefaultFontCharset should be called to obtain
     * a charset encoder.
     */
    protected abstract String getEncoding(String awtFontName,
            String characterSubsetName);

    private CharsetEncoder getFontCharsetEncoder(final String charsetName,
            String fontName) {

        Charset fc = null;
        if (charsetName.equals("default")) {
            fc = charsetRegistry.get(fontName);
        } else {
            fc = charsetRegistry.get(charsetName);
        }
        if (fc != null) {
            return fc.newEncoder();
        }

        if (!charsetName.startsWith("sun.awt.") && !charsetName.equals("default")) {
            fc = Charset.forName(charsetName);
        } else {
            Class<?> fcc = AccessController.doPrivileged(new PrivilegedAction<Class<?>>() {
                    public Class<?> run() {
                        try {
                            return Class.forName(charsetName, true,
                                                 ClassLoader.getSystemClassLoader());
                        } catch (ClassNotFoundException e) {
                        }
                        return null;
                    }
                });

            if (fcc != null) {
                try {
                    fc = (Charset) fcc.getDeclaredConstructor().newInstance();
                } catch (Exception e) {
                }
            }
        }
        if (fc == null) {
            fc = getDefaultFontCharset(fontName);
        }

        if (charsetName.equals("default")){
            charsetRegistry.put(fontName, fc);
        } else {
            charsetRegistry.put(charsetName, fc);
        }
        return fc.newEncoder();
    }

    protected abstract Charset getDefaultFontCharset(
            String fontName);

    /* This retrieves the platform font directories (path) calculated
     * by setAWTFontPathSequence(String[]). The default implementation
     * returns null, its expected that X11 platforms may return
     * non-null.
     */
    public HashSet<String> getAWTFontPathSet() {
        return null;
    }

    ////////////////////////////////////////////////////////////////////////
    // methods for extracting information from the fontconfig data for 2D //
    ////////////////////////////////////////////////////////////////////////

    /**
     * Returns an array of composite font descriptors for all logical font
     * faces.
     */
    public CompositeFontDescriptor[] get2DCompositeFontInfo() {
        CompositeFontDescriptor[] result =
                new CompositeFontDescriptor[NUM_FONTS * NUM_STYLES];
        String defaultFontFile = fontManager.getDefaultFontFile();
        String defaultFontFaceName = fontManager.getDefaultFontFaceName();

        for (int fontIndex = 0; fontIndex < NUM_FONTS; fontIndex++) {
            String fontName = publicFontNames[fontIndex];

            // determine exclusion ranges for font
            // AWT uses separate exclusion range array per component font.
            // 2D packs all range boundaries into one array.
            // Both use separate entries for lower and upper boundary.
            int[][] exclusions = compExclusions[fontIndex];
            int numExclusionRanges = 0;
            for (int i = 0; i < exclusions.length; i++) {
                numExclusionRanges += exclusions[i].length;
            }
            int[] exclusionRanges = new int[numExclusionRanges];
            int[] exclusionRangeLimits = new int[exclusions.length];
            int exclusionRangeIndex = 0;
            int exclusionRangeLimitIndex = 0;
            for (int i = 0; i < exclusions.length; i++) {
                int[] componentRanges = exclusions[i];
                for (int j = 0; j < componentRanges.length; ) {
                    int value = componentRanges[j];
                    exclusionRanges[exclusionRangeIndex++] = componentRanges[j++];
                    exclusionRanges[exclusionRangeIndex++] = componentRanges[j++];
                }
                exclusionRangeLimits[i] = exclusionRangeIndex;
            }
            // other info is per style
            for (int styleIndex = 0; styleIndex < NUM_STYLES; styleIndex++) {
                int maxComponentFontCount = compFontNameIDs[fontIndex][styleIndex].length;
                // fall back fonts listed in the lib/fonts/fallback directory
                if (installedFallbackFontFiles != null) {
                    maxComponentFontCount += installedFallbackFontFiles.length;
                }
                String faceName = fontName + "." + styleNames[styleIndex];

                // determine face names and file names of component fonts
                String[] componentFaceNames = new String[maxComponentFontCount];
                String[] componentFileNames = new String[maxComponentFontCount];

                int index;
                for (index = 0; index < compFontNameIDs[fontIndex][styleIndex].length; index++) {
                    short fontNameID = compFontNameIDs[fontIndex][styleIndex][index];
                    short fileNameID = getComponentFileID(fontNameID);
                    componentFaceNames[index] = getFaceNameFromComponentFontName(getComponentFontName(fontNameID));
                    componentFileNames[index] = mapFileName(getComponentFileName(fileNameID));
                    if (componentFileNames[index] == null ||
                        needToSearchForFile(componentFileNames[index])) {
                        componentFileNames[index] = getFileNameFromComponentFontName(getComponentFontName(fontNameID));
                    }
                    /*
                    System.out.println(publicFontNames[fontIndex] + "." + styleNames[styleIndex] + "."
                        + getString(table_scriptIDs[coreScripts[index]]) + "=" + componentFileNames[index]);
                    */
                }

                if (installedFallbackFontFiles != null) {
                    for (int ifb=0; ifb<installedFallbackFontFiles.length; ifb++) {
                        componentFaceNames[index] = null;
                        componentFileNames[index] = installedFallbackFontFiles[ifb];
                        index++;
                    }
                }

                if (index < maxComponentFontCount) {
                    String[] newComponentFaceNames = new String[index];
                    System.arraycopy(componentFaceNames, 0, newComponentFaceNames, 0, index);
                    componentFaceNames = newComponentFaceNames;
                    String[] newComponentFileNames = new String[index];
                    System.arraycopy(componentFileNames, 0, newComponentFileNames, 0, index);
                    componentFileNames = newComponentFileNames;
                }
                // exclusion range limit array length must match component face name
                // array length - native code relies on this

                int[] clippedExclusionRangeLimits = exclusionRangeLimits;
                if (index != clippedExclusionRangeLimits.length) {
                    int len = exclusionRangeLimits.length;
                    clippedExclusionRangeLimits = new int[index];
                    System.arraycopy(exclusionRangeLimits, 0, clippedExclusionRangeLimits, 0, len);
                    //padding for various fallback fonts
                    for (int i = len; i < index; i++) {
                        clippedExclusionRangeLimits[i] = exclusionRanges.length;
                    }
                }
                /*
                System.out.println(faceName + ":");
                for (int i = 0; i < componentFileNames.length; i++) {
                    System.out.println("    " + componentFaceNames[i]
                         + "  -> " + componentFileNames[i]);
                }
                */
                result[fontIndex * NUM_STYLES + styleIndex]
                        = new CompositeFontDescriptor(
                            faceName,
                            compCoreNum[fontIndex],
                            componentFaceNames,
                            componentFileNames,
                            exclusionRanges,
                            clippedExclusionRangeLimits);
            }
        }
        return result;
    }

    protected abstract String getFaceNameFromComponentFontName(String componentFontName);
    protected abstract String getFileNameFromComponentFontName(String componentFontName);

    /*
    public class 2dFont {
        public String platformName;
        public String fontfileName;
    }
    private 2dFont [] componentFonts = null;
    */

    /* Used on Linux to test if a file referenced in a font configuration
     * file exists in the location that is expected. If it does, no need
     * to search for it. If it doesn't then unless its a fallback font,
     * return that expensive code should be invoked to search for the font.
     */
    HashMap<String, Boolean> existsMap;
    public boolean needToSearchForFile(String fileName) {
        if (!FontUtilities.isLinux) {
            return false;
        } else if (existsMap == null) {
           existsMap = new HashMap<String, Boolean>();
        }
        Boolean exists = existsMap.get(fileName);
        if (exists == null) {
            /* call getNumberCoreFonts() to ensure these are initialised, and
             * if this file isn't for a core component, ie, is a for a fallback
             * font which very typically isn't available, then can't afford
             * to take the start-up penalty to search for it.
             */
            getNumberCoreFonts();
            if (!coreFontFileNames.contains(fileName)) {
                exists = Boolean.TRUE;
            } else {
                exists = Boolean.valueOf((new File(fileName)).exists());
                existsMap.put(fileName, exists);
                if (FontUtilities.debugFonts() &&
                    exists == Boolean.FALSE) {
                    logger.warning("Couldn't locate font file " + fileName);
                }
            }
        }
        return exists == Boolean.FALSE;
    }

    private int numCoreFonts = -1;
    private String[] componentFonts = null;
    HashMap <String, String> filenamesMap = new HashMap<String, String>();
    HashSet <String> coreFontFileNames = new HashSet<String>();

    /* Return the number of core fonts. Note this isn't thread safe but
     * a calling thread can call this and getPlatformFontNames() in either
     * order.
     */
    public int getNumberCoreFonts() {
        if (numCoreFonts == -1) {
            numCoreFonts = coreFontNameIDs.size();
            Short[] emptyShortArray = new Short[0];
            Short[] core = coreFontNameIDs.toArray(emptyShortArray);
            Short[] fallback = fallbackFontNameIDs.toArray(emptyShortArray);

            int numFallbackFonts = 0;
            int i;
            for (i = 0; i < fallback.length; i++) {
                if (coreFontNameIDs.contains(fallback[i])) {
                    fallback[i] = null;
                    continue;
                }
                numFallbackFonts++;
            }
            componentFonts = new String[numCoreFonts + numFallbackFonts];
            String filename = null;
            for (i = 0; i < core.length; i++) {
                short fontid = core[i];
                short fileid = getComponentFileID(fontid);
                componentFonts[i] = getComponentFontName(fontid);
                String compFileName = getComponentFileName(fileid);
                if (compFileName != null) {
                    coreFontFileNames.add(compFileName);
                }
                filenamesMap.put(componentFonts[i], mapFileName(compFileName));
            }
            for (int j = 0; j < fallback.length; j++) {
                if (fallback[j] != null) {
                    short fontid = fallback[j];
                    short fileid = getComponentFileID(fontid);
                    componentFonts[i] = getComponentFontName(fontid);
                    filenamesMap.put(componentFonts[i],
                                     mapFileName(getComponentFileName(fileid)));
                    i++;
                }
            }
        }
        return numCoreFonts;
    }

    /* Return all platform font names used by this font configuration.
     * The first getNumberCoreFonts() entries are guaranteed to be the
     * core fonts - ie no fall back only fonts.
     */
    public String[] getPlatformFontNames() {
        if (numCoreFonts == -1) {
            getNumberCoreFonts();
        }
        return componentFonts;
    }

    /**
     * Returns a file name for the physical font represented by this platform font name,
     * if the font configuration has such information available, or null if the
     * information is unavailable. The file name returned is just a hint; a null return
     * value doesn't necessarily mean that the font is unavailable, nor does a non-null
     * return value guarantee that the file exists and contains the physical font.
     * The file name can be an absolute or a relative path name.
     */
    public String getFileNameFromPlatformName(String platformName) {
        // get2DCompositeFontInfo
        //     ->  getFileNameFromComponentfontName()  (W/M)
        //       ->   getFileNameFromPlatformName()
        // it's a waste of time on Win32, but I have to give X11 a chance to
        // call getFileNameFromXLFD()
        return filenamesMap.get(platformName);
    }

    /**
     * Returns a configuration specific path to be appended to the font
     * search path.
     */
    public String getExtraFontPath() {
        return getString(head[INDEX_appendedfontpath]);
    }

    public String getVersion() {
        return getString(head[INDEX_version]);
    }

    /* subclass support */
    protected static FontConfiguration getFontConfiguration() {
        return fontConfig;
    }

    protected void setFontConfiguration() {
        fontConfig = this;      /* static initialization */
    }

    //////////////////////////////////////////////////////////////////////
    // FontConfig data tables and the index constants in binary file    //
    //////////////////////////////////////////////////////////////////////
    /* The binary font configuration file begins with a short[] "head", which
     * contains the offsets to the starts of the individual data table which
     * immediately follow. The current implementation includes the tables shown
     * below.
     *
     * (00) table_scriptIDs    :stringIDs of all defined CharacterSubsetNames
     * (01) table_scriptFonts  :scriptID x fontIndex x styleIndex->
     *                          PlatformFontNameID mapping. Each scriptID might
     *                          have 1 or 20 entries depends on if it is defined
     *                          via a "allfonts.CharacterSubsetname" or a list of
     *                          "LogicalFontName.StyleName.CharacterSubsetName"
     *                          entries, positive entry means it's a "allfonts"
     *                          entry, a negative value means this is a offset to
     *                          a NUM_FONTS x NUM_STYLES subtable.
     * (02) table_elcIDs       :stringIDs of all defined ELC names, string
     *                          "NULL.NULL.NULL" is used for "default"
     * (03) table_sequences    :elcID x logicalFont -> scriptIDs table defined
     *                          by "sequence.allfonts/LogicalFontName.ELC" in
     *                          font configuration file, each "elcID" has
     *                          NUM_FONTS (5) entries in this table.
     * (04) table_fontfileNameIDs
     *                         :stringIDs of all defined font file names
     * (05) table_componentFontNameIDs
     *                         :stringIDs of all defined PlatformFontNames
     * (06) table_filenames    :platformFontNamesID->fontfileNameID mapping
     *                          table, the index is the platformFontNamesID.
     * (07) table_awtfontpaths :CharacterSubsetNames->awtfontpaths mapping table,
     *                          the index is the CharacterSubsetName's stringID
     *                          and content is the stringID of awtfontpath.
     * (08) table_exclusions   :scriptID -> exclusionRanges mapping table,
     *                          the index is the scriptID and the content is
                                a id of an exclusionRanges int[].
     * (09) table_proportionals:list of pairs of PlatformFontNameIDs, stores
     *                          the replacement info defined by "proportional"
     *                          keyword.
     * (10) table_scriptFontsMotif
     *                         :same as (01) except this table stores the
     *                          info defined with ".motif" keyword
     * (11) table_alphabeticSuffix
     *                         :elcID -> stringID of alphabetic/XXXX entries
     * (12) table_stringIDs    :The index of this table is the string ID, the
     *                          content is the "start index" of this string in
     *                          stringTable, use the start index of next entry
     *                          as the "end index".
     * (13) table_stringTable  :The real storage of all character strings defined
     *                          /used this font configuration, need a pair of
     *                          "start" and "end" indices to access.
     * (14) reserved
     * (15) table_fallbackScripts
     *                         :stringIDs of fallback CharacterSubsetnames, stored
     *                          in the order of they are defined in sequence.fallback.
     * (16) table_appendedfontpath
     *                         :stringtID of the "appendedfontpath" defined.
     * (17) table_version   :stringID of the version number of this fontconfig file.
     */
    private static final int HEAD_LENGTH = 20;
    private static final int INDEX_scriptIDs = 0;
    private static final int INDEX_scriptFonts = 1;
    private static final int INDEX_elcIDs = 2;
    private static final int INDEX_sequences = 3;
    private static final int INDEX_fontfileNameIDs = 4;
    private static final int INDEX_componentFontNameIDs = 5;
    private static final int INDEX_filenames = 6;
    private static final int INDEX_awtfontpaths = 7;
    private static final int INDEX_exclusions = 8;
    private static final int INDEX_proportionals = 9;
    private static final int INDEX_scriptFontsMotif = 10;
    private static final int INDEX_alphabeticSuffix = 11;
    private static final int INDEX_stringIDs = 12;
    private static final int INDEX_stringTable = 13;
    private static final int INDEX_TABLEEND = 14;
    private static final int INDEX_fallbackScripts = 15;
    private static final int INDEX_appendedfontpath = 16;
    private static final int INDEX_version = 17;

    private static short[] head;
    private static short[] table_scriptIDs;
    private static short[] table_scriptFonts;
    private static short[] table_elcIDs;
    private static short[] table_sequences;
    private static short[] table_fontfileNameIDs;
    private static short[] table_componentFontNameIDs;
    private static short[] table_filenames;
    protected static short[] table_awtfontpaths;
    private static short[] table_exclusions;
    private static short[] table_proportionals;
    private static short[] table_scriptFontsMotif;
    private static short[] table_alphabeticSuffix;
    private static short[] table_stringIDs;
    private static char[]  table_stringTable;

    /**
     * Checks consistencies of complied fontconfig data. This method
     * is called only at the build-time from
     * build.tools.compilefontconfig.CompileFontConfig.
     */
    private static void sanityCheck() {
        int errors = 0;

        //This method will only be called during build time, do we
        //need do PrivilegedAction?
        String osName = java.security.AccessController.doPrivileged(
                            new java.security.PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("os.name");
            }
        });

        //componentFontNameID starts from "1"
        for (int ii = 1; ii < table_filenames.length; ii++) {
            if (table_filenames[ii] == -1) {
                // The corresponding finename entry for a component
                // font name is mandatory on Windows, but it's
                // optional on Solaris and Linux.
                if (osName.contains("Windows")) {
                    System.err.println("\n Error: <filename."
                                       + getString(table_componentFontNameIDs[ii])
                                       + "> entry is missing!!!");
                    errors++;
                } else {
                    if (verbose && !isEmpty(table_filenames)) {
                        System.err.println("\n Note: 'filename' entry is undefined for \""
                                           + getString(table_componentFontNameIDs[ii])
                                           + "\"");
                    }
                }
            }
        }
        for (int ii = 0; ii < table_scriptIDs.length; ii++) {
            short fid = table_scriptFonts[ii];
            if (fid == 0) {
                System.out.println("\n Error: <allfonts."
                                   + getString(table_scriptIDs[ii])
                                   + "> entry is missing!!!");
                errors++;
                continue;
            } else if (fid < 0) {
                fid = (short)-fid;
                for (int iii = 0; iii < NUM_FONTS; iii++) {
                    for (int iij = 0; iij < NUM_STYLES; iij++) {
                        int jj = iii * NUM_STYLES + iij;
                        short ffid = table_scriptFonts[fid + jj];
                        if (ffid == 0) {
                            System.err.println("\n Error: <"
                                           + getFontName(iii) + "."
                                           + getStyleName(iij) + "."
                                           + getString(table_scriptIDs[ii])
                                           + "> entry is missing!!!");
                            errors++;
                        }
                    }
                }
            }
        }
        if ("SunOS".equals(osName)) {
            for (int ii = 0; ii < table_awtfontpaths.length; ii++) {
                if (table_awtfontpaths[ii] == 0) {
                    String script = getString(table_scriptIDs[ii]);
                    if (script.contains("dingbats") ||
                        script.contains("symbol")) {
                        continue;
                    }
                    System.err.println("\nError: "
                                       + "<awtfontpath."
                                       + script
                                       + "> entry is missing!!!");
                    errors++;
                }
            }
        }
        if (errors != 0) {
            System.err.println("!!THERE ARE " + errors + " ERROR(S) IN "
                               + "THE FONTCONFIG FILE, PLEASE CHECK ITS CONTENT!!\n");
            System.exit(1);
        }
    }

    private static boolean isEmpty(short[] a) {
        for (short s : a) {
            if (s != -1) {
                return false;
            }
        }
        return true;
    }

    //dump the fontconfig data tables
    private static void dump() {
        System.out.println("\n----Head Table------------");
        for (int ii = 0; ii < HEAD_LENGTH; ii++) {
            System.out.println("  " + ii + " : " + head[ii]);
        }
        System.out.println("\n----scriptIDs-------------");
        printTable(table_scriptIDs, 0);
        System.out.println("\n----scriptFonts----------------");
        for (int ii = 0; ii < table_scriptIDs.length; ii++) {
            short fid = table_scriptFonts[ii];
            if (fid >= 0) {
                System.out.println("  allfonts."
                                   + getString(table_scriptIDs[ii])
                                   + "="
                                   + getString(table_componentFontNameIDs[fid]));
            }
        }
        for (int ii = 0; ii < table_scriptIDs.length; ii++) {
            short fid = table_scriptFonts[ii];
            if (fid < 0) {
                fid = (short)-fid;
                for (int iii = 0; iii < NUM_FONTS; iii++) {
                    for (int iij = 0; iij < NUM_STYLES; iij++) {
                        int jj = iii * NUM_STYLES + iij;
                        short ffid = table_scriptFonts[fid + jj];
                        System.out.println("  "
                                           + getFontName(iii) + "."
                                           + getStyleName(iij) + "."
                                           + getString(table_scriptIDs[ii])
                                           + "="
                                           + getString(table_componentFontNameIDs[ffid]));
                    }
                }

            }
        }
        System.out.println("\n----elcIDs----------------");
        printTable(table_elcIDs, 0);
        System.out.println("\n----sequences-------------");
        for (int ii = 0; ii< table_elcIDs.length; ii++) {
            System.out.println("  " + ii + "/" + getString(table_elcIDs[ii]));
            short[] ss = getShortArray(table_sequences[ii * NUM_FONTS + 0]);
            for (int jj = 0; jj < ss.length; jj++) {
                System.out.println("     " + getString(table_scriptIDs[ss[jj]]));
            }
        }
        System.out.println("\n----fontfileNameIDs-------");
        printTable(table_fontfileNameIDs, 0);

        System.out.println("\n----componentFontNameIDs--");
        printTable(table_componentFontNameIDs, 1);
        System.out.println("\n----filenames-------------");
        for (int ii = 0; ii < table_filenames.length; ii++) {
            if (table_filenames[ii] == -1) {
                System.out.println("  " + ii + " : null");
            } else {
                System.out.println("  " + ii + " : "
                   + getString(table_fontfileNameIDs[table_filenames[ii]]));
            }
        }
        System.out.println("\n----awtfontpaths---------");
        for (int ii = 0; ii < table_awtfontpaths.length; ii++) {
            System.out.println("  " + getString(table_scriptIDs[ii])
                               + " : "
                               + getString(table_awtfontpaths[ii]));
        }
        System.out.println("\n----proportionals--------");
        for (int ii = 0; ii < table_proportionals.length; ii++) {
            System.out.println("  "
                   + getString(table_componentFontNameIDs[table_proportionals[ii++]])
                   + " -> "
                   + getString(table_componentFontNameIDs[table_proportionals[ii]]));
        }
        int i = 0;
        System.out.println("\n----alphabeticSuffix----");
        while (i < table_alphabeticSuffix.length) {
          System.out.println("    " + getString(table_elcIDs[table_alphabeticSuffix[i++]])
                             + " -> " + getString(table_alphabeticSuffix[i++]));
        }
        System.out.println("\n----String Table---------");
        System.out.println("    stringID:    Num =" + table_stringIDs.length);
        System.out.println("    stringTable: Size=" + table_stringTable.length * 2);

        System.out.println("\n----fallbackScriptIDs---");
        short[] fbsIDs = getShortArray(head[INDEX_fallbackScripts]);
        for (int ii = 0; ii < fbsIDs.length; ii++) {
          System.out.println("  " + getString(table_scriptIDs[fbsIDs[ii]]));
        }
        System.out.println("\n----appendedfontpath-----");
        System.out.println("  " + getString(head[INDEX_appendedfontpath]));
        System.out.println("\n----Version--------------");
        System.out.println("  " + getString(head[INDEX_version]));
    }


    //////////////////////////////////////////////////////////////////////
    // Data table access methods                                        //
    //////////////////////////////////////////////////////////////////////

    /* Return the fontID of the platformFontName defined in this font config
     * by "LogicalFontName.StyleName.CharacterSubsetName" entry or
     * "allfonts.CharacterSubsetName" entry in properties format fc file.
     */
    protected static short getComponentFontID(short scriptID, int fontIndex, int styleIndex) {
        short fid = table_scriptFonts[scriptID];
        //System.out.println("fid=" + fid + "/ scriptID=" + scriptID + ", fi=" + fontIndex + ", si=" + styleIndex);
        if (fid >= 0) {
            //"allfonts"
            return fid;
        } else {
            return table_scriptFonts[-fid + fontIndex * NUM_STYLES + styleIndex];
        }
    }

    /* Same as getCompoentFontID() except this method returns the fontID define by
     * "xxxx.motif" entry.
     */
    protected static short getComponentFontIDMotif(short scriptID, int fontIndex, int styleIndex) {
        if (table_scriptFontsMotif.length == 0) {
            return 0;
        }
        short fid = table_scriptFontsMotif[scriptID];
        if (fid >= 0) {
            //"allfonts" > 0 or "not defined" == 0
            return fid;
        } else {
            return table_scriptFontsMotif[-fid + fontIndex * NUM_STYLES + styleIndex];
        }
    }

    private static int[] getExclusionRanges(short scriptID) {
        short exID = table_exclusions[scriptID];
        if (exID == 0) {
            return EMPTY_INT_ARRAY;
        } else {
            char[] exChar = getString(exID).toCharArray();
            int[] exInt = new int[exChar.length / 2];
            int i = 0;
            for (int j = 0; j < exInt.length; j++) {
                exInt[j] = (exChar[i++] << 16) + (exChar[i++] & 0xffff);
            }
            return exInt;
        }
    }

    private static boolean contains(short[] IDs, short id, int limit) {
        for (int i = 0; i < limit; i++) {
            if (IDs[i] == id) {
                return true;
            }
        }
        return false;
    }

    /* Return the PlatformFontName from its fontID*/
    protected static String getComponentFontName(short id) {
        if (id < 0) {
            return null;
        }
        return getString(table_componentFontNameIDs[id]);
    }

    private static String getComponentFileName(short id) {
        if (id < 0) {
            return null;
        }
        return getString(table_fontfileNameIDs[id]);
    }

    //componentFontID -> componentFileID
    private static short getComponentFileID(short nameID) {
        return table_filenames[nameID];
    }

    private static String getScriptName(short scriptID) {
        return getString(table_scriptIDs[scriptID]);
    }

   private HashMap<String, Short> reorderScripts;
   protected short[] getCoreScripts(int fontIndex) {
        short elc = getInitELC();
        /*
          System.out.println("getCoreScripts: elc=" + elc + ", fontIndex=" + fontIndex);
          short[] ss = getShortArray(table_sequences[elc * NUM_FONTS + fontIndex]);
          for (int i = 0; i < ss.length; i++) {
              System.out.println("     " + getString((short)table_scriptIDs[ss[i]]));
          }
          */
        short[] scripts = getShortArray(table_sequences[elc * NUM_FONTS + fontIndex]);
        if (preferLocaleFonts) {
            if (reorderScripts == null) {
                reorderScripts = new HashMap<String, Short>();
            }
            String[] ss = new String[scripts.length];
            for (int i = 0; i < ss.length; i++) {
                ss[i] = getScriptName(scripts[i]);
                reorderScripts.put(ss[i], scripts[i]);
            }
            reorderSequenceForLocale(ss);
            for (int i = 0; i < ss.length; i++) {
                scripts[i] = reorderScripts.get(ss[i]);
            }
        }
         return scripts;
    }

    private static short[] getFallbackScripts() {
        return getShortArray(head[INDEX_fallbackScripts]);
    }

    private static void printTable(short[] list, int start) {
        for (int i = start; i < list.length; i++) {
            System.out.println("  " + i + " : " + getString(list[i]));
        }
    }

    private static short[] readShortTable(DataInputStream in, int len )
        throws IOException {
        if (len == 0) {
            return EMPTY_SHORT_ARRAY;
        }
        short[] data = new short[len];
        byte[] bb = new byte[len * 2];
        in.read(bb);
        int i = 0,j = 0;
        while (i < len) {
            data[i++] = (short)(bb[j++] << 8 | (bb[j++] & 0xff));
        }
        return data;
    }

    private static void writeShortTable(DataOutputStream out, short[] data)
        throws IOException {
        for (short val : data) {
            out.writeShort(val);
        }
    }

    private static short[] toList(HashMap<String, Short> map) {
        short[] list = new short[map.size()];
        Arrays.fill(list, (short) -1);
        for (Entry<String, Short> entry : map.entrySet()) {
            list[entry.getValue()] = getStringID(entry.getKey());
        }
        return list;
    }

    //runtime cache
    private static String[] stringCache;
    protected static String getString(short stringID) {
        if (stringID == 0)
            return null;
        /*
        if (loadingProperties) {
            return stringTable.substring(stringIDs[stringID],
                                         stringIDs[stringID+1]);
        }
        */
        //sync if we want it to be MT-enabled
        if (stringCache[stringID] == null){
            stringCache[stringID] =
              new String (table_stringTable,
                          table_stringIDs[stringID],
                          table_stringIDs[stringID+1] - table_stringIDs[stringID]);
        }
        return stringCache[stringID];
    }

    private static short[] getShortArray(short shortArrayID) {
        String s = getString(shortArrayID);
        char[] cc = s.toCharArray();
        short[] ss = new short[cc.length];
        for (int i = 0; i < cc.length; i++) {
            ss[i] = (short)(cc[i] & 0xffff);
        }
        return ss;
    }

    private static short getStringID(String s) {
        if (s == null) {
            return (short)0;
        }
        short pos0 = (short)stringTable.length();
        stringTable.append(s);
        short pos1 = (short)stringTable.length();

        stringIDs[stringIDNum] = pos0;
        stringIDs[stringIDNum + 1] = pos1;
        stringIDNum++;
        if (stringIDNum + 1 >= stringIDs.length) {
            short[] tmp = new short[stringIDNum + 1000];
            System.arraycopy(stringIDs, 0, tmp, 0, stringIDNum);
            stringIDs = tmp;
        }
        return (short)(stringIDNum - 1);
    }

    private static short getShortArrayID(short[] sa) {
        char[] cc = new char[sa.length];
        for (int i = 0; i < sa.length; i ++) {
            cc[i] = (char)sa[i];
        }
        String s = new String(cc);
        return getStringID(s);
    }

    //utility "empty" objects
    private static final int[] EMPTY_INT_ARRAY = new int[0];
    private static final String[] EMPTY_STRING_ARRAY = new String[0];
    private static final short[] EMPTY_SHORT_ARRAY = new short[0];
    private static final String UNDEFINED_COMPONENT_FONT = "unknown";

    //////////////////////////////////////////////////////////////////////////
    //Convert the FontConfig data in Properties file to binary data tables  //
    //////////////////////////////////////////////////////////////////////////
    static class PropertiesHandler {
        public void load(InputStream in) throws IOException {
            initLogicalNameStyle();
            initHashMaps();
            FontProperties fp = new FontProperties();
            fp.load(in);
            initBinaryTable();
        }

        private void initBinaryTable() {
            //(0)
            head = new short[HEAD_LENGTH];
            head[INDEX_scriptIDs] = (short)HEAD_LENGTH;

            table_scriptIDs = toList(scriptIDs);
            //(1)a: scriptAllfonts scriptID/allfonts -> componentFontNameID
            //   b: scriptFonts    scriptID -> componentFontNameID[20]
            //if we have a "allfonts.script" def, then we just put
            //the "-platformFontID" value in the slot, otherwise the slot
            //value is "offset" which "offset" is where 20 entries located
            //in the table attached.
            head[INDEX_scriptFonts] = (short)(head[INDEX_scriptIDs]  + table_scriptIDs.length);
            int len = table_scriptIDs.length + scriptFonts.size() * 20;
            table_scriptFonts = new short[len];

            for (Entry<Short, Short> entry : scriptAllfonts.entrySet()) {
                table_scriptFonts[entry.getKey().intValue()] = entry.getValue();
            }
            int off = table_scriptIDs.length;
            for (Entry<Short, Short[]> entry : scriptFonts.entrySet()) {
                table_scriptFonts[entry.getKey().intValue()] = (short)-off;
                Short[] v = entry.getValue();
                for (int i = 0; i < 20; i++) {
                    if (v[i] != null) {
                        table_scriptFonts[off++] = v[i];
                    } else {
                        table_scriptFonts[off++] = 0;
                    }
                }
            }

            //(2)
            head[INDEX_elcIDs] = (short)(head[INDEX_scriptFonts]  + table_scriptFonts.length);
            table_elcIDs = toList(elcIDs);

            //(3) sequences  elcID -> XXXX[1|5] -> scriptID[]
            head[INDEX_sequences] = (short)(head[INDEX_elcIDs]  + table_elcIDs.length);
            table_sequences = new short[elcIDs.size() * NUM_FONTS];
            for (Entry<Short, short[]> entry : sequences.entrySet()) {
                //table_sequences[entry.getKey().intValue()] = (short)-off;
                int k = entry.getKey().intValue();
                short[] v = entry.getValue();
                /*
                  System.out.println("elc=" + k + "/" + getString((short)table_elcIDs[k]));
                  short[] ss = getShortArray(v[0]);
                  for (int i = 0; i < ss.length; i++) {
                  System.out.println("     " + getString((short)table_scriptIDs[ss[i]]));
                  }
                  */
                if (v.length == 1) {
                    //the "allfonts" entries
                    for (int i = 0; i < NUM_FONTS; i++) {
                        table_sequences[k * NUM_FONTS + i] = v[0];
                    }
                } else {
                    for (int i = 0; i < NUM_FONTS; i++) {
                        table_sequences[k * NUM_FONTS + i] = v[i];
                    }
                }
            }
            //(4)
            head[INDEX_fontfileNameIDs] = (short)(head[INDEX_sequences]  + table_sequences.length);
            table_fontfileNameIDs = toList(fontfileNameIDs);

            //(5)
            head[INDEX_componentFontNameIDs] = (short)(head[INDEX_fontfileNameIDs]  + table_fontfileNameIDs.length);
            table_componentFontNameIDs = toList(componentFontNameIDs);

            //(6)componentFontNameID -> filenameID
            head[INDEX_filenames] = (short)(head[INDEX_componentFontNameIDs]  + table_componentFontNameIDs.length);
            table_filenames = new short[table_componentFontNameIDs.length];
            Arrays.fill(table_filenames, (short) -1);

            for (Entry<Short, Short> entry : filenames.entrySet()) {
                table_filenames[entry.getKey()] = entry.getValue();
            }

            //(7)scriptID-> awtfontpath
            //the paths are stored as scriptID -> stringID in awtfontpahts
            head[INDEX_awtfontpaths] = (short)(head[INDEX_filenames]  + table_filenames.length);
            table_awtfontpaths = new short[table_scriptIDs.length];
            for (Entry<Short, Short> entry : awtfontpaths.entrySet()) {
                table_awtfontpaths[entry.getKey()] = entry.getValue();
            }

            //(8)exclusions
            head[INDEX_exclusions] = (short)(head[INDEX_awtfontpaths]  + table_awtfontpaths.length);
            table_exclusions = new short[scriptIDs.size()];
            for (Entry<Short, int[]> entry : exclusions.entrySet()) {
                int[] exI = entry.getValue();
                char[] exC = new char[exI.length * 2];
                int j = 0;
                for (int i = 0; i < exI.length; i++) {
                    exC[j++] = (char) (exI[i] >> 16);
                    exC[j++] = (char) (exI[i] & 0xffff);
                }
                table_exclusions[entry.getKey()] = getStringID(new String (exC));
            }
            //(9)proportionals
            head[INDEX_proportionals] = (short)(head[INDEX_exclusions]  + table_exclusions.length);
            table_proportionals = new short[proportionals.size() * 2];
            int j = 0;
            for (Entry<Short, Short> entry : proportionals.entrySet()) {
                table_proportionals[j++] = entry.getKey();
                table_proportionals[j++] = entry.getValue();
            }

            //(10) see (1) for info, the only difference is "xxx.motif"
            head[INDEX_scriptFontsMotif] = (short)(head[INDEX_proportionals] + table_proportionals.length);
            if (scriptAllfontsMotif.size() != 0 || scriptFontsMotif.size() != 0) {
                len = table_scriptIDs.length + scriptFontsMotif.size() * 20;
                table_scriptFontsMotif = new short[len];

                for (Entry<Short, Short> entry : scriptAllfontsMotif.entrySet()) {
                    table_scriptFontsMotif[entry.getKey().intValue()] =
                      (short)entry.getValue();
                }
                off = table_scriptIDs.length;
                for (Entry<Short, Short[]> entry : scriptFontsMotif.entrySet()) {
                    table_scriptFontsMotif[entry.getKey().intValue()] = (short)-off;
                    Short[] v = entry.getValue();
                    int i = 0;
                    while (i < 20) {
                        if (v[i] != null) {
                            table_scriptFontsMotif[off++] = v[i];
                        } else {
                            table_scriptFontsMotif[off++] = 0;
                        }
                        i++;
                    }
                }
            } else {
                table_scriptFontsMotif = EMPTY_SHORT_ARRAY;
            }

            //(11)short[] alphabeticSuffix
            head[INDEX_alphabeticSuffix] = (short)(head[INDEX_scriptFontsMotif] + table_scriptFontsMotif.length);
            table_alphabeticSuffix = new short[alphabeticSuffix.size() * 2];
            j = 0;
            for (Entry<Short, Short> entry : alphabeticSuffix.entrySet()) {
                table_alphabeticSuffix[j++] = entry.getKey();
                table_alphabeticSuffix[j++] = entry.getValue();
            }

            //(15)short[] fallbackScriptIDs; just put the ID in head
            head[INDEX_fallbackScripts] = getShortArrayID(fallbackScriptIDs);

            //(16)appendedfontpath
            head[INDEX_appendedfontpath] = getStringID(appendedfontpath);

            //(17)version
            head[INDEX_version] = getStringID(version);

            //(12)short[] StringIDs
            head[INDEX_stringIDs] = (short)(head[INDEX_alphabeticSuffix] + table_alphabeticSuffix.length);
            table_stringIDs = new short[stringIDNum + 1];
            System.arraycopy(stringIDs, 0, table_stringIDs, 0, stringIDNum + 1);

            //(13)StringTable
            head[INDEX_stringTable] = (short)(head[INDEX_stringIDs] + stringIDNum + 1);
            table_stringTable = stringTable.toString().toCharArray();
            //(14)
            head[INDEX_TABLEEND] = (short)(head[INDEX_stringTable] + stringTable.length());

            //StringTable cache
            stringCache = new String[table_stringIDs.length];
        }

        //////////////////////////////////////////////
        private HashMap<String, Short> scriptIDs;
        //elc -> Encoding.Language.Country
        private HashMap<String, Short> elcIDs;
        //componentFontNameID starts from "1", "0" reserves for "undefined"
        private HashMap<String, Short> componentFontNameIDs;
        private HashMap<String, Short> fontfileNameIDs;
        private HashMap<String, Integer> logicalFontIDs;
        private HashMap<String, Integer> fontStyleIDs;

        //componentFontNameID -> fontfileNameID
        private HashMap<Short, Short>  filenames;

        //elcID -> allfonts/logicalFont -> scriptID list
        //(1)if we have a "allfonts", then the length of the
        //   value array is "1", otherwise it's 5, each font
        //   must have their own individual entry.
        //scriptID list "short[]" is stored as an ID
        private HashMap<Short, short[]> sequences;

        //scriptID ->logicFontID/fontStyleID->componentFontNameID,
        //a 20-entry array (5-name x 4-style) for each script
        private HashMap<Short, Short[]> scriptFonts;

        //scriptID -> componentFontNameID
        private HashMap<Short, Short> scriptAllfonts;

        //scriptID -> exclusionRanges[]
        private HashMap<Short, int[]> exclusions;

        //scriptID -> fontpath
        private HashMap<Short, Short> awtfontpaths;

        //fontID -> fontID
        private HashMap<Short, Short> proportionals;

        //scriptID -> componentFontNameID
        private HashMap<Short, Short> scriptAllfontsMotif;

        //scriptID ->logicFontID/fontStyleID->componentFontNameID,
        private HashMap<Short, Short[]> scriptFontsMotif;

        //elcID -> stringID of alphabetic/XXXX
        private HashMap<Short, Short> alphabeticSuffix;

        private short[] fallbackScriptIDs;
        private String version;
        private String appendedfontpath;

        private void initLogicalNameStyle() {
            logicalFontIDs = new HashMap<String, Integer>();
            fontStyleIDs = new HashMap<String, Integer>();
            logicalFontIDs.put("serif",      0);
            logicalFontIDs.put("sansserif",  1);
            logicalFontIDs.put("monospaced", 2);
            logicalFontIDs.put("dialog",     3);
            logicalFontIDs.put("dialoginput",4);
            fontStyleIDs.put("plain",      0);
            fontStyleIDs.put("bold",       1);
            fontStyleIDs.put("italic",     2);
            fontStyleIDs.put("bolditalic", 3);
        }

        private void initHashMaps() {
            scriptIDs = new HashMap<String, Short>();
            elcIDs = new HashMap<String, Short>();
            componentFontNameIDs = new HashMap<String, Short>();
            /*Init these tables to allow componentFontNameID, fontfileNameIDs
              to start from "1".
            */
            componentFontNameIDs.put("", Short.valueOf((short)0));

            fontfileNameIDs = new HashMap<String, Short>();
            filenames = new HashMap<Short, Short>();
            sequences = new HashMap<Short, short[]>();
            scriptFonts = new HashMap<Short, Short[]>();
            scriptAllfonts = new HashMap<Short, Short>();
            exclusions = new HashMap<Short, int[]>();
            awtfontpaths = new HashMap<Short, Short>();
            proportionals = new HashMap<Short, Short>();
            scriptFontsMotif = new HashMap<Short, Short[]>();
            scriptAllfontsMotif = new HashMap<Short, Short>();
            alphabeticSuffix = new HashMap<Short, Short>();
            fallbackScriptIDs = EMPTY_SHORT_ARRAY;
            /*
              version
              appendedfontpath
            */
        }

        private int[] parseExclusions(String key, String exclusions) {
            if (exclusions == null) {
                return EMPTY_INT_ARRAY;
            }
            // range format is xxxx-XXXX,yyyyyy-YYYYYY,.....
            int numExclusions = 1;
            int pos = 0;
            while ((pos = exclusions.indexOf(',', pos)) != -1) {
                numExclusions++;
                pos++;
            }
            int[] exclusionRanges = new int[numExclusions * 2];
            pos = 0;
            int newPos = 0;
            for (int j = 0; j < numExclusions * 2; ) {
                String lower, upper;
                int lo = 0, up = 0;
                try {
                    newPos = exclusions.indexOf('-', pos);
                    lower = exclusions.substring(pos, newPos);
                    pos = newPos + 1;
                    newPos = exclusions.indexOf(',', pos);
                    if (newPos == -1) {
                        newPos = exclusions.length();
                    }
                    upper = exclusions.substring(pos, newPos);
                    pos = newPos + 1;
                    int lowerLength = lower.length();
                    int upperLength = upper.length();
                    if (lowerLength != 4 && lowerLength != 6
                        || upperLength != 4 && upperLength != 6) {
                        throw new Exception();
                    }
                    lo = Integer.parseInt(lower, 16);
                    up = Integer.parseInt(upper, 16);
                    if (lo > up) {
                        throw new Exception();
                    }
                } catch (Exception e) {
                    if (FontUtilities.debugFonts() &&
                        logger != null) {
                        logger.config("Failed parsing " + key +
                                  " property of font configuration.");

                    }
                    return EMPTY_INT_ARRAY;
                }
                exclusionRanges[j++] = lo;
                exclusionRanges[j++] = up;
            }
            return exclusionRanges;
        }

        private Short getID(HashMap<String, Short> map, String key) {
            Short ret = map.get(key);
            if ( ret == null) {
                map.put(key, (short)map.size());
                return map.get(key);
            }
            return ret;
        }

        @SuppressWarnings("serial") // JDK-implementation class
        class FontProperties extends Properties {
            public synchronized Object put(Object k, Object v) {
                parseProperty((String)k, (String)v);
                return null;
            }
        }

        private void parseProperty(String key, String value) {
            if (key.startsWith("filename.")) {
                //the only special case is "MingLiu_HKSCS" which has "_" in its
                //facename, we don't want to replace the "_" with " "
                key = key.substring(9);
                if (!"MingLiU_HKSCS".equals(key)) {
                    key = key.replace('_', ' ');
                }
                Short faceID = getID(componentFontNameIDs, key);
                Short fileID = getID(fontfileNameIDs, value);
                //System.out.println("faceID=" + faceID + "/" + key + " -> "
                //    + "fileID=" + fileID + "/" + value);
                filenames.put(faceID, fileID);
            } else if (key.startsWith("exclusion.")) {
                key = key.substring(10);
                exclusions.put(getID(scriptIDs,key), parseExclusions(key,value));
            } else if (key.startsWith("sequence.")) {
                key = key.substring(9);
                boolean hasDefault = false;
                boolean has1252 = false;

                //get the scriptID list
                String[] ss = splitSequence(value).toArray(EMPTY_STRING_ARRAY);
                short [] sa = new short[ss.length];
                for (int i = 0; i < ss.length; i++) {
                    if ("alphabetic/default".equals(ss[i])) {
                        //System.out.println(key + " -> " + ss[i]);
                        ss[i] = "alphabetic";
                        hasDefault = true;
                    } else if ("alphabetic/1252".equals(ss[i])) {
                        //System.out.println(key + " -> " + ss[i]);
                        ss[i] = "alphabetic";
                        has1252 = true;
                    }
                    sa[i] = getID(scriptIDs, ss[i]).shortValue();
                    //System.out.println("scriptID=" + si[i] + "/" + ss[i]);
                }
                //convert the "short[] -> string -> stringID"
                short scriptArrayID = getShortArrayID(sa);
                Short elcID = null;
                int dot = key.indexOf('.');
                if (dot == -1) {
                    if ("fallback".equals(key)) {
                        fallbackScriptIDs = sa;
                        return;
                    }
                    if ("allfonts".equals(key)) {
                        elcID = getID(elcIDs, "NULL.NULL.NULL");
                    } else {
                        if (logger != null) {
                            logger.config("Error sequence def: <sequence." + key + ">");
                        }
                        return;
                    }
                } else {
                    elcID = getID(elcIDs, key.substring(dot + 1));
                    //System.out.println("elcID=" + elcID + "/" + key.substring(dot + 1));
                    key = key.substring(0, dot);
                }
                short[] scriptArrayIDs = null;
                if ("allfonts".equals(key)) {
                    scriptArrayIDs = new short[1];
                    scriptArrayIDs[0] = scriptArrayID;
                } else {
                    scriptArrayIDs = sequences.get(elcID);
                    if (scriptArrayIDs == null) {
                       scriptArrayIDs = new short[5];
                    }
                    Integer fid = logicalFontIDs.get(key);
                    if (fid == null) {
                        if (logger != null) {
                            logger.config("Unrecognizable logicfont name " + key);
                        }
                        return;
                    }
                    //System.out.println("sequence." + key + "/" + id);
                    scriptArrayIDs[fid.intValue()] = scriptArrayID;
                }
                sequences.put(elcID, scriptArrayIDs);
                if (hasDefault) {
                    alphabeticSuffix.put(elcID, getStringID("default"));
                } else
                if (has1252) {
                    alphabeticSuffix.put(elcID, getStringID("1252"));
                }
            } else if (key.startsWith("allfonts.")) {
                key = key.substring(9);
                if (key.endsWith(".motif")) {
                    key = key.substring(0, key.length() - 6);
                    //System.out.println("motif: all." + key + "=" + value);
                    scriptAllfontsMotif.put(getID(scriptIDs,key), getID(componentFontNameIDs,value));
                } else {
                    scriptAllfonts.put(getID(scriptIDs,key), getID(componentFontNameIDs,value));
                }
            } else if (key.startsWith("awtfontpath.")) {
                key = key.substring(12);
                //System.out.println("scriptID=" + getID(scriptIDs, key) + "/" + key);
                awtfontpaths.put(getID(scriptIDs, key), getStringID(value));
            } else if ("version".equals(key)) {
                version = value;
            } else if ("appendedfontpath".equals(key)) {
                appendedfontpath = value;
            } else if (key.startsWith("proportional.")) {
                key = key.substring(13).replace('_', ' ');
                //System.out.println(key + "=" + value);
                proportionals.put(getID(componentFontNameIDs, key),
                                  getID(componentFontNameIDs, value));
            } else {
                //"name.style.script(.motif)", we don't care anything else
                int dot1, dot2;
                boolean isMotif = false;

                dot1 = key.indexOf('.');
                if (dot1 == -1) {
                    if (logger != null) {
                        logger.config("Failed parsing " + key +
                                  " property of font configuration.");

                    }
                    return;
                }
                dot2 = key.indexOf('.', dot1 + 1);
                if (dot2 == -1) {
                    if (logger != null) {
                        logger.config("Failed parsing " + key +
                                  " property of font configuration.");

                    }
                    return;
                }
                if (key.endsWith(".motif")) {
                    key = key.substring(0, key.length() - 6);
                    isMotif = true;
                    //System.out.println("motif: " + key + "=" + value);
                }
                Integer nameID = logicalFontIDs.get(key.substring(0, dot1));
                Integer styleID = fontStyleIDs.get(key.substring(dot1+1, dot2));
                Short scriptID = getID(scriptIDs, key.substring(dot2 + 1));
                if (nameID == null || styleID == null) {
                    if (logger != null) {
                        logger.config("unrecognizable logicfont name/style at " + key);
                    }
                    return;
                }
                Short[] pnids;
                if (isMotif) {
                    pnids = scriptFontsMotif.get(scriptID);
                } else {
                    pnids = scriptFonts.get(scriptID);
                }
                if (pnids == null) {
                    pnids =  new Short[20];
                }
                pnids[nameID.intValue() * NUM_STYLES + styleID.intValue()]
                  = getID(componentFontNameIDs, value);
                /*
                System.out.println("key=" + key + "/<" + nameID + "><" + styleID
                                     + "><" + scriptID + ">=" + value
                                     + "/" + getID(componentFontNameIDs, value));
                */
                if (isMotif) {
                    scriptFontsMotif.put(scriptID, pnids);
                } else {
                    scriptFonts.put(scriptID, pnids);
                }
            }
        }
    }
}
