/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* $Id$ */

package org.apache.fop.complexscripts.fonts;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.fop.complexscripts.util.GlyphSequence;
import org.apache.fop.complexscripts.util.ScriptContextTester;

// CSOFF: LineLengthCheck

/**
 * <p>Base class for all advanced typographic glyph tables.</p>
 *
 * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
 */
public class GlyphTable {

    /** logging instance */
    private static final Log log = LogFactory.getLog(GlyphTable.class);

    /** substitution glyph table type */
    public static final int GLYPH_TABLE_TYPE_SUBSTITUTION = 1;
    /** positioning glyph table type */
    public static final int GLYPH_TABLE_TYPE_POSITIONING = 2;
    /** justification glyph table type */
    public static final int GLYPH_TABLE_TYPE_JUSTIFICATION = 3;
    /** baseline glyph table type */
    public static final int GLYPH_TABLE_TYPE_BASELINE = 4;
    /** definition glyph table type */
    public static final int GLYPH_TABLE_TYPE_DEFINITION = 5;

    // (optional) glyph definition table in table types other than glyph definition table
    private GlyphTable gdef;

    // map from lookup specs to lists of strings, each of which identifies a lookup table (consisting of one or more subtables)
    private Map/*<LookupSpec,List<String>>*/ lookups;

    // map from lookup identifiers to lookup tables
    private Map/*<String,LookupTable>*/ lookupTables;

    // cache for lookups matching
    private Map/*<LookupSpec,Map<LookupSpec,List<LookupTable>>>*/ matchedLookups;

    // if true, then prevent further subtable addition
    private boolean frozen;

    /**
     * Instantiate glyph table with specified lookups.
     * @param gdef glyph definition table that applies
     * @param lookups map from lookup specs to lookup tables
     */
    public GlyphTable(GlyphTable gdef, Map/*<LookupSpec,List<String>>*/ lookups) {
        if ((gdef != null) && !(gdef instanceof GlyphDefinitionTable)) {
            throw new AdvancedTypographicTableFormatException("bad glyph definition table");
        } else if (lookups == null) {
            throw new AdvancedTypographicTableFormatException("lookups must be non-null map");
        } else {
            this.gdef = gdef;
            this.lookups = lookups;
            this.lookupTables = new LinkedHashMap/*<String,List<LookupTable>>*/();
            this.matchedLookups = new HashMap/*<LookupSpec,Map<LookupSpec,List<LookupTable>>>*/();
        }
    }

    /**
     * Obtain glyph definition table.
     * @return (possibly null) glyph definition table
     */
    public GlyphDefinitionTable getGlyphDefinitions() {
        return (GlyphDefinitionTable) gdef;
    }

    /**
     * Obtain list of all lookup specifications.
     * @return (possibly empty) list of all lookup specifications
     */
    public List/*<LookupSpec>*/ getLookups() {
        return matchLookupSpecs("*", "*", "*");
    }

    /**
     * Obtain ordered list of all lookup tables, where order is by lookup identifier, which
     * lexicographic ordering follows the lookup list order.
     * @return (possibly empty) ordered list of all lookup tables
     */
    public List/*<LookupTable>*/ getLookupTables() {
        TreeSet/*<String>*/ lids = new TreeSet/*<String>*/(lookupTables.keySet());
        List/*<LookupTable>*/ ltl = new ArrayList/*<LookupTable>*/(lids.size());
        for (Iterator it = lids.iterator(); it.hasNext(); ) {
            String lid = (String) it.next();
            ltl.add(lookupTables.get(lid));
        }
        return ltl;
    }

    /**
     * Obtain lookup table by lookup id. This method is used by test code, and provides
     * access to embedded lookups not normally accessed by {script, language, feature} lookup spec.
     * @param lid lookup id
     * @return table associated with lookup id or null if none
     */
    public LookupTable getLookupTable(String lid) {
        return (LookupTable) lookupTables.get(lid);
    }

    /**
     * Add a subtable.
     * @param subtable a (non-null) glyph subtable
     */
    protected void addSubtable(GlyphSubtable subtable) {
        // ensure table is not frozen
        if (frozen) {
            throw new IllegalStateException("glyph table is frozen, subtable addition prohibited");
        }
        // set subtable's table reference to this table
        subtable.setTable(this);
        // add subtable to this table's subtable collection
        String lid = subtable.getLookupId();
        if (lookupTables.containsKey(lid)) {
            LookupTable lt = (LookupTable) lookupTables.get(lid);
            lt.addSubtable(subtable);
        } else {
            LookupTable lt = new LookupTable(lid, subtable);
            lookupTables.put(lid, lt);
        }
    }

    /**
     * Freeze subtables, i.e., do not allow further subtable addition, and
     * create resulting cached state.
     */
    protected void freezeSubtables() {
        if (!frozen) {
            for (Iterator it = lookupTables.values().iterator(); it.hasNext(); ) {
                LookupTable lt = (LookupTable) it.next();
                lt.freezeSubtables(lookupTables);
            }
            frozen = true;
        }
    }

