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

/* $Id$ */

package org.apache.fop.fonts.truetype;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

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

import org.apache.fontbox.cff.CFFStandardString;

import org.apache.fop.fonts.MultiByteFont;
import org.apache.fop.fonts.cff.CFFDataReader;
import org.apache.fop.fonts.cff.CFFDataReader.CFFIndexData;
import org.apache.fop.fonts.cff.CFFDataReader.DICTEntry;
import org.apache.fop.fonts.cff.CFFDataReader.FDSelect;
import org.apache.fop.fonts.cff.CFFDataReader.FontDict;
import org.apache.fop.fonts.cff.CFFDataReader.Format0FDSelect;
import org.apache.fop.fonts.cff.CFFDataReader.Format3FDSelect;

/**
 * Reads an OpenType CFF file and generates a subset
 * The OpenType specification can be found at the Microsoft
 * Typography site: http://www.microsoft.com/typography/otspec/
 */
public class OTFSubSetFile extends OTFSubSetWriter {

    /** A map containing each glyph to be included in the subset
      * with their existing and new GID's **/
    protected Map<Integer, Integer> subsetGlyphs = new LinkedHashMap<Integer, Integer>();

    /** A map of the new GID to SID used to construct the charset table **/
    protected Map<Integer, Integer> gidToSID;

    protected CFFIndexData localIndexSubr;
    protected CFFIndexData globalIndexSubr;

    /** List of subroutines to write to the local / global indexes in the subset font **/
    protected List<byte[]> subsetLocalIndexSubr;
    protected List<byte[]> subsetGlobalIndexSubr;

    /** For fonts which have an FDSelect or ROS flag in Top Dict, this is used to store the
     * local subroutine indexes for each group as opposed to the above subsetLocalIndexSubr */
    private List<List<byte[]>> fdSubrs;

    /** The subset FD Select table used to store the mappings between glyphs and their
     * associated FDFont object which point to a private dict and local subroutines. */
    private Map<Integer, FDIndexReference> subsetFDSelect;

    /** A list of unique subroutines from the global / local subroutine indexes */
    protected List<Integer> localUniques;
    protected List<Integer> globalUniques;

    /** A store of the number of subroutines each global / local subroutine will store **/
    protected int subsetLocalSubrCount;
    protected int subsetGlobalSubrCount;

    /** A list of char string data for each glyph to be stored in the subset font **/
    protected List<byte[]> subsetCharStringsIndex;

    /** The embedded name to change in the name table **/
    protected String embeddedName;

    /** An array used to hold the string index data for the subset font **/
    protected List<byte[]> stringIndexData = new ArrayList<byte[]>();

    /** The CFF reader object used to read data and offsets from the original font file */
    protected CFFDataReader cffReader;

    /** The class used to represent this font **/
    private MultiByteFont mbFont;

    /** The number of standard strings in CFF **/
    public static final int NUM_STANDARD_STRINGS = 391;
    /** The operator used to identify a local subroutine reference */
    private static final int LOCAL_SUBROUTINE = 10;
    /** The operator used to identify a global subroutine reference */
    private static final int GLOBAL_SUBROUTINE = 29;
    /** The parser used to parse type2 charstring */
    private Type2Parser type2Parser;

    public OTFSubSetFile() throws IOException {
        super();
    }

    public void readFont(FontFileReader in, String embeddedName, String header,
            MultiByteFont mbFont) throws IOException {
        this.mbFont = mbFont;
        readFont(in, embeddedName, header, mbFont.getUsedGlyphs());
    }

    /**
     * Reads and creates a subset of the font.
     *
     * @param in FontFileReader to read from
     * @param embeddedName Name to be checked for in the font file
     * @param header The header of the font file
     * @param usedGlyphs Map of glyphs (glyphs has old index as (Integer) key and
     * new index as (Integer) value)
     * @throws IOException in case of an I/O problem
     */
    void readFont(FontFileReader in, String embeddedName, String header,
            Map<Integer, Integer> usedGlyphs) throws IOException {
        fontFile = in;

        currentPos = 0;
        realSize = 0;

        this.embeddedName = embeddedName;

        //Sort by the new GID and store in a LinkedHashMap
        subsetGlyphs = sortByValue(usedGlyphs);

        output = new byte[in.getFileSize()];

        initializeFont(in);

        cffReader = new CFFDataReader(fontFile);

        //Create the CIDFontType0C data
        createCFF();
    }

    private Map<Integer, Integer> sortByValue(Map<Integer, Integer> map) {
        List<Entry<Integer, Integer>> list = new ArrayList<Entry<Integer, Integer>>(map.entrySet());
        Collections.sort(list, new Comparator<Entry<Integer, Integer>>() {
             public int compare(Entry<Integer, Integer> o1, Entry<Integer, Integer> o2) {
                  return ((Comparable<Integer>) o1.getValue()).compareTo(o2.getValue());
             }
        });

       Map<Integer, Integer> result = new LinkedHashMap<Integer, Integer>();
       for (Entry<Integer, Integer> entry : list) {
           result.put(entry.getKey(), entry.getValue());
       }
       return result;
    }

