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

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.fontbox.cff.CFFFont.Mapping;
import org.apache.fontbox.cff.charset.CFFCharset;
import org.apache.fontbox.cff.charset.CFFExpertCharset;
import org.apache.fontbox.cff.charset.CFFExpertSubsetCharset;
import org.apache.fontbox.cff.charset.CFFISOAdobeCharset;
import org.apache.fontbox.cff.encoding.CFFEncoding;
import org.apache.fontbox.cff.encoding.CFFExpertEncoding;
import org.apache.fontbox.cff.encoding.CFFStandardEncoding;

/**
 * This class represents a parser for a CFF font. 
 * @author Villu Ruusmann
 * @version $Revision: 1.0 $
 */
public class CFFParser
{

    private static final String TAG_OTTO = "OTTO";
    private static final String TAG_TTCF = "ttcf";
    private static final String TAG_TTFONLY = "\u0000\u0001\u0000\u0000";

    private CFFDataInput input = null;
    private Header header = null;
    private IndexData nameIndex = null;
    private IndexData topDictIndex = null;
    private IndexData stringIndex = null;

    /**
     * Parsing CFF Font using a byte array as input.
     * @param bytes the given byte array
     * @return the parsed CFF fonts
     * @throws IOException If there is an error reading from the stream
     */
    public List<CFFFont> parse(byte[] bytes) throws IOException
    {
        input = new CFFDataInput(bytes);

        String firstTag = readTagName(input);
        // try to determine which kind of font we have
        if (TAG_OTTO.equals(firstTag))
        {
            // this is OpenType font containing CFF data
            // so find CFF tag
            short numTables = input.readShort();
            short searchRange = input.readShort();
            short entrySelector = input.readShort();
            short rangeShift = input.readShort();

            boolean cffFound = false;
            for (int q = 0; q < numTables; q++)
            {
                String tagName = readTagName(input);
                long checksum = readLong(input);
                long offset = readLong(input);
                long length = readLong(input);
                if (tagName.equals("CFF "))
                {
                    cffFound = true;
                    byte[] bytes2 = new byte[(int) length];
                    System.arraycopy(bytes, (int) offset, bytes2, 0, bytes2.length);
                    input = new CFFDataInput(bytes2);
                    break;
                }
            }
            if (!cffFound)
            {
                throw new IOException("CFF tag not found in this OpenType font.");
            }
        }
        else if (TAG_TTCF.equals(firstTag))
        {
            throw new IOException("True Type Collection fonts are not supported.");
        }
        else if (TAG_TTFONLY.equals(firstTag))
        {
            throw new IOException("OpenType fonts containing a true type font are not supported.");
        }
        else
        {
            input.setPosition(0);
        }

        header = readHeader(input);
        nameIndex = readIndexData(input);
        topDictIndex = readIndexData(input);
        stringIndex = readIndexData(input);
        IndexData globalSubrIndex = readIndexData(input);

        List<CFFFont> fonts = new ArrayList<CFFFont>();
        for (int i = 0; i < nameIndex.getCount(); i++)
        {
            CFFFont font = parseFont(i);
            font.setGlobalSubrIndex(globalSubrIndex);
            fonts.add(font);
        }
        return fonts;
    }

    private static String readTagName(CFFDataInput input) throws IOException
    {
        byte[] b = input.readBytes(4);
        return new String(b, "ISO-8859-1");
    }

    private static long readLong(CFFDataInput input) throws IOException
    {
        return (input.readCard16() << 16) | input.readCard16();
    }

    private static Header readHeader(CFFDataInput input) throws IOException
    {
        Header cffHeader = new Header();
        cffHeader.major = input.readCard8();
        cffHeader.minor = input.readCard8();
        cffHeader.hdrSize = input.readCard8();
        cffHeader.offSize = input.readOffSize();
        return cffHeader;
    }

    private static IndexData readIndexData(CFFDataInput input) throws IOException
    {
        int count = input.readCard16();
        IndexData index = new IndexData(count);
        if (count == 0)
        {
            return index;
        }
        int offSize = input.readOffSize();
        for (int i = 0; i <= count; i++)
        {
            int offset = input.readOffset(offSize);
            if (offset > input.length())
            {
                throw new IOException("illegal offset value " + offset + " in CFF font");
            }
            index.setOffset(i, offset);
        }
        int dataSize = index.getOffset(count) - index.getOffset(0);
        index.initData(dataSize);
        for (int i = 0; i < dataSize; i++)
        {
            index.setData(i, input.readCard8());
        }
        return index;
    }

    private static DictData readDictData(CFFDataInput input) throws IOException
    {
        DictData dict = new DictData();
        dict.entries = new ArrayList<DictData.Entry>();
        while (input.hasRemaining())
        {
            DictData.Entry entry = readEntry(input);
            dict.entries.add(entry);
        }
        return dict;
    }

