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

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;

import org.apache.commons.io.IOUtils;
import org.apache.fontbox.cff.CFFCharset;
import org.apache.fontbox.cff.CFFFont;
import org.apache.fontbox.cff.CFFStandardString;
import org.apache.fontbox.cmap.CMap;

import org.apache.fontbox.ttf.CmapSubtable;
import org.apache.fontbox.ttf.GlyphData;
import org.apache.fontbox.ttf.TrueTypeFont;

import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSObject;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.font.PDCIDFont;
import org.apache.pdfbox.pdmodel.font.PDCIDFontType0;
import org.apache.pdfbox.pdmodel.font.PDCIDFontType2;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDFontDescriptor;
import org.apache.pdfbox.pdmodel.font.PDTrueTypeFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.font.encoding.GlyphList;

import org.apache.fop.fonts.CIDFontType;
import org.apache.fop.fonts.CustomFont;
import org.apache.fop.fonts.EmbeddingMode;
import org.apache.fop.fonts.FontType;
import org.apache.fop.fonts.MultiByteFont;
import org.apache.fop.pdf.PDFDictionary;
import org.apache.fop.util.CharUtilities;

public class FOPPDFMultiByteFont extends MultiByteFont implements FOPPDFFont {
    protected PDFDictionary ref;
    private Map<Integer, Integer> newWidth = new TreeMap<Integer, Integer>();
    private Map<String, Integer> charMapGlobal = new LinkedHashMap<String, Integer>();
    private MergeFonts mergeFonts;
    //private Map<String, GlyphData> glyphs = new HashMap<String, GlyphData>();
    private final Map<COSDictionary, FontContainer> fontMap = new HashMap<COSDictionary, FontContainer>();

    public FOPPDFMultiByteFont(COSDictionary fontData, String name) throws IOException {
        super(null, EmbeddingMode.SUBSET);
        //this stops fop modifying font later on
        setEmbeddingMode(EmbeddingMode.FULL);
        readFontBBox(fontData);
        setFontName(name);
        addFont(fontData);
        notifyMapOperation();
    }

    public String addFont(COSDictionary fontData) throws IOException {
        FontContainer font = getFont(fontData);
        setProperties(this, font.font);
        PDCIDFont mainFont = null;
        TrueTypeFont ttf = null;
        if (font.font instanceof PDType0Font) {
            PDCIDFont cidFont = ((PDType0Font) font.font).getDescendantFont();
            int dw = cidFont.getCOSObject().getInt(COSName.DW);
            setDefaultWidth(dw);
            mainFont = cidFont;
            if (cidFont instanceof PDCIDFontType0) {
                setCIDType(CIDFontType.CIDTYPE0);
                setFontType(FontType.CIDTYPE0);
            } else {
                ttf = ((PDCIDFontType2) cidFont).getTrueTypeFont();
            }
        } else {
            ttf = ((PDTrueTypeFont) font.font).getTrueTypeFont();
            setDefaultWidth(1000);
        }
        GlyphData[] glyphData = new GlyphData[0];
        if (ttf != null) {
            glyphData = getGlyphs(ttf);
        }
        Map<Integer, Integer> oldToNewGIMap = new HashMap<Integer, Integer>();
        if (charMapGlobal.isEmpty()) {
            oldToNewGIMap.put(0, 0); // .notdef glyph
        }
        CMap c = font.getToUnicodeCMap();
        Map<Integer, String> mapping = getMapping(font, c, glyphData.length);
        if (glyphData.length > 0) {
            differentGlyphData(glyphData, mapping);
        }
        Map<Integer, String> gidToGlyph = new TreeMap<Integer, String>(mapping);
        if (font.font instanceof PDTrueTypeFont) {
            CmapSubtable cmap = ttf.getCmap().getCmaps()[0];
            gidToGlyph.clear();
            for (int i = 1; i < glyphData.length; i++) {
                for (int charCode : cmap.getCharCodes(i)) {
                    String mappedChar = mapping.get(charCode);
                    gidToGlyph.put(i, mappedChar);
                }
            }
        }
        readCharMap(font, gidToGlyph, glyphData, mainFont, oldToNewGIMap);
        InputStream ffr = readFontFile(font.font);
        if (mergeFonts == null) {
            if (ttf != null) {
                mergeFonts = new MergeTTFonts(null);
            } else {
                mergeFonts = new MergeCFFFonts();
            }
        }
        if (mergeFonts instanceof MergeTTFonts) {
            mergeMaxp(ttf, ((MergeTTFonts)mergeFonts).maxp);
            mergeFonts.readFont(ffr, null, null, oldToNewGIMap, true);
        } else {
            mergeFonts.readFont(ffr, getEmbedFontName(), null, null, true);
        }
        return getFontName();
    }