    protected void createCFF() throws IOException {
        //Header
        writeBytes(cffReader.getHeader());

        //Name Index
        writeIndex(Arrays.asList(embedFontName.getBytes()));

        //Keep offset of the topDICT so it can be updated once all data has been written
        int topDictOffset = currentPos;
        //Top DICT Index and Data
        byte[] topDictIndex = cffReader.getTopDictIndex().getByteData();
        int offSize = topDictIndex[2];
        writeBytes(topDictIndex, 0, 3 + (offSize * 2));
        int topDictDataOffset = currentPos;
        writeTopDICT();

        //Create the char string index data and related local / global subroutines
        if (cffReader.getFDSelect() == null) {
            createCharStringData();
        } else {
            createCharStringDataCID();
        }

        //If it is a CID-Keyed font, store each FD font and add each SID
        List<Integer> fontNameSIDs = null;
        List<Integer> subsetFDFonts = null;
        if (cffReader.getFDSelect() != null) {
            subsetFDFonts = getUsedFDFonts();
            fontNameSIDs = storeFDStrings(subsetFDFonts);
        }

        //String index
        writeStringIndex();

        //Global subroutine index
        writeIndex(subsetGlobalIndexSubr);

        //Encoding
        int encodingOffset = currentPos;

        //Charset table
        int charsetOffset = currentPos;
        writeCharsetTable(cffReader.getFDSelect() != null);

        //FDSelect table
        int fdSelectOffset = currentPos;
        if (cffReader.getFDSelect() != null) {
            writeFDSelect();
        }

        //Char Strings Index
        int charStringOffset = currentPos;
        writeIndex(subsetCharStringsIndex);

        if (cffReader.getFDSelect() == null) {
            //Keep offset to modify later with the local subroutine index offset
            int privateDictOffset = currentPos;
            writePrivateDict();

            //Local subroutine index
            int localIndexOffset = currentPos;
            writeIndex(subsetLocalIndexSubr);

            //Update the offsets
            updateOffsets(topDictOffset, charsetOffset, charStringOffset, privateDictOffset,
                    localIndexOffset, encodingOffset);
        } else {
            List<Integer> privateDictOffsets = writeCIDDictsAndSubrs(subsetFDFonts);
            int fdArrayOffset = writeFDArray(subsetFDFonts, privateDictOffsets, fontNameSIDs);

            updateCIDOffsets(topDictDataOffset, fdArrayOffset, fdSelectOffset, charsetOffset,
                    charStringOffset, encodingOffset);
        }
    }

    protected List<Integer> storeFDStrings(List<Integer> uniqueNewRefs) throws IOException {
        List<Integer> fontNameSIDs = new ArrayList<Integer>();
        List<FontDict> fdFonts = cffReader.getFDFonts();
        for (Integer uniqueNewRef : uniqueNewRefs) {
            FontDict fdFont = fdFonts.get(uniqueNewRef);
            byte[] fdFontByteData = fdFont.getByteData();
            Map<String, DICTEntry> fdFontDict = cffReader.parseDictData(fdFontByteData);
            fontNameSIDs.add(stringIndexData.size() + NUM_STANDARD_STRINGS);
            stringIndexData.add(cffReader.getStringIndex().getValue(fdFontDict.get("FontName")
                    .getOperands().get(0).intValue() - NUM_STANDARD_STRINGS));
        }
        return fontNameSIDs;
    }

    protected void writeTopDICT() throws IOException {
        Map<String, DICTEntry> topDICT = cffReader.getTopDictEntries();
        List<String> topDictStringEntries = Arrays.asList("version", "Notice", "Copyright",
                "FullName", "FamilyName", "Weight", "PostScript");
        for (Map.Entry<String, DICTEntry> dictEntry : topDICT.entrySet()) {
            String dictKey = dictEntry.getKey();
            DICTEntry entry = dictEntry.getValue();
            //If the value is an SID, update the reference but keep the size the same
            if (dictKey.equals("ROS")) {
                writeROSEntry(entry);
            } else if (dictKey.equals("CIDCount")) {
                writeCIDCount(entry);
            } else if (topDictStringEntries.contains(dictKey)) {
                writeTopDictStringEntry(entry);
            } else {
                writeBytes(entry.getByteData());
            }
        }
    }

    private void writeROSEntry(DICTEntry dictEntry) throws IOException {
        int sidA = dictEntry.getOperands().get(0).intValue();
        if (sidA > 390) {
            stringIndexData.add(cffReader.getStringIndex().getValue(sidA - NUM_STANDARD_STRINGS));
        }
        int sidAStringIndex = stringIndexData.size() + 390;
        int sidB = dictEntry.getOperands().get(1).intValue();
        if (sidB > 390) {
            stringIndexData.add("Identity".getBytes());
        }
        int sidBStringIndex = stringIndexData.size() + 390;
        byte[] cidEntryByteData = dictEntry.getByteData();
        updateOffset(cidEntryByteData, 0, dictEntry.getOperandLengths().get(0),
                sidAStringIndex);
        updateOffset(cidEntryByteData, dictEntry.getOperandLengths().get(0),
                dictEntry.getOperandLengths().get(1), sidBStringIndex);
        updateOffset(cidEntryByteData, dictEntry.getOperandLengths().get(0)
                + dictEntry.getOperandLengths().get(1), dictEntry.getOperandLengths().get(2), 0);
        writeBytes(cidEntryByteData);
    }

    protected void writeCIDCount(DICTEntry dictEntry) throws IOException {
        byte[] cidCountByteData = dictEntry.getByteData();
        updateOffset(cidCountByteData, 0, dictEntry.getOperandLengths().get(0),
                subsetGlyphs.size());
        writeBytes(cidCountByteData);
    }

    private void writeTopDictStringEntry(DICTEntry dictEntry) throws IOException {
        int sid = dictEntry.getOperands().get(0).intValue();
        if (sid > 391) {
            stringIndexData.add(cffReader.getStringIndex().getValue(sid - 391));
        }

        byte[] newDictEntry = createNewRef(stringIndexData.size() + 390, dictEntry.getOperator(),
                dictEntry.getOperandLength(), true);
        writeBytes(newDictEntry);
    }

