/*
 * Copyright (c) 2011, 2016, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package com.sun.javafx.font;


import java.lang.ref.WeakReference;
import java.io.File;
import java.io.FileNotFoundException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.sun.javafx.geom.RectBounds;
import com.sun.javafx.geom.transform.BaseTransform;
import com.sun.javafx.font.FontFileReader.Buffer;
import static com.sun.javafx.font.PrismMetrics.*;

public abstract class PrismFontFile implements FontResource, FontConstants {

    private int fontInstallationType = -1; // unknown, 0=embedded, 1=system

    // TrueType fonts can have multiple names, most notably split up by
    // platform and locale. Whilst fonts that have different names for
    // different platforms are arguable buggy, those with localised names
    // are not. This can cause problems. Suppose that a font has English,
    // French and German names, and the platform enumerates the name that
    // is most appropriate for the user locale. Then suppose a French
    // developer uses the French name, but for his German user this font
    // is not located by the platform because it reports the German name
    // for that font. At runtime we no longer have any connection to the
    // locale of the developer so we can't look for the name for that
    // locale, even if the platform have us a performant option for that.
    //
    // The English name which some might think is supposed
    // to be the interoperable name is not treated at all specially in
    // the font format and doesn't even come up for either the user or
    // the developer, and in fact doesn't even have to be present.
    // Having said that we'll probably have the best luck for most users
    // and fonts by assuming the English name if the locale name doesn't
    // work. But either way, without platform API support for this
    // then its really expensive as all font files need to be opened.
    //
    String familyName;           /* Family font name (English) */
    String fullName;             /* Full font name (English)   */
    String psName;               /* PostScript font name       */
    String localeFamilyName;
    String localeFullName;
    String styleName;
    String localeStyleName;
    String filename;
    int filesize;
    FontFileReader filereader;
    int numGlyphs = -1;
    short indexToLocFormat;
    int fontIndex; // into a TTC.
    boolean isCFF;
    boolean isEmbedded = false;
    boolean isCopy = false;
    boolean isTracked = false;
    boolean isDecoded = false;
    boolean isRegistered = true;

    /* The glyph image data is stored only in a texture, and we
     * manage how much of that is kept around. We clearly want
     * to keep a reference to the strike that created that data.
     */
    Map<FontStrikeDesc, WeakReference<PrismFontStrike>> strikeMap =
        new ConcurrentHashMap<FontStrikeDesc, WeakReference<PrismFontStrike>>();

    protected PrismFontFile(String name, String filename, int fIndex,
                          boolean register, boolean embedded,
                          boolean copy, boolean tracked) throws Exception {
        this.filename = filename;
        this.isRegistered = register;
        this.isEmbedded = embedded;
        this.isCopy = copy;
        this.isTracked = tracked;
        init(name, fIndex);
    }

    WeakReference<PrismFontFile> createFileDisposer(PrismFontFactory factory) {
        FileDisposer disposer = new FileDisposer(filename, isTracked);
        WeakReference<PrismFontFile> ref = Disposer.addRecord(this, disposer);
        disposer.setFactory(factory, ref);
        return ref;
    }

    void setIsDecoded(boolean decoded) {
        isDecoded = decoded;
    }

    /* This is called only for fonts where a temp file was created
     */
    protected synchronized void disposeOnShutdown() {
        if (isCopy || isDecoded) {
            AccessController.doPrivileged(
                    (PrivilegedAction<Void>) () -> {
                        try {
                            boolean delOK = (new File(filename)).delete();
                            if (!delOK && PrismFontFactory.debugFonts) {
                                 System.err.println("Temp file not deleted : "
                                                    + filename);
                            }
                            /* Embedded fonts (copy) can also be decoded.
                             * Set both flags to false to avoid double deletes.
                             */
                            isCopy = isDecoded = false;
                        } catch (Exception e) {
                        }
                        return null;
                    }
            );
            if (PrismFontFactory.debugFonts) {
                System.err.println("Temp file deleted: " + filename);
            }
        }
    }

    public int getDefaultAAMode() {
        return AA_GREYSCALE;
    }

    public boolean isInstalledFont() {
        if (fontInstallationType == -1) {
            PrismFontFactory factory = PrismFontFactory.getFontFactory();
            fontInstallationType = factory.isInstalledFont(filename) ? 1 : 0;
        }
        return fontInstallationType > 0;
    }

    static class FileDisposer implements DisposerRecord {
        String fileName;
        boolean isTracked;
        PrismFontFactory factory;
        WeakReference<PrismFontFile> refKey;

        public FileDisposer(String fileName, boolean isTracked) {
            this.fileName = fileName;
            this.isTracked = isTracked;
        }

        public void setFactory(PrismFontFactory factory,
                               WeakReference<PrismFontFile> refKey) {
            this.factory = factory;
            this.refKey = refKey;
        }

        public synchronized void dispose() {
            if (fileName != null) {
                AccessController.doPrivileged(
                        (PrivilegedAction<Void>) () -> {
                            try {
                                File file = new File(fileName);
                                int size = (int)file.length();
                                file.delete();
                                // decrement tracker only after
                                // successful deletion.
                                if (isTracked) {
                                    FontFileWriter.FontTracker.
                                        getTracker().subBytes(size);
                                }
                                if (factory != null && refKey != null) {
                                    Object o = refKey.get();
                                    if (o == null) {
                                        factory.removeTmpFont(refKey);
                                        factory = null;
                                        refKey = null;
                                    }
                                }
                                if (PrismFontFactory.debugFonts) {
                                    System.err.println("FileDisposer=" + fileName);
                                }
                            } catch (Exception e) {
                                if (PrismFontFactory.debugFonts) {
                                    e.printStackTrace();
                                }
                            }
                            return null;
                        }
                );
                fileName = null;
            }
        }
    }

    public String getFileName() {
        return filename;
    }

    protected int getFileSize() {
        return filesize;
    }

    protected int getFontIndex() {
        return fontIndex;
    }

    public String getFullName() {
        return fullName;
    }

    public String getPSName() {
        if (psName == null) {
            psName = fullName;
        }
        return psName;
    }

    public String getFamilyName() {
        return familyName;
    }

    public String getStyleName() {
        return styleName;
    }

    public String getLocaleFullName() {
        return localeFullName;
    }

    public String getLocaleFamilyName() {
        return localeFamilyName;
    }

    public String getLocaleStyleName() {
        return localeStyleName;
    }

    /*
     * Returns the features the font supports.
     */
    public int getFeatures() {
        //TODO check font file for features
        return -1;
    }

    public Map getStrikeMap() {
        return strikeMap;
    }

    protected abstract PrismFontStrike createStrike(float size,
                                                    BaseTransform transform,
                                                    int aaMode,
                                                    FontStrikeDesc desc);

    public FontStrike getStrike(float size, BaseTransform transform,
                                int aaMode) {
        FontStrikeDesc desc = new FontStrikeDesc(size, transform, aaMode);
        WeakReference<PrismFontStrike> ref = strikeMap.get(desc);
        PrismFontStrike strike = null;
        if (ref != null) {
            strike = ref.get();
        }
        if (strike == null) {
            strike = createStrike(size, transform, aaMode, desc);
            DisposerRecord disposer = strike.getDisposer();
            if (disposer != null) {
                ref = Disposer.addRecord(strike, disposer);
            } else {
                ref = new WeakReference<PrismFontStrike>(strike);
            }
            strikeMap.put(desc, ref);
        }
        return strike;
    }

    HashMap<Integer, int[]> bbCache = null;
    static final int[] EMPTY_BOUNDS = new int[4];

    protected abstract int[] createGlyphBoundingBox(int gc);

    @Override
    public float[] getGlyphBoundingBox(int gc, float size, float[] retArr) {
        if (retArr == null || retArr.length < 4) {
            retArr = new float[4];
        }
        if (gc >= getNumGlyphs()) {
            retArr[0] = retArr[1] = retArr[2] = retArr[3] = 0;
            return retArr;
        }
        if (bbCache == null) {
            bbCache = new HashMap<Integer, int[]>();
        }
        int[] bb = bbCache.get(gc);
        if (bb == null) {
            bb = createGlyphBoundingBox(gc);
            if (bb == null) bb = EMPTY_BOUNDS;
            bbCache.put(gc, bb);
        }
        float scale = size / getUnitsPerEm();
        retArr[0] = bb[0] * scale;
        retArr[1] = bb[1] * scale;
        retArr[2] = bb[2] * scale;
        retArr[3] = bb[3] * scale;
        return retArr;
    }

    int getNumGlyphs() {
        if (numGlyphs == -1) {
            Buffer buffer = readTable(maxpTag);
            numGlyphs = buffer.getChar(4); // offset 4 bytes in MAXP table.
        }
        return numGlyphs;
    }

    protected boolean isCFF() {
        return isCFF;
    }

    private Object peer;
    public Object getPeer() {
        return peer;
    }

    public void setPeer(Object peer) {
        this.peer = peer;
    }

    synchronized Buffer readTable(int tag) {
        Buffer buffer = null;
        boolean openedFile = false;
        try {
            openedFile = filereader.openFile();
            DirectoryEntry tagDE = getDirectoryEntry(tag);
            if (tagDE != null) {
                buffer = filereader.readBlock(tagDE.offset, tagDE.length);
            }
        } catch (Exception e) {
            if (PrismFontFactory.debugFonts) {
                e.printStackTrace();
            }
        } finally {
            if (openedFile) {
                try {
                    filereader.closeFile();
                } catch (Exception e2) {
                }
            }
        }
        return buffer;
    }

    int directoryCount = 1;

    /**
     * @return number of logical fonts. Is "1" for all but TTC files
     */
    public int getFontCount() {
        return directoryCount;
    }

    int numTables;
    DirectoryEntry[] tableDirectory;
    static class DirectoryEntry {
        int tag;
        int offset;
        int length;
    }

    DirectoryEntry getDirectoryEntry(int tag) {
        for (int i=0;i<numTables;i++) {
            if (tableDirectory[i].tag == tag) {
                return tableDirectory[i];
            }
        }
        return null;
    }

    /* Called from the constructor. Does the basic work of finding
     * the right font in a TTC, the font names and enough info
     * (the table offset directory) to be able to locate tables later.
     * Throws an exception if it doesn't like what it finds.
     */
    private void init(String name, int fIndex) throws Exception {
        filereader = new FontFileReader(filename);
        WoffDecoder decoder = null;
        try {
            if (!filereader.openFile()) {
                throw new FileNotFoundException("Unable to create FontResource"
                        + " for file " + filename);
            }
            Buffer buffer = filereader.readBlock(0, TTCHEADERSIZE);
            int sfntTag = buffer.getInt();

            /* Handle wOFF files */
            if (sfntTag == woffTag) {
                decoder = new WoffDecoder();
                File file = decoder.openFile();
                decoder.decode(filereader);
                decoder.closeFile();

                /* Create a new reader with the decoded file */
                filereader.closeFile();
                filereader = new FontFileReader(file.getPath());
                if (!filereader.openFile()) {
                    throw new FileNotFoundException("Unable to create "
                            + "FontResource for file " + filename);
                }
                buffer = filereader.readBlock(0, TTCHEADERSIZE);
                sfntTag = buffer.getInt();
            }

            filesize = (int)filereader.getLength();
            int headerOffset = 0;
            if (sfntTag == ttcfTag) {
                buffer.getInt(); // skip TTC version ID
                directoryCount = buffer.getInt();
                if (fIndex >= directoryCount) {
                    throw new Exception("Bad collection index");
                }
                fontIndex = fIndex;
                buffer = filereader.readBlock(TTCHEADERSIZE+4*fIndex, 4);
                headerOffset = buffer.getInt();
                buffer = filereader.readBlock(headerOffset, 4);
                sfntTag = buffer.getInt();
            }

            switch (sfntTag) {
            case v1ttTag:
            case trueTag:
                break;

            case ottoTag:
                isCFF = true;
                break;

            default:
                throw new Exception("Unsupported sfnt " + filename);
            }

            /* Now have the offset of this TT font (possibly within a TTC)
             * After the TT version/scaler type field, is the short
             * representing the number of tables in the table directory.
             * The table directory begins at 12 bytes after the header.
             * Each table entry is 16 bytes long (4 32-bit ints)
             */
            buffer = filereader.readBlock(headerOffset+4, 2);
            numTables = buffer.getShort();
            int directoryOffset = headerOffset+DIRECTORYHEADERSIZE;
            Buffer ibuffer = filereader.
                    readBlock(directoryOffset, numTables*DIRECTORYENTRYSIZE);
            DirectoryEntry table;
            tableDirectory = new DirectoryEntry[numTables];
            for (int i=0; i<numTables;i++) {
                tableDirectory[i] = table = new DirectoryEntry();
                table.tag   =  ibuffer.getInt();
                /* checksum */ ibuffer.skip(4);
                table.offset = ibuffer.getInt();
                table.length = ibuffer.getInt();
                if (table.offset + table.length > filesize) {
                    throw new Exception("bad table, tag="+table.tag);
                }
            }

            DirectoryEntry headDE = getDirectoryEntry(headTag);
            Buffer headTable = filereader.readBlock(headDE.offset,
                                                    headDE.length);
            // Important font attribute must be set in order to prevent div by zero
            upem = (float)(headTable.getShort(18) & 0xffff);
            if (!(16 <= upem && upem <= 16384)) {
                upem = 2048;
            }

            indexToLocFormat = headTable.getShort(50);
            // 0 for short offsets, 1 for long
            if (indexToLocFormat < 0 || indexToLocFormat > 1) {
                throw new Exception("Bad indexToLocFormat");
            }

            // In a conventional optimised layout, the
            // hhea table immediately follows the 'head' table.
            Buffer hhea = readTable(hheaTag);
            if (hhea == null) {
                numHMetrics = -1;
            } else {
                // the font table has the sign of ascent and descent
                // reversed from our coordinate system.
                ascent = -(float)hhea.getShort(4);
                descent = -(float)hhea.getShort(6);
                linegap = (float)hhea.getShort(8);
                // advanceWidthMax is max horizontal advance of all glyphs in
                // font. For some fonts advanceWidthMax is much larger then "M"
                // advanceWidthMax = (float)hhea.getChar(10);
                numHMetrics = hhea.getChar(34) & 0xffff;
            }

            // maxp table is before the OS/2 table. Read it now
            // while file is open - will be very cheap as its just
            // 32 bytes and we already have it in a byte[].
            getNumGlyphs();

            setStyle();

            // sanity check the cmap table
            checkCMAP();

            /* Get names last, as the name table is far from the file header.
             * Although its also likely too big to fit in the read cache
             * in which case that would remain valid, but also will help
             * any file read implementation which doesn't have random access.
             */
            initNames();

            if (familyName == null || fullName == null) {
                String fontName = name != null ? name : "";
                if (fullName == null) {
                    fullName = familyName != null ? familyName : fontName;
                }
                if (familyName == null) {
                    familyName = fullName != null ? fullName : fontName;
                }
                throw new Exception("Font name not found.");
            }

            /* update the font resource only if the file was decoded
             * and initialized successfully.
             */
            if (decoder != null) {
                isDecoded = true;
                filename = filereader.getFilename();
                PrismFontFactory.getFontFactory().addDecodedFont(this);
            }
        } catch (Exception e) {
            if (decoder != null) {
                decoder.deleteFile();
            }
            throw e;
        } finally {
            filereader.closeFile();
        }
    }

    /* TrueTypeFont can use the fsSelection fields of OS/2 table
     * or macStyleBits of the 'head' table to determine the style.
     */
    private static final int fsSelectionItalicBit  = 0x00001;
    private static final int fsSelectionBoldBit    = 0x00020;

    private static final int MACSTYLE_BOLD_BIT   = 0x1;
    private static final int MACSTYLE_ITALIC_BIT = 0x2;

    // Comment out some of this until we have both a need and a way to use it.
    // private int embeddingInfo;
    //private int fontWeight;
    private boolean isBold;
    private boolean isItalic;
    private float upem;
    private float ascent, descent, linegap; // in design units
    private int numHMetrics;

    private void setStyle() {
        // A number of fonts on Mac OS X do not have an OS/2
        // table. For those need to get info from a different source.
        DirectoryEntry os2_DE = getDirectoryEntry(os_2Tag);
        if (os2_DE != null) {
            // os2 Table ver 4      DataType    Offset
            //version               USHORT      0
            //xAvgCharWidth         SHORT       2
            //usWeightClass         USHORT      4
            //usWidthClass          USHORT      6
            //fsType                USHORT      8
            //ySubscriptXSize       SHORT      10
            //ySubscriptYSize       SHORT      12
            //ySubscriptXOffset     SHORT      14
            //ySubscriptYOffset     SHORT      16
            //ySuperscriptXSize     SHORT      18
            //ySuperscriptYSize     SHORT      20
            //ySuperscriptXOffset   SHORT      22
            //ySuperscriptYOffset   SHORT      24
            //yStrikeoutSize        SHORT      26
            //yStrikeoutPosition    SHORT      28
            //sFamilyClass          SHORT      30
            //panose[10]            BYTE       32
            //ulUnicodeRange1       ULONG      42
            //ulUnicodeRange2       ULONG      46
            //ulUnicodeRange3       ULONG      50
            //ulUnicodeRange4       ULONG      54
            //achVendID[4]          CHAR       58
            //fsSelection           USHORT     62
            //usFirstCharIndex      USHORT     64
            //usLastCharIndex       USHORT     66
            //sTypoAscender         SHORT      68
            //sTypoDescender        SHORT      70
            //sTypoLineGap          SHORT      72
            //usWinAscent           USHORT     74
            //usWinDescent          USHORT     76
            //ulCodePageRange1      ULONG      78
            //ulCodePageRange2      ULONG      82
            //sxHeight              SHORT      86
            //sCapHeight            SHORT      88
            //usDefaultChar         USHORT     90
            //usBreakChar           USHORT     92
            //usMaxContext          USHORT     94

            Buffer os_2Table = filereader.readBlock(os2_DE.offset,
                                                    os2_DE.length);
            int fsSelection = os_2Table.getChar(62) & 0xffff;
            isItalic = (fsSelection & fsSelectionItalicBit) != 0;
            isBold   = (fsSelection & fsSelectionBoldBit) != 0;
        } else {
            DirectoryEntry headDE = getDirectoryEntry(headTag);
            Buffer headTable = filereader.readBlock(headDE.offset,
                                                    headDE.length);
            short macStyleBits = headTable.getShort(44);
            isItalic = (macStyleBits & MACSTYLE_ITALIC_BIT) != 0;
            isBold = (macStyleBits & MACSTYLE_BOLD_BIT) != 0;
        }
    }

    public boolean isBold() {
        return isBold;
    }

    public boolean isItalic() {
        return isItalic;
    }

    public boolean isDecoded() {
        return isDecoded;
    }

    public boolean isRegistered() {
        return isRegistered;
    }

    public boolean isEmbeddedFont() {
        return isEmbedded;
    }

    /**
     * per the OT spec. this is an unsigned short.
     */
    public int getUnitsPerEm() {
        return (int)upem;
    }

    public short getIndexToLocFormat() {
        return indexToLocFormat;
    }

    /**
     * per the OT spec. this is an unsigned short.
     */
    public int getNumHMetrics() {
        return numHMetrics;
    }

    /* -- ID's used in the 'name' table */
    public static final int MAC_PLATFORM_ID = 1;
    public static final int MACROMAN_SPECIFIC_ID = 0;
    public static final int MACROMAN_ENGLISH_LANG = 0;

    public static final int MS_PLATFORM_ID = 3;
    /* MS locale id for US English is the "default" */
    public static final short MS_ENGLISH_LOCALE_ID = 0x0409; // 1033 decimal
    public static final int FAMILY_NAME_ID = 1;
    public static final int STYLE_NAME_ID = 2;
    public static final int FULL_NAME_ID = 4;
    public static final int PS_NAME_ID = 6;

    void initNames() throws Exception {
        byte[] name = new byte[256];

        DirectoryEntry nameDE = getDirectoryEntry(nameTag);
        Buffer buffer = filereader.readBlock(nameDE.offset, nameDE.length);

        buffer.skip(2); // format - not needed.
        short numRecords = buffer.getShort();
        /* The name table uses unsigned shorts. Many of these
         * are known small values that fit in a short.
         * The values that are sizes or offsets into the table could be
         * greater than 32767, so read and store those as ints
         */
        int stringPtr = buffer.getShort() & 0xffff;

        /* Microsoft Windows font names are preferred but numerous Mac
         * fonts do not have these, so we must also accept these in the
         * absence of the preferred Windows names.
         */
        for (int i=0; i<numRecords; i++) {
            short platformID = buffer.getShort();
            if (platformID != MS_PLATFORM_ID &&
                platformID != MAC_PLATFORM_ID) {
                buffer.skip(10);
                continue; // skip over this record.
            }
            short encodingID = buffer.getShort();
            // only want UTF-16 (inc. symbol) encodingIDs for Windows,
            // or MacRoman on Mac.
            if ((platformID == MS_PLATFORM_ID && encodingID > 1) ||
                (platformID == MAC_PLATFORM_ID &&
                 encodingID != MACROMAN_SPECIFIC_ID)) {
                buffer.skip(8);
                continue;
            }
            short langID     = buffer.getShort();
            if (platformID == MAC_PLATFORM_ID &&
                langID != MACROMAN_ENGLISH_LANG) {
                buffer.skip(6);
                continue;
            }
            short nameID     = buffer.getShort();
            int nameLen    = ((int)buffer.getShort()) & 0xffff;
            int namePtr    = (((int)buffer.getShort()) & 0xffff) + stringPtr;
            String tmpName = null;
            String enc;
            switch (nameID) {

            case FAMILY_NAME_ID:

                if (familyName == null || langID == MS_ENGLISH_LOCALE_ID ||
                    langID == nameLocaleID)
                    {
                        buffer.get(namePtr, name, 0, nameLen);
                        if (platformID == MAC_PLATFORM_ID) {
                            enc = "US-ASCII";
                        } else {
                            enc = "UTF-16BE";
                        }
                        tmpName = new String(name, 0, nameLen, enc);

                        if (familyName == null ||
                            langID == MS_ENGLISH_LOCALE_ID){
                            familyName = tmpName;
                        }
                        if (langID == nameLocaleID) {
                            localeFamilyName = tmpName;
                        }
                    }
                    break;

                case FULL_NAME_ID:

                    if (fullName == null ||
                        langID == MS_ENGLISH_LOCALE_ID ||
                        langID == nameLocaleID)
                    {
                        buffer.get(namePtr, name, 0, nameLen);
                        if (platformID == MAC_PLATFORM_ID) {
                            enc = "US-ASCII";
                        } else {
                            enc = "UTF-16BE";
                        }
                        tmpName = new String(name, 0, nameLen, enc);

                        if (fullName == null ||
                            langID == MS_ENGLISH_LOCALE_ID) {
                            fullName = tmpName;
                        }
                        if (langID == nameLocaleID) {
                            localeFullName = tmpName;
                        }
                    }
                    break;

                case PS_NAME_ID:

                    if (psName == null) {
                        buffer.get(namePtr, name, 0, nameLen);
                        if (platformID == MAC_PLATFORM_ID) {
                            enc = "US-ASCII";
                        } else {
                            enc = "UTF-16BE";
                        }
                        psName = new String(name, 0, nameLen, enc);
                    }
                    break;

                case STYLE_NAME_ID:

                    if (styleName == null ||
                        langID == MS_ENGLISH_LOCALE_ID ||
                        langID == nameLocaleID)
                    {
                        buffer.get(namePtr, name, 0, nameLen);
                        if (platformID == MAC_PLATFORM_ID) {
                            enc = "US-ASCII";
                        } else {
                            enc = "UTF-16BE";
                        }
                        tmpName = new String(name, 0, nameLen, enc);

                        if (styleName == null ||
                            langID == MS_ENGLISH_LOCALE_ID) {
                            styleName = tmpName;
                        }
                        if (langID == nameLocaleID) {
                            localeStyleName = tmpName;
                        }
                    }
                    break;

            default:
                break;
            }

            if (localeFamilyName == null) {
                localeFamilyName = familyName;
            }
            if (localeFullName == null) {
                localeFullName = fullName;
            }
            if (localeStyleName == null) {
                localeStyleName = styleName;
            }
        }
    }

    private void checkCMAP() throws Exception {
        DirectoryEntry cmapDE = getDirectoryEntry(FontConstants.cmapTag);
        if (cmapDE != null) {
            if (cmapDE.length < 4) {
                throw new Exception("Invalid cmap table length");
            }
            Buffer cmapTableHeader = filereader.readBlock(cmapDE.offset, 4);
            short version = cmapTableHeader.getShort();
            short numberSubTables = cmapTableHeader.getShort();
            int indexLength = numberSubTables * 8;
            if (numberSubTables <= 0 || cmapDE.length < indexLength + 4) {
                throw new Exception("Invalid cmap subtables count");
            }
            Buffer cmapTableIndex = filereader.readBlock(cmapDE.offset + 4, indexLength);
            for (int i = 0; i < numberSubTables; i++) {
                short platformID = cmapTableIndex.getShort();
                short encodingID = cmapTableIndex.getShort();
                int offset = cmapTableIndex.getInt();
                if (offset < 0 || offset >= cmapDE.length) {
                    throw new Exception("Invalid cmap subtable offset");
                }
            }
        }
    }

    /*** BEGIN LOCALE_ID MAPPING ****/

    private static Map<String, Short> lcidMap;

    // Return a Microsoft LCID from the given Locale.
    // Used when getting localized font data.

    private static void addLCIDMapEntry(Map<String, Short> map,
                                        String key, short value) {
        map.put(key, Short.valueOf(value));
    }

    private static synchronized void createLCIDMap() {
        if (lcidMap != null) {
            return;
        }

        Map<String, Short> map = new HashMap<String, Short>(200);
        addLCIDMapEntry(map, "ar", (short) 0x0401);
        addLCIDMapEntry(map, "bg", (short) 0x0402);
        addLCIDMapEntry(map, "ca", (short) 0x0403);
        addLCIDMapEntry(map, "zh", (short) 0x0404);
        addLCIDMapEntry(map, "cs", (short) 0x0405);
        addLCIDMapEntry(map, "da", (short) 0x0406);
        addLCIDMapEntry(map, "de", (short) 0x0407);
        addLCIDMapEntry(map, "el", (short) 0x0408);
        addLCIDMapEntry(map, "es", (short) 0x040a);
        addLCIDMapEntry(map, "fi", (short) 0x040b);
        addLCIDMapEntry(map, "fr", (short) 0x040c);
        addLCIDMapEntry(map, "iw", (short) 0x040d);
        addLCIDMapEntry(map, "hu", (short) 0x040e);
        addLCIDMapEntry(map, "is", (short) 0x040f);
        addLCIDMapEntry(map, "it", (short) 0x0410);
        addLCIDMapEntry(map, "ja", (short) 0x0411);
        addLCIDMapEntry(map, "ko", (short) 0x0412);
        addLCIDMapEntry(map, "nl", (short) 0x0413);
        addLCIDMapEntry(map, "no", (short) 0x0414);
        addLCIDMapEntry(map, "pl", (short) 0x0415);
        addLCIDMapEntry(map, "pt", (short) 0x0416);
        addLCIDMapEntry(map, "rm", (short) 0x0417);
        addLCIDMapEntry(map, "ro", (short) 0x0418);
        addLCIDMapEntry(map, "ru", (short) 0x0419);
        addLCIDMapEntry(map, "hr", (short) 0x041a);
        addLCIDMapEntry(map, "sk", (short) 0x041b);
        addLCIDMapEntry(map, "sq", (short) 0x041c);
        addLCIDMapEntry(map, "sv", (short) 0x041d);
        addLCIDMapEntry(map, "th", (short) 0x041e);
        addLCIDMapEntry(map, "tr", (short) 0x041f);
        addLCIDMapEntry(map, "ur", (short) 0x0420);
        addLCIDMapEntry(map, "in", (short) 0x0421);
        addLCIDMapEntry(map, "uk", (short) 0x0422);
        addLCIDMapEntry(map, "be", (short) 0x0423);
        addLCIDMapEntry(map, "sl", (short) 0x0424);
        addLCIDMapEntry(map, "et", (short) 0x0425);
        addLCIDMapEntry(map, "lv", (short) 0x0426);
        addLCIDMapEntry(map, "lt", (short) 0x0427);
        addLCIDMapEntry(map, "fa", (short) 0x0429);
        addLCIDMapEntry(map, "vi", (short) 0x042a);
        addLCIDMapEntry(map, "hy", (short) 0x042b);
        addLCIDMapEntry(map, "eu", (short) 0x042d);
        addLCIDMapEntry(map, "mk", (short) 0x042f);
        addLCIDMapEntry(map, "tn", (short) 0x0432);
        addLCIDMapEntry(map, "xh", (short) 0x0434);
        addLCIDMapEntry(map, "zu", (short) 0x0435);
        addLCIDMapEntry(map, "af", (short) 0x0436);
        addLCIDMapEntry(map, "ka", (short) 0x0437);
        addLCIDMapEntry(map, "fo", (short) 0x0438);
        addLCIDMapEntry(map, "hi", (short) 0x0439);
        addLCIDMapEntry(map, "mt", (short) 0x043a);
        addLCIDMapEntry(map, "se", (short) 0x043b);
        addLCIDMapEntry(map, "gd", (short) 0x043c);
        addLCIDMapEntry(map, "ms", (short) 0x043e);
        addLCIDMapEntry(map, "kk", (short) 0x043f);
        addLCIDMapEntry(map, "ky", (short) 0x0440);
        addLCIDMapEntry(map, "sw", (short) 0x0441);
        addLCIDMapEntry(map, "tt", (short) 0x0444);
        addLCIDMapEntry(map, "bn", (short) 0x0445);
        addLCIDMapEntry(map, "pa", (short) 0x0446);
        addLCIDMapEntry(map, "gu", (short) 0x0447);
        addLCIDMapEntry(map, "ta", (short) 0x0449);
        addLCIDMapEntry(map, "te", (short) 0x044a);
        addLCIDMapEntry(map, "kn", (short) 0x044b);
        addLCIDMapEntry(map, "ml", (short) 0x044c);
        addLCIDMapEntry(map, "mr", (short) 0x044e);
        addLCIDMapEntry(map, "sa", (short) 0x044f);
        addLCIDMapEntry(map, "mn", (short) 0x0450);
        addLCIDMapEntry(map, "cy", (short) 0x0452);
        addLCIDMapEntry(map, "gl", (short) 0x0456);
        addLCIDMapEntry(map, "dv", (short) 0x0465);
        addLCIDMapEntry(map, "qu", (short) 0x046b);
        addLCIDMapEntry(map, "mi", (short) 0x0481);
        addLCIDMapEntry(map, "ar_IQ", (short) 0x0801);
        addLCIDMapEntry(map, "zh_CN", (short) 0x0804);
        addLCIDMapEntry(map, "de_CH", (short) 0x0807);
        addLCIDMapEntry(map, "en_GB", (short) 0x0809);
        addLCIDMapEntry(map, "es_MX", (short) 0x080a);
        addLCIDMapEntry(map, "fr_BE", (short) 0x080c);
        addLCIDMapEntry(map, "it_CH", (short) 0x0810);
        addLCIDMapEntry(map, "nl_BE", (short) 0x0813);
        addLCIDMapEntry(map, "no_NO_NY", (short) 0x0814);
        addLCIDMapEntry(map, "pt_PT", (short) 0x0816);
        addLCIDMapEntry(map, "ro_MD", (short) 0x0818);
        addLCIDMapEntry(map, "ru_MD", (short) 0x0819);
        addLCIDMapEntry(map, "sr_CS", (short) 0x081a);
        addLCIDMapEntry(map, "sv_FI", (short) 0x081d);
        addLCIDMapEntry(map, "az_AZ", (short) 0x082c);
        addLCIDMapEntry(map, "se_SE", (short) 0x083b);
        addLCIDMapEntry(map, "ga_IE", (short) 0x083c);
        addLCIDMapEntry(map, "ms_BN", (short) 0x083e);
        addLCIDMapEntry(map, "uz_UZ", (short) 0x0843);
        addLCIDMapEntry(map, "qu_EC", (short) 0x086b);
        addLCIDMapEntry(map, "ar_EG", (short) 0x0c01);
        addLCIDMapEntry(map, "zh_HK", (short) 0x0c04);
        addLCIDMapEntry(map, "de_AT", (short) 0x0c07);
        addLCIDMapEntry(map, "en_AU", (short) 0x0c09);
        addLCIDMapEntry(map, "fr_CA", (short) 0x0c0c);
        addLCIDMapEntry(map, "sr_CS", (short) 0x0c1a);
        addLCIDMapEntry(map, "se_FI", (short) 0x0c3b);
        addLCIDMapEntry(map, "qu_PE", (short) 0x0c6b);
        addLCIDMapEntry(map, "ar_LY", (short) 0x1001);
        addLCIDMapEntry(map, "zh_SG", (short) 0x1004);
        addLCIDMapEntry(map, "de_LU", (short) 0x1007);
        addLCIDMapEntry(map, "en_CA", (short) 0x1009);
        addLCIDMapEntry(map, "es_GT", (short) 0x100a);
        addLCIDMapEntry(map, "fr_CH", (short) 0x100c);
        addLCIDMapEntry(map, "hr_BA", (short) 0x101a);
        addLCIDMapEntry(map, "ar_DZ", (short) 0x1401);
        addLCIDMapEntry(map, "zh_MO", (short) 0x1404);
        addLCIDMapEntry(map, "de_LI", (short) 0x1407);
        addLCIDMapEntry(map, "en_NZ", (short) 0x1409);
        addLCIDMapEntry(map, "es_CR", (short) 0x140a);
        addLCIDMapEntry(map, "fr_LU", (short) 0x140c);
        addLCIDMapEntry(map, "bs_BA", (short) 0x141a);
        addLCIDMapEntry(map, "ar_MA", (short) 0x1801);
        addLCIDMapEntry(map, "en_IE", (short) 0x1809);
        addLCIDMapEntry(map, "es_PA", (short) 0x180a);
        addLCIDMapEntry(map, "fr_MC", (short) 0x180c);
        addLCIDMapEntry(map, "sr_BA", (short) 0x181a);
        addLCIDMapEntry(map, "ar_TN", (short) 0x1c01);
        addLCIDMapEntry(map, "en_ZA", (short) 0x1c09);
        addLCIDMapEntry(map, "es_DO", (short) 0x1c0a);
        addLCIDMapEntry(map, "sr_BA", (short) 0x1c1a);
        addLCIDMapEntry(map, "ar_OM", (short) 0x2001);
        addLCIDMapEntry(map, "en_JM", (short) 0x2009);
        addLCIDMapEntry(map, "es_VE", (short) 0x200a);
        addLCIDMapEntry(map, "ar_YE", (short) 0x2401);
        addLCIDMapEntry(map, "es_CO", (short) 0x240a);
        addLCIDMapEntry(map, "ar_SY", (short) 0x2801);
        addLCIDMapEntry(map, "en_BZ", (short) 0x2809);
        addLCIDMapEntry(map, "es_PE", (short) 0x280a);
        addLCIDMapEntry(map, "ar_JO", (short) 0x2c01);
        addLCIDMapEntry(map, "en_TT", (short) 0x2c09);
        addLCIDMapEntry(map, "es_AR", (short) 0x2c0a);
        addLCIDMapEntry(map, "ar_LB", (short) 0x3001);
        addLCIDMapEntry(map, "en_ZW", (short) 0x3009);
        addLCIDMapEntry(map, "es_EC", (short) 0x300a);
        addLCIDMapEntry(map, "ar_KW", (short) 0x3401);
        addLCIDMapEntry(map, "en_PH", (short) 0x3409);
        addLCIDMapEntry(map, "es_CL", (short) 0x340a);
        addLCIDMapEntry(map, "ar_AE", (short) 0x3801);
        addLCIDMapEntry(map, "es_UY", (short) 0x380a);
        addLCIDMapEntry(map, "ar_BH", (short) 0x3c01);
        addLCIDMapEntry(map, "es_PY", (short) 0x3c0a);
        addLCIDMapEntry(map, "ar_QA", (short) 0x4001);
        addLCIDMapEntry(map, "es_BO", (short) 0x400a);
        addLCIDMapEntry(map, "es_SV", (short) 0x440a);
        addLCIDMapEntry(map, "es_HN", (short) 0x480a);
        addLCIDMapEntry(map, "es_NI", (short) 0x4c0a);
        addLCIDMapEntry(map, "es_PR", (short) 0x500a);

        lcidMap = map;
    }

    private static short getLCIDFromLocale(Locale locale) {
        // optimize for common case
        if (locale.equals(Locale.US) || locale.getLanguage().equals("en")) {
            return MS_ENGLISH_LOCALE_ID;
        }

        if (lcidMap == null) {
            createLCIDMap();
        }

        String key = locale.toString();
        while (!key.isEmpty()) {
            Short lcidObject = (Short) lcidMap.get(key);
            if (lcidObject != null) {
                return lcidObject.shortValue();
            }
            int pos = key.lastIndexOf('_');
            if (pos < 1) {
                return MS_ENGLISH_LOCALE_ID;
            }
            key = key.substring(0, pos);
        }

        return MS_ENGLISH_LOCALE_ID;
    }


    /* On Windows this is set to the System Locale, which matches how
     * GDI enumerates font names. For display purposes we may want
     * the user locale which could be different.
     */
    static short nameLocaleID = getSystemLCID();

    private static short getSystemLCID() {
        if (PrismFontFactory.isWindows) {
            return PrismFontFactory.getSystemLCID();
        } else {
            return getLCIDFromLocale(Locale.getDefault());
        }
    }

    private OpenTypeGlyphMapper mapper = null;

    public CharToGlyphMapper getGlyphMapper() {
        if (mapper == null) {
            mapper = new OpenTypeGlyphMapper(this);
        }
        return mapper;
    }

    public FontStrike getStrike(float size, BaseTransform transform) {
        return getStrike(size, transform, getDefaultAAMode());
    }

    char[] advanceWidths = null;
    /*
     * This is returning the unhinted advance, should be OK so
     * long as we do unhinted rendering. If we are doing hinted glyphs
     * and I suppose, integer metrics, then we can use the hdmx table.
     * But since the hdmx table doesn't provide anything except integers
     * it will only be useful for some cases. Also even then the ptSize
     * alone doesn't help, since we need to know the graphics scale
     * to know the real glyph size that's required, then of course we
     * have to translate that back into user space. So all of that will
     * need to be looked into, or we reserve this path for unhinted rendering.
     * Note that if there's no hdmx entry for a given size, then we need
     * to scale the glyph to get the hinted advance. However before doing
     * so we should consult the 'gasp' table to see it its a size at
     * which hinting should be performed anyway.
     * (1) The GASP table indicates size at which hinting should be applied
     * usually this is all larger sizes so probably wouldn't help, however
     * (2) If there is a LTSH (Linear Threshold) table, we can use that
     * to see if for the requested 'ppem' size, the glyph scales linearly.
     *
     * Interestingly Amble sets the 'head' flags bit to say non-linear
     * scaling and so legitimately has a LTSH table but this all may be
     * a hold-over from when its gasp table said to apply hints at some sizes.
     * I suppose I am not 100% certain if the gasp table can be trusted to
     * use as a short-cut for when you don't need to scale, or if choosing
     * not to hint means you can always just assume linear scaling, but I
     * do find that to be consistent with the data in Microsoft fonts where
     * they do not provide hdmx entry for sizes below that where hinting is
     * required, suggesting the htmx table is fine for such cases.
     */
    public float getAdvance(int glyphCode, float ptSize) {
        if (glyphCode == CharToGlyphMapper.INVISIBLE_GLYPH_ID)
            return 0f;

        // If we haven't initialised yet, do so now.
        if (advanceWidths == null && numHMetrics > 0) {
            synchronized (this) {
                Buffer hmtx = readTable(hmtxTag);
                if (hmtx == null) {
                    numHMetrics = -1;
                    return 0;
                }
                char[] aw = new char[numHMetrics];
                for (int i=0; i<numHMetrics; i++) {
                    aw[i] = hmtx.getChar(i*4);
                }
                advanceWidths = aw;
            }
        }

        // If we have a valid numHMetrics, look up the advance
        if (numHMetrics > 0) {
            char cadv;
            if (glyphCode < numHMetrics) {
                cadv = advanceWidths[glyphCode];
            } else {
                cadv = advanceWidths[numHMetrics-1];
            }
            return ((float)(cadv & 0xffff)*ptSize)/upem;
        } else { // no valid lookup.
            return 0f;
        }
    }

    public PrismMetrics getFontMetrics(float ptSize) {
        return new PrismMetrics((ascent*ptSize)/upem,
                              (descent*ptSize)/upem,
                              (linegap*ptSize)/upem,
                              this, ptSize);
    }

    private float[] styleMetrics;
    float[] getStyleMetrics(float ptSize) {
        if (styleMetrics == null) {
            float [] smetrics = new float[METRICS_TOTAL];

            Buffer os_2 = readTable(os_2Tag);
            int length = os_2 != null ? os_2.capacity() : 0;

            if (length >= 30) {
                smetrics[STRIKETHROUGH_THICKNESS] = os_2.getShort(26) / upem;
                smetrics[STRIKETHROUGH_OFFSET] = -os_2.getShort(28) / upem;
            } else {
                smetrics[STRIKETHROUGH_THICKNESS] = 0.05f;
                smetrics[STRIKETHROUGH_OFFSET] = -0.4f;
            }
            if (length >= 74) {
                // ascent, descent, leading are set in constructor
                smetrics[TYPO_ASCENT] = -os_2.getShort(68) / upem;
                smetrics[TYPO_DESCENT] = -os_2.getShort(70) / upem;
                smetrics[TYPO_LINEGAP] = os_2.getShort(72) / upem;
            } else {
                smetrics[TYPO_ASCENT] = ascent / upem;
                smetrics[TYPO_DESCENT] = descent / upem;
                smetrics[TYPO_LINEGAP] = linegap / upem;
            }
            // REMIND : OpenType spec introduced xHeight, many fonts
            // won't have this info.
            // xHeight should be available in OS2 font table ver. 3 or greater
            if (length >= 90) {
                smetrics[XHEIGHT] = os_2.getShort(86) / upem;
                smetrics[CAPHEIGHT] = os_2.getShort(88);

                /* Some fonts have bad values for capHeight. For example,
                 * Comic Sans MS. The fix is to ignore the capHeight in the
                 * font file when it is less than half of the ascent */
                if ((smetrics[CAPHEIGHT] / ascent) < 0.5) {
                    smetrics[CAPHEIGHT] = 0;
                } else {
                    smetrics[CAPHEIGHT] /= upem;
                }
            }

            if (smetrics[XHEIGHT] == 0 || smetrics[CAPHEIGHT] == 0) {
                FontStrike strike = getStrike(ptSize, BaseTransform.IDENTITY_TRANSFORM);
                CharToGlyphMapper mapper = getGlyphMapper();
                int missingGlyph = mapper.getMissingGlyphCode();

                if (smetrics[XHEIGHT] == 0) {
                    int gc = mapper.charToGlyph('x');
                    if (gc != missingGlyph) {
                        RectBounds fbds = strike.getGlyph(gc).getBBox();
                        smetrics[XHEIGHT] = fbds.getHeight() / ptSize;
                    } else {
                        smetrics[XHEIGHT] = -ascent * 0.6f / upem;
                    }
                }
                if (smetrics[CAPHEIGHT] == 0) {
                    int gc = mapper.charToGlyph('H');
                    if (gc != missingGlyph) {
                        RectBounds fbds = strike.getGlyph(gc).getBBox();
                        smetrics[CAPHEIGHT] = fbds.getHeight() / ptSize;
                    } else {
                        smetrics[CAPHEIGHT] = -ascent * 0.9f / upem;
                    }
                }
            }

            Buffer postTable = readTable(postTag);
            if (postTable == null || postTable.capacity() < 12) {
                smetrics[UNDERLINE_OFFSET] = 0.1f;
                smetrics[UNDERLINE_THICKESS] = 0.05f;
            } else {
                smetrics[UNDERLINE_OFFSET] = -postTable.getShort(8) / upem;
                smetrics[UNDERLINE_THICKESS] = postTable.getShort(10) / upem;
            }
            styleMetrics = smetrics;
        }

        float[] metrics = new float[METRICS_TOTAL];
        for (int i = 0; i < METRICS_TOTAL; i++) {
            metrics[i] = styleMetrics[i] * ptSize;
        }

        return metrics;
    }

    byte[] getTableBytes(int tag) {
        Buffer buffer = readTable(tag);
        byte[] table = null;
        if(buffer != null){
            table = new byte[buffer.capacity()];
            buffer.get(0, table, 0, buffer.capacity());
        }
        return table;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof PrismFontFile)) {
            return false;
        }
        final PrismFontFile other = (PrismFontFile)obj;
        return filename.equals(other.filename) && fullName.equals(other.fullName);
    }

    @Override
    public int hashCode() {
        return filename.hashCode() + (71 * fullName.hashCode());
    }
}