    private static DictData.Entry readEntry(CFFDataInput input) throws IOException
    {
        DictData.Entry entry = new DictData.Entry();
        while (true)
        {
            int b0 = input.readUnsignedByte();

            if (b0 >= 0 && b0 <= 21)
            {
                entry.operator = readOperator(input, b0);
                break;
            }
            else if (b0 == 28 || b0 == 29)
            {
                entry.operands.add(readIntegerNumber(input, b0));
            }
            else if (b0 == 30)
            {
                entry.operands.add(readRealNumber(input, b0));
            }
            else if (b0 >= 32 && b0 <= 254)
            {
                entry.operands.add(readIntegerNumber(input, b0));
            }
            else
            {
                throw new IllegalArgumentException();
            }
        }
        return entry;
    }

    private static CFFOperator readOperator(CFFDataInput input, int b0) throws IOException
    {
        CFFOperator.Key key = readOperatorKey(input, b0);
        return CFFOperator.getOperator(key);
    }

    private static CFFOperator.Key readOperatorKey(CFFDataInput input, int b0) throws IOException
    {
        if (b0 == 12)
        {
            int b1 = input.readUnsignedByte();
            return new CFFOperator.Key(b0, b1);
        }
        return new CFFOperator.Key(b0);
    }

    private static Integer readIntegerNumber(CFFDataInput input, int b0) throws IOException
    {
        if (b0 == 28)
        {
            int b1 = input.readUnsignedByte();
            int b2 = input.readUnsignedByte();
            return Integer.valueOf((short) (b1 << 8 | b2));
        }
        else if (b0 == 29)
        {
            int b1 = input.readUnsignedByte();
            int b2 = input.readUnsignedByte();
            int b3 = input.readUnsignedByte();
            int b4 = input.readUnsignedByte();
            return Integer.valueOf(b1 << 24 | b2 << 16 | b3 << 8 | b4);
        }
        else if (b0 >= 32 && b0 <= 246)
        {
            return Integer.valueOf(b0 - 139);
        }
        else if (b0 >= 247 && b0 <= 250)
        {
            int b1 = input.readUnsignedByte();
            return Integer.valueOf((b0 - 247) * 256 + b1 + 108);
        }
        else if (b0 >= 251 && b0 <= 254)
        {
            int b1 = input.readUnsignedByte();
            return Integer.valueOf(-(b0 - 251) * 256 - b1 - 108);
        }
        else
        {
            throw new IllegalArgumentException();
        }
    }

    private static Double readRealNumber(CFFDataInput input, int b0) throws IOException
    {
        StringBuffer sb = new StringBuffer();
        boolean done = false;
        boolean exponentMissing = false;
        while (!done)
        {
            int b = input.readUnsignedByte();
            int[] nibbles = { b / 16, b % 16 };
            for (int nibble : nibbles)
            {
                switch (nibble)
                {
                case 0x0:
                case 0x1:
                case 0x2:
                case 0x3:
                case 0x4:
                case 0x5:
                case 0x6:
                case 0x7:
                case 0x8:
                case 0x9:
                    sb.append(nibble);
                    exponentMissing = false;
                    break;
                case 0xa:
                    sb.append(".");
                    break;
                case 0xb:
                    sb.append("E");
                    exponentMissing = true;
                    break;
                case 0xc:
                    sb.append("E-");
                    exponentMissing = true;
                    break;
                case 0xd:
                    break;
                case 0xe:
                    sb.append("-");
                    break;
                case 0xf:
                    done = true;
                    break;
                default:
                    throw new IllegalArgumentException();
                }
            }
        }
        if (exponentMissing)
        {
            // the exponent is missing, just append "0" to avoid an exception
            // not sure if 0 is the correct value, but it seems to fit
            // see PDFBOX-1522
            sb.append("0");
        }
        return Double.valueOf(sb.toString());
    }