    private void writeStringIndex() throws IOException {
        Map<String, DICTEntry> topDICT = cffReader.getTopDictEntries();
        int charsetOffset = topDICT.get("charset").getOperands().get(0).intValue();

        gidToSID = new LinkedHashMap<Integer, Integer>();

        for (Entry<Integer, Integer> subsetGlyph : subsetGlyphs.entrySet()) {
            int gid = subsetGlyph.getKey();
            int sid = cffReader.getSIDFromGID(charsetOffset, gid);
            //Check whether the SID falls into the standard string set
            if (sid < NUM_STANDARD_STRINGS) {
                gidToSID.put(subsetGlyph.getValue(), sid);
                if (mbFont != null) {
                    mbFont.mapUsedGlyphName(subsetGlyph.getValue(),
                            CFFStandardString.getName(sid));
                }
            } else {
                int index = sid - NUM_STANDARD_STRINGS;
                //index is 0 based, should use < not <=
                if (index < cffReader.getStringIndex().getNumObjects()) {
                    if (mbFont != null) {
                        mbFont.mapUsedGlyphName(subsetGlyph.getValue(),
                                new String(cffReader.getStringIndex().getValue(index)));
                    }
                    gidToSID.put(subsetGlyph.getValue(), stringIndexData.size() + 391);
                    stringIndexData.add(cffReader.getStringIndex().getValue(index));
                } else {
                    if (mbFont != null) {
                        mbFont.mapUsedGlyphName(subsetGlyph.getValue(), ".notdef");
                    }
                    gidToSID.put(subsetGlyph.getValue(), index);
                }
            }
        }
        //Write the String Index
        writeIndex(stringIndexData);
    }

    protected void createCharStringDataCID() throws IOException {
        CFFIndexData charStringsIndex = cffReader.getCharStringIndex();

        FDSelect fontDictionary = cffReader.getFDSelect();
        if (fontDictionary instanceof Format0FDSelect) {
            throw new UnsupportedOperationException("OTF CFF CID Format0 currently not implemented");
        } else if (fontDictionary instanceof Format3FDSelect) {
            Format3FDSelect fdSelect = (Format3FDSelect)fontDictionary;
            Map<Integer, Integer> subsetGroups = new HashMap<Integer, Integer>();

            List<Integer> uniqueGroups = new ArrayList<Integer>();
            for (int gid : subsetGlyphs.keySet()) {
                Set<Integer> rangeKeys = fdSelect.getRanges().keySet();
                Integer[] ranges = rangeKeys.toArray(new Integer[rangeKeys.size()]);
                for (int i = 0; i < ranges.length; i++) {
                    int nextRange = -1;
                    if (i < ranges.length - 1) {
                        nextRange = ranges[i + 1];
                    } else {
                        nextRange = fdSelect.getSentinelGID();
                    }
                    if (gid >= ranges[i] && gid < nextRange) {
                        subsetGroups.put(gid, fdSelect.getRanges().get(ranges[i]));
                        if (!uniqueGroups.contains(fdSelect.getRanges().get(ranges[i]))) {
                            uniqueGroups.add(fdSelect.getRanges().get(ranges[i]));
                        }
                    }
                }
            }

            //Prepare resources
            globalIndexSubr = cffReader.getGlobalIndexSubr();

            //Create the new char string index
            subsetCharStringsIndex = new ArrayList<byte[]>();

            globalUniques = new ArrayList<Integer>();

            subsetFDSelect = new LinkedHashMap<Integer, FDIndexReference>();

            List<List<Integer>> foundLocalUniques = new ArrayList<List<Integer>>();
            for (Integer uniqueGroup1 : uniqueGroups) {
                foundLocalUniques.add(new ArrayList<Integer>());
            }
            Map<Integer, Integer> gidHintMaskLengths = new HashMap<Integer, Integer>();
            for (Entry<Integer, Integer> subsetGlyph : subsetGlyphs.entrySet()) {
                int gid = subsetGlyph.getKey();
                int group = subsetGroups.get(gid);
                localIndexSubr = cffReader.getFDFonts().get(group).getLocalSubrData();
                localUniques = foundLocalUniques.get(uniqueGroups.indexOf(group));
                type2Parser = new Type2Parser();

                FDIndexReference newFDReference = new FDIndexReference(uniqueGroups.indexOf(group), group);
                subsetFDSelect.put(subsetGlyph.getValue(), newFDReference);
                byte[] data = charStringsIndex.getValue(gid);
                preScanForSubsetIndexSize(data);
                gidHintMaskLengths.put(gid, type2Parser.getMaskLength());
            }

            //Create the two lists which are to store the local and global subroutines
            subsetGlobalIndexSubr = new ArrayList<byte[]>();

            fdSubrs = new ArrayList<List<byte[]>>();
            subsetGlobalSubrCount = globalUniques.size();
            globalUniques.clear();
            localUniques = null;

            for (List<Integer> foundLocalUnique : foundLocalUniques) {
                fdSubrs.add(new ArrayList<byte[]>());
            }
            List<List<Integer>> foundLocalUniquesB = new ArrayList<List<Integer>>();
            for (Integer uniqueGroup : uniqueGroups) {
                foundLocalUniquesB.add(new ArrayList<Integer>());
            }
            for (Entry<Integer, Integer> subsetGlyph : subsetGlyphs.entrySet()) {
                int gid = subsetGlyph.getKey();
                int value = subsetGlyph.getValue();
                int group = subsetGroups.get(gid);
                localIndexSubr = cffReader.getFDFonts().get(group).getLocalSubrData();
                localUniques = foundLocalUniquesB.get(subsetFDSelect.get(value).getNewFDIndex());
                byte[] data = charStringsIndex.getValue(gid);
                subsetLocalIndexSubr = fdSubrs.get(subsetFDSelect.get(value).getNewFDIndex());
                subsetLocalSubrCount = foundLocalUniques.get(subsetFDSelect.get(value)
                        .getNewFDIndex()).size();
                type2Parser = new Type2Parser();
                type2Parser.setMaskLength(gidHintMaskLengths.get(gid));
                data = readCharStringData(data, subsetLocalSubrCount);
                subsetCharStringsIndex.add(data);
            }
        }
    }