    private GlyphData[] getGlyphs(TrueTypeFont ttf) throws IOException {
        GlyphData[] glyphs = new GlyphData[ttf.getNumberOfGlyphs()];
        try {
            for (int i = 0; i < glyphs.length; i++) {
                glyphs[i] = ttf.getGlyph().getGlyph(i);
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            throw new IOException("Mapping not found in glyphData", e);
        }
        return glyphs;
    }

    private void readCharMap(FontContainer font, Map<Integer, String> gidToGlyph, GlyphData[] glyphData,
                             PDCIDFont mainFont, Map<Integer, Integer> oldToNewGIMap) throws IOException {
        int widthPos = font.getFirstChar() + 1;
        for (Map.Entry<Integer, String> i : gidToGlyph.entrySet()) {
            String mappedChar = i.getValue();
            int key = i.getKey();
            boolean skipWidth = (mappedChar == null) || mappedChar.length() == 0;
            if (skipWidth) {
                mappedChar = (char)charMapGlobal.size() + "tmp";
            } else if (mappedChar.length() > 1) {
                mappedChar = "" + (char)mappedChar.hashCode();
            }
            if (!charMapGlobal.containsKey(mappedChar)) {
                char c = mappedChar.charAt(0);
                if (glyphData.length > 0
                        && glyphData[key] == null
                        && !CharUtilities.isAdjustableSpace(c)) {
                    continue;
                }
                boolean addToEnd = charMapGlobal.containsValue(key);
                if (addToEnd) {
                    int glyphIndex = charMapGlobal.size() + 1;
                    while (charMapGlobal.values().contains(glyphIndex)) {
                        glyphIndex++;
                    }
                    addPrivateUseMapping(c, glyphIndex);
                    charMapGlobal.put(mappedChar, glyphIndex);
                } else {
                    addPrivateUseMapping(c, key);
                    charMapGlobal.put(mappedChar, key);
                }
                int glyph = 0;
                if (hasChar(c)) {
                    glyph = (int) mapChar(c);
                }
                oldToNewGIMap.put(key, glyph);
                if (!skipWidth) {
                    if (!(font.font instanceof PDTrueTypeFont)) {
                        widthPos = key;
                    }
                    float w = font.font.getWidth(widthPos);
                    if (w >= 0) {
                        if (mainFont instanceof PDCIDFontType0) {
                            newWidth.put(key, (int)w);
                        } else {
                            newWidth.put(glyph, (int)w);
                        }
                    }
                }
            }
            if (!skipWidth) {
                widthPos++;
            }
        }
    }

    private Map<Integer, String> getMapping(FontContainer font, CMap c, int len) throws IOException {
        Map<Integer, String> mapping = new HashMap<Integer, String>();
        if (font.font instanceof PDType0Font) {
            PDCIDFont cidFont = ((PDType0Font) font.font).getDescendantFont();
            if (cidFont instanceof PDCIDFontType0) {
                mapping = getStrings(((PDCIDFontType0) cidFont).getCFFFont());
            }
        }
        if (c != null) {
            int last = font.getLastChar();
            if (last == -1) {
                last = len;
            }
            for (int i = font.getFirstChar(); i <= last; i++) {
                String l = c.toUnicode(i);
                if (l != null) {
                    mapping.put(i, l);
                }
            }
        }
        return mapping;
    }

    private Map<Integer, String> getStrings(CFFFont ff) throws IOException {
        CFFCharset cs = ff.getCharset();
        Map<Integer, String> strings = new LinkedHashMap<Integer, String>();
        for (int gid = 0; gid < 256; gid++) {
            int sid = cs.getCIDForGID(gid);
            if (sid != 0) {
                strings.put(sid, GlyphList.getAdobeGlyphList().toUnicode(readString(sid)));
            }
        }
        return strings;
    }

    private String readString(int index) throws IOException {
        if (index >= 0 && index <= 390) {
            return CFFStandardString.getName(index);
        }
        // technically this maps to .notdef, but we need a unique glyph name
        return "SID" + index;
    }

        private boolean differentGlyphData(GlyphData[] data, Map<Integer, String> mapping) throws IOException {
            Map<String, Integer> tmpMap = new HashMap<String, Integer>();
            for (Map.Entry<Integer, String> entry : mapping.entrySet()) {
                if (!tmpMap.containsKey(entry.getValue())) {
                    tmpMap.put(entry.getValue(), entry.getKey());
                }
            }
            mapping.clear();
            for (Map.Entry<String, Integer> entry : tmpMap.entrySet()) {
                mapping.put(entry.getValue(), entry.getKey());
            }

            for (Map.Entry<Integer, String> n : mapping.entrySet()) {
                if (n.getKey() >= data.length) {
                    throw new IOException("Mapping not found in glyphData");
                }
//                if (data[n.getKey()] != null) {
//                    if (glyphs.containsKey(n.getValue()) && !glyphs.get(n.getValue()).equals(data[n.getKey()])) {
//                        return true;
//                    }
//                    glyphs.put(n.getValue(), data[n.getKey()]);
//                }
            }
            return false;
        }

    private InputStream readFontFile(PDFont font) throws IOException {
        PDFontDescriptor fd = font.getFontDescriptor();
        if (font instanceof PDType0Font) {
            PDCIDFont cidFont = ((PDType0Font) font).getDescendantFont();
            fd = cidFont.getFontDescriptor();
        }
        PDStream ff = fd.getFontFile3();
        if (ff == null) {
            ff = fd.getFontFile2();
            if (ff == null) {
                ff = fd.getFontFile();
            }
        }
        if (ff == null) {
            throw new IOException(font.getName() + " no fontfile");
        }
        InputStream is = ff.createInputStream();
        return new ByteArrayInputStream(IOUtils.toByteArray(is));
    }

    public Map<Integer, Integer> getWidthsMap() {
        return newWidth;
    }

    public PDFDictionary getRef() {
        return ref;
    }

    public void setRef(PDFDictionary d) {
        ref = d;
    }

    public int size() {
        if (getFontType() == FontType.CIDTYPE0) {
            return 1;
        }
        return 0;
    }

    private void readFontBBox(COSBase b) throws IOException {
        if (b instanceof COSDictionary) {
            COSDictionary dict = (COSDictionary)b;
            for (Map.Entry<COSName, COSBase> n : dict.entrySet()) {
                readFontBBox(n.getValue());
                if (n.getKey() == COSName.FONT_BBOX) {
                    COSBase bboxArray = n.getValue();
                    if (bboxArray instanceof COSObject) {
                        bboxArray = ((COSObject) bboxArray).getObject();
                    }
                    float[] bboxf = ((COSArray)bboxArray).toFloatArray();
                    int[] bbox = new int[bboxf.length];
                    for (int i = 0; i < bbox.length; i++) {
                        bbox[i] = (int)bboxf[i];
                    }
                    setFontBBox(bbox);
                }
            }
        } else if (b instanceof COSObject) {
            COSObject o = (COSObject)b;
            readFontBBox(o.getObject());
        } else if (b instanceof COSArray) {
            COSArray o = (COSArray)b;
            for (int i = 0; i < o.size(); i++) {
                readFontBBox(o.get(i));
            }
        }
    }

    public boolean isEmbeddable() {
        return true;
    }

    public InputStream getInputStream() throws IOException {
        return new ByteArrayInputStream(mergeFonts.getMergedFontSubset());
    }

    protected FontContainer getFont(COSDictionary fontData) throws IOException {
        if (!fontMap.containsKey(fontData)) {
            if (fontMap.size() > 10) {
                fontMap.clear();
            }
            fontMap.put(fontData, new FontContainer(fontData));
        }
        return fontMap.get(fontData);
    }

    protected static void setProperties(CustomFont cFont, PDFont font) throws IOException {
        if (font.getFontDescriptor() != null) {
            cFont.setCapHeight((int) font.getFontDescriptor().getCapHeight());
            int ascent = (int)font.getFontDescriptor().getAscent();
            if (cFont.getAscender() != 0 && ascent != cFont.getAscender()) {
                throw new IOException("Ascender doesn't match");
            }
            cFont.setAscender(ascent);
            cFont.setDescender((int)font.getFontDescriptor().getDescent());
            cFont.setXHeight((int)font.getFontDescriptor().getXHeight());
            cFont.setStemV((int)font.getFontDescriptor().getStemV());
        }
    }

    protected static void mergeMaxp(TrueTypeFont ttf, MaximumProfileTable outMaxp) throws IOException {
        org.apache.fontbox.ttf.MaximumProfileTable mp = ttf.getMaximumProfile();
        outMaxp.setVersion(mp.getVersion());
        outMaxp.setNumGlyphs(outMaxp.getNumGlyphs() + mp.getNumGlyphs());
        outMaxp.setMaxPoints(outMaxp.getMaxPoints() + mp.getMaxPoints());
        outMaxp.setMaxContours(outMaxp.getMaxContours() + mp.getMaxContours());
        outMaxp.setMaxCompositePoints(outMaxp.getMaxCompositePoints() + mp.getMaxCompositePoints());
        outMaxp.setMaxCompositeContours(outMaxp.getMaxCompositeContours() + mp.getMaxCompositeContours());
        outMaxp.setMaxZones(outMaxp.getMaxZones() + mp.getMaxZones());
        outMaxp.setMaxTwilightPoints(outMaxp.getMaxTwilightPoints() + mp.getMaxTwilightPoints());
        outMaxp.setMaxStorage(outMaxp.getMaxStorage() + mp.getMaxStorage());
        outMaxp.setMaxFunctionDefs(outMaxp.getMaxFunctionDefs() + mp.getMaxFunctionDefs());
        outMaxp.setMaxInstructionDefs(outMaxp.getMaxInstructionDefs() + mp.getMaxInstructionDefs());
        outMaxp.setMaxStackElements(outMaxp.getMaxStackElements() + mp.getMaxStackElements());
        outMaxp.setMaxSizeOfInstructions(outMaxp.getMaxSizeOfInstructions() + mp.getMaxSizeOfInstructions());
        outMaxp.setMaxComponentElements(outMaxp.getMaxComponentElements() + mp.getMaxComponentElements());
        outMaxp.setMaxComponentDepth(outMaxp.getMaxComponentDepth() + mp.getMaxComponentDepth());
    }

    public String getMappedWord(List<String> word, byte[] bytes, FontContainer oldFont) {
        StringBuilder newHex = new StringBuilder();
        for (String str : word) {
            char c = str.charAt(0);
            if (str.length() > 1) {
                c = (char) str.hashCode();
            }
            if (hasChar(c)) {
                int mapped = (int)mapChar(c);
                newHex.append(String.format("%1$04x", mapped & 0xFFFF).toUpperCase(Locale.getDefault()));
            } else {
                return null;
            }
        }
        return "<" + newHex.toString() + ">";
    }
}