    private CFFFont parseFont(int index) throws IOException
    {
        CFFFont font = null;
        DataInput nameInput = new DataInput(nameIndex.getBytes(index));
        String name = nameInput.getString();

        CFFDataInput topDictInput = new CFFDataInput(topDictIndex.getBytes(index));
        DictData topDict = readDictData(topDictInput);
        DictData.Entry syntheticBaseEntry = topDict.getEntry("SyntheticBase");
        if (syntheticBaseEntry != null)
        {
            throw new IOException("Synthetic Fonts are not supported");
        }

        DictData.Entry rosEntry = topDict.getEntry("ROS");
        if (rosEntry != null)
        {
            font = new CFFFontROS();
            ((CFFFontROS) font).setRegistry(readString(rosEntry.getNumber(0).intValue()));
            ((CFFFontROS) font).setOrdering(readString(rosEntry.getNumber(1).intValue()));
            ((CFFFontROS) font).setSupplement(rosEntry.getNumber(2).intValue());
        }

        if (font == null)
        {
            // -- No specific behavior for this font
            font = new CFFFont();
        }

        font.setName(name);

        font.addValueToTopDict("version", getString(topDict, "version"));
        font.addValueToTopDict("Notice", getString(topDict, "Notice"));
        font.addValueToTopDict("Copyright", getString(topDict, "Copyright"));
        font.addValueToTopDict("FullName", getString(topDict, "FullName"));
        font.addValueToTopDict("FamilyName", getString(topDict, "FamilyName"));
        font.addValueToTopDict("Weight", getString(topDict, "Weight"));
        font.addValueToTopDict("isFixedPitch", getBoolean(topDict, "isFixedPitch", false));
        font.addValueToTopDict("ItalicAngle", getNumber(topDict, "ItalicAngle", 0));
        font.addValueToTopDict("UnderlinePosition", getNumber(topDict, "UnderlinePosition", -100));
        font.addValueToTopDict("UnderlineThickness", getNumber(topDict, "UnderlineThickness", 50));
        font.addValueToTopDict("PaintType", getNumber(topDict, "PaintType", 0));
        font.addValueToTopDict("CharstringType", getNumber(topDict, "CharstringType", 2));
        font.addValueToTopDict(
                "FontMatrix",
                getArray(
                        topDict,
                        "FontMatrix",
                        Arrays.<Number> asList(Double.valueOf(0.001), Double.valueOf(0), Double.valueOf(0),
                                Double.valueOf(0.001), Double.valueOf(0), Double.valueOf(0))));
        font.addValueToTopDict("UniqueID", getNumber(topDict, "UniqueID", null));
        font.addValueToTopDict(
                "FontBBox",
                getArray(
                        topDict,
                        "FontBBox",
                        Arrays.<Number> asList(Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0),
                                Integer.valueOf(0))));
        font.addValueToTopDict("StrokeWidth", getNumber(topDict, "StrokeWidth", 0));
        font.addValueToTopDict("XUID", getArray(topDict, "XUID", null));

        DictData.Entry charStringsEntry = topDict.getEntry("CharStrings");
        int charStringsOffset = charStringsEntry.getNumber(0).intValue();
        input.setPosition(charStringsOffset);
        IndexData charStringsIndex = readIndexData(input);
        DictData.Entry charsetEntry = topDict.getEntry("charset");
        CFFCharset charset;
        int charsetId = charsetEntry != null ? charsetEntry.getNumber(0).intValue() : 0;
        if (charsetId == 0)
        {
            charset = CFFISOAdobeCharset.getInstance();
        }
        else if (charsetId == 1)
        {
            charset = CFFExpertCharset.getInstance();
        }
        else if (charsetId == 2)
        {
            charset = CFFExpertSubsetCharset.getInstance();
        }
        else
        {
            input.setPosition(charsetId);
            charset = readCharset(input, charStringsIndex.getCount(), rosEntry != null);
        }
        font.setCharset(charset);
        font.getCharStringsDict().put(".notdef", charStringsIndex.getBytes(0));
        int[] gids = new int[charStringsIndex.getCount()];
        List<CFFCharset.Entry> glyphEntries = charset.getEntries();
        for (int i = 1; i < charStringsIndex.getCount(); i++)
        {
            CFFCharset.Entry glyphEntry = glyphEntries.get(i - 1);
            gids[i - 1] = glyphEntry.getSID();
            font.getCharStringsDict().put(glyphEntry.getName(), charStringsIndex.getBytes(i));
        }
        DictData.Entry encodingEntry = topDict.getEntry("Encoding");
        CFFEncoding encoding;
        int encodingId = encodingEntry != null ? encodingEntry.getNumber(0).intValue() : 0;
        if (encodingId == 0 || rosEntry != null) // --- ROS uses StandardEncoding
        {
            encoding = CFFStandardEncoding.getInstance();
        }
        else if (encodingId == 1)
        {
            encoding = CFFExpertEncoding.getInstance();
        }
        else
        {
            input.setPosition(encodingId);
            encoding = readEncoding(input, gids);
        }
        font.setEncoding(encoding);