    protected void writeFDSelect() {
        if (cffReader.getTopDictEntries().get("CharStrings").getOperandLength() == 2) {
            Map<Integer, Integer> indexs = getFormat3Index();
            writeByte(3); //Format
            writeCard16(indexs.size());
            int count = 0;
            for (Entry<Integer, Integer> x : indexs.entrySet()) {
                writeCard16(count);
                writeByte(x.getKey());
                count += x.getValue();
            }
            writeCard16(subsetFDSelect.size());
        } else {
            writeByte(0); //Format
            for (FDIndexReference e : subsetFDSelect.values()) {
                writeByte(e.getNewFDIndex());
            }
        }
    }

    private Map<Integer, Integer> getFormat3Index() {
        Map<Integer, Integer> indexs = new LinkedHashMap<Integer, Integer>();
        int last = -1;
        int count = 0;
        for (FDIndexReference e : subsetFDSelect.values()) {
            int i = e.getNewFDIndex();
            count++;
            if (i != last) {
                indexs.put(i, count);
                count = 1;
            }
            last = i;
        }
        indexs.put(last, count);
        return indexs;
    }

    protected List<Integer> getUsedFDFonts() {
        List<Integer> uniqueNewRefs = new ArrayList<Integer>();
        for (FDIndexReference e : subsetFDSelect.values()) {
            int fdIndex = e.getOldFDIndex();
            if (!uniqueNewRefs.contains(fdIndex)) {
                uniqueNewRefs.add(fdIndex);
            }
        }
        return uniqueNewRefs;
    }

    protected List<Integer> writeCIDDictsAndSubrs(List<Integer> uniqueNewRefs)
            throws IOException {
        List<Integer> privateDictOffsets = new ArrayList<Integer>();
        List<FontDict> fdFonts = cffReader.getFDFonts();
        for (int i = 0; i < uniqueNewRefs.size(); i++) {
            FontDict curFDFont = fdFonts.get(uniqueNewRefs.get(i));
            Map<String, DICTEntry> fdPrivateDict = cffReader.parseDictData(
                    curFDFont.getPrivateDictData());
            int privateDictOffset = currentPos;
            privateDictOffsets.add(privateDictOffset);
            byte[] fdPrivateDictByteData = curFDFont.getPrivateDictData();
            if (fdPrivateDict.get("Subrs") != null) {
                updateOffset(fdPrivateDictByteData, fdPrivateDict.get("Subrs").getOffset(),
                        fdPrivateDict.get("Subrs").getOperandLength(),
                        fdPrivateDictByteData.length);
            }
            writeBytes(fdPrivateDictByteData);
            writeIndex(fdSubrs.get(i));
        }
        return privateDictOffsets;
    }

    protected int writeFDArray(List<Integer> uniqueNewRefs, List<Integer> privateDictOffsets,
            List<Integer> fontNameSIDs)
            throws IOException {
        int offset = currentPos;
        List<FontDict> fdFonts = cffReader.getFDFonts();

        writeCard16(uniqueNewRefs.size());
        writeByte(1); //Offset size
        writeByte(1); //First offset

        int count = 1;
        for (Integer uniqueNewRef : uniqueNewRefs) {
            FontDict fdFont = fdFonts.get(uniqueNewRef);
            count += fdFont.getByteData().length;
            writeByte(count);
        }

        for (int i = 0; i < uniqueNewRefs.size(); i++) {
            FontDict fdFont = fdFonts.get(uniqueNewRefs.get(i));
            byte[] fdFontByteData = fdFont.getByteData();
            Map<String, DICTEntry> fdFontDict = cffReader.parseDictData(fdFontByteData);
            //Update the SID to the FontName
            updateOffset(fdFontByteData, fdFontDict.get("FontName").getOffset() - 1,
                    fdFontDict.get("FontName").getOperandLengths().get(0),
                    fontNameSIDs.get(i));
            //Update the Private dict reference
            updateOffset(fdFontByteData, fdFontDict.get("Private").getOffset()
                    + fdFontDict.get("Private").getOperandLengths().get(0),
                    fdFontDict.get("Private").getOperandLengths().get(1),
                    privateDictOffsets.get(i));
            writeBytes(fdFontByteData);
        }
        return offset;
    }

    private static class FDIndexReference {
        private int newFDIndex;
        private int oldFDIndex;

        public FDIndexReference(int newFDIndex, int oldFDIndex) {
            this.newFDIndex = newFDIndex;
            this.oldFDIndex = oldFDIndex;
        }

        public int getNewFDIndex() {
            return newFDIndex;
        }

        public int getOldFDIndex() {
            return oldFDIndex;
        }
    }

