/*
 * 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.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.fontbox.cff.CFFStandardString;
import org.apache.fontbox.cff.encoding.CFFEncoding;

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 OTFFile {

    protected byte[] output;
    protected int currentPos;
    private int realSize;

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

    /** A map of the new GID to SID used to construct the charset table **/
    protected LinkedHashMap<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 ArrayList<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 LinkedHashMap<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;

    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 name Name to be checked for in the font file
     * @param header The header of the font file
     * @param glyphs 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 LinkedHashMap<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());
             }
        });

       LinkedHashMap<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(embeddedName.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;
        writeEncoding(fileFont.getEncoding());

        //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 {
        ArrayList<Integer> fontNameSIDs = new ArrayList<Integer>();
        List<FontDict> fdFonts = cffReader.getFDFonts();
        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);
            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 writeBytes(byte[] out) {
        for (int i = 0; i < out.length; i++) {
            writeByte(out[i]);
        }
    }

    protected void writeBytes(byte[] out, int offset, int length) {
        for (int i = offset; i < offset + length; i++) {
            output[currentPos++] = out[i];
            realSize++;
        }
    }

    private void writeEncoding(CFFEncoding encoding) throws IOException {
        LinkedHashMap<String, DICTEntry> topDICT = cffReader.getTopDictEntries();
        DICTEntry encodingEntry = topDICT.get("Encoding");
        if (encodingEntry != null && encodingEntry.getOperands().get(0).intValue() != 0
                && encodingEntry.getOperands().get(0).intValue() != 1) {
            writeByte(0);
            writeByte(gidToSID.size());
            for (int gid : gidToSID.keySet()) {
                int code = encoding.getCode(gidToSID.get(gid));
                writeByte(code);
            }
        }
    }

    protected void writeTopDICT() throws IOException {
        LinkedHashMap<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();
        cidEntryByteData = updateOffset(cidEntryByteData, 0, dictEntry.getOperandLengths().get(0),
                sidAStringIndex);
        cidEntryByteData = updateOffset(cidEntryByteData, dictEntry.getOperandLengths().get(0),
                dictEntry.getOperandLengths().get(1), sidBStringIndex);
        cidEntryByteData = updateOffset(cidEntryByteData, dictEntry.getOperandLengths().get(0)
                + dictEntry.getOperandLengths().get(1), dictEntry.getOperandLengths().get(2), 139);
        writeBytes(cidEntryByteData);
    }

    protected void writeCIDCount(DICTEntry dictEntry) throws IOException {
        byte[] cidCountByteData = dictEntry.getByteData();
        cidCountByteData = 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());
        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 (int gid : subsetGlyphs.keySet()) {
            int sid = cffReader.getSIDFromGID(charsetOffset, gid);
            //Check whether the SID falls into the standard string set
            if (sid < NUM_STANDARD_STRINGS) {
                gidToSID.put(subsetGlyphs.get(gid), sid);
                if (mbFont != null) {
                    mbFont.mapUsedGlyphName(subsetGlyphs.get(gid),
                            CFFStandardString.getName(sid));
                }
            } else {
                int index = sid - NUM_STANDARD_STRINGS;
                if (index <= cffReader.getStringIndex().getNumObjects()) {
                    if (mbFont != null) {
                        mbFont.mapUsedGlyphName(subsetGlyphs.get(gid),
                                new String(cffReader.getStringIndex().getValue(index)));
                    }
                    gidToSID.put(subsetGlyphs.get(gid), stringIndexData.size() + 391);
                    stringIndexData.add(cffReader.getStringIndex().getValue(index));
                } else {
                    if (mbFont != null) {
                        mbFont.mapUsedGlyphName(subsetGlyphs.get(gid), ".notdef");
                    }
                    gidToSID.put(subsetGlyphs.get(gid), 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 (int i = 0; i < uniqueGroups.size(); i++) {
                foundLocalUniques.add(new ArrayList<Integer>());
            }
            for (int gid : subsetGlyphs.keySet()) {
                int group = subsetGroups.get(gid);
                localIndexSubr = cffReader.getFDFonts().get(group).getLocalSubrData();
                localUniques = foundLocalUniques.get(uniqueGroups.indexOf(subsetGroups.get(gid)));

                FDIndexReference newFDReference = new FDIndexReference(
                        uniqueGroups.indexOf(subsetGroups.get(gid)), subsetGroups.get(gid));
                subsetFDSelect.put(subsetGlyphs.get(gid), newFDReference);
                byte[] data = charStringsIndex.getValue(gid);
                preScanForSubsetIndexSize(data);
            }

            //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 (int l = 0; l < foundLocalUniques.size(); l++) {
                fdSubrs.add(new ArrayList<byte[]>());
            }
            List<List<Integer>> foundLocalUniquesB = new ArrayList<List<Integer>>();
            for (int k = 0; k < uniqueGroups.size(); k++) {
                foundLocalUniquesB.add(new ArrayList<Integer>());
            }
            for (Integer gid : subsetGlyphs.keySet()) {
                int group = subsetGroups.get(gid);
                localIndexSubr = cffReader.getFDFonts().get(group).getLocalSubrData();
                localUniques = foundLocalUniquesB.get(subsetFDSelect.get(subsetGlyphs.get(gid)).getNewFDIndex());
                byte[] data = charStringsIndex.getValue(gid);
                subsetLocalIndexSubr = fdSubrs.get(subsetFDSelect.get(subsetGlyphs.get(gid)).getNewFDIndex());
                subsetLocalSubrCount = foundLocalUniques.get(subsetFDSelect.get(subsetGlyphs.get(gid))
                        .getNewFDIndex()).size();
                data = readCharStringData(data, subsetLocalSubrCount);
                subsetCharStringsIndex.add(data);
            }
        }
    }

    protected void writeFDSelect() {
        writeByte(0); //Format
        for (Integer gid : subsetFDSelect.keySet()) {
            writeByte(subsetFDSelect.get(gid).getNewFDIndex());
        }
    }

    protected List<Integer> getUsedFDFonts() {
        List<Integer> uniqueNewRefs = new ArrayList<Integer>();
        for (int gid : subsetFDSelect.keySet()) {
            int fdIndex = subsetFDSelect.get(gid).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));
            HashMap<String, DICTEntry> fdPrivateDict = cffReader.parseDictData(
                    curFDFont.getPrivateDictData());
            int privateDictOffset = currentPos;
            privateDictOffsets.add(privateDictOffset);
            byte[] fdPrivateDictByteData = curFDFont.getPrivateDictData();
            if (fdPrivateDict.get("Subrs") != null) {
                fdPrivateDictByteData = 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 (int i = 0; i < uniqueNewRefs.size(); i++) {
            FontDict fdFont = fdFonts.get(uniqueNewRefs.get(i));
            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
            fdFontByteData = updateOffset(fdFontByteData, fdFontDict.get("FontName").getOffset() - 1,
                    fdFontDict.get("FontName").getOperandLengths().get(0),
                    fontNameSIDs.get(i));
            //Update the Private dict reference
            fdFontByteData = 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 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>();

        for (int gid : subsetGlyphs.keySet()) {
            byte[] data = charStringsIndex.getValue(gid);
            preScanForSubsetIndexSize(data);
        }

        //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);
            //Retrieve modified char string data and fill local / global subroutine arrays
            data = readCharStringData(data, subsetLocalSubrCount);
            subsetCharStringsIndex.add(data);
        }
    }

    private void preScanForSubsetIndexSize(byte[] data) throws IOException {
        boolean hasLocalSubroutines = localIndexSubr != null && localIndexSubr.getNumObjects() > 0;
        boolean hasGlobalSubroutines = globalIndexSubr != null && globalIndexSubr.getNumObjects() > 0;
        BytesNumber operand = new BytesNumber(-1, -1);
        for (int dataPos = 0; dataPos < data.length; dataPos++) {
            int b0 = data[dataPos] & 0xff;
            if (b0 == LOCAL_SUBROUTINE && hasLocalSubroutines) {
                int subrNumber = getSubrNumber(localIndexSubr.getNumObjects(), operand.getNumber());

                if (!localUniques.contains(subrNumber) && subrNumber < localIndexSubr.getNumObjects()) {
                    localUniques.add(subrNumber);
                    byte[] subr = localIndexSubr.getValue(subrNumber);
                    preScanForSubsetIndexSize(subr);
                }
                operand.clearNumber();
            } else if (b0 == GLOBAL_SUBROUTINE && hasGlobalSubroutines) {
                int subrNumber = getSubrNumber(globalIndexSubr.getNumObjects(), operand.getNumber());

                if (!globalUniques.contains(subrNumber) && subrNumber < globalIndexSubr.getNumObjects()) {
                    globalUniques.add(subrNumber);
                    byte[] subr = globalIndexSubr.getValue(subrNumber);
                    preScanForSubsetIndexSize(subr);
                }
                operand.clearNumber();
            } else if ((b0 >= 0 && b0 <= 27) || (b0 >= 29 && b0 <= 31)) {
                operand.clearNumber();
                if (b0 == 19 || b0 == 20) {
                    dataPos += 1;
                }
            } else if (b0 == 28 || (b0 >= 32 && b0 <= 255)) {
                operand = readNumber(b0, data, dataPos);
                dataPos += operand.getNumBytes() - 1;
            }
        }
    }

    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;
        BytesNumber operand = new BytesNumber(-1, -1);
        for (int dataPos = 0; dataPos < data.length; dataPos++) {
            int b0 = data[dataPos] & 0xff;
            if (b0 == 10 && hasLocalSubroutines) {
                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;
                }

                operand.clearNumber();
            } else if (b0 == 29 && hasGlobalSubroutines) {
                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;
                }

                operand.clearNumber();
            } else if ((b0 >= 0 && b0 <= 27) || (b0 >= 29 && b0 <= 31)) {
                operand.clearNumber();
                if (b0 == 19) {
                    dataPos += 1;
                }
            } else if (b0 == 28 || (b0 >= 32 && b0 <= 255)) {
                operand = readNumber(b0, data, dataPos);
                dataPos += operand.getNumBytes() - 1;
            }
        }

        //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 = -1;
        if (!uniquesArray.contains(subrNumber)) {
            if (subrNumber < indexSubr.getNumObjects()) {
                byte[] subr = indexSubr.getValue(subrNumber);
                subr = readCharStringData(subr, subrCount);
                if (!uniquesArray.contains(subrNumber)) {
                    uniquesArray.add(subrNumber);
                    subsetIndexSubr.add(subr);
                    newRef = subsetIndexSubr.size() - 1;
                } else {
                    newRef = uniquesArray.indexOf(subrNumber);
                }
            }
        } 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) {
        //Create the new array with the modified reference
        byte[] newData;
        int startRef = curDataPos - operand.getNumBytes();
        int length = operand.getNumBytes() + 1;
        byte[] preBytes = new byte[startRef];
        System.arraycopy(currentData, 0, preBytes, 0, startRef);
        int newBias = getBias(fullSubsetIndexSize);
        int newRef = curSubsetIndexSize - newBias;
        byte[] newRefBytes = createNewRef(newRef, operatorCode, -1);
        newData = concatArray(preBytes, newRefBytes);
        byte[] postBytes = new byte[currentData.length - (startRef + length)];
        System.arraycopy(currentData, startRef + length, postBytes, 0,
                currentData.length - (startRef + length));
        return concatArray(newData, postBytes);
    }

    public static byte[] createNewRef(int newRef, int[] operatorCode, int forceLength) {
        byte[] newRefBytes;
        int sizeOfOperator = operatorCode.length;
        if ((forceLength == -1 && newRef <= 107) || forceLength == 1) {
            newRefBytes = new byte[1 + sizeOfOperator];
            //The index values are 0 indexed
            newRefBytes[0] = (byte)(newRef + 139);
            for (int i = 0; i < operatorCode.length; i++) {
                newRefBytes[1 + i] = (byte)operatorCode[i];
            }
        } else if ((forceLength == -1 && newRef <= 1131) || forceLength == 2) {
            newRefBytes = new byte[2 + sizeOfOperator];
            if (newRef <= 363) {
                newRefBytes[0] = (byte)247;
            } else if (newRef <= 619) {
                newRefBytes[0] = (byte)248;
            } else if (newRef <= 875) {
                newRefBytes[0] = (byte)249;
            } else {
                newRefBytes[0] = (byte)250;
            }
            newRefBytes[1] = (byte)(newRef - 108);
            for (int i = 0; i < operatorCode.length; i++) {
                newRefBytes[2 + i] = (byte)operatorCode[i];
            }
        } else if ((forceLength == -1 && newRef <= 32767) || forceLength == 3) {
            newRefBytes = new byte[3 + sizeOfOperator];
            newRefBytes[0] = 28;
            newRefBytes[1] = (byte)(newRef >> 8);
            newRefBytes[2] = (byte)newRef;
            for (int i = 0; i < operatorCode.length; i++) {
                newRefBytes[3 + i] = (byte)operatorCode[i];
            }
        } else {
            newRefBytes = new byte[5 + sizeOfOperator];
            newRefBytes[0] = 29;
            newRefBytes[1] = (byte)(newRef >> 24);
            newRefBytes[2] = (byte)(newRef >> 16);
            newRefBytes[3] = (byte)(newRef >> 8);
            newRefBytes[4] = (byte)newRef;
            for (int i = 0; i < operatorCode.length; i++) {
                newRefBytes[5 + i] = (byte)operatorCode[i];
            }
        }
        return newRefBytes;
    }

    public static byte[] concatArray(byte[] a, byte[] b) {
        int aLen = a.length;
        int bLen = b.length;
        byte[] c = new byte[aLen + bLen];
        System.arraycopy(a, 0, c, 0, aLen);
        System.arraycopy(b, 0, c, aLen, bLen);
        return c;
    }

    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.
        int totLength = 0;
        for (int i = 0; i < dataArray.size(); i++) {
            totLength += dataArray.get(i).length;
        }
        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;
        }
        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 (int i = 0; i < dataArray.size(); i++) {
            writeBytes(dataArray.get(i));
        }
        return hdrTotal + total;
    }

    private BytesNumber readNumber(int b0, byte[] input, int curPos) throws IOException {
        if (b0 == 28) {
            int b1 = input[curPos + 1] & 0xff;
            int b2 = input[curPos + 2] & 0xff;
            return new BytesNumber(Integer.valueOf((short) (b1 << 8 | b2)), 3);
        } else if (b0 >= 32 && b0 <= 246) {
            return new BytesNumber(Integer.valueOf(b0 - 139), 1);
        } else if (b0 >= 247 && b0 <= 250) {
            int b1 = input[curPos + 1] & 0xff;
            return new BytesNumber(Integer.valueOf((b0 - 247) * 256 + b1 + 108), 2);
        } else if (b0 >= 251 && b0 <= 254) {
            int b1 = input[curPos + 1] & 0xff;
            return new BytesNumber(Integer.valueOf(-(b0 - 251) * 256 - b1 - 108), 2);
        } else if (b0 == 255) {
            int b1 = input[curPos + 1] & 0xff;
            int b2 = input[curPos + 2] & 0xff;
            return new BytesNumber(Integer.valueOf((short)(b1 << 8 | b2)), 5);
        } else {
            throw new IllegalArgumentException();
        }
    }

    /**
     * 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 {
        writeByte(0);
        for (int gid : gidToSID.keySet()) {
            if (cidFont && gid == 0) {
                continue;
            }
            writeCard16((cidFont) ? gid : gidToSID.get(gid));
        }
    }

    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();
            output = 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();
                //Value needs to be converted to -139 etc.
                int encodeValue = 0;
                if (subroutines.getOperandLength() == 1) {
                    encodeValue = 139;
                }
                output = updateOffset(output, oldLocalSubrOffset, subroutines.getOperandLength(),
                        (localIndexOffset - privateDictOffset) + encodeValue);
            }
        }
    }

    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();
        output = updateOffset(output, oldCharsetOffset, charset.getOperandLength(), charsetOffset);

        //Char string index offset in the private dict
        DICTEntry charString = topDICT.get("CharStrings");
        int oldCharStringOffset = dataTopDictOffset + charString.getOffset();
        output = 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();
            output = updateOffset(output, oldEncodingOffset, encodingEntry.getOperandLength(), encodingOffset);
        }
    }

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

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

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

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

    protected byte[] updateOffset(byte[] out, int position, int length, int replacement) {
        switch (length) {
        case 1:
            out[position] = (byte)(replacement & 0xFF);
            break;
        case 2:
            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;
            }
            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:
        }
        return out;
    }

    /**
     * Appends a byte to the output array,
     * updates currentPost but not realSize
     */
    protected void writeByte(int b) {
        output[currentPos++] = (byte)b;
        realSize++;
    }

    /**
     * Appends a USHORT to the output array,
     * updates currentPost but not realSize
     */
    protected void writeCard16(int s) {
        byte b1 = (byte)((s >> 8) & 0xff);
        byte b2 = (byte)(s & 0xff);
        writeByte(b1);
        writeByte(b2);
    }

    private void writeThreeByteNumber(int s) {
        byte b1 = (byte)((s >> 16) & 0xFF);
        byte b2 = (byte)((s >> 8) & 0xFF);
        byte b3 = (byte)(s & 0xFF);
        writeByte(b1);
        writeByte(b2);
        writeByte(b3);
    }

    /**
     * Appends a ULONG to the output array,
     * at the given position
     */
    private void writeULong(int s) {
        byte b1 = (byte)((s >> 24) & 0xff);
        byte b2 = (byte)((s >> 16) & 0xff);
        byte b3 = (byte)((s >> 8) & 0xff);
        byte b4 = (byte)(s & 0xff);
        writeByte(b1);
        writeByte(b2);
        writeByte(b3);
        writeByte(b4);
    }

    /**
     * Returns a subset of the fonts (readFont() MUST be called first in order to create the
     * subset).
     * @return byte array
     */
    public byte[] getFontSubset() {
        byte[] ret = new byte[realSize];
        System.arraycopy(output, 0, ret, 0, realSize);
        return ret;
    }

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