public class Font {

  private static final Logger logger =
    Logger.getLogger(Font.class.getCanonicalName());

  /**
   * Offsets to specific elements in the underlying data. These offsets are relative to the
   * start of the table or the start of sub-blocks within the table.
   */
  private enum Offset {
    // Offsets within the main directory
    sfntVersion(0),
    numTables(4),
    searchRange(6),
    entrySelector(8),
    rangeShift(10),
    tableRecordBegin(12),
    sfntHeaderSize(12),

    // Offsets within a specific table record
    tableTag(0),
    tableCheckSum(4),
    tableOffset(8),
    tableLength(12),
    tableRecordSize(16);

    private final int offset;

    private Offset(int offset) {
      this.offset = offset;
    }
  }

  /**
   * Ordering of tables for different font types.
   */
  private final static List<Integer> CFF_TABLE_ORDERING;
  private final static List<Integer> TRUE_TYPE_TABLE_ORDERING;
  static {
    Integer[] cffArray = new Integer[] {Tag.head,
        Tag.hhea,
        Tag.maxp,
        Tag.OS_2,
        Tag.name,
        Tag.cmap,
        Tag.post,
        Tag.CFF};
    List<Integer> cffList = new ArrayList<Integer>(cffArray.length);
    Collections.addAll(cffList, cffArray);
    CFF_TABLE_ORDERING = Collections.unmodifiableList(cffList);

    Integer[] ttArray = new Integer[] {Tag.head,
        Tag.hhea,
        Tag.maxp,
        Tag.OS_2,
        Tag.hmtx,
        Tag.LTSH,
        Tag.VDMX,
        Tag.hdmx,
        Tag.cmap,
        Tag.fpgm,
        Tag.prep,
        Tag.cvt,
        Tag.loca,
        Tag.glyf,
        Tag.kern,
        Tag.name,
        Tag.post,
        Tag.gasp,
        Tag.PCLT,
        Tag.DSIG};
    List<Integer> ttList = new ArrayList<Integer>(ttArray.length);
    Collections.addAll(ttList, ttArray);
    TRUE_TYPE_TABLE_ORDERING = Collections.unmodifiableList(ttList);
  }

  /**
   * Platform ids. These are used in a number of places within the font whenever
   * the platform needs to be specified.
   *
   * @see NameTable
   * @see CMapTable
   */
  public enum PlatformId {
    Unknown(-1), Unicode(0), Macintosh(1), ISO(2), Windows(3), Custom(4);

    private final int value;

    private PlatformId(int value) {
      this.value = value;
    }

    public int value() {
      return this.value;
    }

    public boolean equals(int value) {
      return value == this.value;
    }

    public static PlatformId valueOf(int value) {
      for (PlatformId platform : PlatformId.values()) {
        if (platform.equals(value)) {
          return platform;
        }
      }
      return Unknown;
    }
  }

  /**
   * Unicode encoding ids. These are used in a number of places within the font
   * whenever character encodings need to be specified.
   *
   * @see NameTable
   * @see CMapTable
   */
  public enum UnicodeEncodingId {
    // Unicode Platform Encodings
    Unknown(-1),
    Unicode1_0(0),
    Unicode1_1(1),
    ISO10646(2),
    Unicode2_0_BMP(3),
    Unicode2_0(4),
    UnicodeVariationSequences(5);

    private final int value;

    private UnicodeEncodingId(int value) {
      this.value = value;
    }

    public int value() {
      return this.value;
    }

    public boolean equals(int value) {
      return value == this.value;
    }

    public static UnicodeEncodingId valueOf(int value) {
      for (UnicodeEncodingId encoding : UnicodeEncodingId.values()) {
        if (encoding.equals(value)) {
          return encoding;
        }
      }
      return Unknown;
    }
  }

  /**
   * Windows encoding ids. These are used in a number of places within the font
   * whenever character encodings need to be specified.
   *
   * @see NameTable
   * @see CMapTable
   */
  public enum WindowsEncodingId {
    // Windows Platform Encodings
    Unknown(-1),
    Symbol(0),
    UnicodeUCS2(1),
    ShiftJIS(2),
    PRC(3),
    Big5(4),
    Wansung(5),
    Johab(6),
    UnicodeUCS4(10);

    private final int value;

    private WindowsEncodingId(int value) {
      this.value = value;
    }

    public int value() {
      return this.value;
    }

    public boolean equals(int value) {
      return value == this.value;
    }

    public static WindowsEncodingId valueOf(int value) {
      for (WindowsEncodingId encoding : WindowsEncodingId.values()) {
        if (encoding.equals(value)) {
          return encoding;
        }
      }
      return Unknown;
    }
  }

  /**
   * Macintosh encoding ids. These are used in a number of places within the
   * font whenever character encodings need to be specified.
   *
   * @see NameTable
   * @see CMapTable
   */
  public enum MacintoshEncodingId {
    // Macintosh Platform Encodings
    Unknown(-1),
    Roman(0),
    Japanese(1),
    ChineseTraditional(2),
    Korean(3),
    Arabic(4),
    Hebrew(5),
    Greek(6),
    Russian(7),
    RSymbol(8),
    Devanagari(9),
    Gurmukhi(10),
    Gujarati(11),
    Oriya(12),
    Bengali(13),
    Tamil(14),
    Telugu(15),
    Kannada(16),
    Malayalam(17),
    Sinhalese(18),
    Burmese(19),
    Khmer(20),
    Thai(21),
    Laotian(22),
    Georgian(23),
    Armenian(24),
    ChineseSimplified(25),
    Tibetan(26),
    Mongolian(27),
    Geez(28),
    Slavic(29),
    Vietnamese(30),
    Sindhi(31),
    Uninterpreted(32);

    private final int value;

    private MacintoshEncodingId(int value) {
      this.value = value;
    }

    public int value() {
      return this.value;
    }

    public boolean equals(int value) {
      return value == this.value;
    }

    public static MacintoshEncodingId valueOf(int value) {
      for (MacintoshEncodingId encoding : MacintoshEncodingId.values()) {
        if (encoding.equals(value)) {
          return encoding;
        }
      }
      return Unknown;
    }
  }

  public static final int SFNTVERSION_1 = Fixed1616.fixed(1, 0);

  private final int sfntVersion;
  private final byte[] digest;
  private long checksum;

  private Map<Integer, ? extends Table> tables; // these get set in the builder

  /**
   * Constructor.
   *
   * @param sfntVersion the sfnt version
   * @param digest the computed digest for the font; null if digest was not
   *        computed
   */
  private Font(int sfntVersion, byte[] digest) {
    this.sfntVersion = sfntVersion;
    this.digest = digest;
  }

  /**
   * Gets the sfnt version set in the sfnt wrapper of the font.
   *
   * @return the sfnt version
   */
  public int sfntVersion() {
    return this.sfntVersion;
  }

  /**
   * Gets a copy of the fonts digest that was created when the font was read. If
   * no digest was set at creation time then the return result will be null.
   *
   * @return a copy of the digest array or <code>null</code> if one wasn't set
   *         at creation time
   */
  public byte[] digest() {
    if (this.digest == null) {
      return null;
    }
    return Arrays.copyOf(this.digest, this.digest.length);
  }

  /**
   * Get the checksum for this font.
   *
   * @return the font checksum
   */
  public long checksum() {
    return this.checksum;
  }

  /**
   * Get the number of tables in this font.
   *
   * @return the number of tables
   */
  public int numTables() {
    return this.tables.size();
  }

  /**
   * Get an iterator over all the tables in the font.
   *
   * @return a table iterator
   */
  public Iterator<? extends Table> iterator() {
    return this.tables.values().iterator();
  }

  /**
   * Does the font have a particular table.
   *
   * @param tag the table identifier
   * @return true if the table is in the font; false otherwise
   */
  public boolean hasTable(int tag) {
    return this.tables.containsKey(tag);
  }

  /**
   * Get the table in this font with the specified id.
   *
   * @param <T> the type of the table
   * @param tag the identifier of the table
   * @return the table specified if it exists; null otherwise
   */
  @SuppressWarnings("unchecked")
  public <T extends Table> T getTable(int tag) {
    return (T) this.tables.get(tag);
  }

  /**
   * Get a map of the tables in this font accessed by table tag.
   *
   * @return an unmodifiable view of the tables in this font
   */
  public Map<Integer, ? extends Table> tableMap() {
    return Collections.unmodifiableMap(this.tables);
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append("digest = ");
    byte[] digest = this.digest();
    if (digest != null) {
      for (int i = 0; i < digest.length; i++) {
        int d = 0xff & digest[i];
        if (d < 0x10) {
          sb.append("0");
        }
        sb.append(Integer.toHexString(d));
      }
    }
    sb.append("\n[");
    sb.append(Fixed1616.toString(sfntVersion));
    sb.append(", ");
    sb.append(this.numTables());
    sb.append("]\n");
    Iterator<? extends Table> iter = this.iterator();
    while (iter.hasNext()) {
      FontDataTable table = iter.next();
      sb.append("\t");
      sb.append(table);
      sb.append("\n");
    }
    return sb.toString();
  }

  /**
   * Serialize the font to the output stream.
   *
   * @param os the destination for the font serialization
   * @param tableOrdering the table ordering to apply
   * @throws IOException
   */
  void serialize(OutputStream os, List<Integer> tableOrdering) throws IOException {
    List<Integer> finalTableOrdering = this.generateTableOrdering(tableOrdering);
    List<Header> tableRecords = buildTableHeadersForSerialization(finalTableOrdering);
    FontOutputStream fos = new FontOutputStream(os);
    this.serializeHeader(fos, tableRecords);
    this.serializeTables(fos, tableRecords);
  }

  /**
   * Build the table headers to be used for serialization. These headers will be
   * filled out with the data required for serialization. The headers will be
   * sorted in the order specified and only those specified will have headers
   * generated.
   *
   * @param tableOrdering the tables to generate headers for and the order to
   *        sort them
   * @return a list of table headers ready for serialization
   */
  private List<Header> buildTableHeadersForSerialization(List<Integer> tableOrdering) {
    List<Integer> finalTableOrdering = this.generateTableOrdering(tableOrdering);

    List<Header> tableHeaders = new ArrayList<Header>(this.numTables());
    int tableOffset =
        Offset.tableRecordBegin.offset + this.numTables() * Offset.tableRecordSize.offset;
    for (Integer tag : finalTableOrdering) {
      Table table = this.tables.get(tag);
      if (table != null) {
        tableHeaders.add(new Header(
            tag, table.calculatedChecksum(), tableOffset, table.header().length()));
        // write on boundary of 4 bytes
        tableOffset += (table.dataLength() + 3) & ~3;
      }
    }
    return tableHeaders;
  }

  /**
   * Searialize the headers.
   *
   * @param fos the destination stream for the headers
   * @param tableHeaders the headers to serialize
   * @throws IOException
   */
  private void serializeHeader(FontOutputStream fos, List<Header> tableHeaders)
      throws IOException {
    fos.writeFixed(this.sfntVersion);
    fos.writeUShort(tableHeaders.size());
    int log2OfMaxPowerOf2 = FontMath.log2(tableHeaders.size());
    int searchRange = 2 << (log2OfMaxPowerOf2 - 1 + 4);
    fos.writeUShort(searchRange);
    fos.writeUShort(log2OfMaxPowerOf2);
    fos.writeUShort((tableHeaders.size() * 16) - searchRange);

    List<Header> sortedHeaders = new ArrayList<Header>(tableHeaders);
    Collections.sort(sortedHeaders, Header.COMPARATOR_BY_TAG);

    for (Header record : sortedHeaders) {
      fos.writeULong(record.tag());
      fos.writeULong(record.checksum());
      fos.writeULong(record.offset());
      fos.writeULong(record.length());
    }
  }

  /**
   * Serialize the tables.
   *
   * @param fos the destination stream for the headers
   * @param tableHeaders the headers for the tables to serialize
   * @throws IOException
   */
  private void serializeTables(FontOutputStream fos, List<Header> tableHeaders)
      throws IOException {

    for (Header record : tableHeaders) {
      Table table = this.getTable(record.tag());
      if (table == null) {
        throw new IOException("Table out of sync with font header.");
      }
      int tableSize = table.serialize(fos);
      int fillerSize = ((tableSize + 3) & ~3) - tableSize;
      for (int i = 0; i < fillerSize; i++) {
        fos.write(0);
      }
    }
  }

  /**
   * Generate the full table ordering to used for serialization. The full
   * ordering uses the partial ordering as a seed and then adds all remaining
   * tables in the font in an undefined order.
   *
   * @param defaultTableOrdering the partial ordering to be used as a seed for
   *        the full ordering
   * @return the full ordering for serialization
   */
  private List<Integer> generateTableOrdering(List<Integer> defaultTableOrdering) {
    List<Integer> tableOrdering = new ArrayList<Integer>(this.tables.size());
    if (defaultTableOrdering == null) {
      defaultTableOrdering = defaultTableOrdering();
    }

    Set<Integer> tablesInFont = new TreeSet<Integer>(this.tables.keySet());

    // add all the default ordering
    for (Integer tag : defaultTableOrdering) {
      if (this.hasTable(tag)) {
        tableOrdering.add(tag);
        tablesInFont.remove(tag);
      }
    }

    // add all the rest
    for (Integer tag : tablesInFont) {
      tableOrdering.add(tag);
    }

    return tableOrdering;
  }

  /**
   * Get the default table ordering based on the type of the font.
   *
   * @return the default table ordering
   */
  private List<Integer> defaultTableOrdering() {
    if (this.hasTable(Tag.CFF)) {
      return Font.CFF_TABLE_ORDERING;
    }
    return Font.TRUE_TYPE_TABLE_ORDERING;
  }

  /**
   * A builder for a font object. The builder allows the for the creation of
   * immutable {@link Font} objects. The builder is a one use non-thread safe
   * object and cnce the {@link Font} object has been created it is no longer
   * usable. To create a further {@link Font} object new builder will be
   * required.
   *
   * @author Stuart Gill
   *
   */
  public static final class Builder {

    private Map<Integer, Table.Builder<? extends Table>> tableBuilders;
    private FontFactory factory;
    private int sfntVersion = SFNTVERSION_1;
    private int numTables;
    @SuppressWarnings("unused")
    private int searchRange;
    @SuppressWarnings("unused")
    private int entrySelector;
    @SuppressWarnings("unused")
    private int rangeShift;
    private Map<Header, WritableFontData> dataBlocks;
    private byte[] digest;

    private Builder(FontFactory factory) {
      this.factory = factory;
      this.tableBuilders = new HashMap<Integer, Table.Builder<? extends Table>>();
    }

    private void loadFont(InputStream is) throws IOException {
      if (is == null) {
        throw new IOException("No input stream for font.");
      }
      FontInputStream fontIS = null;
      try {
        fontIS = new FontInputStream(is);
        SortedSet<Header> records = readHeader(fontIS);
        this.dataBlocks = loadTableData(records, fontIS);
        this.tableBuilders = buildAllTableBuilders(this.dataBlocks);
      } finally {
        fontIS.close();
      }
    }

    private void loadFont(WritableFontData wfd, int offsetToOffsetTable) throws IOException {
      if (wfd == null) {
        throw new IOException("No data for font.");
      }
      SortedSet<Header> records = readHeader(wfd, offsetToOffsetTable);
      this.dataBlocks = loadTableData(records, wfd);
      this.tableBuilders = buildAllTableBuilders(this.dataBlocks);
    }

    static final Builder
    getOTFBuilder(FontFactory factory, InputStream is) throws IOException {
      Builder builder = new Builder(factory);
      builder.loadFont(is);
      return builder;
    }

    static final Builder getOTFBuilder(
        FontFactory factory, WritableFontData wfd, int offsetToOffsetTable) throws IOException {
      Builder builder = new Builder(factory);
      builder.loadFont(wfd, offsetToOffsetTable);
      return builder;
    }

    static final Builder getOTFBuilder(FontFactory factory) {
      return new Builder(factory);
    }

    /**
     * Get the font factory that created this font builder.
     *
     * @return the font factory
     */
    public FontFactory getFontFactory() {
      return this.factory;
    }

    /**
     * Is the font ready to build?
     *
     * @return true if ready to build; false otherwise
     */
    public boolean readyToBuild() {
      // just read in data with no manipulation
      if (this.tableBuilders == null && this.dataBlocks != null && this.dataBlocks.size() > 0) {
        return true;
      }

      for (Table.Builder<? extends Table> tableBuilder : this.tableBuilders.values()) {
        if (tableBuilder.readyToBuild() == false) {
          return false;
        }
      }
      return true;
    }

    /**
     * Build the {@link Font}. After this call this builder will no longer be
     * usable.
     *
     * @return a {@link Font}
     */
    public Font build() {
      Map<Integer, ? extends Table> tables = null;

      Font font = new Font(this.sfntVersion, this.digest);

      if (this.tableBuilders.size() > 0) {
        tables = buildTablesFromBuilders(font, this.tableBuilders);
      }
      font.tables = tables;
      this.tableBuilders = null;
      this.dataBlocks = null;
      return font;
    }

    /**
     * Set a unique fingerprint for the font object.
     *
     * @param digest a unique identifier for the font
     */
    public void setDigest(byte[] digest) {
      this.digest = digest;
    }

    /**
     * Clear all table builders.
     */
    public void clearTableBuilders() {
      this.tableBuilders.clear();
    }

    /**
     * Does this font builder have the specified table builder.
     *
     * @param tag the table builder tag
     * @return true if there is a builder for that table; false otherwise
     */
    public boolean hasTableBuilder(int tag) {
      return this.tableBuilders.containsKey(tag);
    }

    /**
     * Get the table builder for the given tag. If there is no builder for that
     * tag then return a null.
     *
     * @param tag the table builder tag
     * @return the builder for the tag; null if there is no builder for that tag
     */
    public Table.Builder<? extends Table> getTableBuilder(int tag) {
      Table.Builder<? extends Table> builder = this.tableBuilders.get(tag);
      return builder;
    }

    /**
     * Creates a new empty table builder for the table type given by the table
     * id tag.
     *
     *  This new table will be added to the font and will replace any existing
     * builder for that table.
     *
     * @param tag
     * @return new empty table of the type specified by tag; if tag is not known
     *         then a generic OpenTypeTable is returned
     */
    public Table.Builder<? extends Table> newTableBuilder(int tag) {
      Header header = new Header(tag);
      Table.Builder<? extends Table> builder = Table.Builder.getBuilder(header, null);
      this.tableBuilders.put(header.tag(), builder);

      return builder;
    }

    /**
     * Creates a new table builder for the table type given by the table id tag.
     * It makes a copy of the data provided and uses that copy for the table.
     *
     *  This new table has been added to the font and will replace any existing
     * builder for that table.
     *
     * @param tag
     * @param srcData
     * @return new empty table of the type specified by tag; if tag is not known
     *         then a generic OpenTypeTable is returned
     */
    public Table.Builder<? extends Table> newTableBuilder(int tag, ReadableFontData srcData) {
      WritableFontData data;
      data = WritableFontData.createWritableFontData(srcData.length());
      // TODO(stuartg): take over original data instead?
      srcData.copyTo(data);

      Header header = new Header(tag, data.length());
      Table.Builder<? extends Table> builder = Table.Builder.getBuilder(header, data);

      this.tableBuilders.put(tag, builder);

      return builder;
    }

    /**
     * Get a map of the table builders in this font builder accessed by table
     * tag.
     *
     * @return an unmodifiable view of the table builders in this font builder
     */
    public Map<Integer, Table.Builder<? extends Table>> tableBuilderMap() {
      return Collections.unmodifiableMap(this.tableBuilders);
    }

    /**
     * Remove the specified table builder from the font builder.
     *
     * @param tag the table builder to remove
     * @return the table builder removed
     */
    public Table.Builder<? extends Table> removeTableBuilder(int tag) {
      return this.tableBuilders.remove(tag);
    }

    /**
     * Get the number of table builders in the font builder.
     *
     * @return the number of table builders
     */
    public int tableBuilderCount() {
      return this.tableBuilders.size();
    }

    @SuppressWarnings("unused")
    private int sfntWrapperSize() {
      return Offset.sfntHeaderSize.offset +
      (Offset.tableRecordSize.offset * this.tableBuilders.size());
    }

    private Map<Integer, Table.Builder<? extends Table>> buildAllTableBuilders(
        Map<Header, WritableFontData> tableData) {
      Map<Integer, Table.Builder<? extends Table>> builderMap = 
        new HashMap<Integer, Table.Builder<? extends Table>>();
      Set<Header> records = tableData.keySet();
      for (Header record : records) {
        Table.Builder<? extends Table> builder = getTableBuilder(record, tableData.get(record));
        builderMap.put(record.tag(), builder);
      }
      interRelateBuilders(builderMap);
      return builderMap;
    }

    private Table.Builder<? extends Table> getTableBuilder(Header header, WritableFontData data) {
      Table.Builder<? extends Table> builder = Table.Builder.getBuilder(header, data);
      return builder;
    }

    private static Map<Integer, Table> buildTablesFromBuilders(Font font,
        Map<Integer, Table.Builder<? extends Table>> builderMap) {
      Map<Integer, Table> tableMap = new TreeMap<Integer, Table>();

      interRelateBuilders(builderMap);

      long fontChecksum = 0;
      boolean tablesChanged = false;
      FontHeaderTable.Builder headerTableBuilder = null;
      
      // now build all the tables
      for (Table.Builder<? extends Table> builder : builderMap.values()) {
        Table table = null;
        if (Tag.isHeaderTable(builder.header().tag())) {
          headerTableBuilder = (FontHeaderTable.Builder) builder;
          continue;
        }
        if (builder.readyToBuild()) {
          tablesChanged |= builder.changed();
          table = builder.build();
        }
        if (table == null) {
          throw new RuntimeException("Unable to build table - " + builder);
        }
        long tableChecksum = table.calculatedChecksum();
        fontChecksum += tableChecksum;
        tableMap.put(table.header().tag(), table);
      }
      
      // now fix up the header table
      Table headerTable = null;
      if (headerTableBuilder != null) {
        if (tablesChanged) {
          headerTableBuilder.setFontChecksum(fontChecksum);
        }
        if (headerTableBuilder.readyToBuild()) {
          tablesChanged |= headerTableBuilder.changed();
          headerTable = headerTableBuilder.build();
        }
        if (headerTable == null) {
          throw new RuntimeException("Unable to build table - " + headerTableBuilder);
        }
        fontChecksum += headerTable.calculatedChecksum();
        tableMap.put(headerTable.header().tag(), headerTable);
      }
      
      font.checksum = fontChecksum & 0xffffffffL;
      return tableMap;
    }

    private static void
    interRelateBuilders(Map<Integer, Table.Builder<? extends Table>> builderMap) {
      FontHeaderTable.Builder headerTableBuilder =
        (FontHeaderTable.Builder) builderMap.get(Tag.head);
      HorizontalHeaderTable.Builder horizontalHeaderBuilder =
        (HorizontalHeaderTable.Builder) builderMap.get(Tag.hhea);
      MaximumProfileTable.Builder maxProfileBuilder =
        (MaximumProfileTable.Builder) builderMap.get(Tag.maxp);
      LocaTable.Builder locaTableBuilder =
        (LocaTable.Builder) builderMap.get(Tag.loca);
      HorizontalMetricsTable.Builder horizontalMetricsBuilder =
        (HorizontalMetricsTable.Builder) builderMap.get(Tag.hmtx);
      HorizontalDeviceMetricsTable.Builder hdmxTableBuilder =
        (HorizontalDeviceMetricsTable.Builder) builderMap.get(Tag.hdmx);

      // set the inter table data required to build certain tables
      if (horizontalMetricsBuilder != null) {
        if (maxProfileBuilder != null) {
          horizontalMetricsBuilder.setNumGlyphs(maxProfileBuilder.numGlyphs());
        }
        if (horizontalHeaderBuilder != null) {
          horizontalMetricsBuilder.setNumberOfHMetrics(
              horizontalHeaderBuilder.numberOfHMetrics());
        }
      }

      if (locaTableBuilder != null) {
        if (maxProfileBuilder != null) {
          locaTableBuilder.setNumGlyphs(maxProfileBuilder.numGlyphs());
        }
        if (headerTableBuilder != null) {
          locaTableBuilder.setFormatVersion(headerTableBuilder.indexToLocFormat());
        }
      }

      if (hdmxTableBuilder != null) {
        if (maxProfileBuilder != null) {
          hdmxTableBuilder.setNumGlyphs(maxProfileBuilder.numGlyphs());
        }
      }      
    }