    private void createCharStringData() throws IOException {
        Map<String, DICTEntry> topDICT = cffReader.getTopDictEntries();

        CFFIndexData charStringsIndex = cffReader.getCharStringIndex();

        DICTEntry privateEntry = topDICT.get("Private");
        if (privateEntry != null) {
            int privateOffset = privateEntry.getOperands().get(1).intValue();
            Map<String, DICTEntry> privateDICT = cffReader.getPrivateDict(privateEntry);

            if (privateDICT.get("Subrs") != null) {
                int localSubrOffset = privateOffset + privateDICT.get("Subrs").getOperands().get(0).intValue();
                localIndexSubr = cffReader.readIndex(localSubrOffset);
            } else {
                localIndexSubr = cffReader.readIndex(null);
            }
        }

        globalIndexSubr = cffReader.getGlobalIndexSubr();

        //Create the two lists which are to store the local and global subroutines
        subsetLocalIndexSubr = new ArrayList<byte[]>();
        subsetGlobalIndexSubr = new ArrayList<byte[]>();

        //Create the new char string index
        subsetCharStringsIndex = new ArrayList<byte[]>();

        localUniques = new ArrayList<Integer>();
        globalUniques = new ArrayList<Integer>();
        Map<Integer, Integer> gidHintMaskLengths = new HashMap<Integer, Integer>();
        for (int gid : subsetGlyphs.keySet()) {
            type2Parser = new Type2Parser();
            byte[] data = charStringsIndex.getValue(gid);
            preScanForSubsetIndexSize(data);
            gidHintMaskLengths.put(gid, type2Parser.getMaskLength());
        }

        //Store the size of each subset index and clear the unique arrays
        subsetLocalSubrCount = localUniques.size();
        subsetGlobalSubrCount = globalUniques.size();
        localUniques.clear();
        globalUniques.clear();

        for (int gid : subsetGlyphs.keySet()) {
            byte[] data = charStringsIndex.getValue(gid);
            type2Parser = new Type2Parser();
            //Retrieve modified char string data and fill local / global subroutine arrays
            type2Parser.setMaskLength(gidHintMaskLengths.get(gid));
            data = readCharStringData(data, subsetLocalSubrCount);
            subsetCharStringsIndex.add(data);
        }
    }

    static class Type2Parser {
        /**
         * logging instance
         */
        protected Log log = LogFactory.getLog(Type2Parser.class);

        private List<BytesNumber> stack = new ArrayList<BytesNumber>();
        private int hstemCount;
        private int vstemCount;
        private int lastOp = -1;
        private int maskLength = -1;

        public void pushOperand(BytesNumber v) {
            stack.add(v);
        }

        public BytesNumber popOperand() {
            return stack.remove(stack.size() - 1);
        }

        public void clearStack() {
            stack.clear();
        }

        public int[] getOperands(int numbers) {
            int[] ret = new int[numbers];
            while (numbers > 0) {
                numbers--;
                ret[numbers] = this.popOperand().getNumber();
            }
            return ret;
        }

        public void setMaskLength(int maskLength) {
            this.maskLength = maskLength;
        }

        public int getMaskLength() {
            // The number of data bytes for mask is exactly the number needed, one
            // bit per hint, to reference the number of stem hints declared
            // at the beginning of the charstring program.
            if (maskLength > 0) {
                return maskLength;
            }
            return 1 + (hstemCount + vstemCount  - 1) / 8;
        }

        public int exec(int b0, byte[] data, int dataPos) {
            int posDelta = 0;
            if ((b0 >= 0 && b0 <= 27) || (b0 >= 29 && b0 <= 31)) {
                if (b0 == 12) {
                    log.warn("May not guess the operand count correctly.");
                    posDelta = 1;
                } else if (b0 == 1 || b0 == 18) {
                    // hstem(hm) operator
                    hstemCount += stack.size() / 2;
                    clearStack();
                } else if (b0 == 19 || b0 == 20) {
                    if (lastOp == 1 || lastOp == 18) {
                        //If hstem and vstem hints are both declared at the beginning of
                        //a charstring, and this sequence is followed directly by the
                        //hintmask or cntrmask operators, the vstem hint operator need
                        //not be included.
                        vstemCount += stack.size() / 2;
                    }
                    clearStack();
                    posDelta = getMaskLength();
                } else if (b0 == 3 || b0 == 23) {
                    // vstem(hm) operator
                    vstemCount += stack.size() / 2;
                    clearStack();
                }
                if (b0 != 11 && b0 != 12) {
                    lastOp = b0;
                }
            } else if (b0 == 28 || (b0 >= 32 && b0 <= 255)) {
                BytesNumber operand = readNumber(b0, data, dataPos);
                pushOperand(operand);
                posDelta = operand.getNumBytes() - 1;
            } else {
                throw new UnsupportedOperationException("Operator:" + b0 + " is not supported");
            }
            return posDelta;
        }