        if (rosEntry != null)
        {
            // ---- It is a CIDKeyed Font, The Private Dictionary isn't in the Top Dict But in the Font Dict
            // ---- Font Dict can be accessed by the FDArray and FDSelect
            DictData.Entry fdArrayEntry = topDict.getEntry("FDArray");
            if (fdArrayEntry == null)
            {
                throw new IOException("FDArray is missing for a CIDKeyed Font.");
            }

            int fontDictOffset = fdArrayEntry.getNumber(0).intValue();
            input.setPosition(fontDictOffset);
            IndexData fdIndex = readIndexData(input);

            List<Map<String, Object>> privateDictionaries = new LinkedList<Map<String, Object>>();
            List<Map<String, Object>> fontDictionaries = new LinkedList<Map<String, Object>>();
            CFFFontROS fontRos = (CFFFontROS) font;

            for (int i = 0; i < fdIndex.getCount(); ++i)
            {
                byte[] b = fdIndex.getBytes(i);
                CFFDataInput fontDictInput = new CFFDataInput(b);
                DictData fontDictData = readDictData(fontDictInput);

                Map<String, Object> fontDictMap = new LinkedHashMap<String, Object>();
                fontDictMap.put("FontName", getString(fontDictData, "FontName"));
                fontDictMap.put("FontType", getNumber(fontDictData, "FontType", 0));
                fontDictMap.put("FontBBox", getDelta(fontDictData, "FontBBox", null));
                fontDictMap.put("FontMatrix", getDelta(fontDictData, "FontMatrix", null));
                // TODO OD-4 : Add here other keys
                fontDictionaries.add(fontDictMap);

                DictData.Entry privateEntry = fontDictData.getEntry("Private");
                // --- Font DICT is invalid without "Private" entry
                if (privateEntry == null)
                {
                    throw new IOException("Missing Private Dictionary");
                }

                int privateOffset = privateEntry.getNumber(1).intValue();
                input.setPosition(privateOffset);
                int privateSize = privateEntry.getNumber(0).intValue();
                CFFDataInput privateDictData = new CFFDataInput(input.readBytes(privateSize));
                DictData privateDict = readDictData(privateDictData);

                Map<String, Object> privDict = new LinkedHashMap<String, Object>();
                privDict.put("BlueValues", getDelta(privateDict, "BlueValues", null));
                privDict.put("OtherBlues", getDelta(privateDict, "OtherBlues", null));
                privDict.put("FamilyBlues", getDelta(privateDict, "FamilyBlues", null));
                privDict.put("FamilyOtherBlues", getDelta(privateDict, "FamilyOtherBlues", null));
                privDict.put("BlueScale", getNumber(privateDict, "BlueScale", Double.valueOf(0.039625)));
                privDict.put("BlueShift", getNumber(privateDict, "BlueShift", Integer.valueOf(7)));
                privDict.put("BlueFuzz", getNumber(privateDict, "BlueFuzz", Integer.valueOf(1)));
                privDict.put("StdHW", getNumber(privateDict, "StdHW", null));
                privDict.put("StdVW", getNumber(privateDict, "StdVW", null));
                privDict.put("StemSnapH", getDelta(privateDict, "StemSnapH", null));
                privDict.put("StemSnapV", getDelta(privateDict, "StemSnapV", null));
                privDict.put("ForceBold", getBoolean(privateDict, "ForceBold", false));
                privDict.put("LanguageGroup", getNumber(privateDict, "LanguageGroup", Integer.valueOf(0)));
                privDict.put("ExpansionFactor", getNumber(privateDict, "ExpansionFactor", Double.valueOf(0.06)));
                privDict.put("initialRandomSeed", getNumber(privateDict, "initialRandomSeed", Integer.valueOf(0)));
                privDict.put("defaultWidthX", getNumber(privateDict, "defaultWidthX", Integer.valueOf(0)));
                privDict.put("nominalWidthX", getNumber(privateDict, "nominalWidthX", Integer.valueOf(0)));

                int localSubrOffset = (Integer) getNumber(privateDict, "Subrs", Integer.valueOf(0));
                if (localSubrOffset == 0)
                {
                    font.setLocalSubrIndex(new IndexData(0));
                }
                else
                {
                    input.setPosition(privateOffset + localSubrOffset);
                    font.setLocalSubrIndex(readIndexData(input));
                }

                privateDictionaries.add(privDict);
            }

            fontRos.setFontDict(fontDictionaries);
            fontRos.setPrivDict(privateDictionaries);

            DictData.Entry fdSelectEntry = topDict.getEntry("FDSelect");
            int fdSelectPos = fdSelectEntry.getNumber(0).intValue();
            input.setPosition(fdSelectPos);
            CIDKeyedFDSelect fdSelect = readFDSelect(input, charStringsIndex.getCount(), fontRos);

            font.addValueToPrivateDict("defaultWidthX", Integer.valueOf(1000));
            font.addValueToPrivateDict("nominalWidthX", Integer.valueOf(0));

            fontRos.setFdSelect(fdSelect);

        }
        else
        {
            DictData.Entry privateEntry = topDict.getEntry("Private");
            int privateOffset = privateEntry.getNumber(1).intValue();
            input.setPosition(privateOffset);
            int privateSize = privateEntry.getNumber(0).intValue();
            CFFDataInput privateDictData = new CFFDataInput(input.readBytes(privateSize));
            DictData privateDict = readDictData(privateDictData);
            font.addValueToPrivateDict("BlueValues", getDelta(privateDict, "BlueValues", null));
            font.addValueToPrivateDict("OtherBlues", getDelta(privateDict, "OtherBlues", null));
            font.addValueToPrivateDict("FamilyBlues", getDelta(privateDict, "FamilyBlues", null));
            font.addValueToPrivateDict("FamilyOtherBlues", getDelta(privateDict, "FamilyOtherBlues", null));
            font.addValueToPrivateDict("BlueScale", getNumber(privateDict, "BlueScale", Double.valueOf(0.039625)));
            font.addValueToPrivateDict("BlueShift", getNumber(privateDict, "BlueShift", Integer.valueOf(7)));
            font.addValueToPrivateDict("BlueFuzz", getNumber(privateDict, "BlueFuzz", Integer.valueOf(1)));
            font.addValueToPrivateDict("StdHW", getNumber(privateDict, "StdHW", null));
            font.addValueToPrivateDict("StdVW", getNumber(privateDict, "StdVW", null));
            font.addValueToPrivateDict("StemSnapH", getDelta(privateDict, "StemSnapH", null));
            font.addValueToPrivateDict("StemSnapV", getDelta(privateDict, "StemSnapV", null));
            font.addValueToPrivateDict("ForceBold", getBoolean(privateDict, "ForceBold", false));
            font.addValueToPrivateDict("LanguageGroup", getNumber(privateDict, "LanguageGroup", Integer.valueOf(0)));
            font.addValueToPrivateDict("ExpansionFactor",
                    getNumber(privateDict, "ExpansionFactor", Double.valueOf(0.06)));
            font.addValueToPrivateDict("initialRandomSeed",
                    getNumber(privateDict, "initialRandomSeed", Integer.valueOf(0)));
            font.addValueToPrivateDict("defaultWidthX", getNumber(privateDict, "defaultWidthX", Integer.valueOf(0)));
            font.addValueToPrivateDict("nominalWidthX", getNumber(privateDict, "nominalWidthX", Integer.valueOf(0)));

            int localSubrOffset = (Integer) getNumber(privateDict, "Subrs", Integer.valueOf(0));
            if (localSubrOffset == 0)
            {
                font.setLocalSubrIndex(new IndexData(0));
            }
            else
            {
                input.setPosition(privateOffset + localSubrOffset);
                font.setLocalSubrIndex(readIndexData(input));
            }
        }

