/*
 * 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.render.pdf.pdfbox;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.apache.fop.fonts.cff.CFFDataReader;
import org.apache.fop.fonts.cff.CFFDataReader.DICTEntry;
import org.apache.fop.fonts.truetype.OTFSubSetWriter;

/**
 * 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 abstract class OTFSubSetFile extends OTFSubSetWriter {

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

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


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

    /** 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 number of standard strings in CFF **/
    public static final int NUM_STANDARD_STRINGS = 391;

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

    protected int writeTopDICT() throws IOException {
        Map<String, DICTEntry> topDICT = cffReader.getTopDictEntries();
        List<String> topDictStringEntries = Arrays.asList("version", "Notice", "Copyright",
                "FullName", "FamilyName", "Weight", "PostScript");
        ByteArrayOutputStream dict = new ByteArrayOutputStream();
        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")) {
                dict.write(writeROSEntry(entry));
            } else if (dictKey.equals("CIDCount")) {
                dict.write(writeCIDCount(entry));
            } else if (topDictStringEntries.contains(dictKey)) {
                dict.write(writeTopDictStringEntry(entry));
            } else {
                dict.write(entry.getByteData());
            }
        }
        byte[] topDictIndex = cffReader.getTopDictIndex().getByteData();
        int offSize = topDictIndex[2];
        return writeIndex(Arrays.asList(dict.toByteArray()), offSize) - dict.size();
    }

    private byte[] 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(StandardCharsets.UTF_8));
        }
        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), 139);
        return cidEntryByteData;
    }

    protected byte[] writeCIDCount(DICTEntry dictEntry) throws IOException {
        return dictEntry.getByteData();
    }

    private byte[] 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());
        return newDictEntry;
    }

    public static byte[] createNewRef(int newRef, int[] operatorCode, int forceLength) {
        ByteArrayOutputStream newRefBytes = new ByteArrayOutputStream();
        if ((forceLength == -1 && 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) || forceLength == 2) {
            if (newRef <= 363) {
                newRefBytes.write(247);
            } else if (newRef <= 619) {
                newRefBytes.write(248);
            } else if (newRef <= 875) {
                newRefBytes.write(249);
            } else {
                newRefBytes.write(250);
            }
            newRefBytes.write(newRef - 108);
            for (int i : operatorCode) {
                newRefBytes.write(i);
            }
        } else if ((forceLength == -1 && newRef <= 32767) || forceLength == 3) {
            newRefBytes.write(28);
            newRefBytes.write(newRef >> 8);
            newRefBytes.write(newRef);
            for (int i : operatorCode) {
                newRefBytes.write(i);
            }
        } else {
            newRefBytes.write(29);
            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 totLength = 1;
        for (byte[] aDataArray1 : dataArray) {
            totLength += aDataArray1.length;
        }
        int offSize = getOffSize(totLength);
        return writeIndex(dataArray, offSize);
    }

    protected int writeIndex(List<byte[]> dataArray, int offSize) {
        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.
        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;
    }

    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(Offsets offsets) 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);
        }

        updateFixedOffsets(topDICT, offsets);

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

            //Update the local subroutine index offset in the private dict
            DICTEntry subroutines = privateDICT.get("Subrs");
            if (subroutines != null) {
                int oldLocalSubrOffset = offsets.privateDict + subroutines.getOffset();
                //Value needs to be converted to -139 etc.
                int encodeValue = 0;
                if (subroutines.getOperandLength() == 1) {
                    encodeValue = 139;
                }
                updateOffset(oldLocalSubrOffset, subroutines.getOperandLength(),
                        (offsets.localIndex - offsets.privateDict) + encodeValue);
            }
        }
    }

    static class Offsets {
        Integer topDictData;
        Integer encoding;
        Integer fdSelect;
        Integer charString;
        Integer fdArray;
        Integer privateDict;
        Integer localIndex;
    }

    protected abstract void updateFixedOffsets(Map<String, DICTEntry> topDICT, Offsets offsets) throws IOException;

    protected void updateCIDOffsets(Offsets offsets) throws IOException {
        Map<String, DICTEntry> topDict = cffReader.getTopDictEntries();

        DICTEntry fdArrayEntry = topDict.get("FDArray");
        if (fdArrayEntry != null) {
            updateOffset(offsets.topDictData + fdArrayEntry.getOffset() - 1,
                    fdArrayEntry.getOperandLength(), offsets.fdArray);
        }

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

        updateFixedOffsets(topDict, offsets);
    }

    protected void updateOffset(int position, int length, int replacement) throws IOException {
        byte[] out = output.toByteArray();
        updateOffset(out, position, length, replacement);
        output.reset();
        output.write(out);
    }

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