        private BytesNumber readNumber(int b0, byte[] input, int curPos) {
            if (b0 == 28) {
                int b1 = input[curPos + 1] & 0xff;
                int b2 = input[curPos + 2] & 0xff;
                return new BytesNumber((int) (short) (b1 << 8 | b2), 3);
            } else if (b0 >= 32 && b0 <= 246) {
                return new BytesNumber(b0 - 139, 1);
            } else if (b0 >= 247 && b0 <= 250) {
                int b1 = input[curPos + 1] & 0xff;
                return new BytesNumber((b0 - 247) * 256 + b1 + 108, 2);
            } else if (b0 >= 251 && b0 <= 254) {
                int b1 = input[curPos + 1] & 0xff;
                return new BytesNumber(-(b0 - 251) * 256 - b1 - 108, 2);
            } else if (b0 == 255) {
                int b1 = input[curPos + 1] & 0xff;
                int b2 = input[curPos + 2] & 0xff;
                int b3 = input[curPos + 3] & 0xff;
                int b4 = input[curPos + 4] & 0xff;
                return new BytesNumber((b1 << 24 | b2 << 16 | b3 << 8 | b4), 5);
            } else {
                throw new IllegalArgumentException();
            }
        }
    }
    private void preScanForSubsetIndexSize(byte[] data) throws IOException {
        boolean hasLocalSubroutines = localIndexSubr != null && localIndexSubr.getNumObjects() > 0;
        boolean hasGlobalSubroutines = globalIndexSubr != null && globalIndexSubr.getNumObjects() > 0;
        for (int dataPos = 0; dataPos < data.length; dataPos++) {
            int b0 = data[dataPos] & 0xff;
            if (b0 == LOCAL_SUBROUTINE && hasLocalSubroutines) {
                int subrNumber = getSubrNumber(localIndexSubr.getNumObjects(), type2Parser.popOperand().getNumber());
                if (!localUniques.contains(subrNumber) && subrNumber < localIndexSubr.getNumObjects()) {
                    localUniques.add(subrNumber);
                }
                if (subrNumber < localIndexSubr.getNumObjects()) {
                    byte[] subr = localIndexSubr.getValue(subrNumber);
                    preScanForSubsetIndexSize(subr);
                } else {
                    throw new IllegalArgumentException("callsubr out of range");
                }
            } else if (b0 == GLOBAL_SUBROUTINE && hasGlobalSubroutines) {
                int subrNumber = getSubrNumber(globalIndexSubr.getNumObjects(), type2Parser.popOperand().getNumber());
                if (!globalUniques.contains(subrNumber) && subrNumber < globalIndexSubr.getNumObjects()) {
                    globalUniques.add(subrNumber);
                }
                if (subrNumber < globalIndexSubr.getNumObjects()) {
                    byte[] subr = globalIndexSubr.getValue(subrNumber);
                    preScanForSubsetIndexSize(subr);
                } else {
                    throw new IllegalArgumentException("callgsubr out of range");
                }
            } else  {
                dataPos += type2Parser.exec(b0, data, dataPos);
            }
        }
    }

    private int getSubrNumber(int numSubroutines, int operand) {
        int bias = getBias(numSubroutines);
        return bias + operand;
    }

    private byte[] readCharStringData(byte[] data, int subsetLocalSubrCount) throws IOException {
        boolean hasLocalSubroutines = localIndexSubr != null && localIndexSubr.getNumObjects() > 0;
        boolean hasGlobalSubroutines = globalIndexSubr != null && globalIndexSubr.getNumObjects() > 0;
        for (int dataPos = 0; dataPos < data.length; dataPos++) {
            int b0 = data[dataPos] & 0xff;
            if (b0 == 10 && hasLocalSubroutines) {
                BytesNumber operand = type2Parser.popOperand();
                int subrNumber = getSubrNumber(localIndexSubr.getNumObjects(), operand.getNumber());

                int newRef = getNewRefForReference(subrNumber, localUniques, localIndexSubr, subsetLocalIndexSubr,
                        subsetLocalSubrCount);

                if (newRef != -1) {
                    byte[] newData = constructNewRefData(dataPos, data, operand, subsetLocalSubrCount,
                            newRef, new int[] {10});
                    dataPos -= data.length - newData.length;
                    data = newData;
                }
            } else if (b0 == 29 && hasGlobalSubroutines) {
                BytesNumber operand = type2Parser.popOperand();
                int subrNumber = getSubrNumber(globalIndexSubr.getNumObjects(), operand.getNumber());

                int newRef = getNewRefForReference(subrNumber, globalUniques, globalIndexSubr, subsetGlobalIndexSubr,
                        subsetGlobalSubrCount);

                if (newRef != -1) {
                    byte[] newData = constructNewRefData(dataPos, data, operand, subsetGlobalSubrCount,
                            newRef, new int[] {29});
                    dataPos -= (data.length - newData.length);
                    data = newData;
                }
            } else {
                dataPos += type2Parser.exec(b0, data, dataPos);
            }
        }

        //Return the data with the modified references to our arrays
        return data;
    }

    private int getNewRefForReference(int subrNumber, List<Integer> uniquesArray,
            CFFIndexData indexSubr, List<byte[]> subsetIndexSubr, int subrCount) throws IOException {
        int newRef;
        if (!uniquesArray.contains(subrNumber)) {
            if (subrNumber < indexSubr.getNumObjects()) {
                byte[] subr = indexSubr.getValue(subrNumber);
                subr = readCharStringData(subr, subrCount);
                uniquesArray.add(subrNumber);
                subsetIndexSubr.add(subr);
                newRef = subsetIndexSubr.size() - 1;
            } else {
                throw new IllegalArgumentException("subrNumber out of range");
            }
        } else {
            newRef = uniquesArray.indexOf(subrNumber);
        }
        return newRef;
    }

    private int getBias(int subrCount) {
        if (subrCount < 1240) {
            return 107;
        } else if (subrCount < 33900) {
            return 1131;
        } else {
            return 32768;
        }
    }

    private byte[] constructNewRefData(int curDataPos, byte[] currentData, BytesNumber operand,
            int fullSubsetIndexSize, int curSubsetIndexSize, int[] operatorCode) throws IOException {
        //Create the new array with the modified reference
        ByteArrayOutputStream newData = new ByteArrayOutputStream();
        int startRef = curDataPos - operand.getNumBytes();
        int length = operand.getNumBytes() + 1;
        int newBias = getBias(fullSubsetIndexSize);
        int newRef = curSubsetIndexSize - newBias;
        byte[] newRefBytes = createNewRef(newRef, operatorCode, -1, false);
        newData.write(currentData, 0, startRef);
        newData.write(newRefBytes);
        newData.write(currentData, startRef + length, currentData.length - (startRef + length));
        return newData.toByteArray();
    }