    private SortedSet<Header> readHeader(FontInputStream is) throws IOException {
      SortedSet<Header> records =
          new TreeSet<Header>(Header.COMPARATOR_BY_OFFSET);

      this.sfntVersion = is.readFixed();
      this.numTables = is.readUShort();
      this.searchRange = is.readUShort();
      this.entrySelector = is.readUShort();
      this.rangeShift = is.readUShort();

      for (int tableNumber = 0; tableNumber < this.numTables; tableNumber++) {
        Header table = new Header(is.readULongAsInt(), // safe since the tag is ASCII
            is.readULong(),         // checksum
            is.readULongAsInt(),    // offset
            is.readULongAsInt());   // length
        records.add(table);
      }
      return records;
    }

    private Map<Header, WritableFontData> loadTableData(
        SortedSet<Header> headers, FontInputStream is) throws IOException {
      Map<Header, WritableFontData> tableData =
          new HashMap<Header, WritableFontData>(headers.size());
      logger.fine("########  Reading Table Data");
      for (Header tableHeader : headers) {
        is.skip(tableHeader.offset() - is.position());
        logger.finer("\t" + tableHeader);
        logger.finest("\t\tStream Position = " + Integer.toHexString((int) is.position()));
        // don't close this or the whole stream is gone
        FontInputStream tableIS = new FontInputStream(is, tableHeader.length());
        // TODO(stuartg): start tracking bad tables and other errors
        WritableFontData data = WritableFontData.createWritableFontData(tableHeader.length());
        data.copyFrom(tableIS, tableHeader.length());
        tableData.put(tableHeader, data);
      }
      return tableData;
    }

    private SortedSet<Header> readHeader(ReadableFontData fd, int offset) {
      SortedSet<Header> records =
          new TreeSet<Header>(Header.COMPARATOR_BY_OFFSET);

      this.sfntVersion = fd.readFixed(offset + Offset.sfntVersion.offset);
      this.numTables = fd.readUShort(offset + Offset.numTables.offset);
      this.searchRange = fd.readUShort(offset + Offset.searchRange.offset);
      this.entrySelector = fd.readUShort(offset + Offset.entrySelector.offset);
      this.rangeShift = fd.readUShort(offset + Offset.rangeShift.offset);

      int tableOffset = offset + Offset.tableRecordBegin.offset;
      for (int tableNumber = 0;
      tableNumber < this.numTables;
      tableNumber++, tableOffset += Offset.tableRecordSize.offset) {
        Header table =
        // safe since the tag is ASCII
            new Header(fd.readULongAsInt(tableOffset + Offset.tableTag.offset),
                fd.readULong(tableOffset + Offset.tableCheckSum.offset), // checksum
            fd.readULongAsInt(tableOffset + Offset.tableOffset.offset), // offset
            fd.readULongAsInt(tableOffset + Offset.tableLength.offset)); // length
        records.add(table);
      }
      return records;
    }