    /**
     * Match lookup specifications according to <script,language,feature> tuple, where
     * '*' is a wildcard for a tuple component.
     * @param script a script identifier
     * @param language a language identifier
     * @param feature a feature identifier
     * @return a (possibly empty) array of matching lookup specifications
     */
    public List/*<LookupSpec>*/ matchLookupSpecs(String script, String language, String feature) {
        Set/*<LookupSpec>*/ keys = lookups.keySet();
        List/*<LookupSpec>*/ matches = new ArrayList/*<LookupSpec>*/();
        for (Iterator it = keys.iterator(); it.hasNext();) {
            LookupSpec ls = (LookupSpec) it.next();
            if (!"*".equals(script)) {
                if (!ls.getScript().equals(script)) {
                    continue;
                }
            }
            if (!"*".equals(language)) {
                if (!ls.getLanguage().equals(language)) {
                    continue;
                }
            }
            if (!"*".equals(feature)) {
                if (!ls.getFeature().equals(feature)) {
                    continue;
                }
            }
            matches.add(ls);
        }
        return matches;
    }

    /**
     * Match lookup specifications according to <script,language,feature> tuple, where
     * '*' is a wildcard for a tuple component.
     * @param script a script identifier
     * @param language a language identifier
     * @param feature a feature identifier
     * @return a (possibly empty) map from matching lookup specifications to lists of corresponding lookup tables
     */
    public Map/*<LookupSpec,List<LookupTable>>*/ matchLookups(String script, String language, String feature) {
        LookupSpec lsm = new LookupSpec(script, language, feature, true, true);
        Map/*<LookupSpec,List<LookupTable>>*/ lm = (Map/*<LookupSpec,List<LookupTable>>*/) matchedLookups.get(lsm);
        if (lm == null) {
            lm = new LinkedHashMap();
            List/*<LookupSpec>*/ lsl = matchLookupSpecs(script, language, feature);
            for (Iterator it = lsl.iterator(); it.hasNext(); ) {
                LookupSpec ls = (LookupSpec) it.next();
                lm.put(ls, findLookupTables(ls));
            }
            matchedLookups.put(lsm, lm);
        }
        if (lm.isEmpty() && !OTFScript.isDefault(script) && !OTFScript.isWildCard(script)) {
            return matchLookups(OTFScript.DEFAULT, OTFLanguage.DEFAULT, feature);
        } else {
            return lm;
        }
    }

    /**
     * Obtain ordered list of glyph lookup tables that match a specific lookup specification.
     * @param ls a (non-null) lookup specification
     * @return a (possibly empty) ordered list of lookup tables whose corresponding lookup specifications match the specified lookup spec
     */
    public List/*<LookupTable>*/ findLookupTables(LookupSpec ls) {
        TreeSet/*<LookupTable>*/ lts = new TreeSet/*<LookupTable>*/();
        List/*<String>*/ ids;
        if ((ids = (List/*<String>*/) lookups.get(ls)) != null) {
            for (Iterator it = ids.iterator(); it.hasNext();) {
                String lid = (String) it.next();
                LookupTable lt;
                if ((lt = (LookupTable) lookupTables.get(lid)) != null) {
                    lts.add(lt);
                }
            }
        }
        return new ArrayList/*<LookupTable>*/(lts);
    }

    /**
     * Assemble ordered array of lookup table use specifications according to the specified features and candidate lookups,
     * where the order of the array is in accordance to the order of the applicable lookup list.
     * @param features array of feature identifiers to apply
     * @param lookups a mapping from lookup specifications to lists of look tables from which to select lookup tables according to the specified features
     * @return ordered array of assembled lookup table use specifications
     */
    public UseSpec[] assembleLookups(String[] features, Map/*<LookupSpec,List<LookupTable>>*/ lookups) {
        TreeSet/*<UseSpec>*/ uss = new TreeSet/*<UseSpec>*/();
        for (int i = 0, n = features.length; i < n; i++) {
            String feature = features[i];
            for (Iterator it = lookups.entrySet().iterator(); it.hasNext(); ) {
                Map.Entry/*<LookupSpec,List<LookupTable>>*/ e = (Map.Entry/*<LookupSpec,List<LookupTable>>*/) it.next();
                LookupSpec ls = (LookupSpec) e.getKey();
                if (ls.getFeature().equals(feature)) {
                    List/*<LookupTable>*/ ltl = (List/*<LookupTable>*/) e.getValue();
                    if (ltl != null) {
                        for (Iterator ltit = ltl.iterator(); ltit.hasNext(); ) {
                            LookupTable lt = (LookupTable) ltit.next();
                            uss.add(new UseSpec(lt, feature));
                        }
                    }
                }
            }
        }
        return (UseSpec[]) uss.toArray(new UseSpec [ uss.size() ]);
    }

    /**
     * Determine if table supports specific feature, i.e., supports at least one lookup.
     *
     * @param script to qualify feature lookup
     * @param language to qualify feature lookup
     * @param feature to test
     * @return true if feature supported (has at least one lookup)
     */
    public boolean hasFeature(String script, String language, String feature) {
        UseSpec[] usa = assembleLookups(new String[] { feature }, matchLookups(script, language, feature));
        return usa.length > 0;
    }

    /** {@inheritDoc} */
    public String toString() {
        StringBuffer sb = new StringBuffer(super.toString());
        sb.append("{");
        sb.append("lookups={");
        sb.append(lookups.toString());
        sb.append("},lookupTables={");
        sb.append(lookupTables.toString());
        sb.append("}}");
        return sb.toString();
    }