        return font;
    }

    private String readString(int index) throws IOException
    {
        if (index >= 0 && index <= 390)
        {
            return CFFStandardString.getName(index);
        }
        if (index - 391 < stringIndex.getCount())
        {
            DataInput dataInput = new DataInput(stringIndex.getBytes(index - 391));
            return dataInput.getString();
        }
        else
        {
            return CFFStandardString.getName(0);
        }
    }

    private String getString(DictData dict, String name) throws IOException
    {
        DictData.Entry entry = dict.getEntry(name);
        return (entry != null ? readString(entry.getNumber(0).intValue()) : null);
    }

    private Boolean getBoolean(DictData dict, String name, boolean defaultValue) throws IOException
    {
        DictData.Entry entry = dict.getEntry(name);
        return entry != null ? entry.getBoolean(0) : defaultValue;
    }

    private Number getNumber(DictData dict, String name, Number defaultValue) throws IOException
    {
        DictData.Entry entry = dict.getEntry(name);
        return entry != null ? entry.getNumber(0) : defaultValue;
    }

    // TODO Where is the difference to getDelta??
    private List<Number> getArray(DictData dict, String name, List<Number> defaultValue) throws IOException
    {
        DictData.Entry entry = dict.getEntry(name);
        return entry != null ? entry.getArray() : defaultValue;
    }

    // TODO Where is the difference to getArray??
    private List<Number> getDelta(DictData dict, String name, List<Number> defaultValue) throws IOException
    {
        DictData.Entry entry = dict.getEntry(name);
        return entry != null ? entry.getArray() : defaultValue;
    }

    private CFFEncoding readEncoding(CFFDataInput dataInput, int[] gids) throws IOException
    {
        int format = dataInput.readCard8();
        int baseFormat = format & 0x7f;

        if (baseFormat == 0)
        {
            return readFormat0Encoding(dataInput, format, gids);
        }
        else if (baseFormat == 1)
        {
            return readFormat1Encoding(dataInput, format, gids);
        }
        else
        {
            throw new IllegalArgumentException();
        }
    }

    private Format0Encoding readFormat0Encoding(CFFDataInput dataInput, int format, int[] gids) throws IOException
    {
        Format0Encoding encoding = new Format0Encoding();
        encoding.format = format;
        encoding.nCodes = dataInput.readCard8();
        encoding.code = new int[encoding.nCodes];
        for (int i = 0; i < encoding.code.length; i++)
        {
            encoding.code[i] = dataInput.readCard8();
            encoding.register(encoding.code[i], gids[i]);
        }
        if ((format & 0x80) != 0)
        {
            readSupplement(dataInput, encoding);
        }
        return encoding;
    }

    private Format1Encoding readFormat1Encoding(CFFDataInput dataInput, int format, int[] gids) throws IOException
    {
        Format1Encoding encoding = new Format1Encoding();
        encoding.format = format;
        encoding.nRanges = dataInput.readCard8();
        int count = 0;
        encoding.range = new Format1Encoding.Range1[encoding.nRanges];
        for (int i = 0; i < encoding.range.length; i++)
        {
            Format1Encoding.Range1 range = new Format1Encoding.Range1();
            range.first = dataInput.readCard8();
            range.nLeft = dataInput.readCard8();
            encoding.range[i] = range;
            for (int j = 0; j < 1 + range.nLeft; j++)
            {
                encoding.register(range.first + j, gids[count + j]);
            }
            count += 1 + range.nLeft;
        }
        if ((format & 0x80) != 0)
        {
            readSupplement(dataInput, encoding);
        }
        return encoding;
    }

    private void readSupplement(CFFDataInput dataInput, EmbeddedEncoding encoding) throws IOException
    {
        encoding.nSups = dataInput.readCard8();
        encoding.supplement = new EmbeddedEncoding.Supplement[encoding.nSups];
        for (int i = 0; i < encoding.supplement.length; i++)
        {
            EmbeddedEncoding.Supplement supplement = new EmbeddedEncoding.Supplement();
            supplement.code = dataInput.readCard8();
            supplement.glyph = dataInput.readSID();
            encoding.supplement[i] = supplement;
        }
    }

    /**
     * Read the FDSelect Data according to the format.
     * @param dataInput
     * @param nGlyphs
     * @param ros
     * @return the PDSelect data
     * @throws IOException
     */
    private CIDKeyedFDSelect readFDSelect(CFFDataInput dataInput, int nGlyphs, CFFFontROS ros) throws IOException
    {
        int format = dataInput.readCard8();
        if (format == 0)
        {
            return readFormat0FDSelect(dataInput, format, nGlyphs, ros);
        }
        else if (format == 3)
        {
            return readFormat3FDSelect(dataInput, format, nGlyphs, ros);
        }
        else
        {
            throw new IllegalArgumentException();
        }
    }

    /**
     * Read the Format 0 of the FDSelect data structure.
     * @param dataInput
     * @param format
     * @param nGlyphs
     * @param ros
     * @return the Format 0 of the FDSelect data structure
     * @throws IOException
     */
    private Format0FDSelect readFormat0FDSelect(CFFDataInput dataInput, int format, int nGlyphs, CFFFontROS ros)
            throws IOException
    {
        Format0FDSelect fdselect = new Format0FDSelect(ros);
        fdselect.format = format;
        fdselect.fds = new int[nGlyphs];
        for (int i = 0; i < fdselect.fds.length; i++)
        {
            fdselect.fds[i] = dataInput.readCard8();

        }
        return fdselect;
    }

    /**
     * Read the Format 3 of the FDSelect data structure.
     * 
     * @param dataInput
     * @param format
     * @param nGlyphs
     * @param ros
     * @return the Format 3 of the FDSelect data structure
     * @throws IOException
     */
    private Format3FDSelect readFormat3FDSelect(CFFDataInput dataInput, int format, int nGlyphs, CFFFontROS ros)
            throws IOException
    {
        Format3FDSelect fdselect = new Format3FDSelect(ros);
        fdselect.format = format;
        fdselect.nbRanges = dataInput.readCard16();

        fdselect.range3 = new Range3[fdselect.nbRanges];
        for (int i = 0; i < fdselect.nbRanges; i++)
        {
            Range3 r3 = new Range3();
            r3.first = dataInput.readCard16();
            r3.fd = dataInput.readCard8();
            fdselect.range3[i] = r3;

        }

        fdselect.sentinel = dataInput.readCard16();
        return fdselect;
    }

    /**
     *  Container of a Format 3 FDSelect data (see "The Compact Font Format Specification" chapter "FDSelect" ).
     */
    private static class Format3FDSelect extends CIDKeyedFDSelect
    {
        private int format;
        private int nbRanges;
        private Range3[] range3;
        private int sentinel;

        private Format3FDSelect(CFFFontROS owner)
        {
            super(owner);
        }

        /*
         * (non-Javadoc)
         * 
         * @see org.apache.fontbox.cff.CIDKeyedFDSelect#getFd(int)
         */
        @Override
        public int getFd(int glyph)
        {
            for (int i = 0; i < nbRanges; ++i)
            {
                if (range3[i].first >= glyph)
                {
                    if (i + 1 < nbRanges)
                    {
                        if (range3[i + 1].first > glyph)
                        {
                            return range3[i].fd;
                        }
                        else
                        {
                            // go to next range
                        }
                    }
                    else
                    {
                        // last range reach, the sentinel must be greater than glyph
                        if (sentinel > glyph)
                        {
                            return range3[i].fd;
                        }
                        else
                        {
                            return -1;
                        }
                    }
                }
            }
            return 0;
        }

        @Override
        public String toString()
        {
            return getClass().getName() + "[format=" + format + " nbRanges=" + nbRanges + ", range3="
                    + Arrays.toString(range3) + " sentinel=" + sentinel + "]";
        }
    }

    /**
     * Structure of a Range3 element.
     */
    private static class Range3
    {
        private int first;
        private int fd;

        @Override
        public String toString()
        {
            return getClass().getName() + "[first=" + first + ", fd=" + fd + "]";
        }
    }

    /**
     *  Container of a Format 0 FDSelect data (see "The Compact Font Format Specification" chapter "FDSelect" ).
     */
    private static class Format0FDSelect extends CIDKeyedFDSelect
    {
        private int format;
        private int[] fds;

        private Format0FDSelect(CFFFontROS owner)
        {
            super(owner);
        }

        /*
         * (non-Javadoc)
         * 
         * @see org.apache.fontbox.cff.CIDKeyedFDSelect#getFd(int)
         */
        @Override
        public int getFd(int glyph)
        {
            Map<String, byte[]> charString = owner.getCharStringsDict();
            Set<String> keys = charString.keySet();
            // ---- search the position of the given glyph
            for (Mapping mapping : owner.getMappings())
            {
                if (mapping.getSID() == glyph && charString.containsKey(mapping.getName()))
                {
                    int index = 0;
                    for (String str : keys)
                    {
                        if (mapping.getName().equals(str))
                        {
                            return fds[index];
                        }
                        ++index;
                    }
                }
            }
            return -1;
        }

        @Override
        public String toString()
        {
            return getClass().getName() + "[format=" + format + ", fds=" + Arrays.toString(fds) + "]";
        }
    }

    private CFFCharset readCharset(CFFDataInput dataInput, int nGlyphs, boolean isCID) throws IOException
    {
        int format = dataInput.readCard8();
        if (format == 0)
        {
            return readFormat0Charset(dataInput, format, nGlyphs, isCID);
        }
        else if (format == 1)
        {
            return readFormat1Charset(dataInput, format, nGlyphs, isCID);
        }
        else if (format == 2)
        {
            return readFormat2Charset(dataInput, format, nGlyphs, isCID);
        }
        else
        {
            throw new IllegalArgumentException();
        }
    }

    private Format0Charset readFormat0Charset(CFFDataInput dataInput, int format, int nGlyphs, boolean isCID) throws IOException
    {
        Format0Charset charset = new Format0Charset();
        charset.format = format;
        charset.glyph = new int[nGlyphs - 1];
        for (int i = 0; i < charset.glyph.length; i++)
        {
            charset.glyph[i] = dataInput.readSID();
            registerCharset(charset, charset.glyph[i], isCID);
        }
        return charset;
    }

    private Format1Charset readFormat1Charset(CFFDataInput dataInput, int format, int nGlyphs, boolean isCID) throws IOException
    {
        Format1Charset charset = new Format1Charset();
        charset.format = format;
        List<Format1Charset.Range1> ranges = new ArrayList<Format1Charset.Range1>();
        for (int i = 0; i < nGlyphs - 1;)
        {
            Format1Charset.Range1 range = new Format1Charset.Range1();
            range.first = dataInput.readSID();
            range.nLeft = dataInput.readCard8();
            ranges.add(range);
            for (int j = 0; j < 1 + range.nLeft; j++)
            {
                registerCharset(charset, range.first + j, isCID);
            }
            i += 1 + range.nLeft;
        }
        charset.range = ranges.toArray(new Format1Charset.Range1[0]);
        return charset;
    }

    private Format2Charset readFormat2Charset(CFFDataInput dataInput, int format, int nGlyphs, boolean isCID)
            throws IOException
    {
        Format2Charset charset = new Format2Charset();
        charset.format = format;
        charset.range = new Format2Charset.Range2[0];
        for (int i = 0; i < nGlyphs - 1;)
        {
            Format2Charset.Range2[] newRange = new Format2Charset.Range2[charset.range.length + 1];
            System.arraycopy(charset.range, 0, newRange, 0, charset.range.length);
            charset.range = newRange;
            Format2Charset.Range2 range = new Format2Charset.Range2();
            range.first = dataInput.readSID();
            range.nLeft = dataInput.readCard16();
            charset.range[charset.range.length - 1] = range;
            for (int j = 0; j < 1 + range.nLeft; j++)
            {
                registerCharset(charset, range.first + j, isCID);
            }
            i += 1 + range.nLeft;
        }
        return charset;
    }

    private void registerCharset(EmbeddedCharset charset, int sid, boolean isCID) throws IOException
    {
        // The charset data, although in the same format as non-CIDFonts,
        // will represent CIDs rather than SIDs, i.e. charstrings are "named" by CIDs in a CIDFont.
        String name = isCID ? String.valueOf(sid) : readString(sid);
        charset.register(sid, name);
    }

    /**
     * Inner class holding the header of a CFF font. 
     */
    private static class Header
    {
        private int major;
        private int minor;
        private int hdrSize;
        private int offSize;

        @Override
        public String toString()
        {
            return getClass().getName() + "[major=" + major + ", minor=" + minor + ", hdrSize=" + hdrSize
                    + ", offSize=" + offSize + "]";
        }
    }

    /**
     * Inner class holding the DictData of a CFF font. 
     */
    private static class DictData
    {

        private List<Entry> entries = null;

        public Entry getEntry(CFFOperator.Key key)
        {
            return getEntry(CFFOperator.getOperator(key));
        }

        public Entry getEntry(String name)
        {
            return getEntry(CFFOperator.getOperator(name));
        }

        private Entry getEntry(CFFOperator operator)
        {
            for (Entry entry : entries)
            {
                // Check for null entry before comparing the Font
                if (entry != null && entry.operator != null && entry.operator.equals(operator))
                {
                    return entry;
                }
            }
            return null;
        }

        /**
         * {@inheritDoc} 
         */
        public String toString()
        {
            return getClass().getName() + "[entries=" + entries + "]";
        }

        /**
         * Inner class holding an operand of a CFF font. 
         */
        private static class Entry
        {
            private List<Number> operands = new ArrayList<Number>();
            private CFFOperator operator = null;

            public Number getNumber(int index)
            {
                return operands.get(index);
            }

            public Boolean getBoolean(int index)
            {
                Number operand = operands.get(index);
                if (operand instanceof Integer)
                {
                    switch (operand.intValue())
                    {
                    case 0:
                        return Boolean.FALSE;
                    case 1:
                        return Boolean.TRUE;
                    default:
                        break;
                    }
                }
                throw new IllegalArgumentException();
            }

            // TODO unused??
            public Integer getSID(int index)
            {
                Number operand = operands.get(index);
                if (operand instanceof Integer)
                {
                    return (Integer) operand;
                }
                throw new IllegalArgumentException();
            }

            // TODO Where is the difference to getDelta??
            public List<Number> getArray()
            {
                return operands;
            }

            // TODO Where is the difference to getArray??
            public List<Number> getDelta()
            {
                return operands;
            }

            @Override
            public String toString()
            {
                return getClass().getName() + "[operands=" + operands + ", operator=" + operator + "]";
            }
        }
    }

    /**
     * Inner class representing an embedded CFF encoding. 
     */
    abstract static class EmbeddedEncoding extends CFFEncoding
    {

        private int nSups;
        private Supplement[] supplement;

        @Override
        public boolean isFontSpecific()
        {
            return true;
        }

        List<Supplement> getSupplements()
        {
            if (supplement == null)
            {
                return Collections.<Supplement> emptyList();
            }
            return Arrays.asList(supplement);
        }

        /**
         * Inner class representing a supplement for an encoding. 
         */
        static class Supplement
        {
            private int code;
            private int glyph;

            int getCode()
            {
                return code;
            }

            int getGlyph()
            {
                return glyph;
            }

            @Override
            public String toString()
            {
                return getClass().getName() + "[code=" + code + ", glyph=" + glyph + "]";
            }
        }
    }

    /**
     * Inner class representing a Format0 encoding. 
     */
    private static class Format0Encoding extends EmbeddedEncoding
    {
        private int format;
        private int nCodes;
        private int[] code;

        @Override
        public String toString()
        {
            return getClass().getName() + "[format=" + format + ", nCodes=" + nCodes + ", code="
                    + Arrays.toString(code) + ", supplement=" + Arrays.toString(super.supplement) + "]";
        }
    }

    /**
     * Inner class representing a Format1 encoding. 
     */
    private static class Format1Encoding extends EmbeddedEncoding
    {
        private int format;
        private int nRanges;
        private Range1[] range;

        @Override
        public String toString()
        {
            return getClass().getName() + "[format=" + format + ", nRanges=" + nRanges + ", range="
                    + Arrays.toString(range) + ", supplement=" + Arrays.toString(super.supplement) + "]";
        }

        /**
         * Inner class representing a range of an encoding. 
         */
        private static class Range1
        {
            private int first;
            private int nLeft;

            @Override
            public String toString()
            {
                return getClass().getName() + "[first=" + first + ", nLeft=" + nLeft + "]";
            }
        }
    }

    /**
     * Inner class representing an embedded CFF charset. 
     */
    abstract static class EmbeddedCharset extends CFFCharset
    {
        @Override
        public boolean isFontSpecific()
        {
            return true;
        }
    }

    /**
     * Inner class representing a Format0 charset. 
     */
    private static class Format0Charset extends EmbeddedCharset
    {
        private int format;
        private int[] glyph;

        @Override
        public String toString()
        {
            return getClass().getName() + "[format=" + format + ", glyph=" + Arrays.toString(glyph) + "]";
        }
    }

    /**
     * Inner class representing a Format1 charset. 
     */
    private static class Format1Charset extends EmbeddedCharset
    {
        private int format;
        private Range1[] range;

        @Override
        public String toString()
        {
            return getClass().getName() + "[format=" + format + ", range=" + Arrays.toString(range) + "]";
        }

        /**
         * Inner class representing a range of a charset. 
         */
        private static class Range1
        {
            private int first;
            private int nLeft;

            @Override
            public String toString()
            {
                return getClass().getName() + "[first=" + first + ", nLeft=" + nLeft + "]";
            }
        }
    }

    /**
     * Inner class representing a Format2 charset. 
     */
    private static class Format2Charset extends EmbeddedCharset
    {
        private int format;
        private Range2[] range;

        @Override
        public String toString()
        {
            return getClass().getName() + "[format=" + format + ", range=" + Arrays.toString(range) + "]";
        }

        /**
         * Inner class representing a range of a charset. 
         */
        private static class Range2
        {
            private int first;
            private int nLeft;

            @Override
            public String toString()
            {
                return getClass().getName() + "[first=" + first + ", nLeft=" + nLeft + "]";
            }
        }
    }
}