    public static byte[] createNewRef(int newRef, int[] operatorCode, int forceLength, boolean isDict) {
        ByteArrayOutputStream newRefBytes = new ByteArrayOutputStream();
        if ((forceLength == -1 && newRef >= -107 && newRef <= 107) || forceLength == 1) {
            //The index values are 0 indexed
            newRefBytes.write(newRef + 139);
            for (int i : operatorCode) {
                newRefBytes.write(i);
            }
        } else if ((forceLength == -1 && newRef >= -1131 && newRef <= 1131) || forceLength == 2) {
            if (newRef <= -876) {
                newRefBytes.write(254);
            } else if (newRef <= -620) {
                newRefBytes.write(253);
            } else if (newRef <= -364) {
                newRefBytes.write(252);
            } else if (newRef <= -108) {
                newRefBytes.write(251);
            } else if (newRef <= 363) {
                newRefBytes.write(247);
            } else if (newRef <= 619) {
                newRefBytes.write(248);
            } else if (newRef <= 875) {
                newRefBytes.write(249);
            } else {
                newRefBytes.write(250);
            }
            if (newRef > 0) {
                newRefBytes.write(newRef - 108);
            } else {
                newRefBytes.write(-newRef - 108);
            }
            for (int i : operatorCode) {
                newRefBytes.write(i);
            }
        } else if ((forceLength == -1 && newRef >= -32768 && newRef <= 32767) || forceLength == 3) {
            newRefBytes.write(28);
            newRefBytes.write(newRef >> 8);
            newRefBytes.write(newRef);
            for (int i : operatorCode) {
                newRefBytes.write(i);
            }
        } else {
            if (isDict) {
                newRefBytes.write(29);
            } else {
                newRefBytes.write(255);
            }
            newRefBytes.write(newRef >> 24);
            newRefBytes.write(newRef >> 16);
            newRefBytes.write(newRef >> 8);
            newRefBytes.write(newRef);
            for (int i : operatorCode) {
                newRefBytes.write(i);
            }
        }
        return newRefBytes.toByteArray();
    }

    protected int writeIndex(List<byte[]> dataArray) {
        int hdrTotal = 3;
        //2 byte number of items
        this.writeCard16(dataArray.size());
        //Offset Size: 1 byte = 256, 2 bytes = 65536 etc.
        //Offsets in the offset array are relative to the byte that precedes the object data.
        //Therefore the first element of the offset array is always 1.
        int totLength = 1;
        for (byte[] aDataArray1 : dataArray) {
            totLength += aDataArray1.length;
        }
        int offSize = getOffSize(totLength);
        this.writeByte(offSize);
        //Count the first offset 1
        hdrTotal += offSize;
        int total = 0;
        for (int i = 0; i < dataArray.size(); i++) {
            hdrTotal += offSize;
            int length = dataArray.get(i).length;
            switch (offSize) {
            case 1:
                if (i == 0) {
                    writeByte(1);
                }
                total += length;
                writeByte(total + 1);
                break;
            case 2:
                if (i == 0) {
                    writeCard16(1);
                }
                total += length;
                writeCard16(total + 1);
                break;
            case 3:
                if (i == 0) {
                    writeThreeByteNumber(1);
                }
                total += length;
                writeThreeByteNumber(total + 1);
                break;
            case 4:
                if (i == 0) {
                    writeULong(1);
                }
                total += length;
                writeULong(total + 1);
                break;
            default:
                throw new AssertionError("Offset Size was not an expected value.");
            }
        }
        for (byte[] aDataArray : dataArray) {
            writeBytes(aDataArray);
        }
        return hdrTotal + total;
    }

    private int getOffSize(int totLength) {
        int offSize = 1;
        if (totLength < (1 << 8)) {
            offSize = 1;
        } else if (totLength < (1 << 16)) {
            offSize = 2;
        } else if (totLength < (1 << 24)) {
            offSize = 3;
        } else {
            offSize = 4;
        }
        return offSize;
    }
    /**
     * A class used to store the last number operand and also it's size in bytes
     */
    static class BytesNumber {
        private int number;
        private int numBytes;

        public BytesNumber(int number, int numBytes) {
            this.number = number;
            this.numBytes = numBytes;
        }

        public int getNumber() {
            return this.number;
        }

        public int getNumBytes() {
            return this.numBytes;
        }

        public void clearNumber() {
            this.number = -1;
            this.numBytes = -1;
        }

        public String toString() {
            return Integer.toString(number);
        }

        @Override
        public boolean equals(Object entry) {
            assert entry instanceof BytesNumber;
            BytesNumber bnEntry = (BytesNumber)entry;
            return this.number == bnEntry.getNumber()
                    && this.numBytes == bnEntry.getNumBytes();
        }

        @Override
        public int hashCode() {
            int hash = 1;
            hash = hash * 17 + number;
            hash = hash * 31 + numBytes;
            return hash;
        }
    }

    private void writeCharsetTable(boolean cidFont) throws IOException {
        if (cidFont) {
            writeByte(2);
            for (int entry : gidToSID.keySet()) {
                if (entry == 0) {
                    continue;
                }
                writeCard16(entry);
                writeCard16(gidToSID.size() - 1);
                break;
            }
        } else {
            writeByte(0);
            for (int entry : gidToSID.values()) {
                writeCard16(entry);
            }
        }
    }