    /**
     * Obtain glyph table type from name.
     * @param name of table type to map to type value
     * @return glyph table type (as an integer constant)
     */
    public static int getTableTypeFromName(String name) {
        int t;
        String s = name.toLowerCase();
        if ("gsub".equals(s)) {
            t = GLYPH_TABLE_TYPE_SUBSTITUTION;
        } else if ("gpos".equals(s)) {
            t = GLYPH_TABLE_TYPE_POSITIONING;
        } else if ("jstf".equals(s)) {
            t = GLYPH_TABLE_TYPE_JUSTIFICATION;
        } else if ("base".equals(s)) {
            t = GLYPH_TABLE_TYPE_BASELINE;
        } else if ("gdef".equals(s)) {
            t = GLYPH_TABLE_TYPE_DEFINITION;
        } else {
            t = -1;
        }
        return t;
    }

    /**
     * Resolve references to lookup tables in a collection of rules sets.
     * @param rsa array of rule sets
     * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
     */
    public static void resolveLookupReferences(RuleSet[] rsa, Map/*<String,LookupTable>*/ lookupTables) {
        if ((rsa != null) && (lookupTables != null)) {
            for (int i = 0, n = rsa.length; i < n; i++) {
                RuleSet rs = rsa [ i ];
                if (rs != null) {
                    rs.resolveLookupReferences(lookupTables);
                }
            }
        }
    }

    /**
     * A structure class encapsulating a lookup specification as a <script,language,feature> tuple.
     */
    public static class LookupSpec implements Comparable {

        private final String script;
        private final String language;
        private final String feature;

        /**
         * Instantiate lookup spec.
         * @param script a script identifier
         * @param language a language identifier
         * @param feature a feature identifier
         */
        public LookupSpec(String script, String language, String feature) {
            this (script, language, feature, false, false);
        }

        /**
         * Instantiate lookup spec.
         * @param script a script identifier
         * @param language a language identifier
         * @param feature a feature identifier
         * @param permitEmpty if true then permit empty script, language, or feature
         * @param permitWildcard if true then permit wildcard script, language, or feature
         */
        LookupSpec(String script, String language, String feature, boolean permitEmpty, boolean permitWildcard) {
            if ((script == null) || (!permitEmpty && (script.length() == 0))) {
                throw new AdvancedTypographicTableFormatException("script must be non-empty string");
            } else if ((language == null) || (!permitEmpty && (language.length() == 0))) {
                throw new AdvancedTypographicTableFormatException("language must be non-empty string");
            } else if ((feature == null) || (!permitEmpty && (feature.length() == 0))) {
                throw new AdvancedTypographicTableFormatException("feature must be non-empty string");
            } else if (!permitWildcard && script.equals("*")) {
                throw new AdvancedTypographicTableFormatException("script must not be wildcard");
            } else if (!permitWildcard && language.equals("*")) {
                throw new AdvancedTypographicTableFormatException("language must not be wildcard");
            } else if (!permitWildcard && feature.equals("*")) {
                throw new AdvancedTypographicTableFormatException("feature must not be wildcard");
            }
            this.script = script.trim();
            this.language = language.trim();
            this.feature = feature.trim();
        }

        /** @return script identifier */
        public String getScript() {
            return script;
        }

        /** @return language identifier */
        public String getLanguage() {
            return language;
        }

        /** @return feature identifier  */
        public String getFeature() {
            return feature;
        }

        /** {@inheritDoc} */
        public int hashCode() {
            int hc = 0;
            hc =  7 * hc + (hc ^ script.hashCode());
            hc = 11 * hc + (hc ^ language.hashCode());
            hc = 17 * hc + (hc ^ feature.hashCode());
            return hc;
        }

        /** {@inheritDoc} */
        public boolean equals(Object o) {
            if (o instanceof LookupSpec) {
                LookupSpec l = (LookupSpec) o;
                if (!l.script.equals(script)) {
                    return false;
                } else if (!l.language.equals(language)) {
                    return false;
                } else {
                    return l.feature.equals(feature);
                }
            } else {
                return false;
            }
        }

        /** {@inheritDoc} */
        public int compareTo(Object o) {
            int d;
            if (o instanceof LookupSpec) {
                LookupSpec ls = (LookupSpec) o;
                if ((d = script.compareTo(ls.script)) == 0) {
                    if ((d = language.compareTo(ls.language)) == 0) {
                        if ((d = feature.compareTo(ls.feature)) == 0) {
                            d = 0;
                        }
                    }
                }
            } else {
                d = -1;
            }
            return d;
        }

        /** {@inheritDoc} */
        public String toString() {
            StringBuffer sb = new StringBuffer(super.toString());
            sb.append("{");
            sb.append("<'" + script + "'");
            sb.append(",'" + language + "'");
            sb.append(",'" + feature + "'");
            sb.append(">}");
            return sb.toString();
        }

    }

    /**
     * The <code>LookupTable</code> class comprising an identifier and an ordered list
     * of glyph subtables, each of which employ the same lookup identifier.
     */
    public static class LookupTable implements Comparable {

        private final String id;                                // lookup identifier
        private final int idOrdinal;                            // parsed lookup identifier ordinal
        private final List/*<GlyphSubtable>*/ subtables;        // list of subtables
        private boolean doesSub;                                // performs substitutions
        private boolean doesPos;                                // performs positioning
        private boolean frozen;                                 // if true, then don't permit further subtable additions
        // frozen state
        private GlyphSubtable[] subtablesArray;
        private static GlyphSubtable[] subtablesArrayEmpty       = new GlyphSubtable[0];

        /**
         * Instantiate a LookupTable.
         * @param id the lookup table's identifier
         * @param subtable an initial subtable (or null)
         */
        public LookupTable(String id, GlyphSubtable subtable) {
            this (id, makeSingleton(subtable));
        }