    private Map<Header, WritableFontData> loadTableData(
        SortedSet<Header> headers, WritableFontData fd) {
      Map<Header, WritableFontData> tableData =
          new HashMap<Header, WritableFontData>(headers.size());
      logger.fine("########  Reading Table Data");
      for (Header tableHeader : headers) {
        WritableFontData data = fd.slice(tableHeader.offset(), tableHeader.length());
        tableData.put(tableHeader, data);
      }
      return tableData;
    }
  }
} public final class FontFactory {
  private static final int LOOKAHEAD_SIZE = 4;

  // font building settings
  private boolean fingerprint = false;

  // font serialization settings
  List<Integer> tableOrdering;

  /**
   * Offsets to specific elements in the underlying data. These offsets are relative to the
   * start of the table or the start of sub-blocks within the table.
   */
  private enum Offset {
    // Offsets within the main directory
    TTCTag(0),
    Version(4),
    numFonts(8),
    OffsetTable(12),

    // TTC Version 2.0 extensions
    // offsets from end of OffsetTable
    ulDsigTag(0),
    ulDsigLength(4),
    ulDsigOffset(8);

    private final int offset;

    private Offset(int offset) {
      this.offset = offset;
    }
  }

  /**
   * Constructor.
   */
  private FontFactory() {
    // Prevent construction.
  }

  /**
   * Factory method for the construction of a font factory.
   *
   * @return a new font factory
   */
  public static FontFactory getInstance(/*buffer builder factory*/) {
    return new FontFactory();
  }

  // font building settings

  /**
   * Toggle whether fonts that are loaded are fingerprinted with a SHA-1 hash.
   * If a font is fingerprinted then a SHA-1 hash is generated at load time and stored in the
   * font. This is useful for uniquely identifying fonts. By default this is turned on.
   * @param fingerprint whether fingerprinting should be turned on or off
   * @see #fingerprintFont()
   * @see Font#digest()
   */
  public void fingerprintFont(boolean fingerprint) {
    this.fingerprint= fingerprint;
  }

  /**
   * Get the state of the fingerprinting option for fonts that are loaded.
   * @return true if fingerprinting is turned on; false otherwise
   * @see #fingerprintFont(boolean)
   * @see Font#digest()
   */
  public boolean fingerprintFont() {
    return this.fingerprint;
  }

  // input stream font loading

  /**
   * Load the font(s) from the input stream. The current settings on the factory
   * are used during the loading process. One or more fonts are returned if the
   * stream contains valid font data. Some font container formats may have more
   * than one font and in this case multiple font objects will be returned. If
   * the data in the stream cannot be parsed or is invalid an array of size zero
   * will be returned.
   *
   * @param is the input stream font data
   * @return one or more fonts
   * @throws IOException
   */
  public Font[] loadFonts(InputStream is) throws IOException {
    PushbackInputStream pbis =
      new PushbackInputStream(new BufferedInputStream(is), FontFactory.LOOKAHEAD_SIZE);
    if (isCollection(pbis)) {
      return loadCollection(pbis);
    }
    return new Font[] {loadSingleOTF(pbis) };
  }

  /**
   * Load the font(s) from the input stream into font builders. The current
   * settings on the factory are used during the loading process. One or more
   * font builders are returned if the stream contains valid font data. Some
   * font container formats may have more than one font and in this case
   * multiple font builder objects will be returned. If the data in the stream
   * cannot be parsed or is invalid an array of size zero will be returned.
   *
   * @param is the input stream font data
   * @return one or more font builders
   * @throws IOException
   */
  public Builder[] loadFontsForBuilding(InputStream is) throws IOException {
    PushbackInputStream pbis =
      new PushbackInputStream(new BufferedInputStream(is), FontFactory.LOOKAHEAD_SIZE);
    if (isCollection(pbis)) {
      return loadCollectionForBuilding(pbis);
    }
    return new Builder[] {loadSingleOTFForBuilding(pbis) };
  }

  private Font loadSingleOTF(InputStream is) throws IOException {
    return loadSingleOTFForBuilding(is).build();
  }

  private Font[] loadCollection(InputStream is) throws IOException {
    Font.Builder[] builders = loadCollectionForBuilding(is);
    Font[] fonts = new Font[builders.length];
    for (int i = 0; i < fonts.length; i++) {
      fonts[i] = builders[i].build();
    }
    return fonts;
  }

  private Font.Builder loadSingleOTFForBuilding(InputStream is) throws IOException {
    MessageDigest digest = null;
    if (this.fingerprintFont()) {
      try {
        digest = MessageDigest.getInstance("SHA-1");
      } catch (NoSuchAlgorithmException e) {
        throw new IOException("Unable to get requested message digest algorithm.", e);
      }
      DigestInputStream dis = new DigestInputStream(is, digest);
      is = dis;
    }
    Builder builder = Builder.getOTFBuilder(this, is);
    if (this.fingerprintFont()) {
      builder.setDigest(digest.digest());
    }
    return builder;
  }

  private Font.Builder[] loadCollectionForBuilding(InputStream is) throws IOException {
    WritableFontData wfd = WritableFontData.createWritableFontData(is.available());
    wfd.copyFrom(is);
    // TOOD(stuartg): add collection loading from a stream
    return loadCollectionForBuilding(wfd);
  }

  static private boolean isCollection(PushbackInputStream pbis) throws IOException {
    byte[] tag = new byte[4];
    pbis.read(tag);
    pbis.unread(tag);
    return Tag.ttcf == Tag.intValue(tag);
  }

  // ByteArray font loading
  /**
   * Load the font(s) from the byte array. The current settings on the factory
   * are used during the loading process. One or more fonts are returned if the
   * stream contains valid font data. Some font container formats may have more
   * than one font and in this case multiple font objects will be returned. If
   * the data in the stream cannot be parsed or is invalid an array of size zero
   * will be returned.
   *
   * @param b the font data
   * @return one or more fonts
   * @throws IOException
   */
  public Font[] loadFonts(byte[] b) throws IOException {
    // TODO(stuartg): make a ReadableFontData when block loading moved to
    // FontFactory
    WritableFontData rfd = WritableFontData.createWritableFontData(b);
    if (isCollection(rfd)) {
      return loadCollection(rfd);
    }
    return new Font[] {loadSingleOTF(rfd)};
  }

  /**
   * Load the font(s) from the byte array into font builders. The current
   * settings on the factory are used during the loading process. One or more
   * font builders are returned if the stream contains valid font data. Some
   * font container formats may have more than one font and in this case
   * multiple font builder objects will be returned. If the data in the stream
   * cannot be parsed or is invalid an array of size zero will be returned.
   *
   * @param b the byte array font data
   * @return one or more font builders
   * @throws IOException
   */
  public Font.Builder[] loadFontsForBuilding(byte[] b) throws IOException {
    WritableFontData wfd = WritableFontData.createWritableFontData(b);
    if (isCollection(wfd)) {
      return loadCollectionForBuilding(wfd);
    }
    return new Font.Builder[] {loadSingleOTFForBuilding(wfd, 0)};
  }

  private Font loadSingleOTF(WritableFontData wfd) throws IOException {
    return loadSingleOTFForBuilding(wfd, 0).build();
  }

  private Font[] loadCollection(WritableFontData wfd) throws IOException {
    Font.Builder[] builders = loadCollectionForBuilding(wfd);
    Font[] fonts = new Font[builders.length];
    for (int i = 0; i < fonts.length; i++) {
      fonts[i] = builders[i].build();
    }
    return fonts;
  }

  private Font.Builder loadSingleOTFForBuilding(WritableFontData wfd, int offsetToOffsetTable)
      throws IOException {
    MessageDigest digest = null;
    if (this.fingerprintFont()) {
      // TODO(stuartg): digest of ByteArray
    }
    Font.Builder builder = Font.Builder.getOTFBuilder(this, wfd, offsetToOffsetTable);
    return builder;
  }

  private Font.Builder[] loadCollectionForBuilding(WritableFontData wfd) throws IOException {
    int ttcTag = wfd.readULongAsInt(Offset.TTCTag.offset);
    long version = wfd.readFixed(Offset.Version.offset);
    int numFonts = wfd.readULongAsInt(Offset.numFonts.offset);

    Font.Builder[] builders = new Font.Builder[numFonts];
    int offsetTableOffset = Offset.OffsetTable.offset;
    for (int fontNumber = 0; fontNumber < numFonts; fontNumber++,
        offsetTableOffset += FontData.DataSize.ULONG.size()) {
      int offset = wfd.readULongAsInt(offsetTableOffset);
      builders[fontNumber] = this.loadSingleOTFForBuilding(wfd, offset);
    }
    return builders;
  }

  static private boolean isCollection(ReadableFontData rfd) {
    byte[] tag = new byte[4];
    rfd.readBytes(0, tag, 0, tag.length);
    return Tag.ttcf == Tag.intValue(tag);
  }

  // font serialization

  /**
   * Serialize the font to the output stream.
   *
   * @param font the font to serialize
   * @param os the destination stream for the font
   * @throws IOException
   */
  public void serializeFont(Font font, OutputStream os) throws IOException {
    // TODO(stuartg) should have serialization options somewhere
    font.serialize(os, tableOrdering);
  }

  /**
   * Set the table ordering to be used in serializing a font. The table ordering
   * is an ordered list of table ids and tables will be serialized in the order
   * given. Any tables whose id is not listed in the ordering will be placed in
   * an unspecified order following those listed.
   *
   * @param tableOrdering the table ordering
   */
  public void setSerializationTableOrdering(List<Integer> tableOrdering) {
    this.tableOrdering = new ArrayList<Integer>(tableOrdering);
  }

  // new fonts

  /**
   * Get an empty font builder for creating a new font from scratch.
   *
   * @return an empty font builder
   */
  public Builder newFontBuilder() {
    return Font.Builder.getOTFBuilder(this);
  }
} public final class Tag {
  public static final int ttcf = Tag.intValue(new byte[]{'t', 't', 'c', 'f'});

  /***********************************************************************************
   *
   * Table Type Tags
   *
   ***********************************************************************************/

  // required tables
  public static final int cmap = Tag.intValue(new byte[]{'c', 'm', 'a', 'p'});
  public static final int head = Tag.intValue(new byte[]{'h', 'e', 'a', 'd'});
  public static final int hhea = Tag.intValue(new byte[]{'h', 'h', 'e', 'a'});
  public static final int hmtx = Tag.intValue(new byte[]{'h', 'm', 't', 'x'});
  public static final int maxp = Tag.intValue(new byte[]{'m', 'a', 'x', 'p'});
  public static final int name = Tag.intValue(new byte[]{'n', 'a', 'm', 'e'});
  public static final int OS_2 = Tag.intValue(new byte[]{'O', 'S', '/', '2'});
  public static final int post = Tag.intValue(new byte[]{'p', 'o', 's', 't'});

  // truetype outline tables
  public static final int cvt = Tag.intValue(new byte[]{'c', 'v', 't', ' '});
  public static final int fpgm = Tag.intValue(new byte[]{'f', 'p', 'g', 'm'});
  public static final int glyf = Tag.intValue(new byte[]{'g', 'l', 'y', 'f'});
  public static final int loca = Tag.intValue(new byte[]{'l', 'o', 'c', 'a'});
  public static final int prep = Tag.intValue(new byte[]{'p', 'r', 'e', 'p'});

  // postscript outline tables
  public static final int CFF = Tag.intValue(new byte[]{'C', 'F', 'F', ' '});
  public static final int VORG = Tag.intValue(new byte[]{'V', 'O', 'R', 'G'});

  // opentype bitmap glyph outlines
  public static final int EBDT = Tag.intValue(new byte[]{'E', 'B', 'D', 'T'});
  public static final int EBLC = Tag.intValue(new byte[]{'E', 'B', 'L', 'C'});
  public static final int EBSC = Tag.intValue(new byte[]{'E', 'B', 'S', 'C'});

  // advanced typographic features
  public static final int BASE = Tag.intValue(new byte[]{'B', 'A', 'S', 'E'});
  public static final int GDEF = Tag.intValue(new byte[]{'G', 'D', 'E', 'F'});
  public static final int GPOS = Tag.intValue(new byte[]{'G', 'P', 'O', 'S'});
  public static final int GSUB = Tag.intValue(new byte[]{'G', 'S', 'U', 'B'});
  public static final int JSTF = Tag.intValue(new byte[]{'J', 'S', 'T', 'F'});

  // other
  public static final int DSIG = Tag.intValue(new byte[]{'D', 'S', 'I', 'G'});
  public static final int gasp = Tag.intValue(new byte[]{'g', 'a', 's', 'p'});
  public static final int hdmx = Tag.intValue(new byte[]{'h', 'd', 'm', 'x'});
  public static final int kern = Tag.intValue(new byte[]{'k', 'e', 'r', 'n'});
  public static final int LTSH = Tag.intValue(new byte[]{'L', 'T', 'S', 'H'});
  public static final int PCLT = Tag.intValue(new byte[]{'P', 'C', 'L', 'T'});
  public static final int VDMX = Tag.intValue(new byte[]{'V', 'D', 'M', 'X'});
  public static final int vhea = Tag.intValue(new byte[]{'v', 'h', 'e', 'a'});
  public static final int vmtx = Tag.intValue(new byte[]{'v', 'm', 't', 'x'});

  // AAT Tables
  // TODO(stuartg): some tables may be missing from this list
  public static final int bsln = Tag.intValue(new byte[]{'b', 's', 'l', 'n'});
  public static final int feat = Tag.intValue(new byte[]{'f', 'e', 'a', 't'});
  public static final int lcar = Tag.intValue(new byte[]{'l', 'c', 'a', 'r'});
  public static final int morx = Tag.intValue(new byte[]{'m', 'o', 'r', 'x'});
  public static final int opbd = Tag.intValue(new byte[]{'o', 'p', 'b', 'd'});
  public static final int prop = Tag.intValue(new byte[]{'p', 'r', 'o', 'p'});

  // Graphite tables
  public static final int Feat = Tag.intValue(new byte[]{'F', 'e', 'a', 't'});
  public static final int Glat = Tag.intValue(new byte[]{'G', 'l', 'a', 't'});
  public static final int Gloc = Tag.intValue(new byte[]{'G', 'l', 'o', 'c'});
  public static final int Sile = Tag.intValue(new byte[]{'S', 'i', 'l', 'e'});
  public static final int Silf = Tag.intValue(new byte[]{'S', 'i', 'l', 'f'});

  // truetype bitmap font tables
  public static final int bhed = Tag.intValue(new byte[]{'b', 'h', 'e', 'd'});
  public static final int bdat = Tag.intValue(new byte[]{'b', 'd', 'a', 't'});
  public static final int bloc = Tag.intValue(new byte[]{'b', 'l', 'o', 'c'});

  private Tag() {
    // Prevent construction.
  }

  public static int intValue(byte[] tag) {
    return tag[0] << 24 | tag[1] << 16 | tag[2] << 8 | tag[3];
  }

  public static byte[] byteValue(int tag) {
    byte[] b = new byte[4];
    b[0] = (byte) (0xff & (tag >> 24));
    b[1] = (byte) (0xff & (tag >> 16));
    b[2] = (byte) (0xff & (tag >> 8));
    b[3] = (byte) (0xff & tag);
    return b;
  }

  public static String stringValue(int tag) {
    String s;
    try {
      s = new String(Tag.byteValue(tag), "US-ASCII");
    } catch (UnsupportedEncodingException e) {
      // should never happen since US-ASCII is a guaranteed character set but...
      return "";
    }
    return s;
  }

  public static int intValue(String s) {
    byte[] b = null;
    try {
      b = s.substring(0, 4).getBytes("US-ASCII");
    } catch (UnsupportedEncodingException e) {
      // should never happen since US-ASCII is a guaranteed character set but...
      return 0;
    }
    return intValue(b);
  }

  /**
   * Determines whether the tag is that for the header table.
   * @param tag table tag
   * @return true if the tag represents the font header table
   */
  public static boolean isHeaderTable(int tag) {
    if (tag == Tag.head || tag == Tag.bhed) {
      return true;
    }
    return false;
  }
} public class ReadableFontData extends FontData {

  public static ReadableFontData createReadableFontData(byte[] b) {
    ByteArray<?> ba = new MemoryByteArray(b);
    return new ReadableFontData(ba);
  }


  /**
   * Flag on whether the checksum has been set.
   */
  private volatile boolean checksumSet = false;
  /**
   * Lock on all operations that will affect the value of the checksum.
   */
  private final Object checksumLock = new Object();
  private volatile long checksum;
  private volatile int[] checksumRange;

  /**
   * Constructor.
   *
   * @param array byte array to wrap
   */
  protected ReadableFontData(ByteArray<? extends ByteArray<?>> array) {
    super(array);
  }

  /**
   * Constructor. Creates a bounded wrapper of another ReadableFontData from the
   * given offset until the end of the original ReadableFontData.
   *
   * @param data data to wrap
   * @param offset the start of this data's view of the original data
   */
  protected ReadableFontData(ReadableFontData data, int offset) {
    super(data, offset);
  }

  /**
   * Constructor. Creates a bounded wrapper of another ReadableFontData from the
   * given offset until the end of the original ReadableFontData.
   *
   * @param data data to wrap
   * @param offset the start of this data's view of the original data
   * @param length the length of the other FontData to use
   */
  protected ReadableFontData(ReadableFontData data, int offset, int length) {
    super(data, offset, length);
  }

  /**
   * Makes a slice of this FontData. The returned slice will share the data with
   * the original <code>FontData</code>.
   *
   * @param offset the start of the slice
   * @param length the number of bytes in the slice
   * @return a slice of the original FontData
   */
  @Override
  public ReadableFontData slice(int offset, int length) {
    if (offset < 0 || (offset + length) > this.size()) {
      throw new IndexOutOfBoundsException("Attempt to bind data outside of its limits.");
    }
    ReadableFontData slice = new ReadableFontData(this, offset, length);
    return slice;
  }

  /**
   * Makes a bottom bound only slice of this array. The returned slice will
   * share the data with the original <code>FontData</code>.
   *
   * @param offset the start of the slice
   * @return a slice of the original FontData
   */
  @Override
  public ReadableFontData slice(int offset) {
    if (offset < 0 || offset > this.size()) {
      throw new IndexOutOfBoundsException("Attempt to bind data outside of its limits.");
    }
    ReadableFontData slice = new ReadableFontData(this, offset);
    return slice;
  }

  /**
   * Generates a String representation of the object with a certain number of
   * data bytes.
   *
   * @param length number of bytes of the data to include in the String
   * @return String representation of the object
   */
  public String toString(int length) {
    StringBuilder sb = new StringBuilder();
    sb.append("[l=" + this.length() + ", cs=" + this.checksum() + "]\n");
    sb.append(this.array.toString(this.boundOffset(0), this.boundLength(0, length)));
    return sb.toString();
  }

  @Override
  public String toString() {
    return toString(0);
  }

  /**
   * Gets a computed checksum for the data. This checksum uses the OpenType spec
   * calculation. Every ULong value (32 bit unsigned) in the data is summed and
   * the resulting value is truncated to 32 bits. If the data length in bytes is
   * not an integral multiple of 4 then any remaining bytes are treated as the
   * start of a 4 byte sequence whose remaining bytes are zero.
   *
   * @return the checksum
   */
  public long checksum() {
    if (!this.checksumSet) {
      computeChecksum();
    }
    return this.checksum;
  }

  /**
   * Computes the checksum for the font data using any ranges set for the
   * calculation. Updates the internal state of this object in a threadsafe way.
   */
  private void computeChecksum() {
    synchronized (this.checksumLock) {
      if (this.checksumSet) {
        // another thread computed the checksum while were waiting to do so
        return;
      }
      long sum = 0;
      if (this.checksumRange == null) {
        sum = computeCheckSum(0, this.length());
      } else {
        for (int lowBoundIndex = 0; lowBoundIndex < this.checksumRange.length; lowBoundIndex += 2) {
          int lowBound = this.checksumRange[lowBoundIndex];
          int highBound =
              (lowBoundIndex == this.checksumRange.length - 1) ? this.length() : this.checksumRange[
                  lowBoundIndex + 1];
          sum += computeCheckSum(lowBound, highBound);
        }
      }
      this.checksum = sum & 0xffffffffL;
      this.checksumSet = true;
    }
  }

  /**
   * Do the actual computation of the checksum for a range using the
   * TrueType/OpenType checksum algorithm. The range used is from the low bound
   * to the high bound in steps of four bytes. If any of the bytes within that 4
   * byte segment are not readable then it will considered a zero for
   * calculation.
   *
   * <p>Only called from within a synchronized method so it does not need to be
   * synchronized itself.
   *
   * @param lowBound first position to start a 4 byte segment on
   * @param highBound last possible position to start a 4 byte segment on
   * @return the checksum for the total range
   */
  private long computeCheckSum(int lowBound, int highBound) {
    long sum = 0;
    // checksum all whole 4-byte chunks
    for (int i = lowBound; i <= highBound - 4; i += 4) {
      sum += this.readULong(i);
    }
    // add last fragment if not 4-byte multiple
    int off = highBound & -4;
    if (off < highBound) {
      int b3 = this.readUByte(off);
      int b2 = (off + 1 < highBound) ? this.readUByte(off + 1) : 0;
      int b1 = (off + 2 < highBound) ? this.readUByte(off + 2) : 0;
      int b0 = 0;
      sum += (b3 << 24) | (b2 << 16) | (b1 << 8) | b0;
    }
    return sum;
  }

  /**
   * Sets the ranges to use for computing the checksum. These ranges are in
   * begin and end pairs. If an odd number is given then the final range is
   * assumed to extend to the end of the data. The lengths of each range must be
   * a multiple of 4.
   *
   * @param ranges the range bounds to use for the checksum
   */
  public void setCheckSumRanges(int... ranges) {
    synchronized (this.checksumLock) {
      if (ranges != null && ranges.length > 0) {
        this.checksumRange = Arrays.copyOf(ranges, ranges.length);
      } else {
        this.checksumRange = null;
      }
      this.checksumSet = false;
    }
  }

  /**
   * Gets the ranges that are used for computing the checksum. These ranges are in
   * begin and end pairs. If an odd number is given then the final range is
   * assumed to extend to the end of the data. The lengths of each range must be
   * a multiple of 4.
   *
   * @return the range bounds used for the checksum
   */
  public int[] checkSumRange() {
    synchronized (this.checksumLock) {
      if (this.checksumRange != null && checksumRange.length > 0) {
        return Arrays.copyOf(this.checksumRange, this.checksumRange.length);
      }
      return new int[0];
    }
  }
  
  /**
   * Reads the UBYTE at the given index.
   *
   * @param index index into the font data
   * @return the UBYTE; -1 if outside the bounds of the font data
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int readUByte(int index) {
    int b = this.array.get(this.boundOffset(index));
    if (b < 0) {
      throw new IndexOutOfBoundsException(
          "Index attempted to be read from is out of bounds: " + Integer.toHexString(index));
    }
    return b;
    // return this.array.get(this.boundOffset(index));
  }

  /**
   * Reads the BYTE at the given index.
   *
   * @param index index into the font data
   * @return the BYTE
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int readByte(int index) {
    int b = this.array.get(this.boundOffset(index));
    if (b < 0) {
      throw new IndexOutOfBoundsException(
          "Index attempted to be read from is out of bounds: " + Integer.toHexString(index));
    }
    return (b << 24) >> 24;
    // return (this.array.get(this.boundOffset(index)) << 24) >> 24;
  }

  /**
   * Reads the bytes at the given index into the array.
   *
   * @param index index into the font data
   * @param b the destination for the bytes read
   * @param offset offset in the byte array to place the bytes
   * @param length the length of bytes to read
   * @return the number of bytes actually read; -1 if the index is outside the
   *         bounds of the font data
   */
  public int readBytes(int index, byte[] b, int offset, int length) {
    return this.array.get(this.boundOffset(index), b, offset, this.boundLength(index, length));
  }

  /**
   * Reads the CHAR at the given index.
   *
   * @param index index into the font data
   * @return the CHAR
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int readChar(int index) {
    return this.readUByte(index);
  }

  /**
   * Reads the USHORT at the given index.
   *
   * @param index index into the font data
   * @return the USHORT
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int readUShort(int index) {
    return 0xffff & (this.readUByte(index) << 8 | this.readUByte(index + 1));
  }

  /**
   * Reads the SHORT at the given index.
   *
   * @param index index into the font data
   * @return the SHORT
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int readShort(int index) {
    return ((this.readByte(index) << 8 | this.readUByte(index + 1)) << 16) >> 16;
  }

  /**
   * Reads the UINT24 at the given index.
   *
   * @param index index into the font data
   * @return the UINT24
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int readUInt24(int index) {
    return 0xffffff & (this.readUByte(index) << 16 | this.readUByte(index + 1) << 8
        | this.readUByte(index + 2));
  }

  /**
   * Reads the ULONG at the given index.
   *
   * @param index index into the font data
   * @return the ULONG
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public long readULong(int index) {
    return 0xffffffffL & (this.readUByte(index) << 24 | this.readUByte(index + 1) << 16
        | this.readUByte(index + 2) << 8 | this.readUByte(index + 3));
  }

  /**
   * Reads the ULONG at the given index as an int.
   *
   * @param index index into the font data
   * @return the ULONG
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   * @throws ArithmeticException if the value will not fit into an integer
   */
  public int readULongAsInt(int index) {
    long ulong = this.readULong(index);
    if ((ulong & 0x80000000) == 0x80000000) {
      throw new ArithmeticException("Long value too large to fit into an integer.");
    }
    return (int) ulong;
  }

  /**
   * Reads the ULONG at the given index, little-endian variant.
   *
   * @param index index into the font data
   * @return the ULONG
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public long readULongLE(int index) {
    return 0xffffffffL & (this.readUByte(index) | this.readUByte(index + 1) << 8
        | this.readUByte(index + 2) << 16 | this.readUByte(index + 3) << 24);
  }

  /**
   * Reads the LONG at the given index.
   *
   * @param index index into the font data
   * @return the LONG
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int readLong(int index) {
    return this.readByte(index) << 24 | this.readUByte(index + 1) << 16 | 
    this.readUByte(index + 2) << 8 | this.readUByte(index + 3);
  }

  /**
   * Reads the Fixed at the given index.
   *
   * @param index index into the font data
   * @return the Fixed
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int readFixed(int index) {
    return this.readLong(index);
  }

  /**
   * Reads the F2DOT14 at the given index.
   *
   * @param index index into the font data
   * @return the F2DOT14
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public BigDecimal readF2Dot14(int index) {
    throw new UnsupportedOperationException();
  }

  /**
   * Reads the LONGDATETIME at the given index.
   *
   * @param index index into the font data
   * @return the LONGDATETIME
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public long readDateTimeAsLong(int index) {
    return this.readULong(index) << 32 | this.readULong(index + 4);
  }

  /**
   * Reads the LONGDATETIME at the given index.
   *
   * @param index index into the font data
   * @return the F2DOT14
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public Date readLongDateTime(int index) {
    throw new UnsupportedOperationException();
  }

  /**
   * Reads the FUNIT at the given index.
   *
   * @param index index into the font data
   * @return the FUNIT
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int readFUnit(int index) {
    throw new UnsupportedOperationException();
  }

  /**
   * Reads the FWORD at the given index.
   *
   * @param index index into the font data
   * @return the FWORD
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int readFWord(int index) {
    return this.readShort(index);
  }

  /**
   * Reads the UFWORD at the given index.
   *
   * @param index index into the font data
   * @return the UFWORD
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int readUFWord(int index) {
    return this.readUShort(index);
  }

  /**
   * Copy the FontData to an OutputStream.
   *
   * @param os the destination
   * @return number of bytes copied
   * @throws IOException
   */
  public int copyTo(OutputStream os) throws IOException {
    return this.array.copyTo(os, this.boundOffset(0), this.length());
  }

  /**
   * Copies the FontData to a WritableFontData.
   *
   * @param wfd the destination
   * @return number of bytes copied
   */
  public int copyTo(WritableFontData wfd) {
    return this.array.copyTo(wfd.boundOffset(0), wfd.array, this.boundOffset(0), this.length());
  }

  /**
   * Search for the key value in the range tables provided.
   *
   *  The search looks through the start-end pairs looking for the key value. It
   * is assumed that the start-end pairs are both represented by UShort values,
   * ranges do not overlap, and are monotonically increasing.
   *
   * @param startIndex the position to read the first start value from
   * @param startOffset the offset between subsequent start values
   * @param endIndex the position to read the first end value from
   * @param endOffset the offset between subsequent end values
   * @param length the number of start-end pairs
   * @param key the value to search for
   * @return the index of the start-end pairs in which the key was found; -1
   *         otherwise
   */
  public int searchUShort(int startIndex,
      int startOffset,
      int endIndex,
      int endOffset,
      int length,
      int key) {
    int location = 0;
    int bottom = 0;
    int top = length;
    while (top != bottom) {
      location = (top + bottom) / 2;
      int locationStart = this.readUShort(startIndex + location * startOffset);
      if (key < locationStart) {
        // location is below current location
        top = location;
      } else {
        // is key below the upper bound?
        int locationEnd = this.readUShort(endIndex + location * endOffset);
        if (key <= locationEnd) {
          return location;
        }
        // location is above the current location
        bottom = location + 1;
      }
    }
    return -1;
  }

  /**
   * Search for the key value in the range tables provided.
   *
   *  The search looks through the start-end pairs looking for the key value. It
   * is assumed that the start-end pairs are both represented by ULong values
   * that can be represented within 31 bits, ranges do not overlap, and are
   * monotonically increasing.
   *
   * @param startIndex the position to read the first start value from
   * @param startOffset the offset between subsequent start values
   * @param endIndex the position to read the first end value from
   * @param endOffset the offset between subsequent end values
   * @param length the number of start-end pairs
   * @param key the value to search for
   * @return the index of the start-end pairs in which the key was found; -1
   *         otherwise
   */
  public int searchULong(int startIndex,
      int startOffset,
      int endIndex,
      int endOffset,
      int length,
      int key) {   
    int location = 0;
    int bottom = 0;
    int top = length;
    while (top != bottom) {
      location = (top + bottom) / 2;
      int locationStart = this.readULongAsInt(startIndex + location * startOffset);
      if (key < locationStart) {
        // location is below current location
        top = location;
      } else {
        // is key below the upper bound?
        int locationEnd = this.readULongAsInt(endIndex + location * endOffset);
        if (key <= locationEnd) {
          return location;
        }
        // location is above the current location
        bottom = location + 1;
      }
    }
    return -1;
  }

  /**
   * Search for the key value in the table provided.
   *
   *  The search looks through the values looking for the key value. It is
   * assumed that the are represented by UShort values and are monotonically
   * increasing.
   *
   * @param startIndex the position to read the first start value from
   * @param startOffset the offset between subsequent start values
   * @param length the number of start-end pairs
   * @param key the value to search for
   * @return the index of the start-end pairs in which the key was found; -1
   *         otherwise
   */
  public int searchUShort(int startIndex, int startOffset, int length, int key) {
    int location = 0;
    int bottom = 0;
    int top = length;
    while (top != bottom) {
      location = (top + bottom) / 2;
      int locationStart = this.readUShort(startIndex + location * startOffset);
      if (key < locationStart) {
        // location is below current location
        top = location;
      } else if (key > locationStart) {
        // location is above current location
        bottom = location + 1;
      } else {
        return location;
      }
    }
    return -1;
  }
} public class Table extends FontDataTable {

  private Header header;

  protected Table(Header header, ReadableFontData data) {
    super(data);
    this.header = header;
  }

  /**
   * Get the calculated checksum for the data in the table.
   *
   * @return the checksum
   */
  public long calculatedChecksum() {
    return this.data.checksum();
  }

  /**
   * Get the header for the table.
   *
   * @return the table header
   */
  public Header header() {
    return this.header;
  }

  /**
   * Get the tag for the table from the record header.
   *
   * @return the tag for the table
   * @see #header
   */
  public int headerTag() {
    return this.header().tag();
  }

  /**
   * Get the offset for the table from the record header.
   *
   * @return the offset for the table
   * @see #header
   */
  public int headerOffset() {
    return this.header().offset();
  }

  /**
   * Get the length of the table from the record header.
   *
   * @return the length of the table
   * @see #header
   */
  public int headerLength() {
    return this.header().length();
  }

  /**
   * Get the checksum for the table from the record header.
   *
   * @return the checksum for the table
   * @see #header
   */
  public long headerChecksum() {
    return this.header().checksum();
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append("[");
    sb.append(Tag.stringValue(this.header.tag()));
    sb.append(", cs=0x");
    sb.append(Long.toHexString(this.header.checksum()));
    sb.append(", offset=0x");
    sb.append(Integer.toHexString(this.header.offset()));
    sb.append(", size=0x");
    sb.append(Integer.toHexString(this.header.length()));
    sb.append("]");
    return sb.toString();
  }

  public abstract static class Builder<T extends Table> extends FontDataTable.Builder<T> {
    private Header header;

    protected Builder(Header header, WritableFontData data) {
      super(data);
      this.header = header;
    }

    protected Builder(Header header, ReadableFontData data) {
      super(data);
      this.header = header;
    }

    protected Builder(Header header) {
      this(header, null);
    }

    @Override
    public String toString() {
      return "Table Builder for - " + this.header.toString();
    }

    /***********************************************************************************
     * 
     * Public Interface for Table Building
     * 
     ***********************************************************************************/

    public final Header header() {
      return this.header;
    }

    /***********************************************************************************
     * Internal Interface for Table Building
     ***********************************************************************************/

    @Override
    protected void notifyPostTableBuild(T table) {
      if (this.modelChanged() || this.dataChanged()) {
        Header header = new Header(this.header().tag(), table.dataLength());
        ((Table) table).header = header;
      }
    }

    // static interface for building

    /**
     * Get a builder for the table type specified by the data in the header.
     *
     * @param header the header for the table
     * @param tableData the data to be used to build the table from
     * @return builder for the table specified
     */
    public static Table.Builder<? extends Table> getBuilder(
        Header header, WritableFontData tableData) {

      int tag = header.tag();

      if (tag == Tag.cmap) {
        return CMapTable.Builder.createBuilder(header, tableData);
      } else if (tag == Tag.head) {
        return FontHeaderTable.Builder.createBuilder(header, tableData);
      } else if (tag == Tag.hhea) {
        return HorizontalHeaderTable.Builder.createBuilder(header, tableData);
      } else if (tag == Tag.hmtx) {
        return HorizontalMetricsTable.Builder.createBuilder(header, tableData);
      } else if (tag == Tag.maxp) {
        return MaximumProfileTable.Builder.createBuilder(header, tableData);
      } else if (tag == Tag.name) {
        return NameTable.Builder.createBuilder(header, tableData);
      } else if (tag == Tag.OS_2) {
        return OS2Table.Builder.createBuilder(header, tableData);
      } else if (tag == Tag.post) {
        return PostScriptTable.Builder.createBuilder(header, tableData);
      } else if (tag == Tag.cvt) {
        return ControlValueTable.Builder.createBuilder(header, tableData);
        // } else if (tag == Tag.fpgm) {
        // break;
      } else if (tag == Tag.glyf) {
        return GlyphTable.Builder.createBuilder(header, tableData);
      } else if (tag == Tag.loca) {
        return LocaTable.Builder.createBuilder(header, tableData);
      } else if (tag == Tag.prep) {
        return ControlProgramTable.Builder.createBuilder(header, tableData);
        // } else if (tag == CFF) {
        // break;
        // } else if (tag == VORG) {
        // break;
      } else if (tag == Tag.EBDT) {
        return EbdtTable.Builder.createBuilder(header, tableData);
      } else if (tag == Tag.EBLC) {
        return EblcTable.Builder.createBuilder(header, tableData);
      } else if (tag == Tag.EBSC) {
        return EbscTable.Builder.createBuilder(header, tableData);
        // } else if (tag == BASE) {
        // break;
        // } else if (tag == GDEF) {
        // break;
        // } else if (tag == GPOS) {
        // break;
        // } else if (tag == GSUB) {
        // break;
        // } else if (tag == JSTF) {
        // break;
        // } else if (tag == DSIG) {
        // break;
        // } else if (tag == gasp) {
        // break;
       } else if (tag == Tag.hdmx) {
         return HorizontalDeviceMetricsTable.Builder.createBuilder(header, tableData);
        // break;
        // } else if (tag == kern) {
        // break;
        // } else if (tag == LTSH) {
        // break;
        // } else if (tag == PCLT) {
        // break;
        // } else if (tag == VDMX) {
        // break;
        // } else if (tag == vhea) {
        // break;
        // } else if (tag == vmtx) {
        // break;
      } else if (tag == Tag.bhed) {
        return FontHeaderTable.Builder.createBuilder(header, tableData);
      } else if (tag == Tag.bdat) {
        return EbdtTable.Builder.createBuilder(header, tableData);
      } else if (tag == Tag.bloc) {
        return EblcTable.Builder.createBuilder(header, tableData);
      }
      return GenericTableBuilder.createBuilder(header, tableData);
    }
  }
} public class EblcTable extends SubTableContainerTable {
  private static final boolean DEBUG = false;

  public static final int NOTDEF = -1;

  static enum Offset {
    // header
    version(0),
    numSizes(4),
    headerLength(numSizes.offset + FontData.DataSize.ULONG.size()),

    // bitmapSizeTable
    bitmapSizeTableArrayStart(headerLength.offset),
    bitmapSizeTableLength(48),
    bitmapSizeTable_indexSubTableArrayOffset(0),
    bitmapSizeTable_indexTableSize(4),
    bitmapSizeTable_numberOfIndexSubTables(8),
    bitmapSizeTable_colorRef(12),
    bitmapSizeTable_hori(16),
    bitmapSizeTable_vert(28),
    bitmapSizeTable_startGlyphIndex(40),
    bitmapSizeTable_endGlyphIndex(42),
    bitmapSizeTable_ppemX(44),
    bitmapSizeTable_ppemY(45),
    bitmapSizeTable_bitDepth(46),
    bitmapSizeTable_flags(47),

    // sbitLineMetrics
    sbitLineMetricsLength(12),
    sbitLineMetrics_ascender(0),
    sbitLineMetrics_descender(1),
    sbitLineMetrics_widthMax(2),
    sbitLineMetrics_caretSlopeNumerator(3),
    sbitLineMetrics__caretSlopeDenominator(4),
    sbitLineMetrics_caretOffset(5),
    sbitLineMetrics_minOriginSB(6),
    sbitLineMetrics_minAdvanceSB(7),
    sbitLineMetrics_maxBeforeBL(8),
    sbitLineMetrics_minAfterBL(9),
    sbitLineMetrics_pad1(10),
    sbitLineMetrics_pad2(11),

    // indexSubTable
    indexSubTableEntryLength(8),
    indexSubTableEntry_firstGlyphIndex(0),
    indexSubTableEntry_lastGlyphIndex(2),
    indexSubTableEntry_additionalOffsetToIndexSubtable(4),

    // indexSubHeader
    indexSubHeaderLength(8),
    indexSubHeader_indexFormat(0),
    indexSubHeader_imageFormat(2),
    indexSubHeader_imageDataOffset(4),

    // indexSubTable - all offset relative to the subtable start

    // indexSubTable1
    indexSubTable1_offsetArray(indexSubHeaderLength.offset),
    indexSubTable1_builderDataSize(indexSubHeaderLength.offset),

    // indexSubTable2
    indexSubTable2Length(indexSubHeaderLength.offset + FontData.DataSize.ULONG.size()
        + BitmapGlyph.Offset.bigGlyphMetricsLength.offset),
    indexSubTable2_imageSize(indexSubHeaderLength.offset),
    indexSubTable2_bigGlyphMetrics(indexSubTable2_imageSize.offset + FontData.DataSize.ULONG.size()),
    indexSubTable2_builderDataSize(indexSubTable2_bigGlyphMetrics.offset
        + BigGlyphMetrics.Offset.metricsLength.offset),

    // indexSubTable3
    indexSubTable3_offsetArray(indexSubHeaderLength.offset),
    indexSubTable3_builderDataSize(indexSubTable3_offsetArray.offset),

    // indexSubTable4
    indexSubTable4_numGlyphs(indexSubHeaderLength.offset),
    indexSubTable4_glyphArray(indexSubTable4_numGlyphs.offset + FontData.DataSize.ULONG.size()),
    indexSubTable4_codeOffsetPairLength(2 * FontData.DataSize.USHORT.size()),
    indexSubTable4_codeOffsetPair_glyphCode(0),
    indexSubTable4_codeOffsetPair_offset(FontData.DataSize.USHORT.size()),
    indexSubTable4_builderDataSize(indexSubTable4_glyphArray.offset),

    // indexSubTable5
    indexSubTable5_imageSize(indexSubHeaderLength.offset),
    indexSubTable5_bigGlyphMetrics(indexSubTable5_imageSize.offset + FontData.DataSize.ULONG.size()),
    indexSubTable5_numGlyphs(indexSubTable5_bigGlyphMetrics.offset
        + BitmapGlyph.Offset.bigGlyphMetricsLength.offset),
    indexSubTable5_glyphArray(indexSubTable5_numGlyphs.offset + FontData.DataSize.ULONG.size()),
    indexSubTable5_builderDataSize(indexSubTable5_glyphArray.offset),

    // codeOffsetPair
    codeOffsetPairLength(2 * FontData.DataSize.USHORT.size()),
    codeOffsetPair_glyphCode(0),
    codeOffsetPair_offset(FontData.DataSize.USHORT.size());


    final int offset;

    private Offset(int offset) {
      this.offset = offset;
    }
  }

  /**
   * Lock on all operations that will affect the value of the bitmapSizeTable.
   */
  private final Object bitmapSizeTableLock = new Object();
  private volatile List<BitmapSizeTable> bitmapSizeTable;

  /**
   * @param header
   * @param data
   */
  protected EblcTable(Header header, ReadableFontData data) {
    super(header, data);
  }

  public int version() {
    return this.data.readFixed(Offset.version.offset);
  }

  public int numSizes() {
    return this.data.readULongAsInt(Offset.numSizes.offset);
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder(super.toString());
    sb.append("\nnum sizes = ");
    sb.append(this.numSizes());
    sb.append("\n");
    for (int i = 0; i < this.numSizes(); i++) {
      sb.append(i);
      sb.append(": ");
      BitmapSizeTable size = this.bitmapSizeTable(i);
      sb.append(size.toString());
    }
    return sb.toString();
  }

  public BitmapSizeTable bitmapSizeTable(int index) {
    if (index < 0 || index > this.numSizes()) {
      throw new IndexOutOfBoundsException("Size table index is outside of the range of tables.");
    }
    List<BitmapSizeTable> bitmapSizeTableList = getBitmapSizeTableList();
    return bitmapSizeTableList.get(index);
  }

  private List<BitmapSizeTable> getBitmapSizeTableList() {
    if (this.bitmapSizeTable == null) {
      synchronized (this.bitmapSizeTableLock) {
        if (this.bitmapSizeTable == null) {
          this.bitmapSizeTable = createBitmapSizeTable(this.data, this.numSizes());
        }
      }
    }
    return this.bitmapSizeTable;
  }

  private static List<BitmapSizeTable> createBitmapSizeTable(ReadableFontData data, int numSizes) {
    List<BitmapSizeTable> bitmapSizeTable = new ArrayList<BitmapSizeTable>();
    for (int i = 0; i < numSizes; i++) {
      BitmapSizeTable.Builder sizeBuilder =
          BitmapSizeTable.Builder.createBuilder(data.slice(
              Offset.bitmapSizeTableArrayStart.offset + i * Offset.bitmapSizeTableLength.offset,
              Offset.bitmapSizeTableLength.offset), data);
      BitmapSizeTable size = sizeBuilder.build();
      bitmapSizeTable.add(size);
    }
    return Collections.unmodifiableList(bitmapSizeTable);
  }

  public static final class Builder extends SubTableContainerTable.Builder<EblcTable> {
    private final int version = 0x00020000; // TODO(user) constant/enum
    private List<BitmapSizeTable.Builder> sizeTableBuilders;

    /**
     * Create a new builder using the header information and data provided.
     *
     * @param header the header information
     * @param data the data holding the table
     * @return a new builder
     */
    public static Builder createBuilder(Header header, WritableFontData data) {
      return new Builder(header, data);
    }

    /**
     * Create a new builder using the header information and data provided.
     *
     * @param header the header information
     * @param data the data holding the table
     * @return a new builder
     */
    public static Builder createBuilder(Header header, ReadableFontData data) {
      return new Builder(header, data);
    }

    protected Builder(Header header, WritableFontData data) {
      super(header, data);
    }

    protected Builder(Header header, ReadableFontData data) {
      super(header, data);
    }

    public List<BitmapSizeTable.Builder> bitmapSizeBuilders() {
      return this.getSizeList();
    }

    protected void revert() {
      this.sizeTableBuilders = null;
      this.setModelChanged(false);
    }

    /**
     * Generates the loca list for the EBDT table. The list is intended to be
     * used by the EBDT to allow it to parse the glyph data and generate glyph
     * objects. After returning from this method the list belongs to the caller.
     * The list entries are in the same order as the size table builders are at
     * the time of this call.
     *
     * @return the list of loca maps with one for each size table builder
     */
    public List<Map<Integer, BitmapGlyphInfo>> generateLocaList() {
      List<BitmapSizeTable.Builder> sizeBuilderList = this.getSizeList();

      List<Map<Integer, BitmapGlyphInfo>> locaList =
          new ArrayList<Map<Integer, BitmapGlyphInfo>>(sizeBuilderList.size());
      int sizeIndex = 0;
      for (BitmapSizeTable.Builder sizeBuilder : sizeBuilderList) {
        if (DEBUG) {
          System.out.printf("size table = %d%n", sizeIndex++);
        }
        Map<Integer, BitmapGlyphInfo> locaMap = sizeBuilder.generateLocaMap();
        locaList.add(locaMap);
      }

      return locaList;
    }

    private List<BitmapSizeTable.Builder> getSizeList() {
      if (this.sizeTableBuilders == null) {
        this.sizeTableBuilders = this.initialize(this.internalReadData());
        super.setModelChanged();
      }
      return this.sizeTableBuilders;
    }

    private List<BitmapSizeTable.Builder> initialize(ReadableFontData data) {
      List<BitmapSizeTable.Builder> sizeBuilders = new ArrayList<BitmapSizeTable.Builder>();

      if (data != null) {
        int numSizes = data.readULongAsInt(Offset.numSizes.offset);
        for (int i = 0; i < numSizes; i++) {
          BitmapSizeTable.Builder sizeBuilder =
              BitmapSizeTable.Builder.createBuilder(
                  data.slice(Offset.bitmapSizeTableArrayStart.offset + i
                      * Offset.bitmapSizeTableLength.offset, Offset.bitmapSizeTableLength.offset),
                  data);
          sizeBuilders.add(sizeBuilder);
        }
      }
      return sizeBuilders;
    }

    @Override
    protected EblcTable subBuildTable(ReadableFontData data) {
      return new EblcTable(this.header(), data);
    }

    @Override
    protected void subDataSet() {
      this.revert();
    }

    @Override
    protected int subDataSizeToSerialize() {
      if (this.sizeTableBuilders == null) {
        return 0;
      }
      int size = Offset.headerLength.offset;
      boolean variable = false;
      int sizeIndex = 0;
      for (BitmapSizeTable.Builder sizeBuilder : this.sizeTableBuilders) {
        int sizeBuilderSize = sizeBuilder.subDataSizeToSerialize();
        if (DEBUG) {
          System.out.printf("sizeIndex = 0x%x, sizeBuilderSize = 0x%x%n", sizeIndex++,
              sizeBuilderSize);
        }
        variable = sizeBuilderSize > 0 ? variable : true;
        size += Math.abs(sizeBuilderSize);
      }
      return variable ? -size : size;
    }

    @Override
    protected boolean subReadyToSerialize() {
      if (this.sizeTableBuilders == null) {
        return false;
      }
      for (BitmapSizeTable.Builder sizeBuilder : this.sizeTableBuilders) {
        if (!sizeBuilder.subReadyToSerialize()) {
          return false;
        }
      }
      return true;
    }

    @Override
    protected int subSerialize(WritableFontData newData) {
      // header
      int size = newData.writeFixed(0, this.version);
      size += newData.writeULong(size, this.sizeTableBuilders.size());

      // calculate the offsets

      // offset to the start of the size table array
      int sizeTableStartOffset = size;
      // walking offset in the size table array
      int sizeTableOffset = sizeTableStartOffset;

      // offset to the start of the whole index subtable block
      int subTableBlockStartOffset =
          sizeTableOffset + this.sizeTableBuilders.size() * Offset.bitmapSizeTableLength.offset;
      // walking offset in the index subtable
      // points to the start of the current subtable block
      int currentSubTableBlockStartOffset = subTableBlockStartOffset;

      int sizeIndex = 0;
      for (BitmapSizeTable.Builder sizeBuilder : this.sizeTableBuilders) {
        sizeBuilder.setIndexSubTableArrayOffset(currentSubTableBlockStartOffset);
        List<IndexSubTable.Builder<? extends IndexSubTable>> indexSubTableBuilderList =
            sizeBuilder.indexSubTableBuilders();

        // walking offset within the current subTable array
        int indexSubTableArrayOffset = currentSubTableBlockStartOffset;
        // walking offset within the subTable entries
        int indexSubTableOffset = indexSubTableArrayOffset + indexSubTableBuilderList.size()
            * Offset.indexSubHeaderLength.offset;

        if (DEBUG) {
          System.out.printf(
              "size %d: sizeTable = %x, current subTable Block = %x, index subTable Start = %x%n",
              sizeIndex, sizeTableOffset, currentSubTableBlockStartOffset, indexSubTableOffset);
          sizeIndex++;
        }
        int subTableIndex = 0;

        for (IndexSubTable.Builder<? extends IndexSubTable> indexSubTableBuilder :
            indexSubTableBuilderList) {
          if (DEBUG) {
            System.out.printf("\tsubTableIndex %d: format = %x, ", subTableIndex,
                indexSubTableBuilder.indexFormat());
            System.out.printf("indexSubTableArrayOffset = %x, indexSubTableOffset = %x%n",
                indexSubTableArrayOffset, indexSubTableOffset);
            subTableIndex++;
          }
          // array entry
          indexSubTableArrayOffset +=
              newData.writeUShort(indexSubTableArrayOffset, indexSubTableBuilder.firstGlyphIndex());
          indexSubTableArrayOffset +=
              newData.writeUShort(indexSubTableArrayOffset, indexSubTableBuilder.lastGlyphIndex());
          indexSubTableArrayOffset += newData.writeULong(
              indexSubTableArrayOffset, indexSubTableOffset - currentSubTableBlockStartOffset);

          // index sub table
          int currentSubTableSize =
              indexSubTableBuilder.subSerialize(newData.slice(indexSubTableOffset));
          int padding =
              FontMath.paddingRequired(currentSubTableSize, FontData.DataSize.ULONG.size());
          if (DEBUG) {
            System.out.printf(
                "\t\tsubTableSize = %x, padding = %x%n", currentSubTableSize, padding);
          }

          indexSubTableOffset += currentSubTableSize;
          indexSubTableOffset += newData.writePadding(indexSubTableOffset, padding);
        }

        // serialize size table
        sizeBuilder.setIndexTableSize(indexSubTableOffset - currentSubTableBlockStartOffset);
        sizeTableOffset += sizeBuilder.subSerialize(newData.slice(sizeTableOffset));

        currentSubTableBlockStartOffset = indexSubTableOffset;
      }
      return size + currentSubTableBlockStartOffset;
    }
  }
} public abstract class CMap extends SubTable implements Iterable<Integer> {
  protected final int format;
  protected final CMapId cmapId;

  /**
   * CMap subtable formats.
   *
   */
  public enum CMapFormat {
    Format0(0),
    Format2(2),
    Format4(4),
    Format6(6),
    Format8(8),
    Format10(10),
    Format12(12),
    Format13(13),
    Format14(14);

    final int value;

    private CMapFormat(int value) {
      this.value = value;
    }

    public int value() {
      return this.value;
    }

    public boolean equals(int value) {
      return value == this.value;
    }

    public static CMapFormat valueOf(int value) {
      for (CMapFormat format : CMapFormat.values()) {
        if (format.equals(value)) {
          return format;
        }
      }
      return null;
    }
  }
  
  /**
   * Constructor.
   *
   * @param data data for the cmap
   * @param format the format of the cmap
   * @param cmapId the id information of the cmap
   */
  protected CMap(ReadableFontData data, int format, CMapId cmapId) {
    super(data);
    this.format = format;
    this.cmapId = cmapId;
  }

  /**
   * Gets the format of the cmap.
   *
   * @return the format
   */
  public int format() {
    return this.format;
  }

  /**
   * Gets the cmap id for this cmap.
   *
   * @return cmap id
   */
  public CMapId cmapId() {
    return this.cmapId;
  }

  /**
   * Gets the platform id for this cmap.
   *
   * @return the platform id
   * @see PlatformId
   */
  public int platformId() {
    return this.cmapId().platformId();
  }

  /**
   * Gets the encoding id for this cmap.
   *
   * @return the encoding id
   * @see MacintoshEncodingId
   * @see WindowsEncodingId
   * @see UnicodeEncodingId
   */
  public int encodingId() {
    return this.cmapId().encodingId();
  }

  // TODO(stuartg): simple implementation until all subclasses define their
  // own more efficient version
  protected class CharacterIterator implements Iterator<Integer> {
    private int character = 0;
    private final int maxCharacter;

    CharacterIterator(int start, int end) {
      this.character = start;
      this.maxCharacter = end;
    }

    @Override
    public boolean hasNext() {
      if (character < maxCharacter) {
        return true;
      }
      return false;
    }

    @Override
    public Integer next() {
      if (!hasNext()) {
        throw new NoSuchElementException("No more characters to iterate.");
      }
      return this.character++;
    }

    @Override
    public void remove() {
      throw new UnsupportedOperationException("Unable to remove a character from cmap.");
    }
  }


   @Override
  public int hashCode() {
    return this.cmapId.hashCode();
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (!(obj instanceof CMap)) {
      return false;
    }
    return this.cmapId.equals(((CMap) obj).cmapId);
  }

  /**
   * Gets the language of the cmap.
   *
   *  Note on the language field in 'cmap' subtables: The language field must
   * be set to zero for all cmap subtables whose platform IDs are other than
   * Macintosh (platform ID 1). For cmap subtables whose platform IDs are
   * Macintosh, set this field to the Macintosh language ID of the cmap
   * subtable plus one, or to zero if the cmap subtable is not
   * language-specific. For example, a Mac OS Turkish cmap subtable must set
   * this field to 18, since the Macintosh language ID for Turkish is 17. A
   * Mac OS Roman cmap subtable must set this field to 0, since Mac OS Roman
   * is not a language-specific encoding.
   *
   * @return the language id
   */
  public abstract int language();

  /**
   * Gets the glyph id for the character code provided.
   *
   * The character code provided must be in the encoding used by the cmap table.
   *
   * @param character character value using the encoding of the cmap table
   * @return glyph id for the character code
   */
  public abstract int glyphId(int character);

  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder();
    builder.append("cmap: ");
    builder.append(this.cmapId());
    builder.append(", ");
    builder.append(CMapFormat.valueOf(this.format()));
    builder.append(", Data Size=0x");
    builder.append(Integer.toHexString(this.data.length()));
    return builder.toString();
  }

  public abstract static class Builder<T extends CMap> extends SubTable.Builder<T> {

    private final CMapFormat format;
    private final CMapId cmapId;
    private int language;

    /**
     * Constructor.
     *
     * @param data the data for the cmap
     * @param format cmap format
     * @param cmapId the id for this cmap
     */
    protected Builder(ReadableFontData data, CMapFormat format, CMapId cmapId) {
      super(data);
      this.format = format;
      this.cmapId = cmapId;
    }

    /**
     * @return the id for this cmap
     */
    public CMapId cmapId() {
      return this.cmapId;
    }

    /**
     * Gets the encoding id for the cmap. The encoding will from one of a
     * number of different sets depending on the platform id.
     *
     * @return the encoding id
     * @see MacintoshEncodingId
     * @see WindowsEncodingId
     * @see UnicodeEncodingId
     */
    public int encodingId() {
      return this.cmapId().encodingId();
    }

    /**
     * Gets the platform id for the cmap.
     *
     * @return the platform id
     * @see PlatformId
     */
    public int platformId() {
      return this.cmapId().platformId();
    }

    public CMapFormat format() {
      return this.format;
    }

    public int language() {
      return this.language;
    }

    public void setLanguage(int language) {
      this.language = language;
    }

    /**
     * @param data
     */
    protected Builder(WritableFontData data, CMapFormat format, CMapId cmapId) {
      super(data);
      this.format = format;
      this.cmapId = cmapId;
    }

    @Override
    protected void subDataSet() {
      // NOP
    }

    @Override
    protected int subDataSizeToSerialize() {
      return this.internalReadData().length();
    }

    @Override
    protected boolean subReadyToSerialize() {
      return true;
    }

    @Override
    protected int subSerialize(WritableFontData newData) {
      return this.internalReadData().copyTo(newData);
    }

    static CMap.Builder<? extends CMap> getBuilder(ReadableFontData data, int offset, CMapId cmapId) {
      // read from the front of the cmap - 1st entry is always the format
      int rawFormat = data.readUShort(offset);
      CMapFormat format = CMapFormat.valueOf(rawFormat);

      switch(format) {
        case Format0:
          return new CMapFormat0.Builder(data, offset, cmapId);
        case Format2:
          return new CMapFormat2.Builder(data, offset, cmapId);
        case Format4:
          return new CMapFormat4.Builder(data, offset, cmapId);
        case Format6:
          return new CMapFormat6.Builder(data, offset, cmapId);
        case Format8:
          return new CMapFormat8.Builder(data, offset, cmapId);
        case Format10:
          return new CMapFormat10.Builder(data, offset, cmapId);
        case Format12:
          return new CMapFormat12.Builder(data, offset, cmapId);
        case Format13:
          return new CMapFormat13.Builder(data, offset, cmapId);
        case Format14:
          return new CMapFormat14.Builder(data, offset, cmapId);
        default:
          break;
      }
      return null;
    }

    // TODO: Instead of a root factory method, the individual subtable
    // builders should get created
    // from static factory methods in each subclass
    static CMap.Builder<? extends CMap> getBuilder(CMapFormat cmapFormat, CMapId cmapId) {
      switch(cmapFormat) {
        // TODO: builders for other formats, as they're implemented
        case Format0:
          return new CMapFormat0.Builder(null, 0, cmapId);
        case Format4:
          return new CMapFormat4.Builder(null, 0, cmapId);
        default:
          break;
      }
      return null;
    }

    @Override
    public String toString() {
      return String.format("%s, format = %s", this.cmapId(), this.format());
    }
  }
} public final class CMapTable extends SubTableContainerTable implements Iterable<CMap> {

  /**
   * The .notdef glyph.
   */
  public static final int NOTDEF = 0;

  /**
   * Offsets to specific elements in the underlying data. These offsets are relative to the
   * start of the table or the start of sub-blocks within the table.
   */
  enum Offset {
    version(0),
    numTables(2),
    encodingRecordStart(4),

    // offsets relative to the encoding record
    encodingRecordPlatformId(0),
    encodingRecordEncodingId(2),
    encodingRecordOffset(4),
    encodingRecordSize(8),

    format(0),

    // Format 0: Byte encoding table
    format0Format(0),
    format0Length(2),
    format0Language(4),
    format0GlyphIdArray(6),

    // Format 2: High-byte mapping through table
    format2Format(0),
    format2Length(2),
    format2Language(4),
    format2SubHeaderKeys(6),
    format2SubHeaders(518),
    // offset relative to the subHeader structure
    format2SubHeader_firstCode(0),
    format2SubHeader_entryCount(2),
    format2SubHeader_idDelta(4),
    format2SubHeader_idRangeOffset(6),
    format2SubHeader_structLength(8),

    // Format 4: Segment mapping to delta values
    format4Format(0),
    format4Length(2),
    format4Language(4),
    format4SegCountX2(6),
    format4SearchRange(8),
    format4EntrySelector(10),
    format4RangeShift(12),
    format4EndCount(14),
    format4FixedSize(16),

    // format 6: Trimmed table mapping
    format6Format(0),
    format6Length(2),
    format6Language(4),
    format6FirstCode(6),
    format6EntryCount(8),
    format6GlyphIdArray(10),

    // Format 8: mixed 16-bit and 32-bit coverage
    format8Format(0),
    format8Length(4),
    format8Language(8),
    format8Is32(12),
    format8nGroups(8204),
    format8Groups(8208),
    // ofset relative to the group structure
    format8Group_startCharCode(0),
    format8Group_endCharCode(4),
    format8Group_startGlyphId(8),
    format8Group_structLength(12),

    // Format 10: Trimmed array
    format10Format(0),
    format10Length(4),
    format10Language(8),
    format10StartCharCode(12),
    format10NumChars(16),
    format10Glyphs(20),

    // Format 12: Segmented coverage
    format12Format(0),
    format12Length(4),
    format12Language(8),
    format12nGroups(12),
    format12Groups(16),
    format12Groups_structLength(12),
    // offsets within the group structure
    format12_startCharCode(0),
    format12_endCharCode(4),
    format12_startGlyphId(8),

    // Format 13: Last Resort Font
    format13Format(0),
    format13Length(4),
    format13Language(8),
    format13nGroups(12),
    format13Groups(16),
    format13Groups_structLength(12),
    // offsets within the group structure
    format13_startCharCode(0),
    format13_endCharCode(4),
    format13_glyphId(8),

    // TODO: finish support for format 14
    // Format 14: Unicode Variation Sequences
    format14Format(0),
    format14Length(2);

    final int offset;

    private Offset(int offset) {
      this.offset = offset;
    }
  }

  public static final class CMapId implements Comparable<CMapId> {

    public static final CMapId WINDOWS_BMP =
        CMapId.getInstance(PlatformId.Windows.value(), WindowsEncodingId.UnicodeUCS2.value());
    public static final CMapId WINDOWS_UCS4 =
        CMapId.getInstance(PlatformId.Windows.value(), WindowsEncodingId.UnicodeUCS4.value());
    public static final CMapId MAC_ROMAN =
        CMapId.getInstance(PlatformId.Macintosh.value(), MacintoshEncodingId.Roman.value());

    public static CMapId getInstance(int platformId, int encodingId) {
      return new CMapId(platformId, encodingId);
    }

    private final int platformId;
    private final int encodingId;

    private CMapId(int platformId, int encodingId) {
      this.platformId = platformId;
      this.encodingId = encodingId;
    }

    public int platformId() {
      return this.platformId;
    }

    public int encodingId() {
      return this.encodingId;
    }

    @Override
    public boolean equals(Object obj) {
      if (obj == this) {
        return true;
      }
      if (!(obj instanceof CMapId)) {
        return false;
      }
      CMapId otherKey = (CMapId) obj;
      if ((otherKey.platformId == this.platformId) && (otherKey.encodingId == this.encodingId)) {
        return true;
      }
      return false;
    }

    @Override
    public int hashCode() {
      return this.platformId << 8 | this.encodingId;
    }

    @Override
    public int compareTo(CMapId o) {
      return this.hashCode() - o.hashCode();
    }

    @Override
    public String toString() {
      StringBuilder b = new StringBuilder();
      b.append("pid = ");
      b.append(this.platformId);
      b.append(", eid = ");
      b.append(this.encodingId);
      return b.toString();
    }
  }
  
  /**
   * Constructor.
   *
   * @param header header for the table
   * @param data data for the table
   */
  private CMapTable(Header header, ReadableFontData data) {
    super(header, data);
  }

  /**
   * Get the table version.
   *
   * @return table version
   */
  public int version() {
    return this.data.readUShort(Offset.version.offset);
  }

  /**
   * Gets the number of cmaps within the CMap table.
   *
   * @return the number of cmaps
   */
  public int numCMaps() {
    return this.data.readUShort(Offset.numTables.offset);
  }

  /**
   * Returns the index of the cmap with the given CMapId in the table or -1 if a cmap with the
   * CMapId does not exist in the table.
   *
   * @param id the id of the cmap to get the index for; this value cannot be null
   * @return the index of the cmap in the table or -1 if the cmap with the CMapId does not exist in
   *         the table
   */
  // TODO Modify the iterator to be index-based and used here
  public int getCmapIndex(CMapId id) {
    for (int index = 0; index < numCMaps(); index++) {
      if (id.equals(cmapId(index))) {
        return index;
      }
    }

    return -1;
  }

  /**
   * Gets the offset in the table data for the encoding record for the cmap with
   * the given index. The offset is from the beginning of the table.
   *
   * @param index the index of the cmap
   * @return offset in the table data
   */
  private static int offsetForEncodingRecord(int index) {
    return Offset.encodingRecordStart.offset + index * Offset.encodingRecordSize.offset;
  }

  /**
   * Gets the cmap id for the cmap with the given index.
   *
   * @param index the index of the cmap
   * @return the cmap id
   */
  public CMapId cmapId(int index) {
    return CMapId.getInstance(platformId(index), encodingId(index));
  }

  /**
   * Gets the platform id for the cmap with the given index.
   *
   * @param index the index of the cmap
   * @return the platform id
   */
  public int platformId(int index) {
    return this.data.readUShort(
        Offset.encodingRecordPlatformId.offset + CMapTable.offsetForEncodingRecord(index));
  }

  /**
   * Gets the encoding id for the cmap with the given index.
   *
   * @param index the index of the cmap
   * @return the encoding id
   */
  public int encodingId(int index) {
    return this.data.readUShort(
        Offset.encodingRecordEncodingId.offset + CMapTable.offsetForEncodingRecord(index));
  }

  /**
   * Gets the offset in the table data for the cmap table with the given index.
   * The offset is from the beginning of the table.
   *
   * @param index the index of the cmap
   * @return the offset in the table data
   */
  public int offset(int index) {
    return this.data.readULongAsInt(
        Offset.encodingRecordOffset.offset + CMapTable.offsetForEncodingRecord(index));
  }

  /**
   * Gets an iterator over all of the cmaps within this CMapTable.
   */
  @Override
  public Iterator<CMap> iterator() {
    return new CMapIterator();
  }

  /**
   * Gets an iterator over the cmaps within this CMap table using the provided
   * filter to select the cmaps returned.
   *
   * @param filter the filter
   * @return iterator over cmaps
   */
  public Iterator<CMap> iterator(CMapFilter filter) {
    return new CMapIterator(filter);
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder(super.toString());
    sb.append(" = { ");
    for (int i = 0; i < this.numCMaps(); i++) {
      CMap cmap;
      try {
        cmap = this.cmap(i);
      } catch (IOException e) {
        continue;
      }
      sb.append("[0x");
      sb.append(Integer.toHexString(this.offset(i)));
      sb.append(" = ");
      sb.append(cmap);
      if (i < this.numCMaps() - 1) {
        sb.append("], ");
      } else {
        sb.append("]");
      }
    }
    sb.append(" }");
    return sb.toString();
  }

  /**
   * A filter on cmaps.
   */
  public interface CMapFilter {
    /**
     * Test on whether the cmap is acceptable or not.
     *
     * @param cmapId the id of the cmap
     * @return true if the cmap is acceptable; false otherwise
     */
    boolean accept(CMapId cmapId);
  }

  private class CMapIterator implements Iterator<CMap> {
    private int tableIndex = 0;
    private CMapFilter filter;

    private CMapIterator() {
      // no filter - iterate over all cmap subtables
    }

    private CMapIterator(CMapFilter filter) {
      this.filter = filter;
    }

    @Override
    public boolean hasNext() {
      if (this.filter == null) {
        if (this.tableIndex < numCMaps()) {
          return true;
        }
        return false;
      }
      for (; this.tableIndex < numCMaps(); this.tableIndex++) {
        if (filter.accept(cmapId(this.tableIndex))) {
          return true;
        }
      }
      return false;
    }

    @Override
    public CMap next() {
      if (!hasNext()) {
        throw new NoSuchElementException();
      }
      try {
        return cmap(this.tableIndex++);
      } catch (IOException e) {
        NoSuchElementException newException =
            new NoSuchElementException("Error during the creation of the CMap.");
        newException.initCause(e);
        throw newException;
      }
    }

    @Override
    public void remove() {
      throw new UnsupportedOperationException("Cannot remove a CMap table from an existing font.");
    }
  }

  /**
   * Gets the cmap for the given index.
   *
   * @param index the index of the cmap
   * @return the cmap at the index
   * @throws IOException
   */
  public CMap cmap(int index) throws IOException {
    CMap.Builder<? extends CMap> builder =
        CMapTable.Builder.cmapBuilder(this.readFontData(), index);
    return builder.build();
  }

  /**
   * Gets the cmap with the given ids if it exists.
   *
   * @param platformId the platform id
   * @param encodingId the encoding id
   * @return the cmap if it exists; null otherwise
   */
  public CMap cmap(int platformId, int encodingId) {
    return cmap(CMapId.getInstance(platformId, encodingId));
  }

  public CMap cmap(final CMapId cmapId) {
    Iterator<CMap> cmapIter = this.iterator(new CMapFilter() {
      @Override
      public boolean accept(CMapId foundCMapId) {
        if (cmapId.equals(foundCMapId)) {
          return true;
        }
        return false;
      }
    });
    // can only be one cmap for each set of ids
    if (cmapIter.hasNext()) {
      return cmapIter.next();
    }
    return null;
  }

  /**
   * CMap Table Builder.
   *
   */
  public static class Builder extends SubTableContainerTable.Builder<CMapTable> {

    private int version = 0; // TODO(stuartg): make a CMapTable constant
    private Map<CMapId, CMap.Builder<? extends CMap>> cmapBuilders;

    /**
     * Creates a new builder using the header information and data provided.
     *
     * @param header the header information
     * @param data the data holding the table
     * @return a new builder
     */
    public static Builder createBuilder(Header header, WritableFontData data) {
      return new Builder(header, data);
    }

    /**
     * Constructor.
     *
     * @param header the table header
     * @param data the writable data for the table
     */
    protected Builder(Header header, WritableFontData data) {
      super(header, data);
    }

    /**
     * Constructor. This constructor will try to maintain the data as readable
     * but if editing operations are attempted then a writable copy will be made
     * the readable data will be discarded.
     *
     * @param header the table header
     * @param data the readable data for the table
     */
    protected Builder(Header header, ReadableFontData data) {
      super(header, data);
    }

    /**
     * Static factory method to create a cmap subtable builder.
     *
     * @param data the data for the whole cmap table
     * @param index the index of the cmap subtable within the table
     * @return the cmap subtable requested if it exists; null otherwise
     */
    protected static CMap.Builder<? extends CMap> cmapBuilder(ReadableFontData data, int index) {
      if (index < 0 || index > numCMaps(data)) {
        throw new IndexOutOfBoundsException(
            "CMap table is outside the bounds of the known tables.");
      }

      // read from encoding records
      int platformId = data.readUShort(
          Offset.encodingRecordPlatformId.offset + CMapTable.offsetForEncodingRecord(index));
      int encodingId = data.readUShort(
          Offset.encodingRecordEncodingId.offset + CMapTable.offsetForEncodingRecord(index));
      int offset = data.readULongAsInt(
          Offset.encodingRecordOffset.offset + CMapTable.offsetForEncodingRecord(index));
      CMapId cmapId = CMapId.getInstance(platformId, encodingId);

      CMap.Builder<? extends CMap> builder = CMap.Builder.getBuilder(data, offset, cmapId);
      return builder;
    }

    @Override
    protected void subDataSet() {
      this.cmapBuilders = null;
      super.setModelChanged(false);
    }

    private void initialize(ReadableFontData data) {
      this.cmapBuilders = new /*TreeMap*/ HashMap<CMapId, CMap.Builder<? extends CMap>>();

      int numCMaps = numCMaps(data);
      for (int i = 0; i < numCMaps; i++) {
        CMap.Builder<? extends CMap> cmapBuilder = cmapBuilder(data, i);
        cmapBuilders.put(cmapBuilder.cmapId(), cmapBuilder);
      }
    }

    private Map<CMapId, CMap.Builder<? extends CMap>> getCMapBuilders() {
      if (this.cmapBuilders != null) {
        return this.cmapBuilders;
      }
      this.initialize(this.internalReadData());
      this.setModelChanged();

      return this.cmapBuilders;
    }

    private static int numCMaps(ReadableFontData data) {
      if (data == null) {
        return 0;
      }
      return data.readUShort(Offset.numTables.offset);
    }

    public int numCMaps() {
      return this.getCMapBuilders().size();
    }

    @Override
    protected int subDataSizeToSerialize() {
      if (this.cmapBuilders == null || this.cmapBuilders.size() == 0) {
        return 0;
      }

      boolean variable = false;
      int size = CMapTable.Offset.encodingRecordStart.offset + this.cmapBuilders.size()
      * CMapTable.Offset.encodingRecordSize.offset;

      // calculate size of each table
      for (CMap.Builder<? extends CMap> b : this.cmapBuilders.values()) {
        int cmapSize = b.subDataSizeToSerialize();
        size += Math.abs(cmapSize);
        variable |= cmapSize <= 0;
      }
      return variable ? -size : size;
    }

    @Override
    protected boolean subReadyToSerialize() {
      if (this.cmapBuilders == null) {
        return false;
      }
      // check each table
      for (CMap.Builder<? extends CMap> b : this.cmapBuilders.values()) {
        if (!b.subReadyToSerialize()) {
          return false;
        }
      }
      return true;
    }

    @Override
    protected int subSerialize(WritableFontData newData) {
      int size = newData.writeUShort(CMapTable.Offset.version.offset, this.version());
      size += newData.writeUShort(CMapTable.Offset.numTables.offset, this.cmapBuilders.size());

      int indexOffset = size;
      size += this.cmapBuilders.size() * CMapTable.Offset.encodingRecordSize.offset;
      for (CMap.Builder<? extends CMap> b : this.cmapBuilders.values()) {
        // header entry
        indexOffset += newData.writeUShort(indexOffset, b.platformId());
        indexOffset += newData.writeUShort(indexOffset, b.encodingId());
        indexOffset += newData.writeULong(indexOffset, size);

        // cmap
        size += b.subSerialize(newData.slice(size));
      }
      return size;
    }

    @Override
    protected CMapTable subBuildTable(ReadableFontData data) {
      return new CMapTable(this.header(), data);
    }

    // public building API

    public Iterator<? extends CMap.Builder<? extends CMap>> iterator() {
      return this.getCMapBuilders().values().iterator();
    }

    public int version() {
      return this.version;
    }

    public void setVersion(int version) {
      this.version = version;
    }

    /**
     * Gets a new cmap builder for this cmap table. The new cmap builder will be
     * for the cmap id specified and initialized with the data given. The data
     * will be copied and the original data will not be modified.
     *
     * @param cmapId the id for the new cmap builder
     * @param data the data to copy for the new cmap builder
     * @return a new cmap builder initialized with the cmap id and a copy of the
     *         data
     * @throws IOException
     */
    public CMap.Builder<? extends CMap> newCMapBuilder(CMapId cmapId, ReadableFontData data)
        throws IOException {
      WritableFontData wfd = WritableFontData.createWritableFontData(data.size());
      data.copyTo(wfd);
      CMap.Builder<? extends CMap> builder = CMap.Builder.getBuilder(wfd, 0, cmapId);
      Map<CMapId, CMap.Builder<? extends CMap>> cmapBuilders = this.getCMapBuilders();
      cmapBuilders.put(cmapId, builder);
      return builder;
    }

    public CMap.Builder<? extends CMap> newCMapBuilder(CMapId cmapId, CMapFormat cmapFormat) {
      CMap.Builder<? extends CMap> builder = CMap.Builder.getBuilder(cmapFormat, cmapId);
      Map<CMapId, CMap.Builder<? extends CMap>> cmapBuilders = this.getCMapBuilders();
      cmapBuilders.put(cmapId, builder);
      return builder;
    }

    public CMap.Builder<? extends CMap> cmapBuilder(CMapId cmapId) {
      Map<CMapId, CMap.Builder<? extends CMap>> cmapBuilders = this.getCMapBuilders();
      return cmapBuilders.get(cmapId);
    }

  }
} public static final class CMapId implements Comparable<CMapId> {

  public static final CMapId WINDOWS_BMP =
      CMapId.getInstance(PlatformId.Windows.value(), WindowsEncodingId.UnicodeUCS2.value());
  public static final CMapId WINDOWS_UCS4 =
      CMapId.getInstance(PlatformId.Windows.value(), WindowsEncodingId.UnicodeUCS4.value());
  public static final CMapId MAC_ROMAN =
      CMapId.getInstance(PlatformId.Macintosh.value(), MacintoshEncodingId.Roman.value());

  public static CMapId getInstance(int platformId, int encodingId) {
    return new CMapId(platformId, encodingId);
  }

  private final int platformId;
  private final int encodingId;

  private CMapId(int platformId, int encodingId) {
    this.platformId = platformId;
    this.encodingId = encodingId;
  }

  public int platformId() {
    return this.platformId;
  }

  public int encodingId() {
    return this.encodingId;
  }

  @Override
  public boolean equals(Object obj) {
    if (obj == this) {
      return true;
    }
    if (!(obj instanceof CMapId)) {
      return false;
    }
    CMapId otherKey = (CMapId) obj;
    if ((otherKey.platformId == this.platformId) && (otherKey.encodingId == this.encodingId)) {
      return true;
    }
    return false;
  }

  @Override
  public int hashCode() {
    return this.platformId << 8 | this.encodingId;
  }

  @Override
  public int compareTo(CMapId o) {
    return this.hashCode() - o.hashCode();
  }

  @Override
  public String toString() {
    StringBuilder b = new StringBuilder();
    b.append("pid = ");
    b.append(this.platformId);
    b.append(", eid = ");
    b.append(this.encodingId);
    return b.toString();
  }
} public final class NameTable extends SubTableContainerTable implements Iterable<
    NameTable.NameEntry> {

  /**
   * Offsets to specific elements in the underlying data. These offsets are relative to the
   * start of the table or the start of sub-blocks within the table.
   */
  public enum Offset {
    format(0),
    count(2),
    stringOffset(4),
    nameRecordStart(6),

    // format 1 - offset from the end of the name records
    langTagCount(0),
    langTagRecord(2),

    nameRecordSize(12),
    // Name Records
    nameRecordPlatformId(0),
    nameRecordEncodingId(2),
    nameRecordLanguageId(4),
    nameRecordNameId(6),
    nameRecordStringLength(8),
    nameRecordStringOffset(10);

    private final int offset;

    private Offset(int offset) {
      this.offset = offset;
    }
  }

  public enum NameId {
    Unknown(-1),
    CopyrightNotice(0),
    FontFamilyName(1),
    FontSubfamilyName(2),
    UniqueFontIdentifier(3),
    FullFontName(4),
    VersionString(5),
    PostscriptName(6),
    Trademark(7),
    ManufacturerName(8),
    Designer(9),
    Description(10),
    VendorURL(11),
    DesignerURL(12),
    LicenseDescription(13),
    LicenseInfoURL(14),
    Reserved15(15),
    PreferredFamily(16),
    PreferredSubfamily(17),
    CompatibleFullName(18),
    SampleText(19),
    PostscriptCID(20),
    WWSFamilyName(21),
    WWSSubfamilyName(22);

    private final int value;

    private NameId(int value) {
      this.value = value;
    }

    public int value() {
      return this.value;
    }

    public boolean equals(int value) {
      return value == this.value;
    }

    public static NameId valueOf(int value) {
      for (NameId name : NameId.values()) {
        if (name.equals(value)) {
          return name;
        }
      }
      return Unknown;
    }
  }

  public enum UnicodeLanguageId {
    // Unicode Language IDs (platform ID = 0)
    Unknown(-1), All(0);

    private final int value;

    private UnicodeLanguageId(int value) {
      this.value = value;
    }

    public int value() {
      return this.value;
    }

    public boolean equals(int value) {
      return value == this.value;
    }

    public static UnicodeLanguageId valueOf(int value) {
      for (UnicodeLanguageId language : UnicodeLanguageId.values()) {
        if (language.equals(value)) {
          return language;
        }
      }
      return Unknown;
    }
  }

  /**
   * Macinstosh Language IDs (platform ID = 1)
   *
   */
  public enum MacintoshLanguageId {
    Unknown(-1),
    English(0),
    French(1),
    German(2),
    Italian(3),
    Dutch(4),
    Swedish(5),
    Spanish(6),
    Danish(7),
    Portuguese(8),
    Norwegian(9),
    Hebrew(10),
    Japanese(11),
    Arabic(12),
    Finnish(13),
    Greek(14),
    Icelandic(15),
    Maltese(16),
    Turkish(17),
    Croatian(18),
    Chinese_Traditional(19),
    Urdu(20),
    Hindi(21),
    Thai(22),
    Korean(23),
    Lithuanian(24),
    Polish(25),
    Hungarian(26),
    Estonian(27),
    Latvian(28),
    Sami(29),
    Faroese(30),
    FarsiPersian(31),
    Russian(32),
    Chinese_Simplified(33),
    Flemish(34),
    IrishGaelic(35),
    Albanian(36),
    Romanian(37),
    Czech(38),
    Slovak(39),
    Slovenian(40),
    Yiddish(41),
    Serbian(42),
    Macedonian(43),
    Bulgarian(44),
    Ukrainian(45),
    Byelorussian(46),
    Uzbek(47),
    Kazakh(48),
    Azerbaijani_Cyrillic(49),
    Azerbaijani_Arabic(50),
    Armenian(51),
    Georgian(52),
    Moldavian(53),
    Kirghiz(54),
    Tajiki(55),
    Turkmen(56),
    Mongolian_Mongolian(57),
    Mongolian_Cyrillic(58),
    Pashto(59),
    Kurdish(60),
    Kashmiri(61),
    Sindhi(62),
    Tibetan(63),
    Nepali(64),
    Sanskrit(65),
    Marathi(66),
    Bengali(67),
    Assamese(68),
    Gujarati(69),
    Punjabi(70),
    Oriya(71),
    Malayalam(72),
    Kannada(73),
    Tamil(74),
    Telugu(75),
    Sinhalese(76),
    Burmese(77),
    Khmer(78),
    Lao(79),
    Vietnamese(80),
    Indonesian(81),
    Tagalong(82),
    Malay_Roman(83),
    Malay_Arabic(84),
    Amharic(85),
    Tigrinya(86),
    Galla(87),
    Somali(88),
    Swahili(89),
    KinyarwandaRuanda(90),
    Rundi(91),
    NyanjaChewa(92),
    Malagasy(93),
    Esperanto(94),
    Welsh(128),
    Basque(129),
    Catalan(130),
    Latin(131),
    Quenchua(132),
    Guarani(133),
    Aymara(134),
    Tatar(135),
    Uighur(136),
    Dzongkha(137),
    Javanese_Roman(138),
    Sundanese_Roman(139),
    Galician(140),
    Afrikaans(141),
    Breton(142),
    Inuktitut(143),
    ScottishGaelic(144),
    ManxGaelic(145),
    IrishGaelic_WithDotAbove(146),
    Tongan(147),
    Greek_Polytonic(148),
    Greenlandic(149),
    Azerbaijani_Roman(150);

    private final int value;

    private MacintoshLanguageId(int value) {
      this.value = value;
    }

    public int value() {
      return this.value;
    }

    public boolean equals(int value) {
      return value == this.value;
    }

    public static MacintoshLanguageId valueOf(int value) {
      for (MacintoshLanguageId language : MacintoshLanguageId.values()) {
        if (language.equals(value)) {
          return language;
        }
      }
      return Unknown;
    }
  }

  /**
   * Windows Language IDs (platform ID = 3)
   */
  public enum WindowsLanguageId {
    Unknown(-1),
    Afrikaans_SouthAfrica(0x0436),
    Albanian_Albania(0x041C),
    Alsatian_France(0x0484),
    Amharic_Ethiopia(0x045E),
    Arabic_Algeria(0x1401),
    Arabic_Bahrain(0x3C01),
    Arabic_Egypt(0x0C01),
    Arabic_Iraq(0x0801),
    Arabic_Jordan(0x2C01),
    Arabic_Kuwait(0x3401),
    Arabic_Lebanon(0x3001),
    Arabic_Libya(0x1001),
    Arabic_Morocco(0x1801),
    Arabic_Oman(0x2001),
    Arabic_Qatar(0x4001),
    Arabic_SaudiArabia(0x0401),
    Arabic_Syria(0x2801),
    Arabic_Tunisia(0x1C01),
    Arabic_UAE(0x3801),
    Arabic_Yemen(0x2401),
    Armenian_Armenia(0x042B),
    Assamese_India(0x044D),
    Azeri_Cyrillic_Azerbaijan(0x082C),
    Azeri_Latin_Azerbaijan(0x042C),
    Bashkir_Russia(0x046D),
    Basque_Basque(0x042D),
    Belarusian_Belarus(0x0423),
    Bengali_Bangladesh(0x0845),
    Bengali_India(0x0445),
    Bosnian_Cyrillic_BosniaAndHerzegovina(0x201A),
    Bosnian_Latin_BosniaAndHerzegovina(0x141A),
    Breton_France(0x047E),
    Bulgarian_Bulgaria(0x0402),
    Catalan_Catalan(0x0403),
    Chinese_HongKongSAR(0x0C04),
    Chinese_MacaoSAR(0x1404),
    Chinese_PeoplesRepublicOfChina(0x0804),
    Chinese_Singapore(0x1004),
    Chinese_Taiwan(0x0404),
    Corsican_France(0x0483),
    Croatian_Croatia(0x041A),
    Croatian_Latin_BosniaAndHerzegovina(0x101A),
    Czech_CzechRepublic(0x0405),
    Danish_Denmark(0x0406),
    Dari_Afghanistan(0x048C),
    Divehi_Maldives(0x0465),
    Dutch_Belgium(0x0813),
    Dutch_Netherlands(0x0413),
    English_Australia(0x0C09),
    English_Belize(0x2809),
    English_Canada(0x1009),
    English_Caribbean(0x2409),
    English_India(0x4009),
    English_Ireland(0x1809),
    English_Jamaica(0x2009),
    English_Malaysia(0x4409),
    English_NewZealand(0x1409),
    English_RepublicOfThePhilippines(0x3409),
    English_Singapore(0x4809),
    English_SouthAfrica(0x1C09),
    English_TrinidadAndTobago(0x2C09),
    English_UnitedKingdom(0x0809),
    English_UnitedStates(0x0409),
    English_Zimbabwe(0x3009),
    Estonian_Estonia(0x0425),
    Faroese_FaroeIslands(0x0438),
    Filipino_Philippines(0x0464),
    Finnish_Finland(0x040B),
    French_Belgium(0x080C),
    French_Canada(0x0C0C),
    French_France(0x040C),
    French_Luxembourg(0x140c),
    French_PrincipalityOfMonoco(0x180C),
    French_Switzerland(0x100C),
    Frisian_Netherlands(0x0462),
    Galician_Galician(0x0456),
    Georgian_Georgia(0x0437),
    German_Austria(0x0C07),
    German_Germany(0x0407),
    German_Liechtenstein(0x1407),
    German_Luxembourg(0x1007),
    German_Switzerland(0x0807),
    Greek_Greece(0x0408),
    Greenlandic_Greenland(0x046F),
    Gujarati_India(0x0447),
    Hausa_Latin_Nigeria(0x0468),
    Hebrew_Israel(0x040D),
    Hindi_India(0x0439),
    Hungarian_Hungary(0x040E),
    Icelandic_Iceland(0x040F),
    Igbo_Nigeria(0x0470),
    Indonesian_Indonesia(0x0421),
    Inuktitut_Canada(0x045D),
    Inuktitut_Latin_Canada(0x085D),
    Irish_Ireland(0x083C),
    isiXhosa_SouthAfrica(0x0434),
    isiZulu_SouthAfrica(0x0435),
    Italian_Italy(0x0410),
    Italian_Switzerland(0x0810),
    Japanese_Japan(0x0411),
    Kannada_India(0x044B),
    Kazakh_Kazakhstan(0x043F),
    Khmer_Cambodia(0x0453),
    Kiche_Guatemala(0x0486),
    Kinyarwanda_Rwanda(0x0487),
    Kiswahili_Kenya(0x0441),
    Konkani_India(0x0457),
    Korean_Korea(0x0412),
    Kyrgyz_Kyrgyzstan(0x0440),
    Lao_LaoPDR(0x0454),
    Latvian_Latvia(0x0426),
    Lithuanian_Lithuania(0x0427),
    LowerSorbian_Germany(0x082E),
    Luxembourgish_Luxembourg(0x046E),
    Macedonian_FYROM_FormerYugoslavRepublicOfMacedonia(0x042F),
    Malay_BruneiDarussalam(0x083E),
    Malay_Malaysia(0x043E),
    Malayalam_India(0x044C),
    Maltese_Malta(0x043A),
    Maori_NewZealand(0x0481),
    Mapudungun_Chile(0x047A),
    Marathi_India(0x044E),
    Mohawk_Mohawk(0x047C),
    Mongolian_Cyrillic_Mongolia(0x0450),
    Mongolian_Traditional_PeoplesRepublicOfChina(0x0850),
    Nepali_Nepal(0x0461),
    Norwegian_Bokmal_Norway(0x0414),
    Norwegian_Nynorsk_Norway(0x0814),
    Occitan_France(0x0482),
    Oriya_India(0x0448),
    Pashto_Afghanistan(0x0463),
    Polish_Poland(0x0415),
    Portuguese_Brazil(0x0416),
    Portuguese_Portugal(0x0816),
    Punjabi_India(0x0446),
    Quechua_Bolivia(0x046B),
    Quechua_Ecuador(0x086B),
    Quechua_Peru(0x0C6B),
    Romanian_Romania(0x0418),
    Romansh_Switzerland(0x0417),
    Russian_Russia(0x0419),
    Sami_Inari_Finland(0x243B),
    Sami_Lule_Norway(0x103B),
    Sami_Lule_Sweden(0x143B),
    Sami_Northern_Finland(0x0C3B),
    Sami_Northern_Norway(0x043B),
    Sami_Northern_Sweden(0x083B),
    Sami_Skolt_Finland(0x203B),
    Sami_Southern_Norway(0x183B),
    Sami_Southern_Sweden(0x1C3B),
    Sanskrit_India(0x044F),
    Serbian_Cyrillic_BosniaAndHerzegovina(0x1C1A),
    Serbian_Cyrillic_Serbia(0x0C1A),
    Serbian_Latin_BosniaAndHerzegovina(0x181A),
    Serbian_Latin_Serbia(0x081A),
    SesothoSaLeboa_SouthAfrica(0x046C),
    Setswana_SouthAfrica(0x0432),
    Sinhala_SriLanka(0x045B),
    Slovak_Slovakia(0x041B),
    Slovenian_Slovenia(0x0424),
    Spanish_Argentina(0x2C0A),
    Spanish_Bolivia(0x400A),
    Spanish_Chile(0x340A),
    Spanish_Colombia(0x240A),
    Spanish_CostaRica(0x140A),
    Spanish_DominicanRepublic(0x1C0A),
    Spanish_Ecuador(0x300A),
    Spanish_ElSalvador(0x440A),
    Spanish_Guatemala(0x100A),
    Spanish_Honduras(0x480A),
    Spanish_Mexico(0x080A),
    Spanish_Nicaragua(0x4C0A),
    Spanish_Panama(0x180A),
    Spanish_Paraguay(0x3C0A),
    Spanish_Peru(0x280A),
    Spanish_PuertoRico(0x500A),
    Spanish_ModernSort_Spain(0x0C0A),
    Spanish_TraditionalSort_Spain(0x040A),
    Spanish_UnitedStates(0x540A),
    Spanish_Uruguay(0x380A),
    Spanish_Venezuela(0x200A),
    Sweden_Finland(0x081D),
    Swedish_Sweden(0x041D),
    Syriac_Syria(0x045A),
    Tajik_Cyrillic_Tajikistan(0x0428),
    Tamazight_Latin_Algeria(0x085F),
    Tamil_India(0x0449),
    Tatar_Russia(0x0444),
    Telugu_India(0x044A),
    Thai_Thailand(0x041E),
    Tibetan_PRC(0x0451),
    Turkish_Turkey(0x041F),
    Turkmen_Turkmenistan(0x0442),
    Uighur_PRC(0x0480),
    Ukrainian_Ukraine(0x0422),
    UpperSorbian_Germany(0x042E),
    Urdu_IslamicRepublicOfPakistan(0x0420),
    Uzbek_Cyrillic_Uzbekistan(0x0843),
    Uzbek_Latin_Uzbekistan(0x0443),
    Vietnamese_Vietnam(0x042A),
    Welsh_UnitedKingdom(0x0452),
    Wolof_Senegal(0x0448),
    Yakut_Russia(0x0485),
    Yi_PRC(0x0478),
    Yoruba_Nigeria(0x046A);

    private final int value;

    private WindowsLanguageId(int value) {
      this.value = value;
    }

    public int value() {
      return this.value;
    }

    public boolean equals(int value) {
      return value == this.value;
    }

    public static WindowsLanguageId valueOf(int value) {
      for (WindowsLanguageId language : WindowsLanguageId.values()) {
        if (language.equals(value)) {
          return language;
        }
      }
      return Unknown;
    }
  }

  private NameTable(Header header, ReadableFontData data) {
    super(header, data);
  }

  public int format() {
    return this.data.readUShort(Offset.format.offset);
  }

  /**
   * Get the number of names in the name table.
   * @return the number of names
   */
  public int nameCount() {
    return this.data.readUShort(Offset.count.offset);
  }

  /**
   * Get the offset to the string data in the name table.
   * @return the string offset
   */
  private int stringOffset() {
    return this.data.readUShort(Offset.stringOffset.offset);
  }

  /**
   * Get the offset for the given name record.
   * @param index the index of the name record
   * @return the offset of the name record
   */
  private int offsetForNameRecord(int index) {
    return Offset.nameRecordStart.offset + index * Offset.nameRecordSize.offset;
  }

  /**
   * Get the platform id for the given name record.
   *
   * @param index the index of the name record
   * @return the platform id
   * @see PlatformId
   */
  public int platformId(int index) {
    return this.data.readUShort(
        Offset.nameRecordPlatformId.offset + this.offsetForNameRecord(index));
  }

  /**
   * Get the encoding id for the given name record.
   *
   * @param index the index of the name record
   * @return the encoding id
   *
   * @see MacintoshEncodingId
   * @see WindowsEncodingId
   * @see UnicodeEncodingId
   */
  public int encodingId(int index) {
    return this.data.readUShort(
        Offset.nameRecordEncodingId.offset + this.offsetForNameRecord(index));
  }

  /**
   * Get the language id for the given name record.
   * @param index the index of the name record
   * @return the language id
   */
  public int languageId(int index) {
    return this.data.readUShort(
        Offset.nameRecordLanguageId.offset + this.offsetForNameRecord(index));
  }

  /**
   * Get the name id for given name record.
   * @param index the index of the name record
   * @return the name id
   */
  public int nameId(int index) {
    return this.data.readUShort(
        Offset.nameRecordNameId.offset + this.offsetForNameRecord(index));
  }

  /**
   * Get the length of the string data for the given name record.
   * @param index the index of the name record
   * @return the length of the string data in bytes
   */
  private int nameLength(int index) {
    return this.data.readUShort(
        Offset.nameRecordStringLength.offset + this.offsetForNameRecord(index));
  }

  /**
   * Get the offset of the string data for the given name record.
   * @param index the index of the name record
   * @return the offset of the string data from the start of the table
   */
  private int nameOffset(int index) {
    return this.data.readUShort(
        Offset.nameRecordStringOffset.offset +
        this.offsetForNameRecord(index)) + this.stringOffset();
  }

  /**
   * Get the name as bytes for the given name record.
   * @param index the index of the name record
   * @return the bytes for the name
   */
  public byte[] nameAsBytes(int index) {
    int length = this.nameLength(index);
    byte[] b = new byte[length];
    this.data.readBytes(this.nameOffset(index), b, 0, length);
    return b;
  }

  /**
   * Get the name as bytes for the specified name. If there is no entry for the requested name
   * then <code>null</code> is returned.
   * @param platformId the platform id
   * @param encodingId the encoding id
   * @param languageId the language id
   * @param nameId the name id
   * @return the bytes for the name
   */
  public byte[] nameAsBytes(int platformId, int encodingId, int languageId, int nameId) {
    NameEntry entry = this.nameEntry(platformId, encodingId, languageId, nameId);
    if (entry != null) {
      return entry.nameAsBytes();
    }
    return null;
  }

  /**
   * Get the name as a String for the given name record. If there is no encoding conversion
   * available for the name record then a best attempt String will be returned.
   * @param index the index of the name record
   * @return the name
   */
  public String name(int index) {
    return convertFromNameBytes(
        this.nameAsBytes(index), this.platformId(index), this.encodingId(index));
  }

  /**
   * Get the name as a String for the specified name. If there is no entry for the requested name
   * then <code>null</code> is returned. If there is no encoding conversion
   * available for the name then a best attempt String will be returned.
   * @param platformId the platform id
   * @param encodingId the encoding id
   * @param languageId the language id
   * @param nameId the name id
   * @return the name
   */
  public String name(int platformId, int encodingId, int languageId, int nameId) {
    NameEntry entry = this.nameEntry(platformId, encodingId, languageId, nameId);
    if (entry != null) {
      return entry.name();
    }
    return null;
  }

  /**
   * Get the name entry record for the given name entry.
   * @param index the index of the name record
   * @return the name entry
   */
  public NameEntry nameEntry(int index) {
    return new NameEntry(
        this.platformId(index), this.encodingId(index), this.languageId(index),
        this.nameId(index), this.nameAsBytes(index));
  }

  /**
   * Get the name entry record for the specified name. If there is no entry for the requested name
   * then <code>null</code> is returned.
   * @param platformId the platform id
   * @param encodingId the encoding id
   * @param languageId the language id
   * @param nameId the name id
   * @return the name entry
   */
  public NameEntry nameEntry(
      final int platformId, final int encodingId, final int languageId, final int nameId) {
    Iterator<NameEntry> nameEntryIter = this.iterator(new NameEntryFilter() {
      @Override
      public boolean accept(int pid, int eid, int lid, int nid) {
        if (pid == platformId && eid == encodingId && lid == languageId && nid == nameId) {
          return true;
        }
        return false;
      }
    });
    // can only be one name for each set of ids
    if (nameEntryIter.hasNext()) {
      return nameEntryIter.next();
    }
    return null;
  }

  /**
   * Get all the name entry records.
   * @return the set of all name entry records
   */
  public Set<NameEntry> names() {
    Set<NameEntry> nameSet = new HashSet<NameEntry>(this.nameCount());
    for (NameEntry entry : this) {
      nameSet.add(entry);
    }
    return nameSet;
  }

  private static class NameEntryId implements Comparable<NameEntryId> {
    /* @see Font.PlatformId
     */
    protected int platformId;
    /* @see Font.UnicodeEncodingId
     * @see Font.MacintoshEncodingId
     * @see Font.WindowsEncodingId
     */
    protected int encodingId;
    /* @see NameTable.UnicodeLanguageId
     * @see NameTable.MacintoshLanguageId
     * @see NameTable.WindowsLanguageId
     */
    protected int languageId;
    /* @see NameTable.NameId
     */
    protected int nameId;

    /**
     * @param platformId
     * @param encodingId
     * @param languageId
     * @param nameId
     */
    protected NameEntryId(int platformId, int encodingId, int languageId, int nameId) {
      this.platformId = platformId;
      this.encodingId = encodingId;
      this.languageId = languageId;
      this.nameId = nameId;
    }

    /**
     * Get the platform id.
     *
     * @return the platform id
     */
    protected int getPlatformId() {
      return this.platformId;
    }

    /**
     * Get the encoding id.
     *
     * @return the encoding id
     */
    protected int getEncodingId() {
      return this.encodingId;
    }

    /**
     * Get the language id.
     *
     * @return the language id
     */
    protected int getLanguageId() {
      return this.languageId;
    }

    /**
     * Get the name id.
     *
     * @return the name id
     */
    protected int getNameId() {
      return this.nameId;
    }

    @Override
    public boolean equals(Object obj) {
      if (!(obj instanceof NameEntryId)) {
        return false;
      }
      NameEntryId other = (NameEntryId) obj;
      return (this.encodingId == other.encodingId) && (this.languageId == other.languageId)
          && (this.platformId == other.platformId) && (this.nameId == other.nameId);
    }

    @Override
    public int hashCode() {
      /*
       * - this takes advantage of the sizes of the various entries and the fact
       * that the ranges of their values have an almost zero probability of ever
       * changing - this allows us to generate a unique hash at low cost - if
       * the ranges do change then we will potentially generate non-unique hash
       * values which is a common result
       */
      return ((this.encodingId & 0x3f) << 26) | ((this.nameId & 0x3f) << 16)
          | ((this.platformId & 0x0f) << 12) | (this.languageId & 0xff);
    }

    /**
     * Name entries are sorted by platform id, encoding id, language id, and
     * name id in order of decreasing importance.
     *
     * @return less than zero if this entry is less than the other; greater than
     *         zero if this entry is greater than the other; and zero if they
     *         are equal
     *
     * @see java.lang.Comparable#compareTo(java.lang.Object)
     */
    @Override
    public int compareTo(NameEntryId o) {
      if (this.platformId != o.platformId) {
        return this.platformId - o.platformId;
      }
      if (this.encodingId != o.encodingId) {
        return this.encodingId - o.encodingId;
      }
      if (this.languageId != o.languageId) {
        return this.languageId - o.languageId;
      }
      return this.nameId - o.nameId;
    }

    @Override
    public String toString() {
      StringBuilder sb = new StringBuilder();
      sb.append("P=");
      sb.append(PlatformId.valueOf(this.platformId));
      sb.append(", E=0x");
      sb.append(Integer.toHexString(this.encodingId));
      sb.append(", L=0x");
      sb.append(Integer.toHexString(this.languageId));
      sb.append(", N=");
      NameId nameId = NameId.valueOf(this.nameId);
      if (nameId != null) {
        sb.append(NameId.valueOf(this.nameId));
      } else {
        sb.append("0x");
        sb.append(Integer.toHexString(this.nameId));
      }
      return sb.toString();
    }
  }

  /**
   * Class to represent a name entry in the name table.
   *
   */
  public static class NameEntry {
    NameEntryId nameEntryId;
    protected int length;
    protected byte[] nameBytes;

    protected NameEntry() {
    }

    protected NameEntry(NameEntryId nameEntryId, byte[] nameBytes) {
      this.nameEntryId = nameEntryId;
      this.nameBytes = nameBytes;
    }

    protected NameEntry(
        int platformId, int encodingId, int languageId, int nameId, byte[] nameBytes) {
      this(new NameEntryId(platformId, encodingId, languageId, nameId), nameBytes);
    }

    protected NameEntryId getNameEntryId() {
      return this.nameEntryId;
    }

    /**
     * Get the platform id.
     * @return the platform id
     */
    public int platformId() {
      return this.nameEntryId.getPlatformId();
    }

    /**
     * Get the encoding id.
     * @return the encoding id
     */
    public int encodingId() {
      return this.nameEntryId.getEncodingId();
    }

    /**
     * Get the language id.
     * @return the language id
     */
    public int languageId() {
      return this.nameEntryId.getLanguageId();
    }

    /**
     * Get the name id.
     * @return the name id
     */
    public int nameId() {
      return this.nameEntryId.getNameId();
    }

    /**
     * Get the bytes for name.
     * @return the name bytes
     */
    public byte[] nameAsBytes() {
      return this.nameBytes;
    }

    /**
     * Get the name as a String. If there is no encoding conversion
     * available for the name bytes then a best attempt String will be returned.
     * @return the name
     */
    public String name() {
      return NameTable.convertFromNameBytes(this.nameBytes, this.platformId(), this.encodingId());
    }

    @Override
    public String toString() {
      StringBuilder sb = new StringBuilder();
      sb.append("[");
      sb.append(this.nameEntryId);
      sb.append(", \"");
      String name = this.name();
      sb.append(this.name());
      sb.append("\"]");
      return sb.toString();
    }

    @Override
    public boolean equals(Object obj) {
      if (!(obj instanceof NameEntry)) {
        return false;
      }
      NameEntry other = (NameEntry) obj;
      if (!this.nameEntryId.equals(other.nameEntryId)) {
        return false;
      }
      if (this.nameBytes.length != other.nameBytes.length) {
        return false;
      }
      for (int i = 0; i < this.nameBytes.length; i++) {
        if (this.nameBytes[i] != other.nameBytes[i]) {
          return false;
        }
      }
      return true;
    }

    @Override
    public int hashCode() {
      int hash = this.nameEntryId.hashCode();
      for (int i = 0; i < this.nameBytes.length; i+=4) {
        for (int j = 0; j < 4 && j + i < this.nameBytes.length; j++) {
          hash |= this.nameBytes[j] << j * 8;
        }
      }
      return hash;
    }
  }

  public static class NameEntryBuilder extends NameEntry {

    /**
     * Constructor.
     */
    protected NameEntryBuilder() {
      super();
    }

    protected NameEntryBuilder(NameEntryId nameEntryId, byte[] nameBytes) {
      super(nameEntryId, nameBytes);
    }

    protected NameEntryBuilder(NameEntryId nameEntryId) {
      this(nameEntryId, null);
    }

    protected NameEntryBuilder(NameEntry nameEntry) {
      this(nameEntry.getNameEntryId(), nameEntry.nameAsBytes());
    }

    public void setName(String name) {
      if (name == null) {
        this.nameBytes = new byte[0];
        return;
      }
      this.nameBytes = NameTable.convertToNameBytes(
          name, this.nameEntryId.getPlatformId(), this.nameEntryId.getEncodingId());
    }

    public void setName(byte[] nameBytes) {
      this.nameBytes = Arrays.copyOf(nameBytes, nameBytes.length);
    }

    public void setName(byte[] nameBytes, int offset, int length) {
      this.nameBytes = Arrays.copyOfRange(nameBytes, offset, offset + length);
    }
  }

  /**
   * An interface for a filter to use with the name entry iterator. This allows
   * name entries to be iterated and only those acceptable to the filter will be returned.
   */
  public interface NameEntryFilter {
    /**
     * Callback to determine if a name entry is acceptable.
     * @param platformId platform id
     * @param encodingId encoding id
     * @param languageId language id
     * @param nameId name id
     * @return true if the name entry is acceptable; false otherwise
     */
    boolean accept(int platformId, int encodingId, int languageId, int nameId);
  }

  protected class NameEntryIterator implements Iterator<NameEntry> {
    private int nameIndex = 0;
    private NameEntryFilter filter = null;

    private NameEntryIterator() {
      // no filter - iterate all name entries
    }

    private NameEntryIterator(NameEntryFilter filter) {
      this.filter = filter;
    }

    @Override
    public boolean hasNext() {
      if (this.filter == null) {
        if (this.nameIndex < nameCount()) {
          return true;
        }
        return false;
      }
      for (; this.nameIndex < nameCount(); this.nameIndex++) {
        if (filter.accept(
            platformId(this.nameIndex), encodingId(this.nameIndex),
            languageId(this.nameIndex), nameId(this.nameIndex))) {
          return true;
        }
      }
      return false;
    }

    @Override
    public NameEntry next() {
      if (!hasNext()) {
        throw new NoSuchElementException();
      }
      return nameEntry(this.nameIndex++);
    }

    @Override
    public void remove() {
      throw new UnsupportedOperationException("Cannot remove a CMap table from an existing font.");
    }
  }

  @Override
  public Iterator<NameEntry> iterator() {
    return new NameEntryIterator();
  }

  /**
   * Get an iterator over name entries in the name table.
   * @param filter a filter to select acceptable name entries
   * @return an iterator over name entries
   */
  public Iterator<NameEntry> iterator(NameEntryFilter filter) {
    return new NameEntryIterator(filter);
  }

  // TODO(stuartg): do this in the encoding enums
  private static String getEncodingName(int platformId, int encodingId) {
    String encodingName = null;
    switch (PlatformId.valueOf(platformId)) {
      case Unicode:
        encodingName = "UTF-16BE";
        break;
      case Macintosh:
        switch (MacintoshEncodingId.valueOf(encodingId)) {
          case Roman:
            encodingName = "MacRoman";
            break;
          case Japanese:
            encodingName = "Shift_JIS";
            break;
          case ChineseTraditional:
            encodingName = "Big5";
            break;
          case Korean:
            encodingName = "EUC-KR";
            break;
          case Arabic:
            encodingName = "MacArabic";
            break;
          case Hebrew:
            encodingName = "MacHebrew";
            break;
          case Greek:
            encodingName = "MacGreek";
            break;
          case Russian:
            encodingName = "MacCyrillic";
            break;
          case RSymbol:
            encodingName = "MacSymbol";
            break;
          case Devanagari:
            break;
          case Gurmukhi:
            break;
          case Gujarati:
            break;
          case Oriya:
            break;
          case Bengali:
            break;
          case Tamil:
            break;
          case Telugu:
            break;
          case Kannada:
            break;
          case Malayalam:
            break;
          case Sinhalese:
            break;
          case Burmese:
            break;
          case Khmer:
            break;
          case Thai:
            encodingName = "MacThai";
            break;
          case Laotian:
            break;
          case Georgian:
            // TODO: ??? is it?
            encodingName = "MacCyrillic";
            break;
          case Armenian:
            break;
          case ChineseSimplified:
            encodingName = "EUC-CN";
            break;
          case Tibetan:
            break;
          case Mongolian:
            // TODO: ??? is it?
            encodingName = "MacCyrillic";
            break;
          case Geez:
            break;
          case Slavic:
            // TODO: ??? is it?
            encodingName = "MacCyrillic";
            break;
          case Vietnamese:
            break;
          case Sindhi:
            break;
          case Uninterpreted:
            break;
        }
        break;
      case ISO:
        break;
      case Windows:
        switch (WindowsEncodingId.valueOf(encodingId)) {
          case Symbol:
            encodingName = "UTF-16BE";
            break;
          case UnicodeUCS2:
            encodingName = "UTF-16BE";
            break;
          case ShiftJIS:
            encodingName = "windows-933";
            break;
          case PRC:
            encodingName = "windows-936";
            break;
          case Big5:
            encodingName = "windows-950";
            break;
          case Wansung:
            encodingName = "windows-949";
            break;
          case Johab:
            encodingName = "ms1361";
            break;
          case UnicodeUCS4:
            encodingName = "UCS-4";
            break;
        }
        break;
      case Custom:
        break;
      default:
        break;
    }
    return encodingName;
  }

  // TODO: caching of charsets?
  private static Charset getCharset(int platformId, int encodingId) {
    String encodingName = NameTable.getEncodingName(platformId, encodingId);
    if (encodingName == null) {
      return null;
    }
    Charset charset = null;
    try {
      //FIXME : where is CharsetICU ????
      //charset = CharsetICU.forNameICU(encodingName);
      charset = Charset.forName(encodingName);
    } catch (UnsupportedCharsetException e) {
      return null;
    }
    return charset;
  }

  // TODO(stuartg):
  // do the conversion by hand to detect conversion failures (i.e. no character in the encoding)
  private static byte[] convertToNameBytes(String name, int platformId, int encodingId) {
    Charset cs = NameTable.getCharset(platformId, encodingId);
    if (cs == null) {
      return null;
    }
    ByteBuffer bb = cs.encode(name);
    return bb.array();
  }

  private static String convertFromNameBytes(byte[] nameBytes, int platformId, int encodingId) {
    return NameTable.convertFromNameBytes(ByteBuffer.wrap(nameBytes), platformId, encodingId);
  }

  private static String convertFromNameBytes(ByteBuffer nameBytes, int platformId, int encodingId) {
    Charset cs = NameTable.getCharset(platformId, encodingId);
    if (cs == null) {
      return Integer.toHexString(platformId);
    }
    CharBuffer cb = cs.decode(nameBytes);
    return cb.toString();
  }

  public static class Builder extends SubTableContainerTable.Builder<NameTable> {

    private Map<NameEntryId, NameEntryBuilder> nameEntryMap;

    /**
     * Create a new builder using the header information and data provided.
     *
     * @param header the header information
     * @param data the data holding the table
     * @return a new builder
     */
    public static Builder createBuilder(Header header, WritableFontData data) {
      return new Builder(header, data);
    }

    protected Builder(Header header, WritableFontData data) {
      super(header, data);
    }

    protected Builder(Header header, ReadableFontData data) {
      super(header, data);
    }

    private void initialize(ReadableFontData data) {
      this.nameEntryMap = new TreeMap<NameEntryId, NameEntryBuilder>();

      if (data != null) {
        NameTable table = new NameTable(this.header(), data);

        Iterator<NameEntry> nameIter = table.iterator();
        while (nameIter.hasNext()) {
          NameEntry nameEntry = nameIter.next();
          NameEntryBuilder nameEntryBuilder = new NameEntryBuilder(nameEntry);
          this.nameEntryMap.put(nameEntryBuilder.getNameEntryId(), nameEntryBuilder);
        }
      }
    }

    private Map<NameEntryId, NameEntryBuilder> getNameBuilders() {
      if (this.nameEntryMap == null) {
        this.initialize(super.internalReadData());
      }
      super.setModelChanged();
      return this.nameEntryMap;
    }

    /**
     * Revert the name builders for the name table to the last version that came
     * from data.
     */
    public void revertNames() {
      this.nameEntryMap = null;
      this.setModelChanged(false);
    }

    public int builderCount() {
      return this.getNameBuilders().size();
    }

    /**
     * Clear the name builders for the name table.
     */
    public void clear() {
      this.getNameBuilders().clear();
    }

    public boolean has(int platformId, int encodingId, int languageId, int nameId) {
      NameEntryId probe = new NameEntryId(platformId, encodingId, languageId, nameId);
      return this.getNameBuilders().containsKey(probe);
    }

    public NameEntryBuilder nameBuilder(
        int platformId, int encodingId, int languageId, int nameId) {
      NameEntryId probe = new NameEntryId(platformId, encodingId, languageId, nameId);
      NameEntryBuilder builder = this.getNameBuilders().get(probe);
      if (builder == null) {
        builder = new NameEntryBuilder(probe);
        this.getNameBuilders().put(probe, builder);
      }
      return builder;
    }

    public boolean remove(int platformId, int encodingId, int languageId, int nameId) {
      NameEntryId probe = new NameEntryId(platformId, encodingId, languageId, nameId);
      return (this.getNameBuilders().remove(probe) != null);
    }

    // subclass API implementation

    @Override
    protected NameTable subBuildTable(ReadableFontData data) {
      return new NameTable(this.header(), data);
    }

    @Override
    protected void subDataSet() {
      this.nameEntryMap = null;
      super.setModelChanged(false);
    }

    @Override
    protected int subDataSizeToSerialize() {
      if (this.nameEntryMap == null || this.nameEntryMap.size() == 0) {
        return 0;
      }

      int size = NameTable.Offset.nameRecordStart.offset + this.nameEntryMap.size()
          * NameTable.Offset.nameRecordSize.offset;
      for (Map.Entry<NameEntryId, NameEntryBuilder> entry : this.nameEntryMap.entrySet()) {
        size += entry.getValue().nameAsBytes().length;
      }
      return size;
    }

    @Override
    protected boolean subReadyToSerialize() {
      if (this.nameEntryMap == null || this.nameEntryMap.size() == 0) {
        return false;
      }
      return true;
    }

    @Override
    protected int subSerialize(WritableFontData newData) {
      int stringTableStartOffset =
          NameTable.Offset.nameRecordStart.offset + this.nameEntryMap.size()
              * NameTable.Offset.nameRecordSize.offset;

      // header
      newData.writeUShort(NameTable.Offset.format.offset, 0);
      newData.writeUShort(NameTable.Offset.count.offset, this.nameEntryMap.size());
      newData.writeUShort(NameTable.Offset.stringOffset.offset, stringTableStartOffset);
      int nameRecordOffset = NameTable.Offset.nameRecordStart.offset;
      int stringOffset = 0;
      for (Map.Entry<NameEntryId, NameEntryBuilder> entry : this.nameEntryMap.entrySet()) {
        // lookup table
        newData.writeUShort(nameRecordOffset + NameTable.Offset.nameRecordPlatformId.offset,
            entry.getKey().getPlatformId());
        newData.writeUShort(nameRecordOffset + NameTable.Offset.nameRecordEncodingId.offset,
            entry.getKey().getEncodingId());
        newData.writeUShort(nameRecordOffset + NameTable.Offset.nameRecordLanguageId.offset,
            entry.getKey().getLanguageId());
        newData.writeUShort(nameRecordOffset + NameTable.Offset.nameRecordNameId.offset,
            entry.getKey().getNameId());
        newData.writeUShort(nameRecordOffset + NameTable.Offset.nameRecordStringLength.offset,
            entry.getValue().nameAsBytes().length);
        newData.writeUShort(
            nameRecordOffset + NameTable.Offset.nameRecordStringOffset.offset, stringOffset);
        nameRecordOffset += NameTable.Offset.nameRecordSize.offset;
        // string table
        byte[] nameBytes = entry.getValue().nameAsBytes();
        if (nameBytes.length > 0) {
          stringOffset += newData.writeBytes(
              stringOffset + stringTableStartOffset, entry.getValue().nameAsBytes());
        }
      }
      return stringOffset + stringTableStartOffset;
    }
  }
} public static class NameEntry {
  NameEntryId nameEntryId;
  protected int length;
  protected byte[] nameBytes;

  protected NameEntry() {
  }

  protected NameEntry(NameEntryId nameEntryId, byte[] nameBytes) {
    this.nameEntryId = nameEntryId;
    this.nameBytes = nameBytes;
  }

  protected NameEntry(
      int platformId, int encodingId, int languageId, int nameId, byte[] nameBytes) {
    this(new NameEntryId(platformId, encodingId, languageId, nameId), nameBytes);
  }

  protected NameEntryId getNameEntryId() {
    return this.nameEntryId;
  }

  /**
   * Get the platform id.
   * @return the platform id
   */
  public int platformId() {
    return this.nameEntryId.getPlatformId();
  }

  /**
   * Get the encoding id.
   * @return the encoding id
   */
  public int encodingId() {
    return this.nameEntryId.getEncodingId();
  }

  /**
   * Get the language id.
   * @return the language id
   */
  public int languageId() {
    return this.nameEntryId.getLanguageId();
  }

  /**
   * Get the name id.
   * @return the name id
   */
  public int nameId() {
    return this.nameEntryId.getNameId();
  }

  /**
   * Get the bytes for name.
   * @return the name bytes
   */
  public byte[] nameAsBytes() {
    return this.nameBytes;
  }

  /**
   * Get the name as a String. If there is no encoding conversion
   * available for the name bytes then a best attempt String will be returned.
   * @return the name
   */
  public String name() {
    return NameTable.convertFromNameBytes(this.nameBytes, this.platformId(), this.encodingId());
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append("[");
    sb.append(this.nameEntryId);
    sb.append(", \"");
    String name = this.name();
    sb.append(this.name());
    sb.append("\"]");
    return sb.toString();
  }

  @Override
  public boolean equals(Object obj) {
    if (!(obj instanceof NameEntry)) {
      return false;
    }
    NameEntry other = (NameEntry) obj;
    if (!this.nameEntryId.equals(other.nameEntryId)) {
      return false;
    }
    if (this.nameBytes.length != other.nameBytes.length) {
      return false;
    }
    for (int i = 0; i < this.nameBytes.length; i++) {
      if (this.nameBytes[i] != other.nameBytes[i]) {
        return false;
      }
    }
    return true;
  }

  @Override
  public int hashCode() {
    int hash = this.nameEntryId.hashCode();
    for (int i = 0; i < this.nameBytes.length; i+=4) {
      for (int j = 0; j < 4 && j + i < this.nameBytes.length; j++) {
        hash |= this.nameBytes[j] << j * 8;
      }
    }
    return hash;
  }
} public final class PostScriptTable extends Table {

  private static final int VERSION_1 = 0x10000;
  private static final int VERSION_2 = 0x20000;
  private static final int NUM_STANDARD_NAMES = 258;

  /**
   * Offsets to specific elements in the underlying data. These offsets are relative to the
   * start of the table or the start of sub-blocks within the table.
   */
  private enum Offset {
    version(0),
    italicAngle(4),
    underlinePosition(8),
    underlineThickness(10),
    isFixedPitch(12),
    minMemType42(16),
    maxMemType42(20),
    minMemType1(24),
    maxMemType1(28),

    // TODO: add support for these versions of the table?
    // Version 2.0 table
    numberOfGlyphs(32),
    glyphNameIndex(34);  // start of table

    // Version 2.5 table

    // Version 4.0 table

    private final int offset;
    private Offset(int offset) {
      this.offset = offset;
    }
  }

  private AtomicReference<List<String>> names = new AtomicReference<List<String>>();
  
  private static final String[] STANDARD_NAMES = {
    ".notdef",
    ".null",
    "nonmarkingreturn",
    "space",
    "exclam",
    "quotedbl",
    "numbersign",
    "dollar",
    "percent",
    "ampersand",
    "quotesingle",
    "parenleft",
    "parenright",
    "asterisk",
    "plus",
    "comma",
    "hyphen",
    "period",
    "slash",
    "zero",
    "one",
    "two",
    "three",
    "four",
    "five",
    "six",
    "seven",
    "eight",
    "nine",
    "colon",
    "semicolon",
    "less",
    "equal",
    "greater",
    "question",
    "at",
    "A",
    "B",
    "C",
    "D",
    "E",
    "F",
    "G",
    "H",
    "I",
    "J",
    "K",
    "L",
    "M",
    "N",
    "O",
    "P",
    "Q",
    "R",
    "S",
    "T",
    "U",
    "V",
    "W",
    "X",
    "Y",
    "Z",
    "bracketleft",
    "backslash",
    "bracketright",
    "asciicircum",
    "underscore",
    "grave",
    "a",
    "b",
    "c",
    "d",
    "e",
    "f",
    "g",
    "h",
    "i",
    "j",
    "k",
    "l",
    "m",
    "n",
    "o",
    "p",
    "q",
    "r",
    "s",
    "t",
    "u",
    "v",
    "w",
    "x",
    "y",
    "z",
    "braceleft",
    "bar",
    "braceright",
    "asciitilde",
    "Adieresis",
    "Aring",
    "Ccedilla",
    "Eacute",
    "Ntilde",
    "Odieresis",
    "Udieresis",
    "aacute",
    "agrave",
    "acircumflex",
    "adieresis",
    "atilde",
    "aring",
    "ccedilla",
    "eacute",
    "egrave",
    "ecircumflex",
    "edieresis",
    "iacute",
    "igrave",
    "icircumflex",
    "idieresis",
    "ntilde",
    "oacute",
    "ograve",
    "ocircumflex",
    "odieresis",
    "otilde",
    "uacute",
    "ugrave",
    "ucircumflex",
    "udieresis",
    "dagger",
    "degree",
    "cent",
    "sterling",
    "section",
    "bullet",
    "paragraph",
    "germandbls",
    "registered",
    "copyright",
    "trademark",
    "acute",
    "dieresis",
    "notequal",
    "AE",
    "Oslash",
    "infinity",
    "plusminus",
    "lessequal",
    "greaterequal",
    "yen",
    "mu",
    "partialdiff",
    "summation",
    "product",
    "pi",
    "integral",
    "ordfeminine",
    "ordmasculine",
    "Omega",
    "ae",
    "oslash",
    "questiondown",
    "exclamdown",
    "logicalnot",
    "radical",
    "florin",
    "approxequal",
    "Delta",
    "guillemotleft",
    "guillemotright",
    "ellipsis",
    "nonbreakingspace",
    "Agrave",
    "Atilde",
    "Otilde",
    "OE",
    "oe",
    "endash",
    "emdash",
    "quotedblleft",
    "quotedblright",
    "quoteleft",
    "quoteright",
    "divide",
    "lozenge",
    "ydieresis",
    "Ydieresis",
    "fraction",
    "currency",
    "guilsinglleft",
    "guilsinglright",
    "fi",
    "fl",
    "daggerdbl",
    "periodcentered",
    "quotesinglbase",
    "quotedblbase",
    "perthousand",
    "Acircumflex",
    "Ecircumflex",
    "Aacute",
    "Edieresis",
    "Egrave",
    "Iacute",
    "Icircumflex",
    "Idieresis",
    "Igrave",
    "Oacute",
    "Ocircumflex",
    "apple",
    "Ograve",
    "Uacute",
    "Ucircumflex",
    "Ugrave",
    "dotlessi",
    "circumflex",
    "tilde",
    "macron",
    "breve",
    "dotaccent",
    "ring",
    "cedilla",
    "hungarumlaut",
    "ogonek",
    "caron",
    "Lslash",
    "lslash",
    "Scaron",
    "scaron",
    "Zcaron",
    "zcaron",
    "brokenbar",
    "Eth",
    "eth",
    "Yacute",
    "yacute",
    "Thorn",
    "thorn",
    "minus",
    "multiply",
    "onesuperior",
    "twosuperior",
    "threesuperior",
    "onehalf",
    "onequarter",
    "threequarters",
    "franc",
    "Gbreve",
    "gbreve",
    "Idotaccent",
    "Scedilla",
    "scedilla",
    "Cacute",
    "cacute",
    "Ccaron",
    "ccaron",
    "dcroat"
  };

  private PostScriptTable(Header header, ReadableFontData data) {
    super(header, data);
  }

  // TODO: version enum
  public int version() {
    return this.data.readFixed(Offset.version.offset);
  }

  public int italicAngle() {
    return this.data.readFixed(Offset.italicAngle.offset);
  }

  public int underlinePosition() {
    return this.data.readFWord(Offset.underlinePosition.offset);
  }

  public long isFixedPitchRaw() {
    return this.data.readULong(Offset.isFixedPitch.offset);
  }

  public boolean isFixedPitch() {
    return this.isFixedPitchRaw() != 0;
  }

  public long minMemType42() {
    return this.data.readULong(Offset.minMemType42.offset);
  }

  public long maxMemType42() {
    return this.data.readULong(Offset.maxMemType42.offset);
  }

  public long minMemType1() {
    return this.data.readULong(Offset.minMemType1.offset);
  }

  public long maxMemType1() {
    return this.data.readULong(Offset.maxMemType1.offset);
  }

  public int numberOfGlyphs() {
    if (version() == VERSION_1) {
      return NUM_STANDARD_NAMES;
    } else if (version() == VERSION_2) {
      return this.data.readUShort(Offset.numberOfGlyphs.offset);
    } else {
      // TODO: should probably be better at signaling unsupported format
      return -1;
    }
  }
  
  public String glyphName(int glyphNum) {
    if (glyphNum < 0 || glyphNum >= numberOfGlyphs()) {
      throw new IndexOutOfBoundsException();
    }
    int glyphNameIndex = 0;
    if (version() == VERSION_1) {
      glyphNameIndex = glyphNum;
    } else if (version() == VERSION_2) {
      glyphNameIndex = this.data.readUShort(Offset.glyphNameIndex.offset + 2 * glyphNum);
    }
    if (glyphNameIndex < NUM_STANDARD_NAMES) {
      return STANDARD_NAMES[glyphNameIndex];
    }
    return getNames().get(glyphNameIndex - NUM_STANDARD_NAMES);
  }
  // TODO: add getters for 2.5 and possibly other tables?

  // Defer the actual parsing of the name strings until first use. Note that this
  // method can therefore throw various runtime exceptions if the table is corrupted.
  /**
   * Get a list containing the names in the table. Since parsing this list is potentially
   * expensive and may throw an exception when data is corrupted, parsing is deferred until
   * first use.
   * 
   * Also note that the return value is only valid for version 2 tables (potentially to be
   * expanded to other versions later). A non-null value is guaranteed for version 2 (only).
   */
  private List<String> getNames() {
    List<String> result = names.get();
    if (result == null && version() == VERSION_2) {
      synchronized (names) {
        result = names.get();
        if (result == null) {
          result = parse();
          names.compareAndSet(null, result);
        }
      }
    }
    return result;
  }

  private List<String> parse() {
    List<String> names = null;
    if (version() == VERSION_2) {
      names = new ArrayList<String>();
      int index = Offset.glyphNameIndex.offset + 2 * numberOfGlyphs();
      while (index < dataLength()) {
        int strLen = this.data.readUByte(index);
        byte[] nameBytes = new byte[strLen];
        this.data.readBytes(index + 1, nameBytes, 0, strLen);
        try {
          names.add(new String(nameBytes, "ISO-8859-1"));
        } catch (UnsupportedEncodingException e) {
          // Can't happen; ISO-8859-1 is one of the guaranteed encodings.
        }
        index += 1 + strLen;
      }
    } else if (version() == VERSION_1) {
      throw new IllegalStateException("Not meaningful to parse version 1 table");
    }
    return names;
  }
  
  public static class Builder extends TableBasedTableBuilder<PostScriptTable> {

    /**
     * Create a new builder using the header information and data provided.
     *
     * @param header the header information
     * @param data the data holding the table
     * @return a new builder
     */
    public static Builder createBuilder(Header header, WritableFontData data) {
      return new Builder(header, data);
    }
    
    protected Builder(Header header, WritableFontData data) {
      super(header, data);
    }

    protected Builder(Header header, ReadableFontData data) {
      super(header, data);
    }

    @Override
    protected PostScriptTable subBuildTable(ReadableFontData data) {
      return new PostScriptTable(this.header(), data);
    }
  }
} public abstract class Glyph extends SubTable {
  
  public enum GlyphType {
    Simple,
    Composite;
  }

  protected volatile boolean initialized = false;
  // TOO(stuartg): should we replace this with a shared lock? more contention
  // but less space
  protected final Object initializationLock = new Object();

  private final Glyph.GlyphType glyphType;
  private final int numberOfContours;

  protected Glyph(ReadableFontData data, Glyph.GlyphType glyphType) {
    super(data);
    this.glyphType = glyphType;

    if (this.data.length() == 0) {
      this.numberOfContours = 0;
    } else {
      // -1 if composite
      this.numberOfContours = this.data.readShort(Offset.numberOfContours.offset);
    }
  }

  protected Glyph(ReadableFontData data, int offset, int length, Glyph.GlyphType glyphType) {
    super(data, offset, length);
    this.glyphType = glyphType;

    if (this.data.length() == 0) {
      this.numberOfContours = 0;
    } else {
      // -1 if composite
      this.numberOfContours = this.data.readShort(Offset.numberOfContours.offset);
    }
  }

  private static Glyph.GlyphType glyphType(ReadableFontData data, int offset, int length) {
    if (length == 0) {
      return GlyphType.Simple;
    }
    int numberOfContours = data.readShort(offset);
    if (numberOfContours >= 0) {
      return GlyphType.Simple;
    }
    return GlyphType.Composite;
  }

//  @SuppressWarnings("unchecked")
//  static <T extends Glyph> T getGlyph(
//      GlyphTable table, ReadableFontData data, int offset, int length) {
//    Glyph.GlyphType type = Glyph.glyphType(data, offset, length);
//    if (type == GlyphType.Simple) {
//      return (T) new SimpleGlyph(data, offset, length);
//    }
//    return (T) new CompositeGlyph(data, offset, length);
//  }

  static Glyph getGlyph(
      GlyphTable table, ReadableFontData data, int offset, int length) {
    Glyph.GlyphType type = Glyph.glyphType(data, offset, length);
    if (type == GlyphType.Simple) {
      return new SimpleGlyph(data, offset, length);
    }
    return new CompositeGlyph(data, offset, length);
  }

  protected abstract void initialize();


  @Override
  public int padding() {
    this.initialize();
    return super.padding();
  }

  public Glyph.GlyphType glyphType() {
    return this.glyphType;
  }

  public int numberOfContours() {
    return this.numberOfContours;
  }

  public int xMin() {
    return this.data.readShort(Offset.xMin.offset);
  }

  public int xMax() {
    return this.data.readShort(Offset.xMax.offset);
  }

  public int yMin() {
    return this.data.readShort(Offset.yMin.offset);
  }

  public int yMax() {
    return this.data.readShort(Offset.yMax.offset);
  }

  public abstract int instructionSize();

  public abstract ReadableFontData instructions();

  @Override
  public String toString() {
    return this.toString(0);
  }

  public String toString(int length) {
    StringBuilder sb = new StringBuilder();
    sb.append(this.glyphType());
    sb.append(", contours=");
    sb.append(this.numberOfContours());
    sb.append(", [xmin=");
    sb.append(this.xMin());
    sb.append(", ymin=");
    sb.append(this.yMin());
    sb.append(", xmax=");
    sb.append(this.xMax());
    sb.append(", ymax=");
    sb.append(this.yMax());
    sb.append("]");
    sb.append("\n");
    return sb.toString();
  }

  // TODO(stuartg): interface? need methods from Composite?
  public abstract static class Contour {
    protected Contour() {
    }
  }

  public abstract static class Builder<T extends Glyph> extends SubTable.Builder<T> {
    protected int format;

    protected Builder(WritableFontData data) {
      super(data);
    }

    protected Builder(ReadableFontData data) {
      super(data);
    }

    /**
     * @param data
     * @param offset
     * @param length
     */
    protected Builder(WritableFontData data, int offset, int length) {
      this(data.slice(offset, length));
    }

    static Glyph.Builder<? extends Glyph> getBuilder(
        GlyphTable.Builder tableBuilder, ReadableFontData data) {
      return Glyph.Builder.getBuilder(tableBuilder, data, 0, data.length());
    }

    static Glyph.Builder<? extends Glyph> getBuilder(
        GlyphTable.Builder tableBuilder, ReadableFontData data, int offset, int length) {
      Glyph.GlyphType type = Glyph.glyphType(data, offset, length);
      if (type == GlyphType.Simple) {
        return new SimpleGlyph.SimpleGlyphBuilder(data, offset, length);
      }
      return new CompositeGlyph.CompositeGlyphBuilder(data, offset, length);
    }

    @Override
    protected void subDataSet() {
      // NOP
    }

    @Override
    protected int subDataSizeToSerialize() {
      return this.internalReadData().length();
    }

    @Override
    protected boolean subReadyToSerialize() {
      return true;
    }

    @Override
    protected int subSerialize(WritableFontData newData) {
      return this.internalReadData().copyTo(newData);
    }
  }
} public final class GlyphTable extends SubTableContainerTable {

  /**
   * Offsets to specific elements in the underlying data. These offsets are relative to the
   * start of the table or the start of sub-blocks within the table.
   */
  public enum Offset {
    // header
    numberOfContours(0),
    xMin(2),
    yMin(4),
    xMax(6),
    yMax(8),

    // Simple Glyph Description
    simpleEndPtsOfCountours(10),
    // offset from the end of the contours array
    simpleInstructionLength(0),
    simpleInstructions(2),
    // flags
    // xCoordinates
    // yCoordinates

    // Composite Glyph Description
    compositeFlags(0),
    compositeGyphIndexWithoutFlag(0),
    compositeGlyphIndexWithFlag(2);

    final int offset;

    private Offset(int offset) {
      this.offset = offset;
    }
  }

  private GlyphTable(Header header, ReadableFontData data) {
    super(header, data);
  }

  public Glyph glyph(int offset, int length) {
    return Glyph.getGlyph(this, this.data, offset, length);
  }

  public static class Builder extends SubTableContainerTable.Builder<GlyphTable> {

    private List<Glyph.Builder<? extends Glyph>> glyphBuilders;
    private List<Integer> loca;

    /**
     * Create a new builder using the header information and data provided.
     *
     * @param header the header information
     * @param data the data holding the table
     * @return a new builder
     */
    public static Builder createBuilder(Header header, WritableFontData data) {
      return new Builder(header, data);
    }

    /**
     * Constructor.
     *
     * @param header the table header
     * @param data the data for the table
     */
    protected Builder(Header header, WritableFontData data) {
      super(header, data);
    }

    /**
     * Constructor.
     *
     * @param header the table header
     * @param data the data for the table
     */
    protected Builder(Header header, ReadableFontData data) {
      super(header, data);
    }

    // glyph table level building

    public void setLoca(List<Integer> loca) {
      this.loca = new ArrayList<Integer>(loca);
      this.setModelChanged(false);
      this.glyphBuilders = null;
    }

    /**
     * Generate a loca table list from the current state of the glyph table
     * builder.
     *
     * @return a list of loca information for the glyphs
     */
    public List<Integer> generateLocaList() {
      List<Integer> locas = new ArrayList<Integer>(this.getGlyphBuilders().size());
      locas.add(0);
      if (this.getGlyphBuilders().size() == 0) {
        locas.add(0);
      } else {
        int total = 0;
        for (Glyph.Builder<? extends Glyph> b : this.getGlyphBuilders()) {
          int size = b.subDataSizeToSerialize();
          locas.add(total + size);
          total += size;
        }
      }
      return locas;
    }

    private void initialize(ReadableFontData data, List<Integer> loca) {
      this.glyphBuilders = new ArrayList<Glyph.Builder<? extends Glyph>>();

      if (data != null) {
        int locaValue;
        int lastLocaValue = loca.get(0);
        for (int i = 1; i < loca.size(); i++) {
          locaValue = loca.get(i);
          this.glyphBuilders.add(Glyph.Builder.getBuilder(this, data, lastLocaValue /* offset */,
              locaValue - lastLocaValue /* length */));
          lastLocaValue = locaValue;
        }
      }
    }

    private List<Glyph.Builder<? extends Glyph>> getGlyphBuilders() {
      if (this.glyphBuilders == null) {
        if (this.internalReadData() != null && this.loca == null) {
          throw new IllegalStateException("Loca values not set - unable to parse glyph data.");
        }
        this.initialize(this.internalReadData(), this.loca);
        this.setModelChanged();
      }
      return this.glyphBuilders;
    }

    public void revert() {
      this.glyphBuilders = null;
      this.setModelChanged(false);
    }

    /**
     * Gets the List of glyph builders for the glyph table builder. These may be
     * manipulated in any way by the caller and the changes will be reflected in
     * the final glyph table produced.
     *
     *  If there is no current data for the glyph builder or the glyph builders
     * have not been previously set then this will return an empty glyph builder
     * List. If there is current data (i.e. data read from an existing font) and
     * the <code>loca</code> list has not been set or is null, empty, or
     * invalid, then an empty glyph builder List will be returned.
     *
     * @return the list of glyph builders
     */
    public List<Glyph.Builder<? extends Glyph>> glyphBuilders() {
      return this.getGlyphBuilders();
    }

    /**
     * Replace the internal glyph builders with the one provided. The provided
     * list and all contained objects belong to this builder.
     *
     *  This call is only required if the entire set of glyphs in the glyph
     * table builder are being replaced. If the glyph builder list provided from
     * the {@link GlyphTable.Builder#glyphBuilders()} is being used and modified
     * then those changes will already be reflected in the glyph table builder.
     *
     * @param glyphBuilders the new glyph builders
     */
    public void setGlyphBuilders(List<Glyph.Builder<? extends Glyph>> glyphBuilders) {
      this.glyphBuilders = glyphBuilders;
      this.setModelChanged();
    }

    // glyph builder factories

    public Glyph.Builder<? extends Glyph> glyphBuilder(ReadableFontData data) {
      Glyph.Builder<? extends Glyph> glyphBuilder = Glyph.Builder.getBuilder(this, data);
      return glyphBuilder;
    }


    // internal API for building

    @Override
    protected GlyphTable subBuildTable(ReadableFontData data) {
      return new GlyphTable(this.header(), data);
    }

    @Override
    protected void subDataSet() {
      this.glyphBuilders = null;
      super.setModelChanged(false);
    }

    @Override
    protected int subDataSizeToSerialize() {
      if (this.glyphBuilders == null || this.glyphBuilders.size() == 0) {
        return 0;
      }

      boolean variable = false;
      int size = 0;

      // calculate size of each table
      for (Glyph.Builder<? extends Glyph> b : this.glyphBuilders) {
        int glyphSize = b.subDataSizeToSerialize();
        size += Math.abs(glyphSize);
        variable |= glyphSize <= 0;
      }
      return variable ? -size : size;
    }

    @Override
    protected boolean subReadyToSerialize() {
      if (this.glyphBuilders == null) {
        return false;
      }
      // TODO(stuartg): check glyphs for ready to build?
      return true;
    }

    @Override
    protected int subSerialize(WritableFontData newData) {
      int size = 0;
      for (Glyph.Builder<? extends Glyph> b : this.glyphBuilders) {
        size += b.subSerialize(newData.slice(size));
      }
      return size;
    }
  }
} public final class LocaTable extends Table {

  private IndexToLocFormat version;
  private int numGlyphs;

  private LocaTable(Header header, ReadableFontData data, IndexToLocFormat version, int numGlyphs) {
    super(header, data);
    this.version = version;
    this.numGlyphs = numGlyphs;
  }

  /**
   * Get the table version.
   *
   * @return the table version
   */
  public IndexToLocFormat formatVersion() {
    return this.version;
  }

  public int numGlyphs() {
    return this.numGlyphs;
  }

  /**
   * Return the offset for the given glyph id. Valid glyph ids are from 0 to the
   * one less than the number of glyphs. The zero entry is the special entry for
   * the notdef glyph. The final entry beyond the last glyph id is used to
   * calculate the size of the last glyph.
   *
   * @param glyphId the glyph id to get the offset for; must be less than or
   *        equal to one more than the number of glyph ids
   * @return the offset in the glyph table to the specified glyph id
   */
  public int glyphOffset(int glyphId) {
    if (glyphId < 0 || glyphId >= this.numGlyphs) {
      throw new IndexOutOfBoundsException("Glyph ID is out of bounds.");
    }
    return this.loca(glyphId);
  }

  /**
   * Get the length of the data in the glyph table for the specified glyph id.
   * @param glyphId the glyph id to get the offset for; must be greater than or
   *        equal to 0 and less than the number of glyphs in the font
   * @return the length of the data in the glyph table for the specified glyph id
   */
  public int glyphLength(int glyphId) {
    if (glyphId < 0 || glyphId >= this.numGlyphs) {
      throw new IndexOutOfBoundsException("Glyph ID is out of bounds.");
    }
    return this.loca(glyphId + 1) - this.loca(glyphId);
  }

  /**
   * Get the number of locations or locas. This will be one more than the number
   * of glyphs for this table since the last loca position is used to indicate
   * the size of the final glyph.
   *
   * @return the number of locas
   */
  public int numLocas() {
    return this.numGlyphs + 1;
  }

  /**
   * Get the value from the loca table for the index specified. These are the
   * raw values from the table that are used to compute the offset and size of a
   * glyph in the glyph table. Valid index values run from 0 to the number of
   * glyphs in the font.
   *
   * @param index the loca table index
   * @return the loca table valueÍ
   */
  public int loca(int index) {
    if (index > this.numGlyphs) {
      throw new IndexOutOfBoundsException();
    }
    if (this.version == IndexToLocFormat.shortOffset) {
      return 2 * this.data.readUShort(index * FontData.DataSize.USHORT.size());
    }
    return this.data.readULongAsInt(index * FontData.DataSize.ULONG.size());
  }

  /**
   * Get an iterator over the loca values for the table. The iterator returned
   * does not support the delete operation.
   *
   * @return loca iterator
   * @see #loca
   */
  Iterator<Integer> iterator() {
    return new LocaIterator();
  }

  /**
   * Iterator over the raw loca values.
   *
   */
  private final class LocaIterator implements Iterator<Integer> {
    int index;

    private LocaIterator() {
    }

    @Override
    public boolean hasNext() {
      if (this.index <= numGlyphs) {
        return true;
      }
      return false;
    }

    @Override
    public Integer next() {
      return loca(index++);
    }

    @Override
    public void remove() {
      throw new UnsupportedOperationException();
    }
  }

  /**
   * Builder for a loca table.
   *
   */
  public static class Builder extends Table.Builder<LocaTable> {

    // values that need to be set to properly passe an existing loca table
    private IndexToLocFormat formatVersion = IndexToLocFormat.longOffset;
    private int numGlyphs = -1;
    
    // parsed loca table
    private List<Integer> loca;

    /**
     * Create a new builder using the header information and data provided.
     *
     * @param header the header information
     * @param data the data holding the table
     * @return a new builder
     */
    public static Builder createBuilder(Header header, WritableFontData data) {
      return new Builder(header, data);
    }
    
    private Builder(Header header, WritableFontData data) {
      super(header, data);
    }

    private Builder(Header header, ReadableFontData data) {
      super(header, data);
    }

    /**
     * Initialize the internal state from the data. Done lazily since in many
     * cases the builder will be just creating a table object with no parsing
     * required.
     *
     * @param data the data to initialize from
     */
    private void initialize(ReadableFontData data) {
      this.clearLoca(false);
      if (this.loca == null) {
        this.loca = new ArrayList<Integer>();
      }
      if (data != null) {
        if (this.numGlyphs < 0) {
          throw new IllegalStateException("numglyphs not set on LocaTable Builder.");
        }

        LocaTable table = new LocaTable(this.header(), data, this.formatVersion, this.numGlyphs);
        Iterator<Integer> locaIter = table.iterator();
        while (locaIter.hasNext()) {
          this.loca.add(locaIter.next());
        }
      }
    }

    /**
     * Checks that the glyph id is within the correct range.
     *
     * @param glyphId
     * @return the glyphId
     * @throws IndexOutOfBoundsException if the glyph id is not within the
     *         correct range
     */
    private int checkGlyphRange(int glyphId) {
      if (glyphId < 0 || glyphId > this.lastGlyphIndex()) {
        throw new IndexOutOfBoundsException("Glyph ID is outside of the allowed range.");
      }
      return glyphId;
    }

    private int lastGlyphIndex() {
      return this.loca != null ? this.loca.size() - 2 : this.numGlyphs - 1;
    }

    /**
     * Internal method to get the loca list if already generated and if not to
     * initialize the state of the builder.
     *
     * @return the loca list
     */
    private List<Integer> getLocaList() {
      if (this.loca == null) {
        this.initialize(this.internalReadData());
        this.setModelChanged();
      }
      return this.loca;
    }

    private void clearLoca(boolean nullify) {
      if (this.loca != null) {
        this.loca.clear();
      }
      if (nullify) {
        this.loca = null;
      }
      this.setModelChanged(false);
    }
    
    /**
     * Get the format version that will be used when the loca table is
     * generated.
     *
     * @return the loca table format version
     */
    public IndexToLocFormat formatVersion() {
      return this.formatVersion;
    }

    /**
     * Set the format version to be used when generating the loca table.
     *
     * @param formatVersion
     */
    public void setFormatVersion(IndexToLocFormat formatVersion) {
      this.formatVersion = formatVersion;
    }

    /**
     * Gets the List of locas for loca table builder. These may be manipulated
     * in any way by the caller and the changes will be reflected in the final
     * loca table produced as long as no subsequent call is made to the
     * {@link #setLocaList(List)} method.
     *
     *  If there is no current data for the loca table builder or the loca list
     * have not been previously set then this will return an empty List.
     *
     * @return the list of glyph builders
     * @see #setLocaList(List)
     */
    public List<Integer> locaList() {
      return this.getLocaList();
    }

    /**
     * Set the list of locas to be used for building this table. If any existing
     * list was already retrieved with the {@link #locaList()} method then the
     * connection of that previous list to this builder will be broken.
     *
     * @param list
     * @see #locaList()
     */
    public void setLocaList(List<Integer> list) {
      this.loca = list;
      this.setModelChanged();
    }
    
    /**
     * Return the offset for the given glyph id. Valid glyph ids are from 0 to
     * one more than the number of glyphs. The zero entry is the special entry
     * for the notdef glyph. The final entry beyond the last glyph id is used to
     * calculate the size of the last glyph.
     *
     * @param glyphId the glyph id to get the offset for; must be less than or
     *        equal to one more than the number of glyph ids
     * @return the offset in the glyph table to the specified glyph id
     */
    public int glyphOffset(int glyphId) {
      this.checkGlyphRange(glyphId);
      return this.getLocaList().get(glyphId);
    }

    /**
     * Get the length of the data in the glyph table for the specified glyph id.
     * This is a convenience method that uses the specified glyph id
     *
     * @param glyphId the glyph id to get the offset for; must be less than or
     *        equal to the number of glyphs
     * @return the length of the data in the glyph table for the specified glyph
     *         id
     */
    public int glyphLength(int glyphId) {
      this.checkGlyphRange(glyphId);
      return this.getLocaList().get(glyphId + 1) - this.getLocaList().get(glyphId);
    }

    /**
     * Set the number of glyphs.
     *
     *  This method sets the number of glyphs that the builder will attempt to
     * parse location data for from the raw binary data. This method only needs
     * to be called (and <b>must</b> be) when the raw data for this builder has
     * been changed. It does not by itself reset the data or clear any set loca
     * list.
     *
     * @param numGlyphs the number of glyphs represented by the data
     */
    public void setNumGlyphs(int numGlyphs) {
      this.numGlyphs = numGlyphs;
    }

    /**
     * Get the number of glyphs that this builder has support for.
     *
     * @return the number of glyphs.
     */
    public int numGlyphs() {
      return this.lastGlyphIndex() + 1;
    }
    
    /**
     * Revert the loca table builder to the state contained in the last raw data
     * set on the builder. That raw data may be that read from a font file when
     * the font builder was created, that set by a user of the loca table
     * builder, or null data if this builder was created as a new empty builder.
     */
    public void revert() {
      this.loca = null;
      this.setModelChanged(false);
    }

    /**
     * Get the number of locations or locas. This will be one more than the
     * number of glyphs for this table since the last loca position is used to
     * indicate the size of the final glyph.
     *
     * @return the number of locas
     */
    public int numLocas() {
      return this.getLocaList().size();
    }
    
    /**
     * Get the value from the loca table for the index specified. These are the
     * raw values from the table that are used to compute the offset and size of
     * a glyph in the glyph table. Valid index values run from 0 to the number
     * of glyphs in the font.
     *
     * @param index the loca table index
     * @return the loca table valueÍ
     */
    public int loca(int index) {
      return this.getLocaList().get(index);
    }

    @Override
    protected LocaTable subBuildTable(ReadableFontData data) {
      return new LocaTable(this.header(), data, this.formatVersion, this.numGlyphs);
    }

    @Override
    protected void subDataSet() {
      this.initialize(this.internalReadData());
    }

    @Override
    protected int subDataSizeToSerialize() {
      if (this.loca == null) {
        return 0;
      }
      if (this.formatVersion == IndexToLocFormat.longOffset) {
        return this.loca.size() * FontData.DataSize.ULONG.size();
      }
      return this.loca.size() * FontData.DataSize.USHORT.size();
    }

    @Override
    protected boolean subReadyToSerialize() {
      return this.loca != null;
    }

    @Override
    protected int subSerialize(WritableFontData newData) {
      int size = 0;
      for (int l : this.loca) {
        if (this.formatVersion == IndexToLocFormat.longOffset) {
          size += newData.writeULong(size, l);
        } else {
          size += newData.writeUShort(size, l / 2);
        }
      }
      this.numGlyphs = this.loca.size() - 1;
      return size;
    }
  }
} public final class SimpleGlyph extends Glyph {
  private static final int FLAG_ONCURVE = 0x01;
  private static final int FLAG_XSHORT = 0x01 << 1;
  private static final int FLAG_YSHORT = 0x01 << 2;
  private static final int FLAG_REPEAT = 0x01 << 3;
  private static final int FLAG_XREPEATSIGN = 0x01 << 4;
  private static final int FLAG_YREPEATSIGN = 0x01 << 5;

  private int instructionSize;
  private int numberOfPoints;

  // start offsets of the arrays
  private int instructionsOffset;
  private int flagsOffset;
  private int xCoordinatesOffset;
  private int yCoordinatesOffset;

  private int flagByteCount;
  private int xByteCount;
  private int yByteCount;

  private int[] xCoordinates;
  private int[] yCoordinates;
  private boolean[] onCurve;
  private int[] contourIndex;

  public static final class SimpleContour extends Glyph.Contour {
    protected SimpleContour() {
      super();
    }
  }

  SimpleGlyph(ReadableFontData data, int offset, int length) {
    super(data, offset, length, GlyphType.Simple);
  }

  private SimpleGlyph(ReadableFontData data) {
    super(data, GlyphType.Simple);
  }

  @Override
  protected void initialize() {
    if (this.initialized) {
      return;
    }
    synchronized (this.initializationLock) {
      if (this.initialized) {
        return;
      }

      if (this.readFontData().length() == 0) {
        this.instructionSize = 0;
        this.numberOfPoints = 0;
        this.instructionsOffset = 0;
        this.flagsOffset = 0;
        this.xCoordinatesOffset = 0;
        this.yCoordinatesOffset = 0;
        return;
      }
      this.instructionSize =
          this.data.readUShort(Offset.simpleEndPtsOfCountours.offset + this.numberOfContours()
              * FontData.DataSize.USHORT.size());
      this.instructionsOffset =
          Offset.simpleEndPtsOfCountours.offset + (this.numberOfContours() + 1)
              * FontData.DataSize.USHORT.size();
      this.flagsOffset =
          this.instructionsOffset + this.instructionSize * FontData.DataSize.BYTE.size();
      this.numberOfPoints = this.contourEndPoint(this.numberOfContours() - 1) + 1;
      this.xCoordinates = new int[this.numberOfPoints];
      this.yCoordinates = new int[this.numberOfPoints];
      this.onCurve = new boolean[this.numberOfPoints];
      parseData(false);
      this.xCoordinatesOffset =
          this.flagsOffset + this.flagByteCount * FontData.DataSize.BYTE.size();
      this.yCoordinatesOffset =
          this.xCoordinatesOffset + this.xByteCount * FontData.DataSize.BYTE.size();
      this.contourIndex = new int[this.numberOfContours() + 1];
      contourIndex[0] = 0;
      for (int contour = 0; contour < this.contourIndex.length - 1; contour++) {
        contourIndex[contour + 1] = this.contourEndPoint(contour) + 1;
      }
      parseData(true);
      int nonPaddedDataLength =
          5 * FontData.DataSize.SHORT.size()
              + (this.numberOfContours() * FontData.DataSize.USHORT.size())
              + FontData.DataSize.USHORT.size()
              + (this.instructionSize * FontData.DataSize.BYTE.size())
              + (flagByteCount * FontData.DataSize.BYTE.size())
              + (xByteCount * FontData.DataSize.BYTE.size())
              + (yByteCount * FontData.DataSize.BYTE.size());
      this.setPadding(this.dataLength() - nonPaddedDataLength);
      this.initialized = true;
    }
  }

  // TODO(stuartg): think about replacing double parsing with ArrayList
  private void parseData(boolean fillArrays) {
    int flag = 0;
    int flagRepeat = 0;
    int flagIndex = 0;
    int xByteIndex = 0;
    int yByteIndex = 0;

    for (int pointIndex = 0; pointIndex < this.numberOfPoints; pointIndex++) {
      // get the flag for the current point
      if (flagRepeat == 0) {
        flag = this.flagAsInt(flagIndex++);
        if ((flag & FLAG_REPEAT) == FLAG_REPEAT) {
          flagRepeat = flagAsInt(flagIndex++);
        }
      } else {
        flagRepeat--;
      }

      // on the curve?
      if (fillArrays) {
        this.onCurve[pointIndex] = ((flag & FLAG_ONCURVE) == FLAG_ONCURVE) ? true : false;
      }
      // get the x coordinate
      if ((flag & FLAG_XSHORT) == FLAG_XSHORT) {
        // single byte x coord value
        if (fillArrays) {
          this.xCoordinates[pointIndex] =
              this.data.readUByte(this.xCoordinatesOffset + xByteIndex);
          this.xCoordinates[pointIndex] *=
              ((flag & FLAG_XREPEATSIGN) == FLAG_XREPEATSIGN) ? 1 : -1;
        }
        xByteIndex++;
      } else {
        // double byte coord value
        if (!((flag & FLAG_XREPEATSIGN) == FLAG_XREPEATSIGN)) {
          if (fillArrays) {
            this.xCoordinates[pointIndex] =
                this.data.readShort(this.xCoordinatesOffset + xByteIndex);
          }
          xByteIndex += 2;
        }
      }
      if (fillArrays && pointIndex > 0) {
        this.xCoordinates[pointIndex] += this.xCoordinates[pointIndex - 1];
      }

      // get the y coordinate
      if ((flag & FLAG_YSHORT) == FLAG_YSHORT) {
        if (fillArrays) {
          this.yCoordinates[pointIndex] =
              this.data.readUByte(this.yCoordinatesOffset + yByteIndex);
          this.yCoordinates[pointIndex] *=
              ((flag & FLAG_YREPEATSIGN) == FLAG_YREPEATSIGN) ? 1 : -1;
        }
        yByteIndex++;
      } else {
        if (!((flag & FLAG_YREPEATSIGN) == FLAG_YREPEATSIGN)) {
          if (fillArrays) {
            this.yCoordinates[pointIndex] =
                this.data.readShort(this.yCoordinatesOffset + yByteIndex);
          }
          yByteIndex += 2;
        }
      }
      if (fillArrays && pointIndex > 0) {
        this.yCoordinates[pointIndex] += this.yCoordinates[pointIndex - 1];
      }
    }
    this.flagByteCount = flagIndex;
    this.xByteCount = xByteIndex;
    this.yByteCount = yByteIndex;
  }

  private int flagAsInt(int index) {
    return this.data.readUByte(this.flagsOffset + index * FontData.DataSize.BYTE.size());
  }

  public int contourEndPoint(int contour) {
    return this.data.readUShort(
        contour * FontData.DataSize.USHORT.size() + Offset.simpleEndPtsOfCountours.offset);
  }

  @Override
  public int instructionSize() {
    this.initialize();
    return this.instructionSize;
  }

  @Override
  public ReadableFontData instructions() {
    this.initialize();
    return this.data.slice(this.instructionsOffset, this.instructionSize());
  }

  public int numberOfPoints(int contour) {
    this.initialize();
    if (contour >= this.numberOfContours()) {
      return 0;
    }
    return this.contourIndex[contour + 1] - this.contourIndex[contour];
  }

  public int xCoordinate(int contour, int point) {
    this.initialize();
    return this.xCoordinates[this.contourIndex[contour] + point];
  }

  public int yCoordinate(int contour, int point) {
    this.initialize();
    return this.yCoordinates[this.contourIndex[contour] + point];
  }

  public boolean onCurve(int contour, int point) {
    this.initialize();
    return this.onCurve[this.contourIndex[contour] + point];
  }

  @Override
  public String toString() {
    this.initialize();
    StringBuilder sb = new StringBuilder(super.toString());
    sb.append("\tinstruction bytes = " + this.instructionSize() + "\n");
    for (int contour = 0; contour < this.numberOfContours(); contour++) {
      for (int point = 0; point < this.numberOfPoints(contour); point++) {
        sb.append("\t" + contour + ":" + point + " = [" + this.xCoordinate(contour, point) + ", "
            + this.yCoordinate(contour, point) + ", " + this.onCurve(contour, point) + "]\n");
      }
    }
    return sb.toString();
  }

  public static class SimpleGlyphBuilder extends Glyph.Builder<SimpleGlyph> {
    protected SimpleGlyphBuilder(WritableFontData data, int offset, int length) {
      super(data.slice(offset, length));
    }

    protected SimpleGlyphBuilder(ReadableFontData data, int offset, int length) {
      super(data.slice(offset, length));
    }

    @Override
    protected SimpleGlyph subBuildTable(ReadableFontData data) {
      return new SimpleGlyph(data, 0, data.length());
    }
  }
}
import com.google.typography.font.sfntly.Font;
import com.google.typography.font.sfntly.FontFactory;
import com.google.typography.font.sfntly.Tag;
import com.google.typography.font.sfntly.data.ReadableFontData;
import com.google.typography.font.sfntly.table.Table;
import com.google.typography.font.sfntly.table.bitmap.EblcTable;
import com.google.typography.font.sfntly.table.core.CMap;
import com.google.typography.font.sfntly.table.core.CMapTable;
import com.google.typography.font.sfntly.table.core.CMapTable.CMapId;
import com.google.typography.font.sfntly.table.core.NameTable;
import com.google.typography.font.sfntly.table.core.NameTable.NameEntry;
import com.google.typography.font.sfntly.table.core.PostScriptTable;
import com.google.typography.font.sfntly.table.truetype.Glyph;
import com.google.typography.font.sfntly.table.truetype.GlyphTable;
import com.google.typography.font.sfntly.table.truetype.LocaTable;
import com.google.typography.font.sfntly.table.truetype.SimpleGlyph;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
    System.out.println("\t-g,-glyph\t\tdump the glyphs specified");
    System.out.println("\t-c,-char\t\tdump the characters specified using the Windows En" + 
    "glish Unicode cmap or the cmap specified with the -cm option");
    System.out.println("\trange\t\ttwo 1 to 4 digit numbers seperated by a hyphen that are opti" +
    "onally preceded by an x indicating hex - e.g. x12-234");
    System.out.println("\tlist\t\tone or more 1 to 4 digit numbers seperated commas that are op" +
    "tionally preceded by an x indicating hex - e.g. x12,234,666,x1234");
    System.out.println("\t-?,-h,-help\tprint this help information");
  }

  private static final Pattern RANGE_PATTERN =
    Pattern.compile("(x?)([\\da-fA-F]{1,5})-(x?)([\\da-fA-F]{1,5})");
  private static final BitSet parseRange(String range) {
    BitSet set = null;

    Matcher m = RANGE_PATTERN.matcher(range);
    if (m.matches()) {
      int low = Integer.parseInt(m.group(2), m.group(1).equals("") ? 10 : 16);
      int high = Integer.parseInt(m.group(4), m.group(3).equals("") ? 10 : 16);
      set = new BitSet();
      set.set(low, high + 1);
    }
    return set;
  }

  private static final Pattern NUMBER_PATTERN = Pattern.compile("(x?)([\\da-fA-F]{1,5})");
  private static final BitSet parseList(String list) {
    BitSet set = null;
    String[] items = list.split(",");

    if (items.length == 0) {
      return null;
    }

    set = new BitSet();
    for (String item : items) {
      Matcher m = NUMBER_PATTERN.matcher(item);
      if (!m.matches()) {
        return null;
      }
      int itemNumber = Integer.parseInt(m.group(2), m.group(1).equals("") ? 10 : 16);
      set.set(itemNumber);
    }
    return set;
  }

  public SfntDump() {
    this.fontFactory = FontFactory.getInstance();
  }

  public void countSpecialGlyphs(boolean count) {
    this.countSpecialGlyphs = count;
  }

  public void dumpTableList(boolean dumpTableList) {
    this.dumpTableHeadersInFont = dumpTableList;
  }

  public void dumpCMaps(boolean dumpCMaps) {
    this.dumpCmapList = dumpCMaps;
  }

  public void dumpCMaps(String option) {
    if (option.equals("mapping")) {
      this.cmapMapping = true;
    }
  }

  public void dumpNames(boolean dumpNames) {
    this.dumpNameList = dumpNames;
  }

  public void dumpPost(boolean dumpPost) {
    this.dumpPost = dumpPost;
  }

  public void dumpEblc(boolean dumpEblc) {
    this.dumpEblc = dumpEblc;
  }
  
  public void dumpAll(boolean dumpAll) {
    this.dumpCMaps(dumpAll);
    this.dumpNames(dumpAll);
    this.dumpPost(dumpAll);
    this.dumpTableList(dumpAll);
  }

  public void dumpGlyphs(BitSet set) {
    this.glyphSet = set;
  }

  public void dumpAllGlyphs(boolean dumpAllGlyphs) {
    this.dumpAllGlyphs = dumpAllGlyphs;
  }
  
  public void dumpAllChars(boolean dumpAll) {
    this.dumpAllChars = dumpAll;
  }

  public void dumpChars(BitSet set) {
    this.charSet = set;
  }

  public void dumpTablesAsBinary(String tableTag) {
    this.tablesToBinaryDump.add(tableTag);
  }

  public void useCMap(String cmap) {
    String[] cmapParams = cmap.split("\\D");
    this.cmapId =
      CMapId.getInstance(Integer.parseInt(cmapParams[0]), Integer.parseInt(cmapParams[1]));
  }

  public void dumpFont(File fontFile) throws IOException {
    boolean canDumpGlyphs = true;

    FileInputStream fis = null;
    System.out.println(fontFile + " ============================");
    try {
      fis = new FileInputStream(fontFile);
      Font[] fontArray = fontFactory.loadFonts(fis);