    protected void writePrivateDict() throws IOException {
        Map<String, DICTEntry> topDICT = cffReader.getTopDictEntries();

        DICTEntry privateEntry = topDICT.get("Private");
        if (privateEntry != null) {
            writeBytes(cffReader.getPrivateDictBytes(privateEntry));
        }
    }

    protected void updateOffsets(int topDictOffset, int charsetOffset, int charStringOffset,
            int privateDictOffset, int localIndexOffset, int encodingOffset)
            throws IOException {
        Map<String, DICTEntry> topDICT = cffReader.getTopDictEntries();
        Map<String, DICTEntry> privateDICT = null;

        DICTEntry privateEntry = topDICT.get("Private");
        if (privateEntry != null) {
            privateDICT = cffReader.getPrivateDict(privateEntry);
        }

        int dataPos = 3 + (cffReader.getTopDictIndex().getOffSize()
                * cffReader.getTopDictIndex().getOffsets().length);
        int dataTopDictOffset = topDictOffset + dataPos;

        updateFixedOffsets(topDICT, dataTopDictOffset, charsetOffset, charStringOffset, encodingOffset);

        if (privateDICT != null) {
            //Private index offset in the top dict
            int oldPrivateOffset = dataTopDictOffset + privateEntry.getOffset();
            updateOffset(output, oldPrivateOffset + privateEntry.getOperandLengths().get(0),
                    privateEntry.getOperandLengths().get(1), privateDictOffset);

            //Update the local subroutine index offset in the private dict
            DICTEntry subroutines = privateDICT.get("Subrs");
            if (subroutines != null) {
                int oldLocalSubrOffset = privateDictOffset + subroutines.getOffset();
                updateOffset(output, oldLocalSubrOffset, subroutines.getOperandLength(),
                        (localIndexOffset - privateDictOffset));
            }
        }
    }

    protected void updateFixedOffsets(Map<String, DICTEntry> topDICT, int dataTopDictOffset,
            int charsetOffset, int charStringOffset, int encodingOffset) {
        //Charset offset in the top dict
        DICTEntry charset = topDICT.get("charset");
        int oldCharsetOffset = dataTopDictOffset + charset.getOffset();
        updateOffset(output, oldCharsetOffset, charset.getOperandLength(), charsetOffset);

        //Char string index offset in the private dict
        DICTEntry charString = topDICT.get("CharStrings");
        int oldCharStringOffset = dataTopDictOffset + charString.getOffset();
        updateOffset(output, oldCharStringOffset, charString.getOperandLength(), charStringOffset);

        DICTEntry encodingEntry = topDICT.get("Encoding");
        if (encodingEntry != null && encodingEntry.getOperands().get(0).intValue() != 0
                && encodingEntry.getOperands().get(0).intValue() != 1) {
            int oldEncodingOffset = dataTopDictOffset + encodingEntry.getOffset();
            updateOffset(output, oldEncodingOffset, encodingEntry.getOperandLength(), encodingOffset);
        }
    }

    protected void updateCIDOffsets(int topDictDataOffset, int fdArrayOffset, int fdSelectOffset,
            int charsetOffset, int charStringOffset, int encodingOffset) {
        Map<String, DICTEntry> topDict = cffReader.getTopDictEntries();

        DICTEntry fdArrayEntry = topDict.get("FDArray");
        if (fdArrayEntry != null) {
            updateOffset(output, topDictDataOffset + fdArrayEntry.getOffset() - 1,
                    fdArrayEntry.getOperandLength(), fdArrayOffset);
        }

        DICTEntry fdSelect = topDict.get("FDSelect");
        if (fdSelect != null) {
            updateOffset(output, topDictDataOffset + fdSelect.getOffset() - 1,
                    fdSelect.getOperandLength(), fdSelectOffset);
        }

        updateFixedOffsets(topDict, topDictDataOffset, charsetOffset, charStringOffset, encodingOffset);
    }

    protected void updateOffset(byte[] out, int position, int length, int replacement) {
        switch (length) {
        case 1:
            out[position] = (byte)(replacement + 139);
            break;
        case 2:
            if (replacement <= -876) {
                out[position] = (byte)254;
            } else if (replacement <= -620) {
                out[position] = (byte)253;
            } else if (replacement <= -364) {
                out[position] = (byte)252;
            } else if (replacement <= -108) {
                out[position] = (byte)251;
            } else if (replacement <= 363) {
                out[position] = (byte)247;
            } else if (replacement <= 619) {
                out[position] = (byte)248;
            } else if (replacement <= 875) {
                out[position] = (byte)249;
            } else {
                out[position] = (byte)250;
            }
            if (replacement > 0) {
                out[position + 1] = (byte)(replacement - 108);
            } else {
                out[position + 1] = (byte)(-replacement - 108);
            }
            break;
        case 3:
            out[position] = (byte)28;
            out[position + 1] = (byte)((replacement >> 8) & 0xFF);
            out[position + 2] = (byte)(replacement & 0xFF);
            break;
        case 5:
            out[position] = (byte)29;
            out[position + 1] = (byte)((replacement >> 24) & 0xFF);
            out[position + 2] = (byte)((replacement >> 16) & 0xFF);
            out[position + 3] = (byte)((replacement >> 8) & 0xFF);
            out[position + 4] = (byte)(replacement & 0xFF);
            break;
        default:
        }
    }

    /**
     * Returns the parsed CFF data for the original font.
     * @return The CFFDataReader contaiing the parsed data
     */
    public CFFDataReader getCFFReader() {
        return cffReader;
    }
}