        /**
         * Instantiate a LookupTable.
         * @param id the lookup table's identifier
         * @param subtables a pre-poplated list of subtables or null
         */
        public LookupTable(String id, List/*<GlyphSubtable>*/ subtables) {
            assert id != null;
            assert id.length() != 0;
            assert id.startsWith("lu");
            this.id = id;
            this.idOrdinal = Integer.parseInt(id.substring(2));
            this.subtables = new LinkedList/*<GlyphSubtable>*/();
            if (subtables != null) {
                for (Iterator it = subtables.iterator(); it.hasNext(); ) {
                    GlyphSubtable st = (GlyphSubtable) it.next();
                    addSubtable(st);
                }
            }
        }

        /** @return the subtables as an array */
        public GlyphSubtable[] getSubtables() {
            if (frozen) {
                return (subtablesArray != null) ? subtablesArray : subtablesArrayEmpty;
            } else {
                if (doesSub) {
                    return (GlyphSubtable[]) subtables.toArray(new GlyphSubstitutionSubtable [ subtables.size() ]);
                } else if (doesPos) {
                    return (GlyphSubtable[]) subtables.toArray(new GlyphPositioningSubtable [ subtables.size() ]);
                } else {
                    return null;
                }
            }
        }

        /**
         * Add a subtable into this lookup table's collecion of subtables according to its
         * natural order.
         * @param subtable to add
         * @return true if subtable was not already present, otherwise false
         */
        public boolean addSubtable(GlyphSubtable subtable) {
            boolean added = false;
            // ensure table is not frozen
            if (frozen) {
                throw new IllegalStateException("glyph table is frozen, subtable addition prohibited");
            }
            // validate subtable to ensure consistency with current subtables
            validateSubtable(subtable);
            // insert subtable into ordered list
            for (ListIterator/*<GlyphSubtable>*/ lit = subtables.listIterator(0); lit.hasNext(); ) {
                GlyphSubtable st = (GlyphSubtable) lit.next();
                int d;
                if ((d = subtable.compareTo(st)) < 0) {
                    // insert within list
                    lit.set(subtable);
                    lit.add(st);
                    added = true;
                } else if (d == 0) {
                    // duplicate entry is ignored
                    added = false;
                    subtable = null;
                }
            }
            // append at end of list
            if (!added && (subtable != null)) {
                subtables.add(subtable);
                added = true;
            }
            return added;
        }

        private void validateSubtable(GlyphSubtable subtable) {
            if (subtable == null) {
                throw new AdvancedTypographicTableFormatException("subtable must be non-null");
            }
            if (subtable instanceof GlyphSubstitutionSubtable) {
                if (doesPos) {
                    throw new AdvancedTypographicTableFormatException("subtable must be positioning subtable, but is: " + subtable);
                } else {
                    doesSub = true;
                }
            }
            if (subtable instanceof GlyphPositioningSubtable) {
                if (doesSub) {
                    throw new AdvancedTypographicTableFormatException("subtable must be substitution subtable, but is: " + subtable);
                } else {
                    doesPos = true;
                }
            }
            if (subtables.size() > 0) {
                GlyphSubtable st = (GlyphSubtable) subtables.get(0);
                if (!st.isCompatible(subtable)) {
                    throw new AdvancedTypographicTableFormatException("subtable " + subtable + " is not compatible with subtable " + st);
                }
            }
        }

        /**
         * Freeze subtables, i.e., do not allow further subtable addition, and
         * create resulting cached state. In addition, resolve any references to
         * lookup tables that appear in this lookup table's subtables.
         * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
         */
        public void freezeSubtables(Map/*<String,LookupTable>*/ lookupTables) {
            if (!frozen) {
                GlyphSubtable[] sta = getSubtables();
                resolveLookupReferences(sta, lookupTables);
                this.subtablesArray = sta;
                this.frozen = true;
            }
        }

        private void resolveLookupReferences(GlyphSubtable[] subtables, Map/*<String,LookupTable>*/ lookupTables) {
            if (subtables != null) {
                for (int i = 0, n = subtables.length; i < n; i++) {
                    GlyphSubtable st = subtables [ i ];
                    if (st != null) {
                        st.resolveLookupReferences(lookupTables);
                    }
                }
            }
        }

        /**
         * Determine if this glyph table performs substitution.
         * @return true if it performs substitution
         */
        public boolean performsSubstitution() {
            return doesSub;
        }

        /**
         * Perform substitution processing using this lookup table's subtables.
         * @param gs an input glyph sequence
         * @param script a script identifier
         * @param language a language identifier
         * @param feature a feature identifier
         * @param sct a script specific context tester (or null)
         * @return the substituted (output) glyph sequence
         */
        public GlyphSequence substitute(GlyphSequence gs, String script, String language, String feature, ScriptContextTester sct) {
            if (performsSubstitution()) {
                return GlyphSubstitutionSubtable.substitute(gs, script, language, feature, (GlyphSubstitutionSubtable[]) subtablesArray, sct);
            } else {
                return gs;
            }
        }

        /**
         * Perform substitution processing on an existing glyph substitution state object using this lookup table's subtables.
         * @param ss a glyph substitution state object
         * @param sequenceIndex if non negative, then apply subtables only at specified sequence index
         * @return the substituted (output) glyph sequence
         */
        public GlyphSequence substitute(GlyphSubstitutionState ss, int sequenceIndex) {
            if (performsSubstitution()) {
                return GlyphSubstitutionSubtable.substitute(ss, (GlyphSubstitutionSubtable[]) subtablesArray, sequenceIndex);
            } else {
                return ss.getInput();
            }
        }

        /**
         * Determine if this glyph table performs positioning.
         * @return true if it performs positioning
         */
        public boolean performsPositioning() {
            return doesPos;
        }

        /**
         * Perform positioning processing using this lookup table's subtables.
         * @param gs an input glyph sequence
         * @param script a script identifier
         * @param language a language identifier
         * @param feature a feature identifier
         * @param fontSize size in device units
         * @param widths array of default advancements for each glyph in font
         * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order,
         * with one 4-tuple for each element of glyph sequence
         * @param sct a script specific context tester (or null)
         * @return true if some adjustment is not zero; otherwise, false
         */
        public boolean position(GlyphSequence gs, String script, String language, String feature, int fontSize, int[] widths, int[][] adjustments, ScriptContextTester sct) {
            if (performsPositioning()) {
                return GlyphPositioningSubtable.position(gs, script, language, feature, fontSize, (GlyphPositioningSubtable[]) subtablesArray, widths, adjustments, sct);
            } else {
                return false;
            }
        }

        /**
         * Perform positioning processing on an existing glyph positioning state object using this lookup table's subtables.
         * @param ps a glyph positioning state object
         * @param sequenceIndex if non negative, then apply subtables only at specified sequence index
         * @return true if some adjustment is not zero; otherwise, false
         */
        public boolean position(GlyphPositioningState ps, int sequenceIndex) {
            if (performsPositioning()) {
                return GlyphPositioningSubtable.position(ps, (GlyphPositioningSubtable[]) subtablesArray, sequenceIndex);
            } else {
                return false;
            }
        }

        /** {@inheritDoc} */
        public int hashCode() {
            return idOrdinal;
        }

        /**
         * {@inheritDoc}
         * @return true if identifier of the specified lookup table is the same
         * as the identifier of this lookup table
         */
        public boolean equals(Object o) {
            if (o instanceof LookupTable) {
                LookupTable lt = (LookupTable) o;
                return idOrdinal == lt.idOrdinal;
            } else {
                return false;
            }
        }

        /**
         * {@inheritDoc}
         * @return the result of comparing the identifier of the specified lookup table with
         * the identifier of this lookup table; lookup table identifiers take the form
         * "lu(DIGIT)+", with comparison based on numerical ordering of numbers expressed by
         * (DIGIT)+.
         */
        public int compareTo(Object o) {
            if (o instanceof LookupTable) {
                LookupTable lt = (LookupTable) o;
                int i = idOrdinal;
                int j = lt.idOrdinal;
                if (i < j) {
                    return -1;
                } else if (i > j) {
                    return 1;
                } else {
                    return 0;
                }
            } else {
                return -1;
            }
        }

        /** {@inheritDoc} */
        public String toString() {
            StringBuffer sb = new StringBuffer();
            sb.append("{ ");
            sb.append("id = " + id);
            sb.append(", subtables = " + subtables);
            sb.append(" }");
            return sb.toString();
        }

        private static List/*<GlyphSubtable>*/ makeSingleton(GlyphSubtable subtable) {
            if (subtable == null) {
                return null;
            } else {
                List/*<GlyphSubtable>*/ stl = new ArrayList/*<GlyphSubtable>*/(1);
                stl.add(subtable);
                return stl;
            }
        }

    }

    /**
     * The <code>UseSpec</code> class comprises a lookup table reference
     * and the feature that selected the lookup table.
     */
    public static class UseSpec implements Comparable {

        /** lookup table to apply */
        private final LookupTable lookupTable;
        /** feature that caused selection of the lookup table */
        private final String feature;

        /**
         * Construct a glyph lookup table use specification.
         * @param lookupTable a glyph lookup table
         * @param feature a feature that caused lookup table selection
         */
        public UseSpec(LookupTable lookupTable, String feature) {
            this.lookupTable = lookupTable;
            this.feature = feature;
        }

        /** @return the lookup table */
        public LookupTable getLookupTable() {
            return lookupTable;
        }

        /** @return the feature that selected this lookup table */
        public String getFeature() {
            return feature;
        }

        /**
         * Perform substitution processing using this use specification's lookup table.
         * @param gs an input glyph sequence
         * @param script a script identifier
         * @param language a language identifier
         * @param sct a script specific context tester (or null)
         * @return the substituted (output) glyph sequence
         */
        public GlyphSequence substitute(GlyphSequence gs, String script, String language, ScriptContextTester sct) {
            return lookupTable.substitute(gs, script, language, feature, sct);
        }

        /**
         * Perform positioning processing using this use specification's lookup table.
         * @param gs an input glyph sequence
         * @param script a script identifier
         * @param language a language identifier
         * @param fontSize size in device units
         * @param widths array of default advancements for each glyph in font
         * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order,
         * with one 4-tuple for each element of glyph sequence
         * @param sct a script specific context tester (or null)
         * @return true if some adjustment is not zero; otherwise, false
         */
        public boolean position(GlyphSequence gs, String script, String language, int fontSize, int[] widths, int[][] adjustments, ScriptContextTester sct) {
            return lookupTable.position(gs, script, language, feature, fontSize, widths, adjustments, sct);
        }

        /** {@inheritDoc} */
        public int hashCode() {
            return lookupTable.hashCode();
        }

        /** {@inheritDoc} */
        public boolean equals(Object o) {
            if (o instanceof UseSpec) {
                UseSpec u = (UseSpec) o;
                return lookupTable.equals(u.lookupTable);
            } else {
                return false;
            }
        }

        /** {@inheritDoc} */
        public int compareTo(Object o) {
            if (o instanceof UseSpec) {
                UseSpec u = (UseSpec) o;
                return lookupTable.compareTo(u.lookupTable);
            } else {
                return -1;
            }
        }

    }

    /**
     * The <code>RuleLookup</code> class implements a rule lookup record, comprising
     * a glyph sequence index and a lookup table index (in an applicable lookup list).
     */
    public static class RuleLookup {

        private final int sequenceIndex;                        // index into input glyph sequence
        private final int lookupIndex;                          // lookup list index
        private LookupTable lookup;                             // resolved lookup table

        /**
         * Instantiate a RuleLookup.
         * @param sequenceIndex the index into the input sequence
         * @param lookupIndex the lookup table index
         */
        public RuleLookup(int sequenceIndex, int lookupIndex) {
            this.sequenceIndex = sequenceIndex;
            this.lookupIndex = lookupIndex;
            this.lookup = null;
        }

        /** @return the sequence index */
        public int getSequenceIndex() {
            return sequenceIndex;
        }

        /** @return the lookup index */
        public int getLookupIndex() {
            return lookupIndex;
        }

        /** @return the lookup table */
        public LookupTable getLookup() {
            return lookup;
        }

        /**
         * Resolve references to lookup tables.
         * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
         */
        public void resolveLookupReferences(Map/*<String,LookupTable>*/ lookupTables) {
            if (lookupTables != null) {
                String lid = "lu" + Integer.toString(lookupIndex);
                LookupTable lt = (LookupTable) lookupTables.get(lid);
                if (lt != null) {
                    this.lookup = lt;
                } else {
                    log.warn("unable to resolve glyph lookup table reference '" + lid + "' amongst lookup tables: " + lookupTables.values());
                }
            }
        }

        /** {@inheritDoc} */
        public String toString() {
            return "{ sequenceIndex = " + sequenceIndex + ", lookupIndex = " + lookupIndex + " }";
        }

    }

    /**
     * The <code>Rule</code> class implements an array of rule lookup records.
     */
    public abstract static class Rule {

        private final RuleLookup[] lookups;                     // rule lookups
        private final int inputSequenceLength;                  // input sequence length

        /**
         * Instantiate a Rule.
         * @param lookups the rule's lookups
         * @param inputSequenceLength the number of glyphs in the input sequence for this rule
         */
        protected Rule(RuleLookup[] lookups, int inputSequenceLength) {
            assert lookups != null;
            this.lookups = lookups;
            this.inputSequenceLength = inputSequenceLength;
        }

        /** @return the lookups */
        public RuleLookup[] getLookups() {
            return lookups;
        }

        /** @return the input sequence length */
        public int getInputSequenceLength() {
            return inputSequenceLength;
        }

        /**
         * Resolve references to lookup tables, e.g., in RuleLookup, to the lookup tables themselves.
         * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
         */
        public void resolveLookupReferences(Map/*<String,LookupTable>*/ lookupTables) {
            if (lookups != null) {
                for (int i = 0, n = lookups.length; i < n; i++) {
                    RuleLookup l = lookups [ i ];
                    if (l != null) {
                        l.resolveLookupReferences(lookupTables);
                    }
                }
            }
        }

        /** {@inheritDoc} */
        public String toString() {
            return "{ lookups = " + Arrays.toString(lookups) + ", inputSequenceLength = " + inputSequenceLength + " }";
        }

    }

    /**
     * The <code>GlyphSequenceRule</code> class implements a subclass of <code>Rule</code>
     * that supports matching on a specific glyph sequence.
     */
    public static class GlyphSequenceRule extends Rule {

        private final int[] glyphs;                             // glyphs

        /**
         * Instantiate a GlyphSequenceRule.
         * @param lookups the rule's lookups
         * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
         * @param glyphs the rule's glyph sequence to match, starting with second glyph in sequence
         */
        public GlyphSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] glyphs) {
            super(lookups, inputSequenceLength);
            assert glyphs != null;
            this.glyphs = glyphs;
        }

        /**
         * Obtain glyphs. N.B. that this array starts with the second
         * glyph of the input sequence.
         * @return the glyphs
         */
        public int[] getGlyphs() {
            return glyphs;
        }

        /**
         * Obtain glyphs augmented by specified first glyph entry.
         * @param firstGlyph to fill in first glyph entry
         * @return the glyphs augmented by first glyph
         */
        public int[] getGlyphs(int firstGlyph) {
            int[] ga = new int [ glyphs.length + 1 ];
            ga [ 0 ] = firstGlyph;
            System.arraycopy(glyphs, 0, ga, 1, glyphs.length);
            return ga;
        }

        /** {@inheritDoc} */
        public String toString() {
            StringBuffer sb = new StringBuffer();
            sb.append("{ ");
            sb.append("lookups = " + Arrays.toString(getLookups()));
            sb.append(", glyphs = " + Arrays.toString(glyphs));
            sb.append(" }");
            return sb.toString();
        }

    }

    /**
     * The <code>ClassSequenceRule</code> class implements a subclass of <code>Rule</code>
     * that supports matching on a specific glyph class sequence.
     */
    public static class ClassSequenceRule extends Rule {

        private final int[] classes;                            // glyph classes

        /**
         * Instantiate a ClassSequenceRule.
         * @param lookups the rule's lookups
         * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
         * @param classes the rule's glyph class sequence to match, starting with second glyph in sequence
         */
        public ClassSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] classes) {
            super(lookups, inputSequenceLength);
            assert classes != null;
            this.classes = classes;
        }

        /**
         * Obtain glyph classes. N.B. that this array starts with the class of the second
         * glyph of the input sequence.
         * @return the classes
         */
        public int[] getClasses() {
            return classes;
        }

        /**
         * Obtain glyph classes augmented by specified first class entry.
         * @param firstClass to fill in first class entry
         * @return the classes augmented by first class
         */
        public int[] getClasses(int firstClass) {
            int[] ca = new int [ classes.length + 1 ];
            ca [ 0 ] = firstClass;
            System.arraycopy(classes, 0, ca, 1, classes.length);
            return ca;
        }

        /** {@inheritDoc} */
        public String toString() {
            StringBuffer sb = new StringBuffer();
            sb.append("{ ");
            sb.append("lookups = " + Arrays.toString(getLookups()));
            sb.append(", classes = " + Arrays.toString(classes));
            sb.append(" }");
            return sb.toString();
        }

    }

    /**
     * The <code>CoverageSequenceRule</code> class implements a subclass of <code>Rule</code>
     * that supports matching on a specific glyph coverage sequence.
     */
    public static class CoverageSequenceRule extends Rule {

        private final GlyphCoverageTable[] coverages;           // glyph coverages

        /**
         * Instantiate a ClassSequenceRule.
         * @param lookups the rule's lookups
         * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
         * @param coverages the rule's glyph coverage sequence to match, starting with first glyph in sequence
         */
        public CoverageSequenceRule(RuleLookup[] lookups, int inputSequenceLength, GlyphCoverageTable[] coverages) {
            super(lookups, inputSequenceLength);
            assert coverages != null;
            this.coverages = coverages;
        }

        /** @return the coverages */
        public GlyphCoverageTable[] getCoverages() {
            return coverages;
        }

        /** {@inheritDoc} */
        public String toString() {
            StringBuffer sb = new StringBuffer();
            sb.append("{ ");
            sb.append("lookups = " + Arrays.toString(getLookups()));
            sb.append(", coverages = " + Arrays.toString(coverages));
            sb.append(" }");
            return sb.toString();
        }

    }

    /**
     * The <code>ChainedGlyphSequenceRule</code> class implements a subclass of <code>GlyphSequenceRule</code>
     * that supports matching on a specific glyph sequence in a specific chained contextual.
     */
    public static class ChainedGlyphSequenceRule extends GlyphSequenceRule {

        private final int[] backtrackGlyphs;                    // backtrack glyphs
        private final int[] lookaheadGlyphs;                    // lookahead glyphs

        /**
         * Instantiate a ChainedGlyphSequenceRule.
         * @param lookups the rule's lookups
         * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
         * @param glyphs the rule's input glyph sequence to match, starting with second glyph in sequence
         * @param backtrackGlyphs the rule's backtrack glyph sequence to match, starting with first glyph in sequence
         * @param lookaheadGlyphs the rule's lookahead glyph sequence to match, starting with first glyph in sequence
         */
        public ChainedGlyphSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] glyphs, int[] backtrackGlyphs, int[] lookaheadGlyphs) {
            super(lookups, inputSequenceLength, glyphs);
            assert backtrackGlyphs != null;
            assert lookaheadGlyphs != null;
            this.backtrackGlyphs = backtrackGlyphs;
            this.lookaheadGlyphs = lookaheadGlyphs;
        }

        /** @return the backtrack glyphs */
        public int[] getBacktrackGlyphs() {
            return backtrackGlyphs;
        }

        /** @return the lookahead glyphs */
        public int[] getLookaheadGlyphs() {
            return lookaheadGlyphs;
        }

        /** {@inheritDoc} */
        public String toString() {
            StringBuffer sb = new StringBuffer();
            sb.append("{ ");
            sb.append("lookups = " + Arrays.toString(getLookups()));
            sb.append(", glyphs = " + Arrays.toString(getGlyphs()));
            sb.append(", backtrackGlyphs = " + Arrays.toString(backtrackGlyphs));
            sb.append(", lookaheadGlyphs = " + Arrays.toString(lookaheadGlyphs));
            sb.append(" }");
            return sb.toString();
        }

    }

    /**
     * The <code>ChainedClassSequenceRule</code> class implements a subclass of <code>ClassSequenceRule</code>
     * that supports matching on a specific glyph class sequence in a specific chained contextual.
     */
    public static class ChainedClassSequenceRule extends ClassSequenceRule {

        private final int[] backtrackClasses;                    // backtrack classes
        private final int[] lookaheadClasses;                    // lookahead classes

        /**
         * Instantiate a ChainedClassSequenceRule.
         * @param lookups the rule's lookups
         * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
         * @param classes the rule's input glyph class sequence to match, starting with second glyph in sequence
         * @param backtrackClasses the rule's backtrack glyph class sequence to match, starting with first glyph in sequence
         * @param lookaheadClasses the rule's lookahead glyph class sequence to match, starting with first glyph in sequence
         */
        public ChainedClassSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] classes, int[] backtrackClasses, int[] lookaheadClasses) {
            super(lookups, inputSequenceLength, classes);
            assert backtrackClasses != null;
            assert lookaheadClasses != null;
            this.backtrackClasses = backtrackClasses;
            this.lookaheadClasses = lookaheadClasses;
        }

        /** @return the backtrack classes */
        public int[] getBacktrackClasses() {
            return backtrackClasses;
        }

        /** @return the lookahead classes */
        public int[] getLookaheadClasses() {
            return lookaheadClasses;
        }

        /** {@inheritDoc} */
        public String toString() {
            StringBuffer sb = new StringBuffer();
            sb.append("{ ");
            sb.append("lookups = " + Arrays.toString(getLookups()));
            sb.append(", classes = " + Arrays.toString(getClasses()));
            sb.append(", backtrackClasses = " + Arrays.toString(backtrackClasses));
            sb.append(", lookaheadClasses = " + Arrays.toString(lookaheadClasses));
            sb.append(" }");
            return sb.toString();
        }

    }

    /**
     * The <code>ChainedCoverageSequenceRule</code> class implements a subclass of <code>CoverageSequenceRule</code>
     * that supports matching on a specific glyph class sequence in a specific chained contextual.
     */
    public static class ChainedCoverageSequenceRule extends CoverageSequenceRule {

        private final GlyphCoverageTable[] backtrackCoverages;  // backtrack coverages
        private final GlyphCoverageTable[] lookaheadCoverages;  // lookahead coverages

        /**
         * Instantiate a ChainedCoverageSequenceRule.
         * @param lookups the rule's lookups
         * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
         * @param coverages the rule's input glyph class sequence to match, starting with first glyph in sequence
         * @param backtrackCoverages the rule's backtrack glyph class sequence to match, starting with first glyph in sequence
         * @param lookaheadCoverages the rule's lookahead glyph class sequence to match, starting with first glyph in sequence
         */
        public ChainedCoverageSequenceRule(RuleLookup[] lookups, int inputSequenceLength, GlyphCoverageTable[] coverages, GlyphCoverageTable[] backtrackCoverages, GlyphCoverageTable[] lookaheadCoverages) {
            super(lookups, inputSequenceLength, coverages);
            assert backtrackCoverages != null;
            assert lookaheadCoverages != null;
            this.backtrackCoverages = backtrackCoverages;
            this.lookaheadCoverages = lookaheadCoverages;
        }

        /** @return the backtrack coverages */
        public GlyphCoverageTable[] getBacktrackCoverages() {
            return backtrackCoverages;
        }

        /** @return the lookahead coverages */
        public GlyphCoverageTable[] getLookaheadCoverages() {
            return lookaheadCoverages;
        }

        /** {@inheritDoc} */
        public String toString() {
            StringBuffer sb = new StringBuffer();
            sb.append("{ ");
            sb.append("lookups = " + Arrays.toString(getLookups()));
            sb.append(", coverages = " + Arrays.toString(getCoverages()));
            sb.append(", backtrackCoverages = " + Arrays.toString(backtrackCoverages));
            sb.append(", lookaheadCoverages = " + Arrays.toString(lookaheadCoverages));
            sb.append(" }");
            return sb.toString();
        }

    }

    /**
     * The <code>RuleSet</code> class implements a collection of rules, which
     * may or may not be the same rule type.
     */
    public static class RuleSet {

        private final Rule[] rules;                             // set of rules

        /**
         * Instantiate a Rule Set.
         * @param rules the rules
         * @throws AdvancedTypographicTableFormatException if rules or some element of rules is null
         */
        public RuleSet(Rule[] rules) throws AdvancedTypographicTableFormatException {
            // enforce rules array instance
            if (rules == null) {
                throw new AdvancedTypographicTableFormatException("rules[] is null");
            }
            this.rules = rules;
        }

        /** @return the rules */
        public Rule[] getRules() {
            return rules;
        }

        /**
         * Resolve references to lookup tables, e.g., in RuleLookup, to the lookup tables themselves.
         * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
         */
        public void resolveLookupReferences(Map/*<String,LookupTable>*/ lookupTables) {
            if (rules != null) {
                for (int i = 0, n = rules.length; i < n; i++) {
                    Rule r = rules [ i ];
                    if (r != null) {
                        r.resolveLookupReferences(lookupTables);
                    }
                }
            }
        }

        /** {@inheritDoc} */
        public String toString() {
            return "{ rules = " + Arrays.toString(rules) + " }";
        }

    }

    /**
     * The <code>HomogenousRuleSet</code> class implements a collection of rules, which
     * must be the same rule type (i.e., same concrete rule class) or null.
     */
    public static class HomogeneousRuleSet extends RuleSet {

        /**
         * Instantiate a Homogeneous Rule Set.
         * @param rules the rules
         * @throws AdvancedTypographicTableFormatException if some rule[i] is not an instance of rule[0]
         */
        public HomogeneousRuleSet(Rule[] rules) throws AdvancedTypographicTableFormatException {
            super(rules);
            // find first non-null rule
            Rule r0 = null;
            for (int i = 1, n = rules.length; (r0 == null) && (i < n); i++) {
                if (rules[i] != null) {
                    r0 = rules[i];
                }
            }
            // enforce rule instance homogeneity
            if (r0 != null) {
                Class c = r0.getClass();
                for (int i = 1, n = rules.length; i < n; i++) {
                    Rule r = rules[i];
                    if ((r != null) && !c.isInstance(r)) {
                        throw new AdvancedTypographicTableFormatException("rules[" + i + "] is not an instance of " + c.getName());
                    }
                }
            }

        }

    }

}
